Skip to content

Fix a bunch of renders#101

Merged
SimonCropp merged 24 commits intomainfrom
fix-a-bunch-of-renders
May 10, 2026
Merged

Fix a bunch of renders#101
SimonCropp merged 24 commits intomainfrom
fix-a-bunch-of-renders

Conversation

@SimonCropp
Copy link
Copy Markdown
Owner

No description provided.

SimonCropp and others added 24 commits May 8, 2026 18:44
ScoreFace was penalising width mismatch 1000:1 against weight, so Arial_700.ttf
(which reports usWidthClass=3 Condensed in its OS/2 table) lost to arial.ttf
(width=5) for Bold/Black requests — leaving requested-bold runs rendered with
the regular face. Drop the multiplier to 100. Also embolden synthetically in
Skia when the resolved face is still 200+ weight units below target (e.g.
Arial Black 900 → Arial Bold 700) so RAINBOW / Arial Black Style / Google
Style render with the heavier strokes Word produces.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Skia's SKFont.Size is in pixels (the typeface is constructed with size *
Scale), but ImageSharp's Font.Size is in points — the same scenario test code
wasn't accounting for the difference, so the strikethrough sat ~1.3× closer
to the baseline than it should and read as a near-baseline underline. Multiply
by context.Scale so it lands mid-x-height as intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Word's highlighter pen uses w:highlight (named-color palette) instead of w:shd
(arbitrary RGB). The parser only handled w:shd, so runs decorated with
<w:highlight w:val="yellow"/> rendered as plain text with no background.
Map the 16 named colors to RGB hex via HighlightToHex and feed the result
through the existing BackgroundColorHex pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A fixed 1px tonal companion is invisible against 72pt Impact, so the second
EMBOSSED in the wordart fixture rendered indistinguishable from a flat dark
glyph. Scale the offset to ~4% of font size (clamped to ≥1px) in both
backends so the highlight/shadow companion is visibly offset on display-size
runs while still pixel-tight on body text.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
ParseBorderEdge dropped the w:val style, so <w:top w:val=\"double\"/> resolved
to a single thicker line. Capture the style on BorderEdge and split it across
both renderers: each line gets ~1/3 of the declared width, separated by a
gap totalling the remaining 1/3, so total span matches the OOXML width.
Other non-single styles fall back to single until they're needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Tables that set <w:tblW w:type=\"dxa\"/> declare a fixed preferred width, but
CalculateColumnWidths' autofit branch grew columns to fill the page anyway —
so a 3000-twip table aligned right rendered as full-width and the alignment
became invisible. Capture the dxa value as PreferredWidthPoints during parse
and skip the autofit-grow when it's set; pct/auto tables still flow as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Both inline-SDT parsing paths bailed via `continue` after seeing a w:br child,
so a run like <w:r><w:br/><w:t>Sharma</w:t></w:r> emitted the newline but
discarded the trailing text. The 'Chanchal\nSharma' resume heading rendered
as just 'Chanchal'; address blocks dropped continuation lines after a soft
break. Emit the newline run, then fall through to ParseRun so the same run's
<w:t> children still get parsed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
MeasureCellHeight subtracted padding.Top from the first paragraph's
SpacingBefore (and likewise for the bottom), on the assumption padding
\"absorbed\" leading/trailing paragraph spacing. Word doesn't collapse the
two — padding sits between the cell border and the content area, paragraph
spacing lives inside that area, and they sum. Removing the collapse makes
tables that set w:tblCellMar (table_cell_padding, table_cell_padding_varied,
business-plan/wedding decks) render at the row heights Word produces.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The recent table-cell padding fix tightened row heights for autofit-grid
tables, so the previously-stale verified PNG no longer matched. Promote
the received image to baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
When a table sets <w:tblW w:type=\"pct\"/> (fill the container) but its cells
have no explicit dxa widths, CalculateContentBasedColumnWidths fell into the
\"hug content\" branch and produced narrow columns starting at the table's
left edge — so vmerge / page-break / two-column layout tables collapsed
their right cells next to the left cells instead of spreading across the
page. Track the pct-fill intent on TableProperties and scale natural
content widths up to the available width when set.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Word emits some section divider rules as a single <wps:wsp> with
prstGeom prst="line" sitting directly under <a:graphicData> — no
<wpg:wgp> wrapper, no <pic> child. The inline-image parser was bailing
on this shape silently, dropping the divider lines that resume / cover-
letter templates rely on.

Wrap the standalone connector into a one-element InlineShapeGroup so the
existing line-rendering path picks it up. Also reorder ParseSolidFillShape
so the IsLineShape branch runs before the noFill bail-out (line connectors
always carry an explicit <a:noFill/> alongside the stroke).
Word fills in an 8pt paragraph spacing-after when a docx doesn't carry
its own pPrDefault — minimal docs (table-only templates, hand-built
section-break tests) come through this path. Previously we assumed
"styles.xml exists ⇒ author opted out of all defaults" and dropped to
0pt, which left rows 50% shorter than Word's render and packed extra
content onto the wrong page in section-break flows.

Switching the missing-docDefaults branch to 8pt brings 30+ scenarios
visibly closer to expected_*.png. Several baselines updated to match,
and resumes/10 dropped from 3 received pages to 2 — its old page-3
PNG was deleted as an orphan.
When a table has explicit cell widths (w:tcW) but no w:tblW, Word fits
the table to those cell widths and leaves whitespace on the right rather
than growing them to fill the page. The previous rule only suppressed
the grow when w:tblW dxa was set, which caused narrow tables (e.g. a
vertical-text sidebar) to span the full content width.

Restrict the autofit-grow to FillContainer (w:tblW pct) so it only fires
when the table actually asked to fill its container. Several baselines
updated to match the now-tighter layout.
Word's behaviour for a TOC entry inside a narrow table cell: the dot
leader fills the cell width and the page number after the tab is hidden
(it would have lived past the cell's right edge). Previously we clamped
the right-tab destination to the cell edge but still rendered the
page number at that point, producing visible numbers Word doesn't show.

Extend TabStopResolver to return a suppressFollowing flag when the
clamp fires, and have both renderers skip the run sequence between the
tab and the next tab/line break when set.
The IsDecorativeShape filter rejected any wsp containing cubic /
quadratic bezier segments, dropping the floral overlays on wedding /
cards / agendas-minutes templates and the gift-box artwork on
greeting-card templates. The polygon flattener already turns those
beziers into a polyline approximation via ExtractPolygonPoints; the
guard was protecting a pipeline state that no longer exists.

Drop the bezier-count rejection (both in ShapeParser.IsDecorativeShape
and the parallel >50-bezier guard in DocumentParser.ParseSolidFillShape)
and only filter genuinely unrenderable cases — ArcTo segments and the
existing >50:1 aspect-ratio thin-line heuristic. ~18 templated
backgrounds now render as solid-fill polygons; baselines updated.
The OOXML spec calls for top: 0, bottom: 0 default cell margins, but
real Word adds about 2pt of breathing room top and bottom. Without it
each cell row measures at line-height + spacing-after only — too tight
to match Word's actual layout, so 30-row tables fit on one page in our
renderer where Word splits across two.

Use 2pt vertical implicit padding when neither w:tblCellMar nor a table
style supplies one. Also makes most table-related ErrorMetrics improve
slightly because rows now match Word's rendered height. Fixes #17
(table_multipage now paginates to 2 pages); ~20 baselines updated.
…pace

Letter-style layouts use a w:tblPr table with one or two hRule="exact"
rows where the body row consumes most of a page (~530pt). With prior
content on the page (a heading, an intro), the fixed slot can't fit in
the remaining bottom margin — Word lifts the entire table to the next
page rather than overflowing or shrinking the slot.

When a table contains an exact-height row and would overflow remaining
space (with a 5pt buffer to avoid splitting on line-rounding noise),
finish the current page and start a new one before drawing. Restricted
to non-floating tables and to cases where the table fits on a fresh
page so we don't trade overflow for ping-ponging through row-by-row.
Fixes #18 (table_layout_tall_row paginates to 2 pages).
The renderer treated <w:docGrid w:linePitch="..."/> as the line slot for
empty end-of-cell paragraphs — which made every empty checkbox cell in
a 360-twip-grid template (wedding/04) measure at 18pt instead of the
style font's natural ~12pt. Across a 42-row checklist that 6pt-per-row
overhead added 250pt of phantom height and pushed half the content onto
an extra page.

Word's docGrid is for East Asian character-grid layouts; it doesn't
override individual paragraph line metrics in tables. Drop the empty-
paragraph linePitch enforcement so empty cells use the same compact
line-height calculation as text-bearing ones. Page counts collapse to
match Word for wedding/04, brochures/02 and several others; ~25
baselines updated.
Two related fixes drive page 2 of wedding/02 to render the correct
floral overlays (and improve letters/13, newsletters/12, /14 by similar
margins):

1. Behind-text FloatingImageElement now triggers
   AdvanceToBackgroundsTargetPage, the same as FloatingShapeElement.
   Without this, anchored decorations whose parent paragraph sits
   between two tables were left on the current page even when the next
   table forced a page break.

2. AdvanceToBackgroundsTargetPage's look-ahead used to bail at the next
   background, on the assumption "that one will handle its own advance".
   For a sequence of decorations belonging to the same target table,
   this anchored only the last decoration to the new page; earlier ones
   stayed behind. Walk past every background in the sequence so the
   whole group lifts together.

3. EstimatedNextElementHeight summed every row's declared height (or
   25pt fallback) for tables, instead of returning only the first
   row's. Multi-row tables without explicit trHeights previously
   estimated to zero, so the advance check never fired.
@SimonCropp SimonCropp merged commit 6b9ec7a into main May 10, 2026
3 of 4 checks passed
@SimonCropp SimonCropp deleted the fix-a-bunch-of-renders branch May 10, 2026 01:48
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.

1 participant