Repository navigation
fix(docx): hang a body paragraph with a negative indent into the margin - #2382
Merged
Merged
Conversation
Word lets a paragraph's w:ind w:left or w:right be negative: its lines start at the margin plus the signed left indent (plus the first-line offset on the first line), and a negative right indent ends them past the right margin. paragraph_indent_pt clamped both to zero on the claim that a Typst inset cannot be negative, which is false: block insets accept negative values, as picture paragraphs already rely on. The flow paragraph now emits the signed inset. In docxide-pdf's paragraph-offset-frame-no-frame, "About providing information" (left -70.9pt, firstLine +78pt) starts at 92.15pt where Word draws it at 92.05pt; main drew it at 163.05pt. Its framed sibling moves to 72.4pt against Word's 72.28pt. Table cells keep the clamp until they clip their content at the cell edge (#2261): Word hides the part of a hanging cell paragraph that crosses the edge, so painting it into the next cell would not match. Related: #2233 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> 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.
File submission policy
Before attaching or committing files, read the submission policy.
Complete a security review and obtain your organization's internal approval for
public sharing where applicable. You are responsible for lawful disclosure;
the project and maintainers disclaim related liability to the extent permitted
by applicable law.
Summary
First milestone of #2233: a body paragraph with a negative
w:ind w:leftorw:rightnow hangs into the margin, as Word does.paragraph_indent_ptclamped both indents to zero, on the stated grounds that "a Typst inset cannot be negative". That is false:#block(inset: ...)takes negative values, and picture paragraphs have emitted signed insets since #2260. The flow paragraph now emits the signed inset. Its lines start at margin + left, the first line also adds its signed first-line offset, and a negative right indent ends the line past the right margin.TODO(#2261 cell clip). Word clips a cell paragraph that hangs past its cell at the cell's edge: docxide-pdf case193/194/198/199, recorded on DOCX: a negative w:ind w:left is clamped to zero, so a paragraph that hangs into the margin starts 71pt right of Word #2233. Cells do not clip yet (DOCX: table cells paint hanging first-line text outside the native cell clip #2261), so a signed cell inset would paint into the neighbouring cell, which does not match Word either. Cell output is unchanged; the corpus scan below checks it.typst_gen_lists.rs). docxide-pdf case97 (numbered,w:left="-100" w:hanging="5"): Word puts "1." at 66.78pt, and this PR leaves ours at 72pt. That stays on DOCX: a negative w:ind w:left is clamped to zero, so a paragraph that hangs into the margin starts 71pt right of Word #2233.Related issue
Related: #2233, #2261, #1874, #2243.
Testing
cargo test --locked --workspace --profile ci: 4,510 passed, 0 failed, 15 ignored.cargo fmt --all -- --checkandcargo clippy --locked --workspace --all-targets --profile ci -- -D warnings: clean (local clippy 1.97).typst_gen_paragraph_tests.rs; both failed on main's code before the fix:a_negative_paragraph_indent_hangs_its_lines_into_the_marginlays out a wrapping paragraph on A4 with 72pt margins and reads every line's start. Expected starts: left −36 gives 36/36; left −36 with firstLine 40 gives 76/36; left −36 with hanging 18 gives 18/36; the positive control left 20 gives 92/92. A right-aligned line moves +18pt for right −18 and −18pt for right 18. On main the first case gave 72 for every line.a_paragraph_hanging_into_the_margin_starts_where_word_starts_ituses the tracked, unmodified docxide-pdf fixturesspaced_section_mark.docx(paragraph-offset-frame-no-frame) andoffset_text_frame.docx(paragraph-offset-frame-immediate). "About providing information" must start at Word's 92.05pt in the body and 72.28pt in its text-anchored frame, within 0.25pt. Main gave 163.05pt.5ea31dedand this branch. The inputs were every DOCX with a negativew:indin any part: 151 files from thefixtures-v1bulk corpus and docxide-pdffe7b8beacases/andscraped/, plus 40 random controls. No conversion failed. The 40 controls and every table-cell case (118–120, 162–175, 193–199) have identical text geometry, and so does the list case97. 29 files changed. Each of the 16 changed docxidescraped/documents was checked against its Word reference:polish_council_session_notice70.85 → 65.20 (65.03)russian_construction_org_council_minutes85.05 → 58.05 (58.02)eco_int_agriculture_registration231.14 → 217.64 (217.60)ukrainian_municipal_heating_resolution130.93 → 120.36 (120.30)mongolian_human_rights_law217.20 → 226.20 (226.10)radiographer_interventional_job_desc207.08 → 204.71 (204.85)learning_cultures_dissertation's check marks 382–503 → 320.11 (319.13)feminist_voice_dissertation(left −284) now keeps "Date……" on the Chairperson line at 396pt, as Word does (396.17); main wrapped it to the next line.alfies_arc_adult_safeguarding_policy(justified, right −46) now breaks its second line where Word does.chiseldon_parish_planning_agenda106.73 → 102.23 (96.80), andarizona_physical_education_standards76.04 → 72.34 (26.50). Of its lines that re-wrap, 22 of this PR's match a Word line, against 17 of main's.estonian_community_development_grant,japanese_medical_consultation_form,tokyo_welfare_infrastructure_briefingandpolish_building_procurement_specmove by 0.1–0.8pt, following their small signed indents.indents.docxlines labelled "Negative 2cm" move −56.7pt, andtestOuterShdw's justifiedw:right="-360"lines widen by 18pt. Intdf116410(Strict conformance) the tab after "A" now falls to the default stop at the margin, because the style's explicit 85.05pt stop is never read (DOCX: a Strict-conformance package's styles part is never read, so its theme face and stated w:sz are lost #2243); its −14.15pt indent arrives as −14pt (Tracker: sub-materiality visual deviations (revisit in bulk) #1874).Visual impact
Visual audit
tests/fixtures/docx/docxide/spaced_section_mark.docx(unmodified docxide-pdfcases/paragraph-offset-frame-no-frame/input.docxatfe7b8beafa7612cc89ad62a7d27b2c6141c4d6ed, Apache-2.0; GT is that case'sreference.pdf,Creator: Microsoft Word,check_gt_integrity.py --sourceclean)pdftoppmviacompare_render.py, 150 DPIfixassets/bugfixes/issue-2233/layout-audit.jsonassets/bugfixes/issue-2233/render-clusters-page-1.jsonassets/bugfixes/issue-2233/gt.jpgassets/bugfixes/issue-2233/before.jpgassets/bugfixes/issue-2233/after.jpgVisual comparison
Required inspection
Deviation audit
compare_text_layer.pyfinds normalized content identical and no codepoint class difference.Final performance pass
/usr/bin/time -f "%e %M"), median of 5 runs after one warm-up, per input and binary.cargo build --locked --release -p office2pdf-cli) built into separate run-owned targets: main5ea31ded(base) and this branch (cand).spaced_section_mark.docx;offset_text_frame.docx; POI'sbug65649.docx(177 pages, 32 negative indents); LibreOffice'sposition-and-rotation.docx(control, no negative indent).maxcalls and adds a clamp at the cell call site; it adds no work per paragraph.Checklist
Signed-off-byline🤖 Generated with Claude Code