v2.4.1 — 2026-09-21
Performance
- A barcode is drawn as vector shapes, not as an image, in PDF and PPTX.
PdfBarcodeFragmentRenderHandlerturned ZXing's bit matrix into a bitmap, onesetRGB
call per pixel, wrote it to PNG withImageIO— which, with its default cache, buffers a
write to a stream through a temporary file injava.io.tmpdir— and passed the PNG to
PDImageXObject.createFromByteArray, which decoded it and compressed the pixels again. The
handler now fills the background as one rectangle and the dark cells, merged row by row
into rectangles, as one path under a single transform to matrix cells: no bitmap, no
image stream, no file. The matrix is the one ZXing produced for the bitmap, stretched over
the fragment box the same way, so every symbology keeps its placement — rasterised on
screen, the modules land on the same pixels and only anti-aliased edges differ — while the
edges stay sharp at any zoom. Measured locally on the feature-rich benchmark document (a
QR code and a Code 128; interleaved A/B against the previous handler, three rounds):
median render 29 → 1.7 ms, allocation per document 7.0 → 0.7 MB, PDF 6.3 → 4.1 KB, and
outliers of up to 390 ms — the temporary-file writes — are gone. The committed barcode
showcase preview drops from 11.4 KB to 4.3 KB.
PptxBarcodeFragmentRenderHandlerplaced a bitmap of the same matrix as a picture, written
to PNG through the same temporary-file path. It now draws the same matrix as two freeform
shapes — the background over the box and the dark cells as one path — so a slide carries a
sharp barcode made of native shapes and no picture.
Colours composite as they did in the bitmap, where each cell is foreground or background
and never both: when the foreground is not opaque, the dark cells are cut out of the
background, so a translucent or transparent foreground lands on whatever lies under the
barcode rather than on the background. A translucent barcode rendered both ways matches at
every cell centre; only anti-aliased cell edges differ. No public API change.
Tests
PdfBarcodeRenderTestrasterises the page and reads the barcode back: each of the eight
formats decodes to its content, every cell of the matrix lands in place and in the chosen
colour for all eight, each translucent cell composites with the page rather than with the
other colour, a transparent background shows the page through, a transparent foreground
cuts the cells out of the background, and translucent colours stay inside the barcode.
BarcodeRunsTestholds the row merge to covering every dark cell exactly once. The
canonical features test now checks the QR code through its drawn rectangles rather than
through the presence of an image.PptxVectorFragmentsTestrenders the slide and scans
the QR code off it, with an opaque and with a transparent foreground, checks a translucent
foreground composites with the slide, and checks the background shape lands on the
fragment box.
Build
-
A CI artifact expires on a schedule that matches what it is for. No
actions/upload-artifactstep declaredretention-days, so all seven inherited the
repository default of 90 days — the ceiling. Artifact storage is billed and capped per
account rather than per repository, and this repository had grown to 10.7 GB across some
2968 live artifacts, which exhausted the shared quota; the failure surfaced where it could
not be diagnosed from, in another repository whose unrelated uploads began failing with
Artifact storage quota has been hit. Two families were nearly the whole bill —
examples-pdfsat 7.57 GB over 1009 artifacts andcoverage-core-aggregateat 2.66 GB
over 837 — and nothing downstream reads any of them: there is noactions/download-artifact
anywhere in.github/workflows/, so each exists for a person to open, and the right window
is however long a person plausibly wants it. Those two now keep 7 days, the span of a
review.japicmp-reportkeeps 30, because it answers "when did this signature move, and
against which baseline" during release prep rather than during the pull request.
benchmark-smokeandbenchmark-gate-reportskeep 14 days together, deliberately the same
number, since the gate verdict in one explains the numbers in the other and a shorter window
on either would leave an investigation holding half a pair. The two weekly trend series,
benchmark-fullandjmh-results, keep the full 90: at one run a week a short window holds
a point or two and shows no trend at all, and they cost tens of KB each. What proves a render
weeks later is the committed layout-snapshot and visual baselines, not a retained artifact. -
Build and test dependencies move with the
maven-minor-patchgroup.exec-maven-plugin
3.6.3 → 3.6.4 inbenchmarks/andexamples/,maven-install-pluginand
maven-deploy-plugin3.1.4 → 3.2.0, and the test-scopebyte-buddypin 1.18.13 → 1.18.14
incore/. Every one is build- or test-scope, so the dependency set a consumer inherits
from a published artifact is what it was in v2.4.0.
Documentation
-
The showcase site's menu and section links reach the gallery, and its pages link
only to files the site publishes. A category section is rendered only while its
filter is shown, so after picking Features the Templates menu link changed the
address and moved nothing. A gallery anchor now selects its filter, whether it is
followed from the menu, reached with Back or Forward, or opened directly, and the
filter pills keep the address in step. The no-JavaScript index linked to three PDFs
the site does not publish, and two featured ids named no card, so the featured strip
showed six of its eight tiles without a sign.ShowcaseSiteGuardTestfails the build
on a featured id that is not a card, a card file missing fromshowcase/, a page link
to a site file that does not exist, and an anchor or filter pill that names nothing
the page shows. The structured data said JVM 21+ where every module targets Java 17,
the page counted 16 CV presets where 26 ship, and the template-authoring links
pointed atdevelopinstead of the released docs onmain. -
The gallery shows each document's whole first page. A card cropped its preview to a
248-pixel band, so a page was judged by its header and a wide slide lost its sides.
The preview now shrinks into a fixed-height box at its own aspect ratio, featured
tiles use the same fit instead of an A4-shaped frame, and the image tags no longer
declare an A4 size that was wrong for 26 of the 117 previews. -
The gallery opens a viewer that pages through one family at a time. A card, a
featured tile or a family tile opens its family (CV, cover letters, invoices and so on)
in a viewer that shows the whole first page, moves with Previous, Next and the arrow
keys, shows where it is in the family, and links the PDF and source of the document
shown. A switch moves to the other families of the category, and each reopens on the
document it was left on. The address#/<category>/<family>/<id>reopens the same
document on a reload or from a shared link and follows Back and Forward. The viewer
replaces the zoom lightbox. A drag across the page moves between documents on a touch
screen, a strip under the page holds every document of the family and marks the one on
screen, and the pages either side are fetched before they are asked for. A reader who
has asked to save data gets neither: no strip of page-sized previews, and nothing fetched
ahead.ShowcaseSiteGuardTestnow also requires unique, address-safe card and family ids,
and holds any viewer address written into a page to a family and document that exist;
scripts/site/gallery-viewer.test.mjstests the addresses, the navigation and the dialog
in CI. -
The catalogue says what each document is, and the gallery stops jumping as it loads.
web/examples.jsoncarries aschemaVersion, and every card now carries the preset it
renders and the model that preset composes, the artifacts a reader needs to run it, the
path to its source, its page count, and the pixel size of its preview — so the image
reserves its slot at the right shape instead of appearing out of nothing, and no single
size stands in for previews that are not all A4. The viewer's strip and the family tiles
read thumbnails generated at 320px rather than whole pages: opening the CV family fetches
1.4 MiB of strip images where it fetched 5.2 MiB.ShowcasePresetRegistrationTestholds
each card's preset and model to the example that builds them — it found two feature cards
asking a reader for the engine alone while rendering a template preset — and
ShowcaseSiteGuardTestfails the build on a manifest without aschemaVersion, a card
whose measurements are not its preview's, a page count below one, a thumbnail that is not
published, or a preset count in the page copy the catalogue does not hold. -
The version the showcase shows is written down once, and a release moves every copy of
it. The published site stated the release in five places that inherit from no pom, and
the cut rewrote the first match of each — so a page carrying a second install snippet
kept it a release behind while every check passed, and a spot that stopped matching was
skipped in silence, leaving the cut to report success on a page still naming the previous
release. An inlinerelease-contextblock now holdsstableVersion,releaseTagand
javaMinimum. The JSON-LD, the Maven Central download link, the hero badge and the
install snippets still repeat the version, because a crawler and a reader with no
JavaScript both have to see the right release — but each is now a copy of that block, and
VersionConsistencyGuardTestholds every occurrence of all seven spots equal to it,
including the download link it never read before. A pattern that matches nothing stops the
cut and names the spot, rather than leaving it behind. -
Every document in the gallery shows what reproducing it takes — and 53 of them were
asking readers for a dependency set that cannot render them. Under the document, the
viewer now shows the Maven and Gradle coordinates at the release the page names, the
preset class and the record it composes, its family's worked snippet, the command that
runs the example, and the runnable source and family guide at the release tag. Building
that panel exposed a defect in the catalogue it reads: a document drawn in a bundled face
— PT Serif and the rest left the engine in v1.8.0 — cannot be reproduced from
graph-compose+graph-compose-templates, which compiles and then throws
Bundled font resource not foundat the first glyph, and the artifact carrying those
faces is versioned independently of the release, so naming it at the release version is a
404 on Maven Central. Those cards now send a reader tograph-compose-bundle, the one
published coordinate that carries the faces at the release's own version. Whether a
document needs them is measured from the PDF rather than declared, because it differs card
by card inside a single family: 25 of 27 CVs embed a face, 4 of 7 invoices do. The
snippets are the blocksDocumentationSnippetCompileTestalready compiles, copied into
the manifest because the site is served fromweb/alone and cannot reach a page under
docs/.Fonts were not the only thing the catalogue left out. Ten cards reach a second backend and
asked a reader for the engine alone, which compiles and then throws
MissingBackendExceptionat render. Two name the DOCX backend in an import. Nine need the
PPTX one, which is discovered by format and so appears in no source at all: eight publish a
deck beside their PDF — four of those rendered by a sibling class, so not even their own
example mentions it — and one renders a deck it does not publish. The requirement now
follows from what a card publishes as well as from what its example names, and where a
document also needs the bundled faces the aggregate stands in for the engine and templates
without swallowing the backend beside it. Two more cards (table-advanced,transforms) have nomainof their own —
GenerateAllExamplesrenders them — and were being offered anexec:javacommand that
answers "doesn't contain a main method"; they now say what does render them.Each claim is checked against the example's own source or its rendered document:
ShowcaseBundledFontClaimTestholds every font claim in both directions,
ShowcaseCardInstructionsTestholds the backends and the run command the same way,
ShowcaseSnippetScopeTestholds every published snippet inside the set of pages the
compile gate actually scans, andShowcaseSiteGuardTestfails on a snippet that is no
longer the block it was compiled from, on a preset card missing anything the panel shows,
and on a family guide the panel links that is no longer a page. A consumer project renders
a CV from the published aggregate with nothing else installed, which is how the first of
these defects surfaced. -
The showcase site's pages are generated from its catalogue.
web/index.htmland
web/sitemap.xmlare rendered fromweb-src/byscripts/site/build.mjsand committed;
GitHub Pages still servesweb/exactly as committed and runs no build of its own. The index
a visitor without JavaScript gets named 38 of the 117 documents the site publishes, and the
preset counts and structured data were hand-copied beside a catalogue that already knew them.
That index now names every document, under its category and group, and the counts are computed
from the catalogue by the same rule the guard checks them against. The release the pages
advertise is written down once, inweb-src/data/release.json, and the build injects it into
the seven spots that inherit from no pom; the release cut moves those two values and rebuilds
the pages after the catalogue sync instead of editing them, so a later build cannot undo the
release's own version.scripts/site/build.test.mjsfails when what is committed underweb/
is not whatweb-src/builds, and the build refuses rather than publishes when a template
token, a featured id or a card's title has gone. -
The showcase home page leads with a result, and the catalogue uses words a newcomer knows.
The menu is Templates, Examples, Documentation, Releases and GitHub, with a Get started button.
The hero shows one whole document with an Invoice / CV / Proposal / Report switch — each a real
catalogue document that opens as a PDF or as its own page — where it used to fan out
three cropped previews; a phone now gets one compact document instead of none, and the heading
stays within two lines at every width from 320 to 1440 pixels. The feature demonstrations are listed as Examples and the
large complete documents as Showcase, instead of Features and Flagships. Category ids and
every published URL are unchanged, so existing links and shared viewer addresses keep working.
The template-authoring guide moved from the top of the page into a Documentation block. -
The showcase says what to install for what you are building, and what each output format
keeps. The install section offers four scenarios where it used to offer the engine alone: a PDF
from your own layout (graph-compose), ready-made templates (graph-compose-bundle, which pins
the independently versioned fonts and emoji for you), an editable PowerPoint deck
(graph-compose-render-pptx, Beta) and a Word document (graph-compose-render-docx), each at the
release the page names. A new block sets PDF, PowerPoint and Word side by side with their limits,
taken from the backend capability matrix it links to. Muted text in the light theme sat at
4.45:1, under the 4.5:1 minimum for body text, and now reads at 6:1; the featured tiles no longer
skip a heading level; and a menu link to a section lands with its heading below the sticky header,
which on a phone used to cover it. -
Every document in the catalogue has a page of its own. Each of the 117 documents gets a
generated page at<category>/<family>/<id>/— the same three segments as its viewer address —
that works without JavaScript and that a crawler can read: every page of the document, each
linking into the PDF; the PDF, and the deck where one is published; what reproducing it takes;
and the other documents of its family. Each page has a canonical address, a description, link
preview tags and structured data, and the sitemap lists them all. The no-JavaScript index, the
hero and a new Details link in the viewer lead there. What a page tells a reader to add, run and
read is not written a second time: the viewer's panel became a pure model ingallery-viewer.js
that the build loads to render the page, andscripts/site/build.test.mjsholds every page's
section to that model and to nothing besides it. Building the pages exposed a label that was
wrong in the viewer too: a family's worked snippet composes one preset — the CV block builds
BoxedSections— yet it was captioned "Compose it" on every card of the family, promising Blue
Banner's reader code that builds a different CV. Only the card of the preset the snippet composes
says so now; the others say the snippet comes from the docs, and where it is published on a page
other than the family guide — the CV block is onusing-templates.md, the family starts at the
quickstart — they link that page. The build owns only the pages it wrote where they sit: a page
no card builds any more is deleted,--checkfails on a page that is missing, stale or orphaned,
and the release cut stages the pages a rebuild added or deleted along with the ones it rewrote.
ShowcaseSiteGuardTestfinds the generated pages and checks their links and anchors from each
page's own directory. The viewer also stops captioning every document "First page shown", which
was untrue of the 33 documents it pages through. -
The showcase has a Documentation page, and the site reads its guides at the release it names.
The menu's Documentation used to scroll to a block on the home page that linked three template
guides. It now opens a page that gathers the guides already in the repository — a first document,
templates, recipes, output formats, testing and production, versions and upgrades, and the
internals for changing GraphCompose itself — each with a line on what it covers, checked against
the guide itself, and each linked at the release tag. No guide is copied, so there is no second
version of one to fall behind, and a guide path that is not a file in the repository, spelled as
GitHub serves it, stops the site build. The rest of the site's repository links follow: the home
page's authoring guides, the capability matrix, the Changelog, the migration guide and the examples
tree usedmain, beside panels that already linked the release, and now name the release too; only
the licence stays onmain. Text set in the accent colour — section labels, the hero's links, tags,
the footer's links, the ghost button — read at 3.7 to 4.2:1 in both themes, under the 4.5:1 minimum
for text its size; it now reads at 5.3:1 at worst, taken over the page's background glow. And the
gallery's Show all button, which turned a pale blue on hover and focus, no longer all but vanishes
(1.05:1) when a keyboard focuses it in the light theme.