fix(docx): advance paragraphs by the font's full line box#465
Merged
Conversation
Word advances every body line by the full line box - the font's hhea single-spacing line, or the document grid pitch for East Asian text under a w:docGrid - and measures w:spacing w:before/w:after from that box's edges. office2pdf pinned the text edges to the font's typographic ascender/descender and recovered the remainder as par(leading:), but Typst inserts leading only *between* the lines of one paragraph. Every paragraph therefore came out exactly one leading short, however many lines it had: consecutive 9pt Courier New command lines advanced 7.2pt against Word's 10.1pt, a 28% shortfall that accumulated to 15.5pt down the page. Span the whole advance with the line box itself, split by the font's ascender/descender ratio, and set leading to 0. A paragraph's height is then n * advance exactly as in Word, and the block's above/below sit where Word measures its gaps from, so the grid-only boundary top-up added for #394 is no longer needed. That top-up's counterpart went the other way: the parser injected an 8pt default w:spacing w:after whenever neither the paragraph nor its style chain set one, which masked the missing leading. ECMA-376 leaves that gap at zero, and a Word export of a document whose styles.xml defines no Normal spacing confirms it, so the default is now 0pt. It stays an explicit Some(0.0) rather than None so the paragraph block still pins its `below` and Typst's own 1.2em default block spacing cannot leak in. With both, per-line pitch matches the Word golden PDFs within 0.15pt and mean absolute vertical drift over the business corpus drops from 14.61pt to 13.52pt - 04_resume_en from 4.90pt to 0.69pt, 09_offer_letter_en from 2.57pt to 1.41pt, 07_product_spec_en from 4.56pt to 1.20pt. The list path's metric_leading_pt is now always zero, so it becomes a wrapper_spans_full_line flag. Fixes #452 Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
par(leading:)w:spacing w:afterto 0pt, per ECMA-376, instead of injecting 8ptmetric_leading_ptwith awrapper_spans_full_lineflagRelated issue
Fixes #452
Details
Word advances every body line by the full line box - the font's hhea single-spacing line, or the document grid pitch for East Asian text under a
w:docGrid- and measuresw:spacing w:before/w:afterfrom that box's edges. office2pdf pinned the text edges to the font's typographic ascender/descender and recovered the remainder aspar(leading:), but Typst inserts leading only between the lines of one paragraph. Every paragraph came out exactly one leading short however many lines it had: consecutive 9pt Courier New command lines advanced 7.2pt against Word's 10.1pt.Spanning the whole advance with the box, split by the font's ascender/descender ratio, makes a paragraph's height
n * advanceexactly as in Word, and puts the block edges where Word measures its gaps from.The parser's 8pt default
w:spacing w:afterwas the compensating half of the same model: it masked the missing leading. It cannot be left in place alongside the fix without double-counting, so it goes in the same change. It stays an explicitSome(0.0)rather thanNoneso the paragraph block still pins itsbelowand Typst's own 1.2em default block spacing cannot leak in.Measured against the Word golden PDFs, per-line pitch now matches within 0.15pt and mean absolute vertical drift over the business corpus drops from 14.61pt to 13.52pt:
Testing
cargo fmt --all -- --checkcargo test --workspacecargo clippy --workspace --all-targets --all-featurespython3 scripts/validate_business_golden_mocks.pypdftotext -bboxline-pitch comparison against all 10 Word-produced DOCX golden PDFspdftoppm -r 150rendering, matched region crops, andmagick compare -metric AE -fuzz 5%Visual impact
Visual audit
tests/golden_mocks/business/sources/docx/05_technical_manual_en.docxpdftoppm, 150 DPIfixassets/bugfixes/issue-452/gt.jpgassets/bugfixes/issue-452/before.jpgassets/bugfixes/issue-452/after.jpgVisual comparison
Required inspection
The 5% fuzz sweep against the size-normalized GT falls from 98,872 to 88,314 differing pixels. Every remaining cluster is explained below. The page's only hairline is the header rule, which matches GT in position, width, and weight on the full-resolution crop; the fixture has no dashed or dotted borders. Bold headings and the monospace runs carry the same weight and style as GT.
Deviation audit
w:ind w:left; the page box, text column, and shading band height match GT, and the residual vertical drift comes from #463F4F4F4code shading is the same color, though its left edge inherits #464Checklist
Signed-off-byline