Summary
With tests/fixtures/texts/style-leading-2.psd, we observe slightly different text baselines between Photoshop and SVG foreignObject renderings. Specifically, the first line in SVG foreignObject is positioned upwards and overflows at the top of the div container.
Root Cause
The issue stems from a fundamental difference between Photoshop's baseline calculation and CSS's inline box baseline model:
Photoshop (SVG with dominant-baseline="hanging")
- Reference point: Cap-height (top of capital letters) at the top edge
- First line starts with cap-height at y=0 (or close to it)
- Baseline is positioned below the cap-height by the font's ascent value
CSS foreignObject (with line-height and inline-block)
- Reference point: Baseline centered in the line box
- With
line-height: 16px, first baseline is at 16/2 = 8px from top
- With current
margin-top: -8px compensation, baseline moves to 8-8 = 0px
- However, cap-height is above the baseline by ~70-75% of font-size
The Core Problem
The current margin-top compensation formula:
margin_top_compensation = -(leading - font_size) / 2
This assumes that removing half-leading will align the baselines. However:
- CSS baseline is centered in the line-box (at line-height/2)
- Photoshop's
hanging baseline puts the cap-height (not baseline) at the top
- Cap-height ≠ Baseline - cap-height is typically ~70-75% of font-size above the baseline
- This creates a systematic offset of approximately
0.7-0.75 * font_size
Example with Arial (style-leading-2.psd)
Font size: 32px
Leading: 16px
Typical cap-height ratio: 0.715
CSS foreignObject (current):
- Baseline at: 0px (after margin-top: -8px)
- Cap-height at: ~-22.9px (overflows above container)
Photoshop hanging baseline:
- Cap-height at: ~0px
- Baseline at: ~+22.9px
Vertical offset difference: ~22.9px
Test Case
File: tests/fixtures/texts/style-leading-2.psd
Photoshop settings:
- Font: Arial, 32px
- Leading: 16px (manual, not auto)
- Bounding box: 240×119.43px
- Expected lines: 7.5 (119.43 / 16)
Current SVG output:
<foreignObject x="0" y="0.57" width="240" height="119.43">
<div style="width: 240px; height: 119.43px; margin: 0; padding: 0; overflow: hidden; box-sizing: border-box">
<p style="margin: 0; padding: 0; line-height: 16px; margin-top: -8px">
<span style="display: inline-block; line-height: 16px; font-family: 'Arial'; font-size: 32px">
Lorem ipsum dolor sit amet...
</span>
</p>
</div>
</foreignObject>
Observed issue:
- First line overflows at the top of the container
- Text appears shifted upward compared to Photoshop
Related Issues & Context
The recent fixes (#274, #271) successfully addressed the line-height expansion issue where spans with font-size > line-height were expanding the parent line box. However, the first-line baseline positioning discrepancy remains due to the cap-height vs baseline difference.
Technical Details
Current Implementation
Location: src/psd2svg/core/text.py:759-802
# Line height and margin-top compensation calculation
leading = paragraph.compute_leading()
margin_top_compensation = 0.0
if leading > 0:
styles["line-height"] = svg_utils.num2str_with_unit(leading)
# Calculate half-leading compensation for all paragraphs
# CSS line-height centers text within a line box, adding unwanted space
# above each line. We calculate the negative margin as:
# margin-top = -(leading - font_size) / 2
if paragraph.spans:
first_span = paragraph.spans[0]
font_size = first_span.style.font_size
if leading > font_size:
# Calculate half-leading compensation
margin_top_compensation = -(leading - font_size) / 2
CSS Inline Formatting Context
The issue is related to how CSS defines the baseline and line box:
- Line box height =
line-height (16px in our case)
- Inline box = The box around inline content (the
<span>)
- Baseline = The reference line for aligning inline boxes
- Cap-height = Top of capital letters (what Photoshop's "hanging" uses)
With display: inline-block and explicit line-height on spans (from #274):
- The span's inline box is properly constrained to
line-height
- But the baseline is still centered in the line box
- Font glyphs extend both above and below the baseline according to font metrics
Potential Solutions
Option 1: Adjust margin-top to account for cap-height
Use font metrics (ascent/descent ratios) to calculate the offset:
# Approximate cap-height ratio for common fonts (Arial: ~0.715)
CAP_HEIGHT_RATIO = 0.715
cap_height_offset = font_size * CAP_HEIGHT_RATIO - leading / 2
margin_top_compensation = -cap_height_offset
Pros:
- Addresses the root cause
- Could work across different fonts
Cons:
- Requires accurate font metrics (varies by font)
- May need per-font calibration
- Complex to implement generically
Option 2: Use CSS text-box-trim (experimental)
The CSS text-box-trim property (currently experimental) allows trimming the half-leading space from the first and last lines.
Pros:
- Native CSS solution
- Semantically correct
Cons:
- Limited browser support (experimental)
- Not available in most SVG renderers
Option 3: Use absolute positioning for first line
Position the first line explicitly using position: relative with top offset.
Pros:
- Precise control over first-line position
Cons:
- Breaks normal flow
- Complex interaction with line wrapping
Option 4: Accept the limitation and document
Document that foreignObject mode has slight first-line baseline differences and recommend native SVG text for precise positioning.
Pros:
- Simple, honest approach
- ForeignObject mode still useful for text wrapping
Cons:
- Doesn't solve the issue
- May disappoint users expecting pixel-perfect matching
Recommended Approach
I suggest a combination of Option 1 (with empirical calibration) and Option 4 (documentation):
- Calibrate the offset empirically for common fonts (Arial, Helvetica, etc.)
- Add a configuration option for cap-height-to-font-size ratio (default: 0.715)
- Document the limitation in the foreignObject mode documentation
- Add tests to track the baseline offset for regression detection
This provides a practical improvement while acknowledging the inherent complexity of matching Photoshop's text rendering in CSS.
Implementation Notes
The fix would modify _get_foreign_object_paragraph_styles() in src/psd2svg/core/text.py:
# Proposed adjustment (pseudo-code)
if paragraph.spans:
first_span = paragraph.spans[0]
font_size = first_span.style.font_size
if leading > font_size:
# Half-leading compensation
half_leading = (leading - font_size) / 2
# Cap-height adjustment (font-specific, default for Arial)
cap_height_ratio = get_cap_height_ratio(font_family) # ~0.715 for Arial
cap_height = font_size * cap_height_ratio
# Align cap-height to top instead of baseline
margin_top_compensation = -(cap_height - leading / 2)
Success Criteria
Environment:
- psd2svg version: main branch (latest)
- Test file:
tests/fixtures/texts/style-leading-2.psd
- Related code:
src/psd2svg/core/text.py:759-802
Labels: enhancement, text-rendering, foreignObject
Summary
With
tests/fixtures/texts/style-leading-2.psd, we observe slightly different text baselines between Photoshop and SVG foreignObject renderings. Specifically, the first line in SVG foreignObject is positioned upwards and overflows at the top of the div container.Root Cause
The issue stems from a fundamental difference between Photoshop's baseline calculation and CSS's inline box baseline model:
Photoshop (SVG with
dominant-baseline="hanging")CSS foreignObject (with
line-heightandinline-block)line-height: 16px, first baseline is at16/2 = 8pxfrom topmargin-top: -8pxcompensation, baseline moves to8-8 = 0pxThe Core Problem
The current margin-top compensation formula:
This assumes that removing half-leading will align the baselines. However:
hangingbaseline puts the cap-height (not baseline) at the top0.7-0.75 * font_sizeExample with Arial (style-leading-2.psd)
Test Case
File:
tests/fixtures/texts/style-leading-2.psdPhotoshop settings:
Current SVG output:
Observed issue:
Related Issues & Context
The recent fixes (#274, #271) successfully addressed the line-height expansion issue where spans with
font-size > line-heightwere expanding the parent line box. However, the first-line baseline positioning discrepancy remains due to the cap-height vs baseline difference.Technical Details
Current Implementation
Location:
src/psd2svg/core/text.py:759-802CSS Inline Formatting Context
The issue is related to how CSS defines the baseline and line box:
line-height(16px in our case)<span>)With
display: inline-blockand explicitline-heighton spans (from #274):line-heightPotential Solutions
Option 1: Adjust margin-top to account for cap-height
Use font metrics (ascent/descent ratios) to calculate the offset:
Pros:
Cons:
Option 2: Use CSS
text-box-trim(experimental)The CSS
text-box-trimproperty (currently experimental) allows trimming the half-leading space from the first and last lines.Pros:
Cons:
Option 3: Use absolute positioning for first line
Position the first line explicitly using
position: relativewithtopoffset.Pros:
Cons:
Option 4: Accept the limitation and document
Document that foreignObject mode has slight first-line baseline differences and recommend native SVG text for precise positioning.
Pros:
Cons:
Recommended Approach
I suggest a combination of Option 1 (with empirical calibration) and Option 4 (documentation):
This provides a practical improvement while acknowledging the inherent complexity of matching Photoshop's text rendering in CSS.
Implementation Notes
The fix would modify
_get_foreign_object_paragraph_styles()insrc/psd2svg/core/text.py:Success Criteria
Environment:
tests/fixtures/texts/style-leading-2.psdsrc/psd2svg/core/text.py:759-802Labels:
enhancement,text-rendering,foreignObject