Skip to content

fix(examples): render the weights the styles declare - #486

Merged
DemchaAV merged 6 commits into
developfrom
fix/example-bold-faces
Aug 2, 2026
Merged

fix(examples): render the weights the styles declare#486
DemchaAV merged 6 commits into
developfrom
fix/example-bold-faces

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Found while building the asset-comparison gate for #461: the comparator's first run reported committed decks disagreeing with a fresh render, and the disagreement was weight.

Why

This module named a face constant — HELVETICA_BOLD, TIMES_BOLD, COURIER_BOLD — in 138 places and set no decoration. 115 of them were a plain fontName(FontName.FACE) call; the other 23 reached the constant through a ternary or a helper argument. The library rewrites such a name to its base family before the lookup, so the constant contributed nothing. It is the defect #479 fixed in the library, one module over and a hundred times; none of the 115 was paired with a decoration(...).

It stayed invisible until #450 taught the PPTX backend to take the face from the decoration for standard-14 names — correct, and the same rule PDF already followed. From that day a fresh render of the decks came out regular, while the committed previews, rendered before it, still showed bold. Nothing regenerates those previews, so nobody saw it.

Bold runs per deck, counting develop's committed file against develop's own examples rendered fresh:

deck committed fresh render on develop
financial-report.pptx 29 0
business-report.pptx 25 0
maven-banner.pptx 28 8
twin-output.pptx 4 0
master-showcase.pptx 75 75
social-card.pptx 3 3

web/showcase is regenerated on every cut, so the next release would have published the de-bolded decks.

What

The styles name a family and a decoration. Where a chain already set one, only the constant is renamed — it was redundant, not load-bearing. The helpers that picked the constant through a ternary (bold ? HELVETICA_BOLD : HELVETICA) needed the flag moved to the decoration instead; those were invisible to a search for the literal call form, which is how the first pass missed them.

The four decks that had lost their weights render them again, at the counts their committed previews already showed. master-showcase is the one that moves further: it gains 21 bold runs against develop, emphasis its styles had always asked for and never received.

The weekly schedule stopped fitting, and it is the one place where restoring intent was not enough. Its day notes span four fixed sub-columns; bold needed 107.38pt in a 105.5pt span, and a spanned cell cannot borrow width from fixed neighbours. The columns are recomputed from the printable width rather than nudged: 87.38 + 7 × 105.5 comes within 0.01pt of the 825.89 available — the name column is floored to the hundredth — where the previous 90 + 7 × 105 left 0.89pt unused on a board meant to be printed. The name column is derived from the page and the margins rather than written down, so the two cannot drift apart again. The note size drops 5.9 → 5.75 to clear the span.

Thirty-one previews and the README hero re-rendered, chosen by comparing content — extracted text plus a rasterised page for PDFs, package parts for decks — so nothing churns on a fresh /ID alone. The other 36 were left alone: this change does not touch them, and bringing every committed asset level with the code is the asset gate's job, not this PR's.

PptxCanonicalContentTest defines what a deck can be compared by, which is the part the asset gate needs settled before it can run. Zip timestamps rule out bytes, and three further differences turned out to be decided by the machine rather than the document — the platform's line separator after the XML declaration, the box a freeform's path is normalised against, and the antialiasing of rasterised text. Each was measured against a run on the runner: line endings alone put a one-byte difference into sixteen parts of an untouched deck, and one freeform came out with every point shifted by a constant 272 EMU across and 489 down while its declared box shrank by exactly as much, landing the path on the same place on the slide to the unit. So a deck is compared as the document decides it: paths read where they land, images pixel for pixel after decoding into one colour model, everything else verbatim. The watermark is the single part an allowlist exempts, by name rather than by folder, with both renders of it committed as fixtures so the entry keeps having to earn itself. Dropping the box is the load-bearing half, so it is asserted in both directions, and against real markup as well as written-here markup.

It does not hold a committed asset against a fresh render. A committed asset is rendered at the released version and a working tree renders the next one, so the two are different documents before anything else is weighed; and a committed asset is not otherwise guaranteed to be level with the code that produces it. Comparing them needs the display version as an input and a way to refresh whatever has drifted — one job, belonging to the asset gate rather than to a unit test.

Tests

examples 64 tests and qa 688, green locally; the CI run is green, including Examples Generation Smoke Test, CI Gate, Architecture and Documentation Guards and CodeQL. The whole catalogue renders: 102 documents, no failures.

Four layout snapshots are re-baselined. The diff is widths and vertical positions only — no startPage/endPage moved, so nothing repaginated:

-    "placementWidth" : 245.105,   +   260.61
-    "placementWidth" : 394.905,   +   425.88
Check Result
the catalogue is exactly the published decks plus the deliberately unpublished ones passes
the two deck lists do not overlap passes
the committed decks are exactly the curated subset passes
two images of one size are not one image passes
the named unstable raster absorbs the measured difference passes
every allowlisted raster names a part that exists passes
a freeform's box is absorbed, a freeform that moves is not passes
the rewrite reaches the markup POI actually writes passes
the same deck rendered twice is one document passes
a part written with either line ending is one part passes

Every one of the 115 font styles in this module named a face constant —
HELVETICA_BOLD, TIMES_BOLD, COURIER_BOLD — and set no decoration. The
library rewrites such a name to its base family before the lookup, so the
constant contributed nothing; the same defect the donut KPI had, one
module over and a hundred times.

It was invisible until the PPTX backend started taking the face from the
decoration in the 2.1 line. From that point a fresh render of the decks
came out regular while the committed previews, rendered earlier, still
showed bold — and nothing regenerated them, so nobody saw it. Three decks
lost weight that way; a fourth gained twenty-one runs it had always asked
for and never received.

The styles now name a family and a decoration. Where a chain already set
one, only the constant is renamed: it was redundant, not load-bearing.
Three helpers chose the constant through a ternary and needed the flag
moved to the decoration instead.

Two consequences worth naming. Bold text is wider, so four layout
snapshots move — widths and vertical positions only, no page breaks. And
the weekly schedule stopped fitting: its day notes span four fixed
sub-columns, a spanned cell cannot borrow width from fixed neighbours,
and the board is printed. Its columns are recomputed from the printable
width rather than adjusted by eye — 87.38 + 7 x 105.5 against 825.89
available, where the old 90 + 7 x 105 left 0.89pt unused.

Thirty-one committed previews are re-rendered, chosen by comparing
content rather than bytes so nothing churns on a fresh /ID alone.
PptxCanonicalContentTest keeps that comparison: a deck is compared by its
package parts, since zip timestamps make byte equality a coin toss.
CI failed the comparator on six decks, including social-card, which this
branch never touched and which matched locally. The cause was neither a
missing asset nor a stale render: POI ends the XML declaration with the
platform's line separator, so every XML part of a deck written on Windows
differs from the same deck written on Linux by one byte. Sixteen parts of
an untouched deck differed for that reason alone. The digest normalises
line endings in XML parts and leaves binary parts alone, where a stray CR
would be content.

The comparator also walked from the generated tree to the committed one
and skipped whatever had no pair, so it could not see a deck added to the
catalogue and never committed, or a committed deck whose example is gone
— both leave every surviving pair matching. The curated subset is written
down and asserted as a set, and two generated decks sharing a basename
now fail rather than one standing in for the other: the committed gallery
is flat.

The schedule's column widths are derived from the page and margins rather
than written out, and the comment that described the first attempt's
numbers is gone with them. The values are unchanged, so no asset moves.
Normalising line endings closed four of the six decks the runner rejected;
the last two came from the same root and needed the same treatment. In one,
a freeform's path had every point shifted by a constant 272 EMU across and
489 down while its declared box shrank by exactly as much — the path landed
on the same place on the slide to the unit, and only the normalisation moved.
Points are now read where they land, and the box that says how they were
normalised is dropped. In the other, an embedded image of a text watermark
differed along the glyph edges alone: same glyphs, same positions, different
antialiasing. Block-averaging leaves deltas of 138 of 255 behind, so no
tolerance absorbs that honestly; images are compared by their dimensions and
pixel layout, and the Javadoc says which changes that misses.

Both are checked against a run of the deck on the runner rather than argued
from: all six committed decks match under the comparison.

Dropping the box is the load-bearing half, so it is asserted in both
directions — the same line normalised two ways is one shape, a line that
moved is not — and against real markup as well as written-here markup, so
that a reordered attribute cannot leave the rewrite matching nothing.
…more

The six assets that carry a version or a date are re-rendered from the
merged sources at the released version, so they hold the deterministic
render and the restored weights at once instead of one or the other. The
showcase deck had come back with the day it was rendered on printed in its
header; it reads Q2 2026 again, and still shows the 21 runs of emphasis its
styles ask for.

The deck comparison no longer holds a committed asset against a fresh
render. It cannot: a committed asset is rendered at the released version
and a working tree renders the next one, so the two are different documents
before anything else is considered, and a committed asset is not otherwise
guaranteed to be level with the code that produces it. Comparing the two
needs the display version as an input and a way to refresh whatever has
drifted — one job, and not a unit test's. What the class pins is what a
deck can be compared by, proven in both directions, which is the part the
asset gate needs to exist before it can run.

Four decks lost their weights, not three; the CHANGELOG said three.
@DemchaAV
DemchaAV force-pushed the fix/example-bold-faces branch from 58e0054 to d913930 Compare August 2, 2026 14:54
…t renders

Reading a raster part by its dimensions absorbed one measured artefact by
giving up on images entirely: a logo swapped, a screenshot replaced or a
watermark retyped at the same size all compared equal. Images are now
compared pixel for pixel, decoded into one colour model so that how a part
was stored does not count as content, and the single part measured as
machine-dependent is named in an allowlist rather than inferred from its
folder. The two renders of that watermark — Windows and the runner — are
committed as fixtures, so the entry has to keep earning itself: they must
still differ as images, and must still be the same size for the allowlist
to be reading them honestly. A misspelled entry exempts nothing, so the
allowlist is checked against the parts that exist.

The deck lists cover the catalogue instead of sitting inside it. Asserting
that the curated decks are among the generated ones let a new example ship
a deck nobody had decided to publish or leave out — the same silence the
pairing this replaced used to keep. Both categories are written down, they
may not overlap, and together they must equal what the catalogue renders.

The class no longer opens by claiming it proves the committed decks match a
fresh render; that is the one thing it says it cannot do.
…unt what changed

The two image checks called the pixel reader directly, so a wrong media
prefix or a mistyped allowlist key was covered by neither: the part would
have fallen through to raw bytes and failed on a runner instead. Both now
go through the same entry point the digest uses, and the negative one names
a deck the allowlist does not, so the key has to be read as deck and part
rather than part alone. The allowlisted part is checked in the rendered deck
as well as the committed one — the rendered one is what a gate compares.

Line endings were the one canonicalisation nothing pinned. It is also the
one that cannot fail here, since every test runs on a single machine where
both sides carry the same ending, so it is asserted directly.

The CHANGELOG said 115 styles. That is how many named a face constant in a
plain fontName call; another 23 reached one through a ternary or a helper
argument, and all 138 changed. The class summary said every image is read
pixel for pixel, one paragraph above the exception it names.
@DemchaAV
DemchaAV merged commit ec6e5d3 into develop Aug 2, 2026
10 checks passed
@DemchaAV
DemchaAV deleted the fix/example-bold-faces branch August 2, 2026 16:11
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