Skip to content

fix(docx): render a paragraph's left and right indent#470

Merged
developer0hye merged 3 commits into
mainfrom
fix/docx-paragraph-indent
Jul 26, 2026
Merged

fix(docx): render a paragraph's left and right indent#470
developer0hye merged 3 commits into
mainfrom
fix/docx-paragraph-indent

Conversation

@developer0hye

@developer0hye developer0hye commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • render a paragraph's w:ind left and right indent as a block inset
  • keep the shading and border on the inner block, where Word paints them: from the left indent to the right indent
  • keep w:spacing on the outermost block so an indent cannot break gap collapsing

Related issue

Fixes #464

Details

w:pPr/w:ind reached the IR but only the list path ever read it, so every indented body paragraph started at the page margin. In the technical-manual golden mock the shaded command lines carry a 240-twip (12pt) indent and rendered 11.95pt left of Word, while every unindented line on the same page matched within 0.05pt.

Line left edge (xMin) on page 1:

Line Word GT before after
$ cargo install office2pdf-cli 82.80 70.85 82.85
$ office2pdf --version 82.80 70.85 82.85
office2pdf 0.6.0 82.80 70.85 82.85
$ office2pdf report.docx 82.80 70.85 82.85
$ office2pdf *.xlsx --outdir ./pdfs/ 82.80 70.85 82.85

Word paints w:shd from the indent, not from the margin. Sampling the shaded band of the first code block at 150 DPI: Word starts it at 81.6pt against a 70.8pt margin, office2pdf started it at 71.0pt and now starts it at 83.0pt. The fill therefore belongs to a block nested inside the inset.

Vertical drift over the business corpus is unchanged at 13.52pt mean absolute, so the indent costs nothing elsewhere.

Testing

  • cargo fmt --all -- --check
  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features
  • python3 scripts/validate_business_golden_mocks.py
  • pdftotext -bbox x-position comparison against all 10 Word-produced DOCX golden PDFs
  • shaded-band pixel sampling at 150 DPI, matched region crops, and magick compare -metric AE -fuzz 5%

Visual impact

  • No rendered PDF change
  • Rendered PDF change or visual evidence added
  • Reason: indented paragraphs and their shading now start at the indent.

Visual audit

  • Issue: DOCX: paragraph w:ind w:left is dropped outside lists #464
  • Fixture: tests/golden_mocks/business/sources/docx/05_technical_manual_en.docx
  • Page(s): page 1 of the Word GT and of the office2pdf output
  • Renderer and DPI: pdftoppm, 150 DPI
  • Evidence mode: fix
  • New follow-up issues found in this audit: None
  • GT: assets/bugfixes/issue-464/gt.jpg
  • Before: assets/bugfixes/issue-464/before.jpg
  • After: assets/bugfixes/issue-464/after.jpg

Visual comparison

GT Before After
GT Before After

Required inspection

  • Rendered all evidence at 150 DPI or higher
  • Stored progressive JPEG quality 86 assets with metadata stripped
  • Inspected matched region crops at full resolution
  • Ran the 5% fuzz pixel-difference sweep
  • Inventoried hairlines and border dash styles
  • Inventoried font weight, italic, and underline emphasis

The 5% fuzz sweep against the size-normalized GT falls from 57,544 to 54,480 differing pixels, both measured against current main so the #463 list-spacing fix is in each. The residual clusters are font antialiasing. The page's only hairline is the header rule, which matches GT in position, width, and weight on the full-resolution crop; there are no dashed or dotted borders. Bold headings, the monospace runs, and the italic disclaimer all keep the same emphasis as GT.

Deviation audit

Check Result
Page count/order Matches GT - two pages in the same order
Element presence Matches GT - every heading, paragraph, command line, and the exit-code table are present
Position/size Fixed - every command line now matches Word's x position within 0.05pt, and no line on the page deviates by more than 1pt horizontally
Rotation/flip No deviation observed - the fixture contains no rotated or flipped elements
Fill Fixed - the F4F4F4 shading now starts at the indent, 83.0pt against Word's 81.6pt
Stroke/border No deviation observed - the header rule matches GT and there are no other strokes
Text content Matches GT - all strings on both pages are preserved
Font family/weight/style Matches GT - Arial body, Courier New code, bold headings, and the italic disclaimer all match
Text color Matches GT - the navy headings, grey header, and black body all match
Alignment Matches GT - the justified body paragraphs and centered footer match
Line/paragraph spacing Matches GT - the list-boundary gap this fixture used to overshoot was fixed by #463, which is now on main
Clipping/overflow No deviation observed - no glyph is clipped and nothing overflows the text column

Checklist

  • Commits include a Signed-off-by line
  • PR scope contains one root cause
  • Remaining visual deviations each reference an open issue

`w:pPr/w:ind` reached the IR but only the list path ever read it, so every
indented body paragraph started at the page margin. In the technical-manual
golden mock the shaded command lines carry a 240-twip indent and rendered
11.95pt left of Word, while every unindented line on the same page matched
within 0.05pt.

Wrap an indented paragraph in a block whose inset is the indent. The fill
and border stay on the inner block, because Word paints `w:shd` and
`w:pBdr` from the left indent to the right indent rather than across the
whole text column: measured on the Word export, the shaded band of the 12pt
indented code block starts at 81.6pt against a 70.8pt margin. The
`w:spacing` gaps move to the outer block so an indent cannot separate them
from the neighbouring paragraphs they collapse against.

Every command line now matches Word's x position within 0.05pt and the
shaded band starts at the indent. Vertical drift over the business corpus is
unchanged at 13.52pt mean absolute.

`needs_block_wrapper` deliberately still ignores the indent: the fixed-text
paths share that predicate but emit no inset, so counting it there would
open a bare wrapper that only leaks Typst's default block spacing.

Fixes #464

Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
An indented paragraph with no runs takes `generate_paragraph`'s early
return, which closed the style block but not the indent wrapper opened
before it. The generated Typst then carried an unclosed delimiter and the
whole document failed to compile: 73 third-party fixtures went from success
to error, dropping the bulk success rate from 99.07% to 96.52%.

Close the wrapper on that path too, and assert bracket balance for the case
in a unit test.

Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
@developer0hye
developer0hye force-pushed the fix/docx-paragraph-indent branch from dd2bfed to e7a1aa4 Compare July 26, 2026 01:38
Both evidence renders now come from current main, which carries the #463
list-spacing fix, so the before/after pair isolates the indent change alone.
The 5% fuzz difference against the Word export goes 57,544 to 54,480, and
the deviation audit no longer defers the list boundary to #463, which has
since merged.

Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
@developer0hye
developer0hye merged commit d72b898 into main Jul 26, 2026
17 checks passed
@developer0hye
developer0hye deleted the fix/docx-paragraph-indent branch July 26, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DOCX: paragraph w:ind w:left is dropped outside lists

1 participant