Skip to content

fix(docx): autofit table columns by compressible slack, not a uniform scale - #726

Merged
developer0hye merged 1 commit into
mainfrom
fix/docx-table-autofit
Jul 30, 2026
Merged

fix(docx): autofit table columns by compressible slack, not a uniform scale#726
developer0hye merged 1 commit into
mainfrom
fix/docx-table-autofit

Conversation

@developer0hye

Copy link
Copy Markdown
Owner

Summary

A w:tbl without w:tblLayout fixed is auto-fitted by both Word and office2pdf — by different rules. We scaled per-column tcW maxima uniformly: the invoice's conflicting 4200-twip claims put Description and Amount at an identical 161.32pt (the equal-share signature), Unit Price lost 9.6pt, a wrap flipped, and the lower half of the page sat 11.3pt high.

Word compresses each grid column in proportion to its compressible slack above min-content, never below it: k = (W − Σmin)/(Σpref − Σmin), width_i = min_i + (pref_i − min_i)·k. Preferences track grid-column occupancy through gridSpan; min-content is the widest unbreakable token measured with the run's resolved face/weight/size plus tcMar (every CJK character is its own token; NBSP does not break). W is tblW capped at the grid sum. Derived and verified on the invoice's Word GT: every column within 0.10pt, the row-2 wrap flips to Word's two lines, the totals ride the col-4/5 boundary within 0.28pt.

Measurement uses a new cached render::pdf::text_advance_em (hmtx sums, per-(family, weight) face cache, wasm None twin, embedded/user font paths documented as the same accepted limitation as #621's accessor). The model runs only in the GT-verified compression direction; conflict-free tables return the grid verbatim; any unmeasurable token degrades to the exact pre-change uniform scale — all nine other business docx mocks and the 39-page technical brief are byte-identical to main.

Related issue

Fixes #624

Testing

  • cargo test --workspace — green (11 red-first unit tests: slack distribution, gridSpan occupancy, min floors, grid-verbatim, degrade paths, CJK singleton tokens — the reviewer's 200pt-table CJK probe no longer overflows to 377pt — NBSP tokens, tblW cap, undershoot gate, all-empty parity; text_advance_em pins incl. bold-face selection)
  • cargo +1.97 clippy --workspace --all-targets — clean; cargo fmt --check — clean; wasm check clean
  • GT: invoice columns 27.93/156.99/47.74/65.24/153.40 vs Word 27.9/156.9/47.8/65.3/153.3; table bottom 334.26 vs 334.56 (was 11.2pt high); compare_layout 14/14 lines, 0 re-wrapped

Visual impact

  • No rendered PDF change
  • Rendered PDF change or visual evidence added
  • Reason:

Visual audit

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

Deviation audit

Check Result
Page count/order Matches GT — one A4 page each
Element presence Matches GT — all elements and all 14 grid hairline segments present (GT's higher rect census is per-segment op-splitting, confirmed visually)
Position/size Remaining: #649 — cell text 0.37–0.50pt high / 0.14–0.35pt left of Word's inset; Remaining: #638 — the gap under 'Bill To' is 0.41pt short. The column widths this PR fixes are within 0.09pt of Word and the table bottom within 0.30pt (was 11.2pt high)
Rotation/flip No deviation observed
Fill Remaining: #647 — band colours match; shading spans centreline-to-centreline where Word fills to the border's inner edge (0.24pt/side)
Stroke/border Remaining: #724 — borders centred on the boundary instead of edge-aligned, grid 0.1–0.2pt left of Word; all segments present, weights within 0.02pt
Text content Fixed — the row-2 wrap now matches Word ('Additional render workers (4' / 'vCPU)'); every other string identical
Font family/weight/style Remaining: #628 — family/weight/style match, but unconditional pair kerning sets runs 1.0–1.4% tighter
Text color Matches GT
Alignment Remaining: #646 — the centred footer field sits 0.74pt right; Remaining: #725 — the '- PAGE -' footer paragraph sets 17% narrower. The totals this PR fixes align within 0.28pt (was 8.1pt left)
Line/paragraph spacing Remaining: #638 — wrapped-cell pitch 0.12pt tight and inter-block gaps 0.28–0.41pt short (max-ascent/descent line-box model)
Clipping/overflow No deviation observed

Checklist

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

🤖 Generated with Claude Code

… scale

A w:tbl without w:tblLayout fixed is auto-fitted by both Word and
office2pdf — by different rules. We scaled the per-column tcW maxima
uniformly, so the invoice's conflicting 4200-twip claims put Description
and Amount at an identical 161.32pt (the equal-share signature), Unit
Price lost 9.6pt, a wrap flipped, and everything below sat 11.3pt high.

Word compresses each grid column in proportion to its compressible
slack above min-content, never below it:

    k       = (W - Σmin) / (Σpref - Σmin)
    width_i = min_i + (pref_i - min_i) · k

pref_i is the widest single-column tcW occupying grid column i (tracked
through gridSpan: a cell following a span-4 cell claims column 5), with
gridCol_i as fallback; min_i is the widest unbreakable token measured
with the run's resolved face, weight and size plus the tcMar side
margins — bold runs use the bold face, every CJK character is its own
token (Word breaks between them), and NBSP does not break. W is tblW
when it does not exceed the grid sum, else the grid sum. Derived and
verified on the invoice's Word GT: every column lands within 0.10pt of
27.9/156.9/47.8/65.3/153.3, the row-2 wrap flips to Word's two lines,
and the totals ride the col-4/5 boundary within 0.28pt.

Measurement uses a new cached render-side text_advance_em (hmtx sums,
per-(family, weight) face cache, wasm None twin). The model runs only
in the GT-verified compression direction; conflict-free tables return
the grid verbatim, and any unmeasurable token — wasm, unresolved face,
missing glyph, all-empty tables — degrades to the exact pre-change
uniform scale, so every other document is byte-identical: all nine
remaining business docx mocks and the 39-page technical brief convert
to byte-identical PDFs against main.

Related: #624 #355

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
@developer0hye
developer0hye merged commit 725d690 into main Jul 30, 2026
17 checks passed
@developer0hye
developer0hye deleted the fix/docx-table-autofit branch July 30, 2026 17:08
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: an auto-layout table splits its columns evenly, flipping a wrap and lifting the rest of the page 11.3pt

1 participant