Changelog
June 18, 2026 · View on GitHub
All notable changes to GraphCompose are documented here. Versions follow semantic versioning; release dates are ISO 8601.
v1.8.0 — 2026-06-18
Codenamed "illustrative". Native vector charts (bar / line / pie, inline
sparklines, monotone & smooth interpolation), SVG path & icon import with native
gradients, free-form ShapeOutline.Path clipping, the keepTogether()
pagination control, and a leaner publication — the bundled Google fonts split
into the independently-versioned graph-compose-fonts artifact. Core document
APIs stay source- and binary-compatible with v1.7; the two consumption changes
are the fonts split and the removal of ConfigLoader (both detailed below).
Public API
- Line-chart interpolation modes (
@since 1.8.0). NewLineInterpolationenum selects how a line series connects its points:LINEAR(straight, exact),SMOOTH(the existing pretty Catmull-Rom curve, which may overshoot local extremes on sharp swings), and the newMONOTONE(Fritsch-Carlson) — a curve that looks just as smooth but is constrained to never overshoot, staying within the value range of the points it spans, for an accurate yet smooth reading of the data. Set it withChartSpec.line().interpolation(LineInterpolation.MONOTONE)— the single, explicit knob for line shape. All three render through the same native PDF curve operators with zero tessellation, so geometry stays deterministic and the hot path is unchanged. ChartData.Seriesrejects non-finite values. ANaN/ ±∞ entry now fails at construction — naming the series and the offending index — instead of poisoning axis derivation and surfacing as a misleading "height must be finite" failure deep in the layout pass.nullentries are still allowed as gaps.- Block-level horizontal alignment (
@since 1.8.0). Fixed-size flow children (paths, images, SVG icons, barcodes, shape containers) left-align by default — there was no built-in way to centre or right-align one without wrapping it in a full-width container and hand-computing the content width. NewAlignNode+HorizontalAlign(LEFT / CENTER / RIGHT) seat any node across the available width:flow.addAligned(HorizontalAlign.CENTER, node)and the icon sugarflow.addSvgIcon(icon, width, HorizontalAlign.CENTER). The wrapper fills the width and reuses the stack placement engine (one anchor), so there is no new render handler and no hot-path change. - Native vector charts (
@since 1.8.0). Newcom.demcha.compose.document.chartpackage with a layered, serialization-friendly API:ChartData(categories + series, type/colour-agnostic), sealedChartSpec(bar()/line()with axis, legend, value-label, and sizing knobs),ChartStyle(nullable-field cascade merged overChartThemetokens, per-series paint overrides), andDocumentPaint(solid, linear, and radial — see the gradient entry below). Charts compile at layout time into existing primitives (shapes, lines, paragraphs) viaChartDefinition— no new render handlers, deterministic geometry, covered by the standard snapshot machinery; any fixed-layout backend renders charts with no chart-specific code, while the semantic DOCX export (which has no layout pass) falls back to the chart's categories-by-series data table with a one-time capability warning. DSL:section.chart(spec)/chart(spec, style). DeclarativeNumberFormatSpeckeeps specs JSON-serializable. The one unsupported combination (ValueLabelMode.INSIDE) fails fast withUnsupportedOperationExceptioninstead of rendering silently wrong. - Horizontal bars, smooth lines, area fills, stacked totals, legend
placement.
ChartSpec.bar().horizontal(true)transposes the chart (categories on Y in reading order, value axis on X, labels at bar ends); stacked bars label the category total.ChartSpec.line().smooth(true)draws deterministic Catmull-Rom curves as native cubic Béziers through the vector path primitive — onePathNodeper run, perfectly smooth at any zoom level, zero tessellation;.area(true)fills each series down to the baseline with a translucent series colour (ChartStyle.areaOpacity, default 0.35) — alpha-blended fills layer legibly, and in smooth mode the fill closes the exact stroke curve so fill and stroke edges coincide.LegendPosition.TOPandRIGHTnow lay out as a top strip / right column for every chart kind, including pie. The chart resolver is split per kind (BarChartLayout/LineChartLayout/PieChartLayoutover a sharedChartLayoutSupport). - Axis / grid / label visibility toggles.
AxisSpec.showTickLabels(false)hides the numeric axis and collapses its gutter;showGridLines(false)andChartStyle.GridStylecontrol horizontal/vertical grid lines;ChartSpec.bar()/line().showCategoryLabels(false)hides the category axis — down to a minimal "bars + value numbers only" chart. - Pie / donut charts (
@since 1.8.0).ChartSpec.pie()— one slice per category from a single series (multi-series data is rejected loudly). Configurable:donutRatio(hole size),startAngleDegrees,clockwise,SliceLabelMode(VALUE / PERCENT / CATEGORY / CATEGORY_PERCENT) with independent value/percent formats, donut-centre KPI text, and a category-listing legend. Style cascade addssliceStroke(separator),sliceGapDegrees(pad angle), anddonutCenterTextStyle. Sectors compile into the new general-purposePolygonNode(arc-tessellated ring polygons at a fixed 3° step — deterministic vertices, no new render handlers), which also lays the groundwork for SVG icon-path import. - Vector path primitive (
@since 1.8.0). NewPathNode— the open-path, curve-capable sibling ofPolygonNode: normalizedDocumentPathSegments (moveTo/lineTo/ cubiccubicTo/close; Bézier control points are free to overshoot the unit box) are scaled to the node's box and rendered with native PDF curve operators, so curves stay perfectly smooth at any zoom level instead of being tessellated into straight pieces. Atomic pagination, deterministic layout snapshots, fill (non-zero winding rule) and/or stroke. This is the leaf vehicle for smooth chart lines, decorative design shapes, and future SVG path import. DSL:addPath(p -> p.moveTo(...).curveTo(...).closePath().fillColor(...))on every flow builder authors design shapes directly, anddashed(on, off, ...)makes the stroke dashed with the sameDocumentDashPatterncontract as lines — the pattern follows the curve. - Path-outline clipper (
@since 1.8.0).ShapeOutline.Pathjoins the sealed outline family as the curve-capable sibling ofPolygon, so a shape container can clip its children to — and fill / stroke along — an arbitrary native-curve silhouette.ShapeContainerBuilder.path(w, h, segments)takes rawDocumentPathSegments;path(w, h, svgPath)(beta) clips to an imported SVG path, turning any icon or logo into a content mask underClipPolicy.CLIP_PATH. The outline rides the existing vector-path fragment pipeline (one source of truth for native curves) and the clip handler emits the sameaddPathSegmentsgeometry, so fill, clip, andaddPath(...)all agree. The newPathpermit is additive and keeps the artifact binary-compatible (thejapicmpgate stays green); only consumer code that exhaustivelyswitches overShapeOutlinewould need a new branch, and the canonical authoring surface exposes no such switch. - SVG path import (
@since 1.8.0, beta — annotated@Betawhile the surface hardens against real-world exporter output).SvgPath.parse(d)/parse(d, viewBox...)in the newdocument.svgpackage lowers the full SVG 1.1 path grammar — absolute/relativeM L H V C S Q T A Z, implicit repetition, quadratics (exact cubic elevation), smooth shorthands, and elliptical arcs (deterministic W3C endpoint-to-center conversion, ≤90° cubic slices) — into normalized, y-flippedDocumentPathSegments.PathBuilder.svg(svgPath)drops the result straight intoaddPath(...): any icon'sdstring renders as native PDF curves, no tessellation. Syntax errors report the character position; fills keep SVG's default non-zero winding rule. On top of it,SvgIcon.read(file)/parse(xml)reads the practical subset of a whole SVG file — every<path>plusrect/circle/ellipse/line/polyline/polygonlowered to path data,<g>nesting withtranslate/scale/rotate/matrixtransforms (affine maps are exact on Bézier control points), andfill/stroke/stroke-widthstyling with SVG inheritance and defaults — into ordered layers, andaddSvgIcon(icon, width)stacks them back-to-front on the page.SvgIcon#node(width)packages the same layers as one ready-to-place node whose box is exactly the icon box, so it anchors true insideShapeContainer/LayerStacknine-point grids (and rows now acceptShapeContainerNodechildren directly — it is the same atomic overlay composite as the already-allowedLayerStackNode). Gradients render natively:linearGradient/radialGradientreferenced viaurl(#id)— on fills and strokes — map to PDF axial / radial shadings with exact endpoints (userSpaceOnUseandobjectBoundingBoxunits,gradientTransform, percentage offsets, multi-stop stitching, onehrefhop for split definitions); gradient strokes ride a shading-pattern stroking colour. Underneath,DocumentPaintgains endpoint-exactLinearAxis/RadialCircleforms andPathNode/PathBuildergrowfill(paint)/strokePaint(paint)with solid paints normalising to the flat-colour path (byte-identical output for non-gradient documents). Stroke fidelity: the reader honoursstroke-linecap/stroke-linejoin(rendered as native PDFJ/joperators via newDocumentLineCap/DocumentLineJoin, also onPathBuilder.lineCap()/lineJoin()) andstroke-dasharray, the full CSS named-colour table (147 keywords),rgb()/rgba()with numbers or percentages,#rgb/#rgba/#rrggbb/#rrggbbaahex, and absolute length units (px/pt/pc/in/mm/cm) on stroke widths; relative units and unknown colours fail with the supported alternatives listed.SvgIcon#node(width)now scales stroke widths and dash lengths with the geometry (they live in user units), so an icon drawn smaller than its source no longer renders an over-thick outline. Content the reader can't render (text,image,use, masks, clips, filters) is dropped with a single deduplicated warn-log per kind instead of silently, and the DOCX backend warns once per geometry-only node kind (path,polygon,shape, …) it drops. The XML reader refuses DOCTYPEs (no XXE); CSS stylesheets, text, filters, focal radials, non-padspreadMethodand translucent gradient stops stay deliberately out of scope — the reader fails loudly rather than rendering them wrong. Every reader error names the offending element and why: an unsupported colour / transform / gradient / unit is reported asin <circle fill="…" …>: <reason — and the supported set>, pinpointing the deepest failing element (not its wrapping<g>); a blank result explains itself (no drawable geometry — skipped text; this reader renders vector shapes only) instead of a bare "no geometry". - Inline sparklines (
@since 1.8.0).RichText.sparkline(w, h, color, values...)draws a filled mini-area silhouette on the text baseline, andsparklineLine(w, h, thickness, color, values...)a constant-thickness line band (full thickness preserved at the peaks). Both runs are smoothed with the same Catmull-Rom curve the chart engine uses (densified to 12 sub-segments per span — facets stay under half a point at sparkline sizes), and both compile into the existing inline-shape polygon run — a KPI trend next to a number, a skill trajectory inside a CV line. - Configurable line-chart point markers.
PointMarkerdraws an ellipse at every data point — independent width/height axes, explicit fill (or the series paint), and an optional outline ring (PointMarker.circle(5) .withStroke(...)) that keeps joints legible where lines meet; markers always render above all line strokes. Per-point value labels sit at a configurableChartStyle.valueLabelOffset(...)from the marker (or bar top) in the cascadingvalueLabelTextStyle, draw above strokes and markers behind a configurable halo chip (ChartStyle.valueLabelHalo(...), themed white) so digits stay legible where lines cross them, and deterministically flip below their point when two series' labels would collide at the same category. - Gradient fills (
@since 1.8.0).DocumentPaintgraduates tocom.demcha.compose.document.styleas the shared paint vocabulary, and gradients now actually render:ShapeNodegains an optionalfillPaint(ShapeBuilder.fill(paint)) that wins overfillColor. The PDF backend paintsDocumentPaint.linearas a native axial shading (0° = left→right, 90° = bottom→top; two stops exponential, more stops stitched) andDocumentPaint.radialas a radial shading reaching the farthest corner, clipped to the shape path — rounded corners included. Chart bars now carry their full series paint, so a gradient palette renders as gradients instead of degrading to the first stop. Solid paints normalise to the plain fill-colour path, keeping existing documents byte-identical; backends without shading support fall back toprimaryColor()by contract. The flagshipBusinessReportExamplehero is now fully vector — gradient-sky shape plus polygon mountain ranges replace the last Graphics2D raster. - Translucent shape colours (
@since 1.8.0).DocumentColor.rgba(r, g, b, a)andwithOpacity(0..1): the PDF backend honours the alpha channel on shape fills and strokes (rectangles/panels/bars, chart value-label halos, ellipse point markers, polygons, inline shapes) via a graphics-state alpha constant — e.g. a semi-transparent chart halo lets crossing lines show through faintly. Fully opaque colours emit no graphics-state entry, so existing documents stay byte-identical. Text/lines and the DOCX backend still render opaquely. keepTogether()pagination control (@since 1.8.0). Opt-in flag onSectionBuilder,ModuleBuilder, andTimelineBuilder(pluskeepEntriesTogether()for per-entry timeline integrity): a block that does not fit in the remaining page space relocates whole to the next page instead of orphaning its heading from the content below. Blocks taller than a page still flow. Default off — existing layouts are byte-identical.- Removed:
ConfigLoader(breaking). Thecom.demcha.compose.ConfigLoaderYAML/JSON config-file helper was an application-bootstrap utility with no connection to document rendering — nothing in the library, tests, or examples referenced it. Gone with it: the<optional>jackson-dataformat-yamldependency (ConfigLoader was its only consumer) and the YAML entry in theNoClassDefFoundErrortroubleshooting section. Consumers who relied on the helper can copy the former ~100-line class into their own codebase or load configs directly with Jackson (new ObjectMapper(new YAMLFactory()).readValue(...)). - Debug node labels (
@since 1.8.0). The debug overlay grew a second layer: backend-neutralDocumentDebugOptions(guides + node labels + label-text mode, indocument.outputnext to the other neutral output options) configures fixed-layout rendering viaGraphCompose.document(...).debug(...),DocumentSession.debug(...), orPdfFixedLayoutBackend.builder().debug(...). WithnodeLabels()enabled, every rendered node prints its stable semantic path — the same pathlayoutSnapshot()reports — once per node and page, as a small corner badge straddling the top edge of the node's bounds (right-aligned 5pt Helvetica on a pale halo), so a misplaced block on the sheet reads straight back to the builder call that authored it. Labels paint as a single deterministic post-pass after all content, so badges always sit on top — a container's children or a higher layer can never overdraw the label that annotates them.LabelText.NAME(default) prints the compact own segment (PriceSummaryTitle[0]);FULL_PATHprints the whole ancestry. Label text degrades through the shared WinAnsi fallback (accents likeésurvive, anything outside WinAnsi becomes?with aglyph.missinglog). The overlay draws strictly on top of content and never touches measurement or pagination.guideLines(boolean)everywhere became sugar over the options object with uniform last-write-wins semantics on all three surfaces — node-label settings survive the toggle,debug(none())reliably disables everything — and disabled debug output stays byte-identical.
Build & distribution
- Bundled Google fonts moved to a separate, independently-versioned
artifact (
io.github.demchaav:graph-compose-fonts). Breaking for consumers who use the bundled families. The ~18 MB of curated Google fonts no longer ship inside thegraph-composejar, so an engine upgrade never re-downloads them and the engine artifact drops from ~40 MB to a few MB. The publicFontNameconstants and theDefaultFontscatalog are unchanged (source- and binary-compatible), and the classpath layoutfonts/google/...is preserved byte-for-byte. To keep the bundled fonts, addio.github.demchaav:graph-compose-fonts(its own version line, starting at1.0.0) to your build, or depend on the new "batteries-included"io.github.demchaav:graph-compose-bundle(engine + fonts at compatible versions). With neither on the classpath, standard-14 documents render unchanged and requesting a bundled family fails fast with a message that names the missing dependency. See docs/migration/v1.8.0-fonts.md. - Leaner Maven Central publication. The release build no longer attaches or
uploads the
-testsclassifier jar (it stays a local-only build aid for the benchmarks module), and with the fonts gone the-sources.jarno longer carries font binaries either. The published artifact set is now just the engine bytecode plus the small template assets. graph-compose-fontsreleases on its ownfonts-v*tag via a dedicated publish workflow, so the font set ships only when it actually changes, independent of the engine'sv*release cadence.
Bug fixes
- A stray non-drawing element no longer breaks a whole SVG icon. A
visible-painted SVG element that lowers to a moveto-only or moveto+close
path —
d="M12 12", a zero-length arc, the stray subpaths real exporters emit — drew no ink, yet a lone moveto threw atSvgIcon#node(...)(an emptyPathNode) and a moveto+close rendered blank.SvgIconReadernow drops a layer with no drawing segment, so one degenerate element no longer fails the icon; an icon of only such elements still fails loudly with "no drawable geometry". - Stacked bars anchor at zero even with an explicit positive axis minimum.
A stacked bar chart with
valueAxis().min(positive)lifted the baseline while segment heights stayed measured from zero, so the stack overshot its total and ran past the plot top. The stacked floor is now pinned to zero (parts summing to a whole), independent of the requested minimum. Grouped bars still honour an explicit minimum. - Grouped bars emanate from the zero baseline. A grouped (non-stacked) bar
measured its height from the axis nice-floor, so on an axis that crossed zero
a negative value rendered as a short upward column anchored at the floor —
visually indistinguishable from a small positive value — and positive bars
overshot below zero. Grouped bars now grow from the zero line (positive up,
negative hanging below it), matching the standard bar-chart convention and
the stacked-bar behaviour. When zero is off-scale — an explicit non-zero
valueAxis().min(...)orbaselineAtZero(false)over a range that excludes zero — the baseline clamps to the nearest visible bound, so a deliberately zoomed axis still anchors its bars at the plot floor. Charts with positive data on a zero-based axis are byte-identical. ChartStyle.paintForSeriesrejects a negative series index with a value-namingIllegalArgumentExceptioninstead of leaking a bareIndexOutOfBoundsExceptionfrom the palette modulo.- A translucent gradient stop is rejected instead of silently rendering
opaque. Gradients render through PDF axial / radial shadings, which carry
no alpha channel, so
PdfShadingSupportdropped a stop colour's alpha and a translucent stop rendered fully opaque with no diagnostic.DocumentPaint.Stopnow rejects a colour with alpha below 255 at construction, naming the offending alpha — flatten the transparency into the stop colour, or apply opacity to the whole shape. This matches the SVG reader, which already refusesstop-opacity, and reaches theDocumentPaint.linear(from, to)sugar too. Opaque gradients are unaffected. - SVG path reader no longer hangs on malformed
ddata. AZ/zclose command (which consumes no operands) followed by a stray non-command token — e.g."M0 0 Z5"— made the scanner loop forever, appending a close op every pass until the heap was exhausted. A single malformed or hostile path string could therefore DoS the@BetaSvgPath.parse/SvgIconreader. The scanner now fails fast with the usual position-carryingIllegalArgumentExceptionwhen an iteration consumes neither a command nor an operand. BEHIND_CONTENTwatermarks no longer wash out the page. The PDF watermark renderer set its low-opacity graphics state in a prepended content stream without a save/restore pair; PDFBox'sresetContextonly isolates appended streams, so the watermark alpha leaked into the entire page and every element rendered nearly invisible. The watermark now wraps its drawing inq/Q, keeping page content at full strength. This affected every document using the defaultDocumentWatermarklayer.- DOCX export no longer drops lists.
DocxSemanticBackendhad no branch forListNode, soaddList(...)content silently vanished from Word exports. Lists now map to marker-prefixed paragraphs in the list's text style, with nested items indented per depth and keeping their own markers. (Found by the recipe fact-check: the docx-export recipe's "what is skipped" list could not honestly be written without it.) - DOCX list items no longer double-space after the marker. The new list
branch concatenated
ListMarker.value()— which already carries its trailing space — with another literal space, so every exported item read"• text", and markerless lists gained a stray leading space. The export now usesListMarker.prefix(), matching the fixed-layout text pipeline. - DOCX list export fully matches the PDF list pipeline. The semantic Word
backend resolved nested-item marker fallbacks against the flat-list marker
and skipped flat-item normalization, so the two outputs of one session
disagreed: a nested item without an explicit marker exported as the list
bullet where the PDF renders the depth cascade (
•→◦→▪→·), an author-typed"- item"doubled up as"• - item", and blank items produced marker-only paragraphs. Both rules now live in one shared place —ListMarker.defaultForDepth(int)andListMarker.normalizeItemText(String, boolean)(@since 1.8.0) — and the fixed-layout pipeline and the DOCX export both call them. - SVG gradient number errors read in the reader's house style. A
non-numeric gradient coordinate, radius, or stop value (e.g.
x1="abc",r="x%",offset="?") leaked the raw JDKNumberFormatException("For input string: …") as the reason.SvgGradientsnow parses through one shared helper that throws"<field> must be a number, got '…'"with the cause chained — matching the rest of the beta SVG reader, where the per-element wrapper already names the referencing element.
Documentation
- Contract-drift Javadoc fixes on the new 1.8 surface.
LegendPositionno longer claimsRIGHT/TOPare "reserved and rejected by validation" — all four placements are laid out for every chart kind, as the resolver and its tests already prove.DocumentPaintdocuments why theLinear/Radial(angle/corner-reaching) andLinearAxis/RadialCircle(exact endpoint/radius) forms coexist.ShapeContainerBuilder's missing-outline error and class Javadoc now name the full set of outline setters (includingpath).PathBuilder.dashed(double...)documents theIllegalArgumentExceptionit throws eagerly, andSvgIcondocuments that a gradienthrefinherits stops only, not geometry attributes. - Browsable feature-catalog PDF. New flagship
FeatureCatalogExamplerenders every shipped capability as a self-documenting block: the heading lands in the PDF outline (the bookmarks panel works as a clickable index), a code panel shows the exact API call, and the live result renders right under it — rich text, sparklines, nested lists, timelines, tables, every chart kind, images (COVER vs CONTAIN fit), gradients, translucency, polygons, vector paths (solid and dashed native Béziers), SVG path import and a betaSvgIcontile row, shape basics (dividers, ellipses, soft cards), clipped containers, canvas, transforms, barcodes, the debug-overlay switch, and the document's own chrome — 23 blocks across 7 pages. Blocks usekeepTogether(), so a snippet is never orphaned from its result. - Landscape capability deck on real benchmark data. New flagship
EngineDeckExamplerenders GraphCompose about itself: a full-page banner (DSL code → engine grid → output backends → real rendered-document thumbnails), an authoring-pipeline page, and two pages that load the repository's comparative benchmark result file and draw the table and charts (GraphCompose vs iText 9 vs JasperReports) straight from it. Content lives in anEngineDeckDatadata layer; anEngineDeckLayoutSnapshotTestlocks the layout. - Recipe coverage is complete. Nine new cookbook pages close every gap the
recipe index tracked: rich text, lists, timelines, barcodes, images,
PDF chrome (metadata / watermark / running header-footer / protection /
links / bookmarks), translucency, semantic DOCX export, and layout-snapshot
regression testing. Every snippet is verified against the current API;
the folder index (
docs/recipes/README.md) no longer carries a "not yet covered" list. - Word-export example. New
WordExportExample(examples/features/docx) renders the sameDocumentSessionas a fixed-layout PDF and an editable Word file viaDocxSemanticBackend, one section per capability-table row: inline runs, nested lists with custom markers, tables, side-by-side rows, an embedded image, a page break, the chart→data-table fallback, and the geometry that stays PDF-only. Committed previews live underassets/readme/examples/(word-export-companion.pdf/.docx); the examples module adds the optionalpoi-ooxmldependency exactly like a consuming project would. BusinessReportExamplechart is now a native vector chart. The flagship report's five-quarter Revenue/Profit block previously rasterised a bar chart through Graphics2D into an embedded PNG; it now usesChartSpec.bar()with aChartStylepalette override (navy/gold) and an explicit 0–100 axis — ~90 lines of hand-drawn AWT geometry replaced by a declarative spec.- Chart showcase contrasts SMOOTH vs MONOTONE.
ChartShowcaseExamplegains a paired before/after on a volatile series — the pretty Catmull-Rom curve overshooting its peaks next to the monotone curve that stays within the data range — and the committedassets/readme/chart-showcase.pnghero preview now shows that comparison.
Internal
- CV / cover-letter template icons moved from PNG to recolorable SVG.
The bundled contact / social glyphs (phone, email, location, website,
LinkedIn, GitHub, …) and the sidebar-portrait avatar now ship as SVG
instead of raster PNG. A new internal
SvgGlyphhelper flattens an icon's filled layers into one outline that the presets fill with each template's own accent colour viarich.shape(...)— so one bundled glyph recolours per template with no per-template copies, and the icons stay crisp at any zoom. The sidebar-portrait avatar is a swappable SVG placeholder. This shrinks the bundledtemplates/cvassets from ~717 KB to ~133 KB (the 431 KBportrait.pngalone becomes a ~4 KB SVG), trimming the published jar. No public API change; the CV / cover-letter presets render the same layout (visual baselines refreshed for the new glyphs; the sidebar-portrait layout snapshot updated for the vector avatar). - Benchmark suite cleanup (not shipped). Removed three redundant
benchmark mains:
FullCvBenchmark(superseded by the JMHTemplateCvJmhBenchmark),GraphComposeBenchmark(early-engine relic duplicatingCurrentSpeedBenchmark'sengine-simplescenario), andScalabilityBenchmark(its thread-scaling sweep folded intoCurrentSpeedBenchmark's full-profile throughput run, now1,2,4,8,16). Dropped the matchingrun-benchmarks.ps1steps and doc entries. - Feature-object benchmarks for the v1.8 vector surface (not shipped).
The suite previously exercised only text/table primitives. Added JMH render
benches and deterministic probes over the new vector features:
SvgJmhBenchmark(path parse / whole-file icon read / icon→node) plus aSvgParseAllocProbe;ChartJmhBenchmark(bar + line + pie render) plus aChartAllocProbe(layout-compile allocation);VectorRenderOperatorProbe(the same paths drawn flat vs. gradient vs. translucent, counted as PDF content-stream operators);IconRampJmhBenchmark(icon-placement scaling,@Param8/32/128); andMixedShowcaseJmhBenchmark(one document combining prose, inline sparklines, bar + pie charts, SVG icons and a gradient path). SharedSvgBenchmarkFixtures/ChartBenchmarkFixtureshold the inputs so each bench and its probe measure identical data. - Current-speed report carries a stage breakdown and a run summary (not
shipped).
CurrentSpeedBenchmarkpersists a per-scenario compose / layout / render split (stages[], median ms) to the JSON and astagesCSV, and writes a readablesummary.md.BenchmarkDiffToolconsumesstages[], prints a per-stage delta table, and reports the scenarios added/removed between two runs. - Every current-speed scenario is now covered by the smoke perf gate (not
shipped). The
long-tokenscenario previously had no SMOKE threshold and silently escaped the gate; it now has one, andCurrentSpeedScenarioGateTestfails the build if any scenario lacks a threshold. - Benchmark coverage for the render hot paths (not shipped). Added an image
embed/scale gate (
ImageCacheOperatorProbe+ImageBenchmarkFixtures+ImageJmhBenchmark, withImageCacheGateTestpinningPdfImageCachereuse), a single-shot cold-start render bench (ColdStartJmhBenchmark), a report-scaling sweep inComparativeBenchmark(equivalent content across GraphCompose / iText 9 / JasperReports at 40 / 200 / 1000 table rows — iText upgraded from the EOL 5.5.x to current 9.x — printing a per-size GraphCompose-advantage ratio plus a post-run sample-PDF dump per library/size), a production-scaleLargeTableJmhBenchmark, an allocation-rate / GC-pressure probe (AllocationRateProbe), and an accented-Latin measurement scenario. - Deterministic benchmark gates run on every PR (not shipped). The benchmarks
module's tests never ran in CI; the
perf-smokejob now runs them, so the image-cache, render-operator (F5 coalescing), vector-paint (flat / gradient / alpha / stroked / dashed operator structure), and scenario-coverage gates fail a PR on a structural regression. Avector-richscenario (charts + SVG icons + gradient) joins the gated current-speed harness;BenchmarkMedianToolcarries the stage breakdown into its aggregate; and the smoke gate's GC-noisypeakHeapMbcheck is now advisory (fails only on average latency). Chart-layout variants (horizontal / stacked / donut / value-axis-min), a sparkline ramp, and a per-paint-mode vector render bench round out the JMH suite. - Removed the
java.awt.*/java.util.*co-wildcard in four files.InvoiceTemplateComposer,ProposalTemplateComposer,WeeklyScheduleTemplateComposer, and the enginePdfRenderingSystemECSimported both wildcards, leavingListresolvable from eitherjava.awt.Listorjava.util.List— sound today only becausejava.awt.Listwas never referenced. Each used onlyjava.awt.Color, so the wildcard is now an explicitimport java.awt.Color;. No behaviour change. - Sweep follow-up note for future bisectors. The v1.8.0 import/Javadoc
sweep (
f04a7dce, part of #162) also carried mechanical code rewrites in roughly 40 files beyond its stated scope: ~30 private presetTemplateclasses converted to records, constructor copy-loops replaced withCollections.addAll, explicit imports collapsed to wildcards, and five presets' explicitsection == nullguards folded intoSectionLookup.hasContent's null tolerance (now documented on the method and pinned bySectionLookupTest). All rewrites were verified behavior-preserving by the full gate at merge time; recorded here so a future bisect does not skip that commit on the strength of its message.
Tests
- Pinned the fail-loud guards on the new value types so a future refactor
cannot silently drop one:
PolygonNodeTest(fewer than three points, non-positive /NaN/ ∞ box, defensive vertex-ring copy),DocumentColorTest(withOpacityrange +NaNrejection, boundary alpha rounding,rgbaalpha),ShapeOutline.Pathcases inShapeOutlineTest(segment-count /MoveTo-first / null guards, defensive copy), andPathBuilderdashed-pattern rejection plus the documentedbuild()-snapshot contract. ExtendedPublicApiNoEngineLeakTestto cover the new publicdocument.svgpackage (it is engine-clean today; the guard now keeps it that way). - Chart geometry pinned without rendering:
NiceScaleTestgolden tables andChartLayoutResolverTestexact-position assertions on a font-independent text-metrics fake;ChartLayoutSnapshotTestlayout snapshots + a fragment-lowering assertion;SectionKeepTogetherTestcovers section, module, and timeline relocation plus the unchanged default. - Audit-driven edge-case coverage. DOCX semantic export: nested lists indent
two spaces per depth, per-depth custom markers survive, lists inside
sections export, empty lists are a no-op. Pagination: a keep-together
section taller than a full page still flows instead of relocating. Charts:
negative grouped bars extend the axis below zero and hang from the zero
baseline (positive and negative bars meet at zero, heights proportional to
|value|), an explicit positive axis minimum anchors grouped bars at the visible floor, stacked bars skip non-positive segments, a one-point smooth/area line keeps its marker and label, long category labels stay slot-sized, tight-width legends keep every entry, all-negativeNiceScaleranges. - Monotone interpolation pinned in
ChartLayoutResolverTest: theMONOTONEcurve's bounding box stays within theLINEARdata range (ground truth) whileSMOOTHovershoots it, plus a one-native-Bézier-run assertion and acharts/line_monotonelayout snapshot.
v1.7.1 — 2026-06-09
Open cycle — bug-fix / housekeeping. Entries land here as they merge.
Performance
-
Text wrapping stops re-measuring the growing line prefix. The greedy line wrapper in
TextFlowSupportnow keeps a running line width and measures each token once, instead of re-measuring the whole accumulated line on every token. This removes O(line-length × tokens) measured-character work — and the per-glyph sanitize/encode it triggered — from paragraph layout. Output is byte-identical: all layout and visual-regression snapshots pass unchanged. The effect is workload-dependent and concentrated in long-text documents; measured locally (same-session A/B, full profile) a long multi-page proposal rendered markedly faster, and a measurement-count probe showed ~9× fewer measured characters on a long paragraph. No public API or behaviour change. -
Long-token line breaking is no longer quadratic.
TextFlowSupport.fitCharactersnow binary-searches the break point instead of re-measuring every growing prefix one character at a time. For an unbreakable run (long URL/ID, no-space CJK, or a very narrow column) this cuts measurement calls and measured characters by ~80–85% (probe: 652 → 97 width calls, 36k → 7k measured chars on a 600-char token). Output is byte-identical — the fit predicate is monotonic, so the search returns the same break index. No public API or behaviour change. -
Text measurement no longer embeds binary fonts into a throwaway document. The layout measurement pipeline used to subset-embed every Google/custom font family into a private
PDDocumentthat was immediately discarded — repeated on every newDocumentSession, because each render in a server opens a fresh session. Measurement now resolves binary families to a per-thread cached font (mirroring the existing parsed-TrueType cache) bound to a reusable, never-saved document, so a family embeds once per worker thread instead of once per session, and opening measurement resources owns no PDF document at all. Output is byte-identical — both paths read glyph widths and metrics from the same parsedTrueTypeFont; proven by a 960-case render-vs-measurement width-parity check (max |Δ| = 0.0), a newMeasurementFontParityTest, and the full visual-regression / snapshot suite passing unchanged. Only Google/custom-font documents are affected (the standard-14 path never embedded); a measurement probe showed the per-session embed waste drop ~94–97% (≈1.5–3 MB and ≈2–4.5 ms of font subsetting removed per session after the first on a thread). Standard-14-only documents are unaffected. No public API or behaviour change. -
Glyph-coverage probing is memoized instead of repeated per glyph. The render sanitizer (
GlyphFallbackLogger.sanitize— shared by paragraph spans, table cells, watermark and header/footer chrome, and by width measurement) used to callPDFont.encodefor every code point of every string — allocating aStringper glyph and, for any glyph the font cannot encode, throwing and catching an exception — at measurement and again at render. Coverage is now memoized per(font, code point):encoderuns once per distinct glyph, then it is a map lookup, and kept glyphs append by code point with no per-glyphString. Output is byte-identical — the substitution decision is the sameencode, only cached; the glyph-fallback warning cadence is unchanged (pinned byPdfFontSanitizerTest, and width parity byMeasurementFontParityTest). This removes real per-glyph work from the render hot path: a long document re-probed tens of thousands of glyph occurrences that now collapse to roughly the number of distinct characters it uses. No public API or behaviour change. -
Paragraph render writes font and colour operators only when they change. The paragraph render handler emitted a
setFont(Tf) andsetNonStrokingColor(rg) operator for every text span, even across the spans of a single-style paragraph. It now tracks the last-written(font, size)and colour across the paragraph's graphics-state block and re-emits only on a real change (invalidating after inline images/shapes), so a multi-span single-style paragraph carries oneTf+ onerginstead of one pair per span — fewer operators for PDFBox to serialize. Rendered output is unchanged (the skipped operators were redundant); pinned by the visual-regression suite plus a content-stream test asserting oneTfacross many drawn spans. No public API or behaviour change. -
Table cell text is sanitized once per cell instead of three times. Resolving a table ran each cell's lines through
sanitizeCellLinesseparately in the natural-width, natural-height and resolve passes, rebuilding the list and its per-line control-character cleanup up to three times per cell. The sanitized lines are now computed once when the logical grid is built and reused by all three passes. Output is byte-identical (sanitization is deterministic); on a large table this removes the dominant per-cell layout allocation. No public API or behaviour change. -
Process-wide line-metrics cache stops inserting instead of flushing when full. The static line-metrics cache
clear()-ed every entry once it passed 50,000 distinct styles — a full flush whose non-atomic check-then-clear is a thundering-herd recompute under concurrent rendering. It now stops inserting at the cap and keeps the existing entries (distinct styles are few in real use, so this is only a pathological-explosion guard; it runs on a cache miss, never on the per-measurement path). Measured line metrics are unchanged. No public API or behaviour change. -
Auto-size font fitting binary-searches the size grid. A paragraph with
autoSize(...)resolved its font size by scanning every step from max down to min, re-measuring the line at each candidate (up to ~50 measurements). Line width is linear in font size, so the fit is monotonic — the search now binary-searches the grid for the same boundary in ~log2(n) measurements instead of n. Output is byte-identical — it returns the same grid size the linear scan did (covered by the existing auto-size integration and snapshot tests). No public API or behaviour change. -
Table pagination stops re-copying the tail on every page split. A table that spans many pages is split page-by-page, and each split re-sliced the shrinking tail by
List.copyOf-ing its row and row-height lists — even though the source layout already holds those lists immutably, so the copy made continuation O(rows × pages). The body-only slice now reuses the immutable sub-list views directly. Output is byte-identical — same rows in the same order (all table layout, pagination, and visual-regression tests pass unchanged); a deterministic allocation probe on a 2,500-row / 68-page table shows warm compile allocation drop 11,155 KB → 9,851 KB (−11.7%). No public API or behaviour change.
Deprecations
-
Font.adjustFontSizeToFit(...)is deprecated. The engine-internalFont#adjustFontSizeToFit(and itsPdfFont/WordFontimplementations) is unused and incorrect — the only real implementation re-measured with the unchanged style, so it always returned the minimum size. Canonical auto-size is resolved by the layout compiler. The method is kept for binary compatibility and scheduled for removal in the next major. -
The legacy ECS engine packages are deprecated.
com.demcha.compose.engine.core,engine.layout(andengine.layout.container), andengine.paginationare the originalEntity-based layout/pagination engine — a parallel second engine whose execution path the canonical pipeline (GraphCompose.document() → DocumentSession → LayoutCompiler) never runs; it imports nothing from them directly, and the formerGraphCompose.pdf(...)entry point has already been removed. The ECS execution engine runs only under the legacy engine regression tests. The packages are now@Deprecated(package level, so no deprecation-warning cascade) with corrected package docs, to stop misdirecting contributors into optimizing a dead engine. The genuinely shared engine packages (engine.components,engine.measurement,engine.font,engine.render) are not deprecated. No public API or behaviour change. -
TextMeasurementSystemdecoupled fromengine.core.SystemECS. The shared text-measurement contract (engine.measurement.TextMeasurementSystem) dropped its vestigialextends SystemECSand the no-opprocess(EntityManager)default it carried — it was never consumed as an ECS system. The legacy ECS engine now obtains the measurement service viaSystemRegistry.registerTextMeasurement(...)/textMeasurement()instead of enrolling it as aprocess()-driven system, completing the isolation of the deprecatedengine.corefrom live and shared code (only the legacy engine regression tests still reference it). Dropping the super-interface is binary-incompatible on paper, soengine.measurement.TextMeasurementSystemis excluded from the japicmp gate until the baseline advances past this release. No canonical API or behaviour change. -
The legacy ECS PDF render pipeline is deprecated. Follow-up to the ECS engine deprecation above. The
Entity-based PDFBox renderer (PdfRenderingSystemECSand its collaborators —PdfRenderSession,PdfCanvas,PdfStream,PdfImageCache,PdfFileManagerSystem,PdfGuidesRenderer, the render-marker handlers, and theTableCellBox/PdfBookmarkBuilderhelpers) is the renderer for the removedGraphCompose.pdf(...)surface and now runs only under the legacy engine regression tests; canonical PDF output goes throughcom.demcha.compose.document.backend.fixed.pdf. Becauseengine.render.pdfis a mixed package — it also holds the canonical-sharedPdfFont,GlyphFallbackLogger, and the header/footer + watermark post-processors — the legacy classes were physically moved into a newengine.render.pdf.ecs(with.handlers/.helperssub-packages), which is then@Deprecatedat package level (so no deprecation-warning cascade, same pattern as the ECS engine packages). The four genuinely sharedengine.render.pdftypes are not deprecated and stay put. No behaviour change. The relocated renderer has no public entry point and carries no binary-compatibility promise, so the move is excluded from the japicmp gate rather than treated as a breaking removal.
Internal
- Text-measurement line metrics resolve through the
Fontcontract instead of a PDF-specific fast path.FontLibraryTextMeasurementSystempreviously special-casedinstanceof PdfFontto obtain real ascent/descent/leading — every other backend font fell back to a degradedlineHeight-only metric — which coupled the shared measurement system toengine.render.pdf.PdfFontand meant a new backend could get first-class metrics only by editing shared code. Vertical metrics and the process-wide cache key now live on the backend-neutralFont<T>seam (Font.lineMetrics(...)+Font.measurementCacheKey(...), bothdefaultmethods; newFontLineMetricsrecord), so a backend supplies first-class metrics by overriding the contract and the shared measurement system no longer importsPdfFont. Binary-compatible (default methods only; japicmp green) and behaviour-neutral — PDF and Word produce identical metrics, covered by the existing suite plus new polymorphism tests.
Tests / tooling
-
Benchmark regression gate and measurement probe (benchmarks module, not part of the published library).
BenchmarkVerdictToolcompares a current-speed run to the committed baseline (baselines/current-speed-full.json) and reports improved / neutral / regressed. The hard gate fails only on an average-latency regression beyond the noise band; peak heap is advisory (thepeakHeapMbused-heap delta is GC-timing noisy — use the probe's per-compile allocation bytes for deterministic heap). A single run is advisory; the hard gate needs a median (-Repeat>= 2).MeasurementCountBenchmark+CountingTextMeasurementSystemcapture deterministic measurement-call counts and per-compile allocation bytes for proving algorithmic / allocation changes (the probe warms up the JVM before its allocation window, soAlloc KBreflects steady state, not one-time class-load / JIT cold-start).scripts/run-benchmarks.ps1gains the11-verdict-current-speedstep (skippable via-SkipVerdict). -
Cross-platform A/B benchmark harness.
scripts/ab-bench.sh(Linux / macOS / Windows Git Bash) joins the PowerShellscripts/ab-bench.ps1to compare engine speed between two branches — interleaved runs, median, per-scenario diff via the existingBenchmarkMedianTool/BenchmarkDiffTool. A path-filteredab-bench-smokeCI job runs it on Linux;.gitattributespins*.shandmvnwto LF so the wrappers stay runnable cross-platform. Benchmark tooling only — not part of the published library.
v1.7.0 — 2026-06-07
Canonical DSL primitives — additive only, zero breaking changes. Adding public API turns the open cycle into a minor.
Public API
- Inline shape runs — geometry-based dots, diamonds, stars and bullets. New
com.demcha.compose.document.node.InlineShapeRun(@since 1.7.0) joins the sealedInlineRunhierarchy alongside text and image runs. It draws anyShapeOutlinefigure on the paragraph baseline directly from geometry — no raster payload, no font glyph — so skill rating dots (Java ●●●●○), custom bullets and inline status markers no longer depend on a font shippingU+25CFand friends. Authored throughParagraphBuilder/RichTextdot(...),ellipse(...),diamond(...),triangle(...),star(...)and the genericshape(ShapeOutline, ...); measured into line width and height like inline images. Anullfill paints an outlined figure, anullstroke a filled one; at least one must be present. - New polygon shape geometry, usable block-level and inline.
ShapeOutline(com.demcha.compose.document.style) gains aPolygonkind plus a family of factories built from normalizedShapePointvertices (@since 1.7.0):diamond,triangle,star,polygon,arrow/arrowRight/arrowLeft(4-wayDirection),chevron,checkmark,plusandregularPolygon(sides). Arrows and chevrons read as directional list bullets or inline markers between text ("Step 1 → Step 2", "Home › Docs").ParagraphBuilder/RichTextaddarrow(size, Direction, fill)andchevron(...)shortcuts (every other kind is reachable throughshape(ShapeOutline, ...));ShapeContainerBuilderexposes matching block outlines. Rectangle, rounded-rectangle and ellipse shape containers are unchanged. - Inline checkboxes + composite (multi-layer) inline figures. An inline
shape run is now a stack of paint layers
(
com.demcha.compose.document.node.ShapeLayer,@since 1.7.0) drawn overlaid and centred, so a figure can compose several outlines — each with its own fill/stroke — and still measure and place as one unit on the baseline.ParagraphBuilder/RichTextgaincheckbox(size, checked, color)/checkbox(size, checked, boxColor, checkColor)(@since 1.7.0): a rounded frame plus, in the checked state, a centred tick — the todo / checklist marker for "some items done, some not". The single-outlineInlineShapeRunconvenience constructors are unchanged; every other kind still renders as one layer. - Swappable tick and arrow designs (the "pick your figure" seam).
ShapeOutlineaddsCheckmarkStyle(CLASSIC,HEAVY) andArrowStyle(BLOCK,TRIANGLE) enums plus the overloadscheckmark(w, h, CheckmarkStyle)andarrow(w, h, Direction, ArrowStyle)(@since 1.7.0); the no-style factories delegate toCLASSIC/BLOCK, so the default look is unchanged.checkbox(...),RichText.arrow(...)andParagraphBuilder.arrow(...)gain matching style overloads, andcheckboxalso accepts a rawShapeOutlinemark for fully custom ticks. Adding a new design is one enum constant plus its vertex ring — the foundation for letting a caller choose which tick or arrow to render. softPanel(...)gains stroke overloads — rounded fill + outline in one node.AbstractFlowBuilder.softPanel(color, radius, padding, stroke)andsoftPanel(color, cornerRadius, padding, stroke)(@since 1.7.0) apply a border stroke alongside the panel fill on the same flow node (section, module, page flow), so a rounded, padded background with a thin outline no longer needs a separate wrapping node. Equivalent to the always-availablesoftPanel(...).stroke(...)chain — these overloads just make the one-node form discoverable.- Per-corner radius on shape containers.
ShapeContainerBuilder.roundedRect(width, height, DocumentCornerRadius)plus the newShapeOutline.RoundedRectanglePerCorner(@since 1.7.0) round a container's four corners independently — a card "rounded on the left, square on the right" no longer needs a CLIP_PATH-parent workaround, since both the outline fill/stroke and the child clip now follow the per-corner geometry.DocumentCornerRadius.left/right/top/bottom(...)give the common asymmetric presets. The single-radiusroundedRect(w, h, double)overload is unchanged, and uniform rounded rectangles render byte-for-byte identically (the clip and fill now share one per-corner path implementation, called with four equal radii). - Vertical text alignment for shape-container labels.
ParagraphBuilder.verticalAlign(TextVerticalAlign)(new enumcom.demcha.compose.document.node.TextVerticalAlign,@since 1.7.0) seats a single line by its cap band within its line box —TOP(cap top to the box top),CENTER(cap band centred) orBOTTOM(baseline to the box bottom). Combined with a vertically-centred layer placement (.center(...)/.centerLeft(...)), a label dropped into a tallerShapeContainer/LayerStack"pill" sits where you ask instead of always on the font baseline — no compensating offset hacks.TextVerticalAlign.DEFAULTkeeps the pre-1.7.0 baseline seating; the correction is derived from font metrics (ascent, descent, leading, cap height), not a magic number. Render-only and opt-in — existing layouts are byte-for-byte unchanged. - Bundled font: JetBrains Mono. New
FontName.JETBRAINS_MONO(@since 1.7.0) joins the built-inDefaultFontscatalog with its Regular / Bold / Italic / Bold-Italic faces (bundled from the OFL-1.1 release), so monospaced code and data blocks render without a system-font install. Usable through anyDocumentTextStyle.fontName(...)and listed in the font showcase. - Dashed and dotted lines.
LineBuilder.dashed(double... pattern)/dashed()/dashed(DocumentDashPattern)plus the new value typecom.demcha.compose.document.style.DocumentDashPattern(@since 1.7.0) make aline(...)paint an on/off dash instead of a solid stroke — section and résumé dividers, timeline connectors, cut-here rules. The pattern alternates paint-on and paint-off lengths in points (dashed(8, 5);dashed()is a balanced 3pt-on / 2pt-off;dashed(1, 4)reads as dotted). Carried on the line independently ofDocumentStroke, so the stroke value stays a stable two-component record; lines are solid by default and the PDF backend honours the dash (other backends fall back to a solid stroke). - Semantic timelines.
addTimeline(timeline -> ...)on every flow / section / module, plusTimelineBuilder,TimelineMarkerandTimelineEntryBuilder(com.demcha.compose.document.dsl,@since 1.7.0), lay out a vertical timeline where eachentry(marker, e -> e.title(...).meta(...).body(...))pairs a marker with its content along a continuous connector rail — work history, project milestones, numbered process steps. Markers areTimelineMarker.dot,circle,numberedorsquare; the rail colour/width, gutter, entry spacing and default title / meta / body styles are all tunable. Declaring the marker-to-content relationship replaces the hand-placed bullet-plus-margin pattern; the timeline paginates between entries and a tall entry splits within itself, the rail continuing across the page break. DocumentSession.availableHeight(). A one-call alias forcanvas().innerHeight()(@since 1.7.0) — the usable page content height (page height minus top and bottom margins), the value a composition reads to decide how much vertical room a section, sidebar or spacer may fill. Previously reachable only through the layout-inspection facade.headingBar(text, bar -> ...)— one-call filled title band. Every flow, section and module gainsheadingBar(String)andheadingBar(String, Consumer<HeadingBarStyle>)(@since 1.7.0) onAbstractFlowBuilder: a filled, rounded heading band with a single label, added as a child above the body.HeadingBarStyle(com.demcha.compose.document.dsl) tunes fill, corner radius, padding, margin, label text style, alignment and an optional outline stroke, each with a sensible default (a light-grey band with a centred bold label), sobar -> bar.fill(brand).textStyle(white)is enough. Sugar over thesoftPanel(...).addParagraph(...)recipe — no new node type, just discoverable.
Fixed
position(node, dx, dy, align)offsets are now honored for stacks nested inside a fixed slot. ALayerStack/ShapeContainerplaced inside a row column or another layer compiled through the fixed-slot stack path, which silently dropped the per-layer offsets (anchoring on alignment only) — so a positioned badge or cap could not be nudged from its anchor once nested, even though the same call worked at the document root. The nested path now feeds the samePreparedStackLayoutoffsets as the root path. Layout for documents that did not useposition(...)inside a nested stack is unchanged.
Documentation
- New recipe pages for page composition and font coverage (closing the
docs/recipes/discoverability gaps G2 / G6):page-backgrounds.md(PageBackgroundFillcolumns / bands / point-based fills / layering),layered-page-design.md(choosing between page backgrounds, rows, layer stacks and canvases),absolute-placement.md(addCanvas+position(x, y)), andfont-coverage.md(WinAnsi limits,●vs•, and the inline-shape / bundled-font alternatives). Linked from the README recipes index,docs/README.md, anddocs/recipes.md.
Build
- Showcase website separated from documentation (
docs/→web/), now deployed via GitHub Actions. The static GitHub Pages site (index.html,styles.css,examples.js, the generatedexamples.json+showcase/gallery assets,robots.txt,sitemap.xml, logo) moved out ofdocs/— which previously had to host it because branch-based Pages can only serve repo-root or/docs— into a new top-levelweb/folder, sodocs/now holds only documentation. A newdeploy-web.ymlpublishesweb/to Pages from the "GitHub Actions" source; the old branch-/docsdeploy-site.ymlwas removed.ShowcaseSyncnow writesweb/showcase+web/examples.json,VersionConsistencyGuardTestreadsweb/index.html, andcut-release.ps1bumps / commitsweb/. The unused Next.js rebuild undersite/(added in v1.6.8 but never deployed) was removed. Also renameddocs/SHOWCASE.md→web/README.md. ⚠️ Action required before the next release reachesmain: set Settings → Pages → Source = "GitHub Actions" — once the move lands onmain,/docsno longer holdsindex.html, so a branch-/docsPages source would 404. The live site is unaffected until then.
v1.6.9 — 2026-06-03
Housekeeping cycle plus the public pixel-level visual-regression API (Track N).
Public API
- Promoted the pixel-level visual-regression harness to public API.
com.demcha.compose.testing.visual.PdfVisualRegressionandcom.demcha.compose.testing.visual.ImageDiff(@since 1.6.9) move from the test source set intosrc/main/java, alongside the existingcom.demcha.compose.testing.layout.*semantic snapshot helpers. Library consumers can now run the same render-PDF → diff-PNG baseline gate against their own presets and templates instead of copying the harness. Behaviour is unchanged; the PDF→image step is inlined on PDFBox'sPDFRenderer. - Exposed
PdfVisualRegression.APPROVE_PROPERTY(@since 1.6.9) — thegraphcompose.visual.approvesystem-property name — so consumers can toggle baseline-approve mode without hard-coding the string (mirrorsLayoutSnapshotAssertions.UPDATE_PROPERTY).
Documentation
- Added
docs/operations/visual-regression-testing.md: pixel-vs-semantic guidance, thePdfVisualRegressionAPI, approve mode, baseline layout, and cross-platform tolerance calibration. - README "Which API should I use?" gains a pixel-level visual-regression row.
- Made the entire
com.demcha.compose.document.*public API Javadoc doclint-clean. Added the missing@param/@return/@throwstags and element descriptions across 142 files somvn javadoc:javadoc(doclint=all) runs warning-free. Java's default-Xmaxwarns=100cap had masked ~90% of the gaps (true count: 929 warnings, not the ~100 first visible). Additive Javadoc only — no behaviour change; the only code additions are 16 behaviour-neutral no-arg constructors inlayout/definitions/*(documenting the otherwise-synthesised public default constructor) and removal of the@deprecatedblock-tagsdoclintforbids inpackage-info.java(the@Deprecatedannotation + prose body already carry the notice).
Build
- CI Javadoc validation (
maven-javadoc-plugin,doclint=all) now covers the publiccom.demcha.compose.testing.*helpers (testing.layout+testing.visual) in addition to the canonicaldocumentAPI, so Javadoc regressions on the testing surface fail fast in CI. No artifact or behaviour change. - Bumped
central-publishing-maven-plugin0.9.0 → 0.10.0 (the Maven Central publishing plugin) and removed the Dependabot block on 0.10.0; the release-profile build is verified locally and the Central upload is exercised at the next publish.
v1.6.8 — 2026-06-01
CV v2 migration completion + design-token expansion. v1.6.8
finishes the CV v2 migration with hyperlink-aware project / entry
titles: a row authored as "[GraphCompose](https://github.com/x/y) (Java, PDFBox)" now renders the title as a clickable link in the
final PDF, with the technology stack remaining a plain
(Java, PDFBox) tail. The mechanism is a small extension to
the inline-Markdown parser used by every CV / cover-letter body
row — the [label](url) syntax produces a RichText.link(...)
run; bare brackets stay literal; everything else (**bold**,
*italic*, _italic_) keeps working as before. The release also
ships four contemporary BusinessTheme factory presets
(nordic(), editorial(), cinematic(), monochrome())
alongside the classic / modern / executive trio, expanding the
built-in design-token range to seven presets. Senior-review
follow-ups from v1.6.7 round out the release: the two registry
mutation entry points on DocumentSession are now fully
interchangeable (both refuse to mutate a closed session and both
invalidate the layout cache), target-branch: develop is pinned
in Dependabot config so future bumps land on the integration
branch, and logback-classic rolls forward to 1.5.34 which
fixes CVE-2026-9828
(deserialisation whitelist bypass).
Zero breaking public API changes. The japicmp gate against
the v1.6.7 baseline reports semver PATCH, compatible bug fix
across every PR in the cycle. New BusinessTheme factories are
pure additions; MarkdownInline.append and plainText extend
their behaviour without changing their signatures; ProjectLabel. parse keeps its two-field record shape (the title() field now
preserves Markdown rather than returning a pre-flattened
projection, but the type contract is unchanged and the visible
text projection is one call away via MarkdownInline.plainText( title)). 1058 tests pass at the release-prep tip.
Migration from v1.6.7. No code changes required for typical
usage. If you build a custom renderer on top of
ProjectLabel.parse:
- Old
title()was already the visible plain text (emphasis + link syntax stripped). Newtitle()preserves the original inline-Markdown. Wrap withMarkdownInline.plainText(...)to recover the old behaviour, or route throughMarkdownInline.append(rich, title, style)to get emphasis / link rendering for free (the same pathProjectRenderernow uses). MarkdownInline.appendconsumers automatically pick up link rendering for[label](url)syntax. If any CV / cover-letter fixture in your codebase contained a literal[...](...)string that previously rendered as text, it will now render as a hyperlink. Escape with HTML entities or restructure the string if you need to keep it literal.
The next release is v1.7.0 — the additive canonical-DSL feature minor (LineBuilder.dashed, inline shapes, TimelineBuilder, dx shortcuts, recipes docs). See ROADMAP.md.
Fixes
- The two
DocumentSessionregistration entry points are now fully interchangeable, not just cache-equivalent.session.registry().register(...)now callsensureOpen()before mutating, matching the behaviour ofsession.registerNodeDefinition(...). Previouslyregistry().register(...)on a closed session silently mutated the registry and invalidated a closed-session cache (harmless but semantically odd). After this change both paths throwIllegalStateExceptionon a closed session. (Track J2 — carry- over polish from the v1.6.7 senior review.)
Internal
NodeRegistryJavadoc updated to call out the v1.6.7 non-final relaxation explicitly (Track J4). The class became non-final in v1.6.7 (Track I3) soDocumentSessioncould install the auto-invalidating subclass; the change was already binary- compatible (japicmp classified it assemver PATCH). The Javadoc just makes the rationale discoverable without reading the CHANGELOG.
Public API
MarkdownInline.append(...)(the inline-markdown adapter used by every CV / cover-letter body / row / entry renderer) now recognises standard Markdown link syntax[label](url)and emits a clickable hyperlink run viaRichText.link(label, url). Pure parser extension — noCvRowdata-shape change required.MarkdownInline.plainText(...)is updated in lockstep to strip link syntax cleanly so callers that pull a plain-text projection (e.g.ProjectLabel.parse) keep getting just the visible label.ProjectRenderer.inline(...)andProjectRenderer.titleThenBody(...)now route the project-row title segment throughMarkdownInline.append(...)instead of emitting it as a flatRichText.style(...)run. End-to-end consequence: a CV row withlabel = "[GraphCompose](https://gc) (Java, PDFBox)"renders the title as a clickable hyperlink and the stack as plain" (Java, PDFBox)". Labels without inline Markdown render identically to before.ProjectRenderer.plainInline(...)(the one-line listing variant) intentionally continues to drop link syntax viaMarkdownInline.plainText(...)because a clickable link would not survive the compact formatting context.ProjectLabel.parse(...)now preserves inline Markdown syntax inside the returnedtitle(the legacy implementation eagerly flattened**emphasis**and[links](url)viaplainTextand then split on the last(). The split heuristic now targets a trailing\s+\([^()]*\)\s*$pattern so a leading[name](https://...)URL's(...)segment is not mistaken for the technology-stack delimiter. Callers that only need the visible-text projection should passtitle()back throughMarkdownInline.plainText(...).- Four new
BusinessThemefactory presets@since 1.6.8:BusinessTheme.nordic()(Scandinavian minimal — cool whites + slate-blue accent + generous whitespace, for design-studio reports and product launch decks),BusinessTheme.editorial()(warm cream surface + deep ink + brick-red accent on a serif body, for long-form proposals and annual reports),BusinessTheme.cinematic()(inverted dark navy surface with light text + bright copper accent, for investor pitch decks and product launch one-pagers), andBusinessTheme.monochrome()(pure black-on-white with a single bold yellow accent, for brutalist editorial layouts where typographic contrast carries the identity). Pure additions — no change to the existingclassic()/modern()/executive()presets. japicmp gate against v1.6.7 reportssemver PATCH(compatible additions only).
Build
- Bumped
jackson-bom2.21.3 → 2.21.4 (broken 2.22.0 skipped via the.github/dependabot.ymlignore entry added in v1.6.7),logback-classic1.5.32 → 1.5.34 (fixes CVE-2026-9828 — deserialization whitelist bypass inHardenedModelInputStream),central-publishing-maven-plugin0.7.0 → 0.9.0 (0.10.0 blocked by the existing ignore entry; revisit after a focused release-profile evaluation),japicmp-maven-plugin0.23.1 → 0.26.1, and a handful ofmaven-*-pluginminor/patch bumps (clean / site / resources / enforcer 3.5.0 → 3.6.3 / surefire 3.5.5 → 3.5.6 / source 3.3.1 → 3.4.0 / gpg 3.2.7 → 3.2.8) (#115, cherry-picked frommainto aligndevelop).
CI
.github/dependabot.ymlnow pins both ecosystems (maven,github-actions) totarget-branch: developso future grouped PRs land on the integration branch instead ofmain. Closes the divergence root cause behind the v1.6.7-era #111 / #115 episodes where every Dependabot PR force-split history between branches and required a cherry-pick to align.
Documentation
- New quickstart guide Testing your document — end-to-end recipe (author the document → add a layout snapshot test → bless the baseline → CI guards the shape on every PR), with a "when to use which layer" table for the three protection tiers (smoke / layout snapshot / pixel-level visual). Complements the existing layout-snapshot-testing.md reference: that one is reference-style, the new one is tutorial-style. README's "What can I do with this?" table row now links to both.
Web
- New Next.js showcase site under
site/is now the official GitHub Pages deploy target for v1.6.8 onwards. Fully static one-page marketing / playground built with Next.js 14 App Router + TypeScript + Tailwind.next buildemits./out(4 static pages, 99.7 kB first-load JS) and the new.github/workflows/deploy-site.yml(removed in v1.7.0) uploads it to Pages on every push tomainthat touchessite/**. Repo Settings → Pages source must be flipped to "GitHub Actions" for the workflow to take over from the legacy branch-based deploy ofdocs/index.html; both files coexist in the tree for one more cycle as a rollback. - Live code snippets in the Hero / Playground sections mirror
the canonical README hello-world,
examples/.../InvoiceFileExample, andModernProfessional.create()paths, so a visitor copying any snippet into a fresh Maven project pulled atio.github.demchaav:graph-compose:1.6.8gets compiling code. Gallery enumerates the full 16-preset cv/v2 lineup (15 paired cover letters;MinimalUnderlinedships without a paired letter by design). scripts/cut-release.ps1learns a newUpdate-SiteDepsVersionstep so the Maven / Gradle install snippets insite/lib/deps.tsflip in lockstep with the README + pom versions at cut time — no more silent drift between the site and the real released coordinates. The same release commit now also stagessite/lib/deps.ts.
v1.6.7 — 2026-06-01
Transitive dependency cleanup. v1.6.7 narrows the runtime
classpath GraphCompose imposes on consumers. The Kotlin standard
library is gone (the codebase is Java-first; no production
.kt sources exist), the flexmark-all aggregator is replaced
with the three modules MarkDownParser actually references,
jackson-dataformat-yaml is marked <optional>true</optional>
(mirroring the existing poi-ooxml pattern — only consumers that
load YAML configs through ConfigLoader need to pull it in),
jackson-module-jsonSchema and the explicit snakeyaml
declaration are dropped as unused, and jcl-over-slf4j is added
explicitly so PDFBox's commons-logging call sites keep routing
through SLF4J after the flexmark narrowing (the bridge was
previously provided transitively via flexmark-all). The cycle
also fixes a latent layout-cache staleness bug on
DocumentSession.registry().register(...) (Track I3): the
registry returned by registry() is now a session-owned wrapper
that invalidates the layout cache on every mutation, matching the
semantics of DocumentSession.registerNodeDefinition(...).
Zero breaking public API changes. The japicmp gate against
the v1.6.6 baseline reports semver PATCH, compatible bug fix —
the one surface delta is NodeRegistry becoming non-final so
DocumentSession can install the auto-invalidating subclass
described above. All existing call sites compile and run
unchanged. The transitive cleanup is a runtime-classpath change,
not a compile-surface change.
Migration from v1.6.6. Consumers that relied on dependencies flowing transitively through GraphCompose must now declare them explicitly:
| If you transitively depended on… | Add to your build |
|---|---|
| Kotlin stdlib via GraphCompose | org.jetbrains.kotlin:kotlin-stdlib-jdk8 |
| Flexmark extensions (tables, footnotes, gfm-strikethrough, …) | the relevant com.vladsch.flexmark:flexmark-ext-* modules |
YAML config loading through ConfigLoader | com.fasterxml.jackson.dataformat:jackson-dataformat-yaml |
jackson-module-jsonSchema | com.fasterxml.jackson.module:jackson-module-jsonSchema |
The commons-logging API beyond SLF4J routing | declare commons-logging:commons-logging explicitly (GraphCompose intentionally excludes it from PDFBox and bridges via jcl-over-slf4j) |
No code changes are required for typical usage — pure-PDF
consumers and JSON-only ConfigLoader callers carry on as before.
The next minor with new canonical DSL primitives is v1.7.0
(see ROADMAP.md).
Build
- Dropped the
kotlin-stdlib-jdk8compile dependency, thekotlin-testtest dependency, and thekotlin-maven-pluginbuild extension. GraphCompose is Java-first; no production Kotlin sources exist, and the runtime now no longer carries the Kotlin standard library transitively. Consumers that relied onkotlin-stdlibflowing through GraphCompose must declare it explicitly. - Replaced the
flexmark-allaggregator dependency with the three modules actually referenced byMarkDownParser:flexmark(core parser + AST),flexmark-util-ast(Node / NodeVisitor / VisitHandler), andflexmark-util-data(MutableDataSet). No extension modules (tables, footnotes, gfm-strikethrough, etc.) are used by GraphCompose. Consumers that relied on extensions flowing through GraphCompose must depend on the relevantflexmark-ext-*modules explicitly. - Added
jcl-over-slf4jas an explicit compile dependency. PDFBox 3.0.7'sPDDocument.<clinit>callsorg.apache.commons.logging. LogFactorydirectly; we exclude PDFBox's owncommons-loggingartifact to keep one logging facade, and the bridge routes those calls through SLF4J. Previously the bridge was provided transitively viaflexmark-all; making it explicit keeps the classpath reproducible after the flexmark narrowing above. - Marked
jackson-dataformat-yamlas<optional>true</optional>, mirroring the existingpoi-ooxmlpattern. The only consumer isConfigLoader.loadConfigWithEnv(...)when the caller passes a.yaml/.ymlresource; library consumers that load JSON configs (or skipConfigLoaderaltogether) no longer pull in the ~1.7 MB SnakeYAML transitive footprint. Applications that load YAML configs through this helper must now declarejackson-dataformat-yamlin their own build. - Removed the unused
jackson-module-jsonSchemadependency — no code path references it. - Removed the explicit
snakeyamldependency declaration and thesnakeyaml.versionproperty. SnakeYAML is now resolved transitively (andoptional) throughjackson-dataformat-yaml, which version-aligns it with Jackson's BOM. - Bumped
net.sf.jasperreports:jasperreports6.21.3 → 7.0.7 in the benchmarks module. Benchmarks are a sibling Maven module consumed only by the manual performance harness — no impact on library consumers (#111).
Documentation
ConfigLoader.loadConfigWithEnvJavadoc now states the YAML path requiresjackson-dataformat-yamlon the classpath and throwsNoClassDefFoundErrorwhen the optional dep is absent.DocumentSession.registry()Javadoc now explains that the returned registry is a session-owned wrapper whoseregister(...)mutates the registry and invalidates the layout cache, making the two registration entry points (session.registry().register(...)andsession.registerNodeDefinition(...)) interchangeable.
Fixes
DocumentSession.registry().register(...)now invalidates the layout cache the same wayDocumentSession.registerNodeDefinition(...)does. Previously, registering a node definition throughregistry()mutated the registry in place but left the cachedLayoutGraphpinned to the previous compile, so a follow-up call torender(...)orlayoutGraph()silently returned the stale graph routed through the old definition. Implemented by wrapping the session'sNodeRegistryin a private session-owned subclass that funnels everyregister(...)call throughinvalidate(). (Track I3.)
Internal
NodeRegistryis no longerfinalsoDocumentSessioncan install a session-owned subclass that auto-invalidates the layout cache on mutation (see Fixes above). StandaloneNodeRegistryinstances retain their previous behaviour.- Replaced eight residual
org.jetbrains.annotations.NotNull/@Nullableusages withlombok.NonNull(where the surrounding file already used Lombok) or removed them entirely (private methods and test fixtures).org.jetbrains:annotationsis no longer on the runtime classpath after the Kotlin removal.
v1.6.6 — 2026-05-31
First Maven Central release. GraphCompose now ships under
io.github.demchaav:graph-compose:1.6.6 — note the hyphenated
artifactId, chosen for readability ahead of the Central debut. The
release adds publishable sources/javadoc jars, GPG-signed artefacts,
a binary-compatibility gate against v1.6.5, the metadata Maven
Central requires, and a substantial documentation polish for the
maturity / stability / migration story.
Zero breaking changes from v1.6.5. Existing JitPack callers continue
to resolve through the same coordinates (com.github.DemchaAV:GraphCompose:v1.6.5);
existing API surface compiles and runs unchanged (validated by the new
japicmp gate against the v1.6.5 baseline). New: the @Beta
annotation marker, the @since 1.0.0 class-level Javadoc on
entry-point packages, and a curated docs pass (decision guide for
the two template surfaces, examples maturity index, explicit API
stability policy).
Migration from v1.6.5: no code changes required. Swap the
JitPack <dependency> for the Maven Central form
(io.github.demchaav:graph-compose:1.6.6). The legacy JitPack URL
keeps resolving for callers pinned to v1.6.5 and earlier.
Build
- Binary-compatibility gate against v1.6.5 (
japicmpprofile, Track E1). The newbinary-compatCI job builds the artifact on every pull request and diffs it againstcom.github.DemchaAV:GraphCompose:v1.6.5pulled from JitPack. Binary-incompatible modifications to the public surface fail the build; source-incompatible changes are reported only (phased policy, will tighten after the 1.6.6 cut). Run locally with./mvnw -DskipTests -P japicmp verify -pl .; HTML/MD/XML reports land intarget/japicmp/. JitPack repository is scoped to thejapicmpprofile, so downstream consumers do not inherit it. - Maven Central publish workflow (Track D4). New
.github/workflows/publish.ymlfires on the samev*tag push that triggers the existingrelease.yml. It re-runsmvnw verifyat the tagged commit, imports the GPG key (Track D2) into the runner keyring, writes the<server id="central">credentials block into~/.m2/settings.xmlviaactions/setup-java@v5, then invokes./mvnw -P release -Dgpg.skip=false deploy— thecentral-publishing-maven-plugin(Track D3) uploads to Central and blocks until Sonatype's validator responds. Hyphenated tags (-rc,-alpha,-beta,-snapshot) are explicitly skipped — those ship only to JitPack and the GitHub Release pre-release surface. Aworkflow_dispatchinput lets the maintainer re-publish an existing tag without re-cutting it if Central had a transient validator hiccup. The workflow is dormant until four GitHub repo secrets are wired:MAVEN_GPG_PRIVATE_KEY,MAVEN_GPG_PASSPHRASE,CENTRAL_USERNAME,CENTRAL_TOKEN. docs/contributing/release-process.mdupdated with the end-to-end Maven Central runbook (Track D4 docs). New § 2.C "One-time Maven Central setup (maintainer)" walks through GPG key generation, keyserver upload, Sonatype account / namespace verification, Central user-token generation, the four GitHub secrets, and the release-candidate dry-run strategy. § 2.B post-release checklist gains a new step 9 for the Central publish alongside the existing JitPack step.- Hosted Javadocs via
javadoc.io(Track H3). README's distribution-status note now points callers at javadoc.io/doc/io.github.demchaav/graph-compose, which auto-mirrors any artefact published to Maven Central within minutes — no separate hosting infrastructure required. The note also pins Maven Central as the going-forward primary distribution starting v1.6.6 (JitPack stays available alongside for existing callers). The full Central install snippet ("Central as primary, JitPack as fallback") lands in the v1.6.6 release-prep PR after the first Central publish proves the pipeline end-to-end. central-publishing-maven-pluginin thereleaseprofile (Track D3). Adds Sonatype'scentral-publishing-maven-plugin0.7.0 to the existingreleaseprofile as a packaging extension. Replaces the legacynexus-staging-maven-plugin+ manual staging-repository workflow with a singledeploycall. Configuration:publishingServerId=central(matches the<server id="central">entry the publish workflow writes fromCENTRAL_USERNAME/CENTRAL_TOKENsecrets),autoPublish=false(validation gate before the artefact goes live — flips totrueonce we're confident post-D4),waitUntil=validated(the build waits for Sonatype's validator so any rejection surfaces in the workflow run, not a silent stuck upload). Requires theio.github.demchaavnamespace to be verified oncentral.sonatype.com(one-time human step via GitHub auth or DNS TXT record). The plugin loads inert until D4's workflow provides the credentials.- GPG signing in the
releaseprofile (Track D2). Addsmaven-gpg-plugin3.2.7 to the existingreleaseprofile, binding to theverifyphase to sign main / sources / javadoc / pom artefacts — Maven Central rejects unsigned uploads. Off by default: a new property<gpg.skip>true</gpg.skip>keeps localmvn -P release packageruns working without a configured GPG key. The publish workflow (Track D4) flips it explicitly with-Dgpg.skip=falseonce theMAVEN_GPG_PRIVATE_KEYandMAVEN_GPG_PASSPHRASEsecrets are wired.gpgArgumentsdeclares--pinentry-mode loopbackso non-interactive CI runs accept the passphrase from-Dgpg.passphrase/MAVEN_GPG_PASSPHRASEwithout needing a TTY forgpg-agent. releaseMaven profile with sources + javadoc jars (Track D1). Activated with-P release, attaches*-sources.jarand*-javadoc.jarto thepackagephase via the standardmaven-source-plugin(3.3.1) andmaven-javadoc-plugin(3.12.0) configurations Maven Central requires. The Javadoc plugin runs withdoclint=noneandfailOnError=falseso Lombok-generated members and@Internalengine surface don't block a publish; warnings are surfaced quietly. Defaultmvnw verifystill does not pay the ~30 s of extra packaging — the profile is off by default and turned on bycut-release.ps1(once Track D3's central-publishing plugin lands) and the publish workflow (Track D4).- SCM block canonicalised in
pom.xml(Track D1 polish). The Central metadata validator is strict about the<scm>block:<connection>now usesscm:git:https://…(HTTPS, not the legacygit://transport) and<developerConnection>now usesscm:git:ssh://git@github.com/…(the canonical SSH URL with thegit@user, not the olderssh://github.com:…form). Matches the shape every Central artefact's POM carries. - New
benchmarks/README.md(Track B1). Honest framing for the manual benchmark layer ahead of the Maven Central debut: explicitly positions the harness as a smoke / diff / endurance tool — not a JMH-grade benchmark — and tells callers when not to use it (publishable performance claims, architectural decisions, cross-library comparisons that read too much into a single number). Documents the file-by-file role of each runner / report tool, the exact CI smoke invocation, and a "How to read a report" cheat sheet. Cross-links the planned JMH chain (Track C, B3 → B6 in 1.7.0) so a reader knows what's coming and how to identify "rigorous" measurements when they arrive. - Class-level
@since 1.0.0Javadoc on the public entry-point surface (Track H1). 26 public types in the canonical user-reached packages (com.demcha.compose.GraphCompose,com.demcha.compose.document.api.{DocumentSession, DocumentPageSize, PageBackgroundFill},com.demcha.compose.document.dsl.{DocumentDsl, RichText, Transformable}plus all 19 DSL builders) now carry class-level@since 1.0.0Javadoc tags so callers can see the introduction version at IDE quick-doc / generated Javadoc time without trawling CHANGELOG history. New guard testPublicApiSinceTagCoverageTestsource-scans the three entry-point roots and fails the build if a new public top-level type lands without a class-level@sincetag;internal/sub-packages are excluded by convention (InternalAnnotationCoverageTestcovers those). Method-level@sincebackfill for the ~380 public methods in these packages is intentionally out of scope here and tracked separately. maven-enforcer-plugingate (Track E2). Binds three rules to thevalidatephase so the build refuses to start when a precondition is broken:requireJavaVersion(≥ 17 — the declared baseline, catches accidental JDK 11 / 15 attempts),requireMavenVersion(≥ 3.8.0 — the oldest version the planned central-publishing pipeline supports), andrequirePluginVersions(every plugin must declare an explicit non-LATEST/ non-RELEASE/ non-SNAPSHOTversion — the generalisation of the PR-7.1 exec-plugin drift lesson). Default-lifecycle plugins (clean/install/site/resources/deploy) are now pinned in a new<pluginManagement>block sorequirePluginVersionshas nothing to flag. Minimums and versions live in<properties>(enforcer.requireMavenVersion,enforcer.requireJavaVersion,maven.enforcer.plugin.version).- Parallel-session stress test (Track I2). New
DocumentSessionParallelStressTestdrives 32 independentDocumentSessioninstances on a fixed-size thread pool through 4 iterations and asserts (a) all parallel renders produce a layout-graph signature byte-equal to the sequential baseline — exercising the shared font registry, glyph cache, built-in node definitions, and shape-outline cache for race conditions; (b) every PDF output starts with the%PDFmagic, is at least 256 bytes, and has size variance under 256 bytes across threads (catching corruption or rare non-determinism without locking exact byte counts that timestamps could drift). 128 + 128 = 256 renders complete in ~1.6 s locally, so the test does not bloat CI. The contract is that eachDocumentSessionis single-threaded but the process-wide machinery handles concurrent independent sessions safely; this test pins that. no-poiMaven profile + CI job (Track I1). Thepoi-ooxmldependency is declared<optional>true</optional>so callers that render only PDFs don't pay the ~10 MB POI footprint; this PR adds a regression gate that proves it. Running./mvnw -P no-poi test -pl .excludespoi-ooxml(and itspoi/poi-ooxml-litetransitives) from the surefire test classpath and sets the system propertyno.poi=true. DOCX-specific tests (DocxSemanticBackendTestand the one DOCX export inDocumentSessionTest) now carry@DisabledIfSystemProperty(named = "no.poi", matches = "true")and skip cleanly. The rest of the canonical suite (1029 tests, 4 skipped under-P no-poi) runs green without POI on the classpath. A newno-poi-suiteCI job exercises the profile on every pull request.
Public API
- New
@Betaannotation (Track H2). Companion to the existing@Internalmarker:com.demcha.compose.document.api.Betasignals an Extension SPI or Experimental surface — a deliberately-exposed seam library users can implement or call, but whose shape may still evolve between minor releases per the API stability policy § 1. First application:com.demcha.compose.document.layout.NodeDefinition— the canonical custom-node-type seam, carved out of the otherwise@Internaldocument.layoutpackage. NewBetaAnnotationDocumentationTestpins the annotation's retention / target /@Documented-ness / source-Javadoc contract in the same shapeInternalAnnotationDocumentationTestalready pins for@Internal. Additional Extension SPI surfaces (render-handler interfaces, fragment-payload interfaces) will gain the marker incrementally as their contract solidifies.
Documentation
- New flagship example:
EngineShowcase+ regeneratedassets/readme/repository_showcase_render.pnghero image ahead of the Maven Central debut. A presentation audit before v1.6.6 flagged that the existing hero PDF was a dated single-page render and the GitHub Pages showcase had 20 broken asset paths (CV v2 migration added-v2suffixes thatdocs/index.htmlnever picked up). Fixed in three commits: (a)docs/index.htmlpath repair so every CV / cover-letter preview resolves; (b) new flagshipexamples/.../flagships/EngineShowcase.javarenders a single-page cinematic brand promo — a navy + electric-orange composition with a rounded clip-frame hero (semantic-graph → polished-PDFs visual metaphor), a magazine-headline lockup ("Documents as code. / Cinematic by default."), three KPI cards (Templates v2 · 1,033 tests · v1.6.6 Maven Central), a three-column capability grid (Semantic DSL · Deterministic Layout · Cinematic Themes), and a footer brand stripe — exercisingShapeContainerNode+ClipPolicy.CLIP_PATHfor the hero frame, classpath-loaded image embedding (examples/src/main/resources/engine-hero.png),softPanel(...)+accentLeft(...)decorators on V2 sections, and mixed serif/sans typography; (c) page 1 rasterised toassets/readme/repository_showcase_render.pngvia the new persistent helpercom.demcha.examples.support.PdfPageRasterizer(PDFBox-based, no external Ghostscript / ImageMagick dependency). The hero now reads as the engine's brand register rather than a Lorem-ipsum template render. docs/architecture/package-map.mdupdated alongside H2. A new intro paragraph documents the stability-marker convention (Stable default; engine packages are package-level@Internal; individual Extension SPI seams carved out of@Internalpackages carry@Beta), and thedocument.layoutrow calls outNodeDefinitionas the current@Betaseam.docs/api-stability.mdrevised alongside H2 —@Betaannotation reference cells in §1 are no longer hedged as "pending"; the associated quote block lists both annotations side-by-side with the guard tests that pin them.
Engine internals (no behaviour change)
RowSlotshelper extracted fromLayoutCompilerandNodeDefinitionSupport. The defence-in-depthIllegalArgumentExceptionguard added in v1.6.5 (PR-7.3) for the row weights / children size mismatch lived as duplicated inline code at both engine call sites with no direct test — a future refactor could have silently deleted either copy. The validation now lives incom.demcha.compose.document.layout.RowSlots#validateWeightsMatchChildren(package-private), withRowSlotsTestdriving it directly. Error message is unchanged.GraphCompose.DocumentBuilder#pageBackgrounds(...)Javadoc now spells out the empty-list-clears semantics in prose, not only in the@paramline.
Documentation
- New decision guide:
docs/templates/which-template-system.md(Track G1). The repo ships two parallel canonical template surfaces —cv.presets.*(the "classic" v1.6 rebuild) andcv.v2.presets.*(the layered architecture, recommended) — under confusingly similar names. The new page pins the terminology once, gives a status matrix (Recommended / Supported / Legacy / Internal) for every template surface and the canonical DSL, walks a decision tree for new code, and provides a preset-by-preset migration table fromclassictolayeredplus a 1.x → 2.0 deprecation inventory naming every type scheduled for removal.CanonicalSurfaceGuardTestallowlist updated so the deprecation-inventory section's literal mentions ofGraphCompose.pdf(...),PdfComposer, etc. don't trip the legacy-token scan (same allowlist class as the v1.5 → v1.6 migration log already in there). examples/README.mdreorganised by maturity (Track G2). The gallery section was grouped by the GraphCompose release that introduced each example (Built-in templates / Cinematic v1.5 / v1.5 feature showcases / v1.6 feature showcases / Public-API surface / Production patterns / Operational documents) — useful history for maintainers, less useful for someone landing on the examples folder for the first time. The gallery now categorises by maturity / intent: 🚀 Start here, 🧱 Core DSL, 📋 Templates recommended, 🔧 Advanced SPI, 🗄️ Legacy. All 26 examples retained their anchor IDs, so existing deep links continue to resolve; only the gallery index is restructured. A maturity legend introduces the five tiers and links todocs/templates/which-template-system.mdfor the V1 → V2 path that the Legacy tier points at.- New API stability policy:
docs/api-stability.md(Track G3). User-facing companion to ADR-0003: pins the four stability tiers (Stable, Extension SPI, Internal, Experimental) with what each one promises in patch / minor / major releases, the sealed-hierarchy permit-list policy (additive variants must degrade gracefully withoutdefault-branch failures), the deprecation window (≥ 1 minor release with@Deprecated, removed in next major), a per-package tier-lookup table for the canonical surface plus the legacy packages headed for 2.0 removal, and an "anti-policy" section (no pixel-stable PDFs, no bit-stable artefact bytes, no sealed-permit exhaustiveness across minor releases for Stable hierarchies).CanonicalSurfaceGuardTestallowlist extended so the page can namecom.demcha.templates.*/com.demcha.compose.v2.*and the legacypdf(Path)factory in the package-tier and deprecation-example sections.
v1.6.5 — 2026-05-30
Templates v2
- Added the
CenteredHeadlineCV preset to thecv/v2layered template surface, including its isolated theme tokens, visual regression baselines, and reusableSubheadline/SectionHeader.flatSpacedCapswidget support. - Added the Mint Editorial template set: a two-page, two-column
editorial CV preset
MintEditorial(centred spaced-caps masthead with a full-width mint accent rule; sidebar contact / interests / education / expertise / skill-bars / social beside a profile / experience / awards / references main column) and its pairedMintEditorialLetter, both onCvTheme.mintEditorial()and with visual regression baselines. - Added two reusable
cv/v2/widgets:SkillBar(data-driven proficiency bar — spaced-caps label above a track with a level-positioned marker; no bar when the level is absent) andIconTextRow(inline icon + text row, optionally a single click target), withWidgetSmokeTestcoverage. - Added optional proficiency levels to
SkillGroupvia the newCvSkillrecord andSkillsSection.Builder.leveledGroup(...). Fully backward-compatible: name-only skills carry no level and every existing name-based renderer is unaffected. - Added
MintEditorial.Options(and a matchingMintEditorialLetter.Options) — an additive masthead colour API (accent, rule, name, and an optional full-width page-1 header band) whose defaults reproduce the stock render exactly, so the committed look and the parity baselines are unchanged.
Public API
PageBackgroundFillband helpers. AddedtopBand,bottomBand,band,topBandPoints, andbandPointsfactory methods for full-width horizontal background bands (top, bottom, or arbitrary vertical offset; ratio- or point-based), complementing the existing column helpers and building on the v1.6.5 y-coordinate fix below.
Bug fixes
PageBackgroundFilly-coordinate. A partial-height page-background fill (heightRatio < 1.0) was painted from the page bottom upward instead of from theyRatiotop edge the API documents, so a band withyRatio = 0rendered at the bottom of the page. Fills now convert the top-down ratios to the PDF bottom-up origin correctly (y = (1 - yRatio - heightRatio) * pageHeight); full-page and full-height column fills are unchanged. Adds top-/bottom-/mid-band regression tests.GraphCompose.document().pageBackgrounds(emptyList())now actually clears. The builder's Javadoc promised that an explicit empty list overrides any earlierpageBackground(color)on the same builder, but the implementation skipped empty lists, sopageBackground(LIGHT_GRAY)followed bypageBackgrounds(List.of())still emitted the grey background. The guard is removed; the empty list is now the documented clear. Adds a regression test.distributeRowSlotWidthsweights / children mismatch. When a row was constructed with aweightslist whose size did not match the number of children (only reachable by bypassingRowBuilderand building aRowNodedirectly), the engine's row distribution code walked off the end of theweightslist with a rawIndexOutOfBoundsException. Both row-distribution call sites (LayoutCompiler#distributeRowSlotWidths,NodeDefinitionSupport#measureRow) now reject the mismatch with anIllegalArgumentExceptionwhose message names both sizes and the expected fix.RowNode's canonical constructor already validated this at construction time; the new engine guards are defence-in-depth for any path that bypasses it (e.g. reflection-based deserialization). Adds regression tests for the canonical-constructor IAE and theRowBuilder.build()ISE.
Build
byte-buddyis now<scope>test</scope>. Mockito already excludes its transitivebyte-buddyand the project pins a single version in a standalone dependency; that dependency was missing a scope, so the published POM advertisedbyte-buddyas a compile dependency even though no production code references it. Setting<scope>test</scope>keeps the version pin but keepsbyte-buddyout of consumers' runtime classpath (mvn dependency:treeshows it only as:test).- CI
exec-maven-pluginversion drift removed. The CI workflow's three benchmark steps invokedorg.codehaus.mojo:exec-maven-plugin:3.5.0:javadirectly, whilebenchmarks/pom.xmlalready declaredexec-maven-pluginat3.6.3for local runs — a silent version split between CI and local invocations that grew the surface area to keep aligned. CI now calls the configured plugin viaexec:java, picking up the pinned3.6.3frombenchmarks/pom.xml. No behaviour change; one fewer hardcoded version to bump.
v1.6.4 — 2026-05-22
Bug fix + structured-block patch. Adds two new public Block types —
WorkHistoryBlock and EducationBlock — that let template authors
declare work-history and education entries with explicit (title,
organisation, date, description) / (degree, institution, year,
details) fields instead of relying on the legacy
MultiParagraphBlock pipe-separated string parser. Also closes a
Boxed Sections layout defect that bundled the date and description
into the right-aligned date column for any author-supplied line that
used an em-dash (" — "), en-dash (" – "), or contained
prose-shaped content the parser misread as a date. No public API
break — the sealed Block permit list grows from six to eight,
existing MultiParagraphBlock work-history strings continue to
parse, and the deprecated parser path stays in place for backward
compatibility.
Templates — new structured blocks
WorkHistoryBlock. New public record block carrying a list ofItem(title, organisation, date, description)entries. TheBoxedSectionspreset renders each item as a structured row: title bold on the left, date right-aligned on the same row, organisation italic on the next line under the title, and description as a full-width paragraph beneath. Other presets fall back to a single concatenated paragraph per item. Authors who useWorkHistoryBlockbypass the legacyBoxedSections#parseWorkEntryheuristic parser entirely.EducationBlock. New public record block carrying a list ofItem(degree, institution, year, details)entries. Renders with the same structured layout asWorkHistoryBlock(degree bold left, year right, institution italic, details paragraph) so Education & Certifications sections visually match Professional Experience.- Sample data migrated.
ExampleDataFactory.sampleCvSpecV2now usesWorkHistoryBlockfor Professional Experience andEducationBlockfor Education & Certifications. The legacyMultiParagraphBlockpattern remains supported and is exercised byPresetLayoutSnapshotTest/PresetVisualParityTestto lock the backward-compat path.
Templates — parser robustness (legacy path)
parseWorkEntryaccepts em-dash and en-dash. Used to split the post-pipe segment on ASCII" - "only; now tries" — "," – ", and" - "in order, mirroringsplitHeading. Authors who typed"*2024-Present* — Led reusable document flows."saw the whole tail collapse into the date column — this no longer happens.parseWorkEntryrejects prose dressed up as a date. The looselooksLikeDatecheck accepted any string containing a year and a hyphen anywhere, which caused education lines like"... | 2019. First-class honours. Specialisation ..."to parse as work entries (the hyphen inside"First-class"was enough to satisfy the heuristic). Parser now rejects post-pipe segments that contain sentence-ending punctuation (.,:,;) when no explicit date / description separator was found, letting these lines fall back to plain paragraph rendering. Marked@Deprecatedwith a@deprecatedJavadoc pointing callers toWorkHistoryBlock/EducationBlock.parseProjectItempicks up the same em-dash / en-dash / ASCII separator set so future Project items typed with em-dash don't regress into "title only" rendering.
Tests
BlockTest.blockSealingPermitsAllEightVariantsupdated for the two new permitted block types.PresetVisualGalleryTest.sampleSpecmigrated toWorkHistoryBlockso the visible "primary example" exercises the new structured shape.PresetLayoutSnapshotTestintentionally retained onMultiParagraphBlockto lock the legacy parser's behaviour.
v1.6.3 — 2026-05-22
Bug fix patch. Closes two independent hyperlink clickable-area defects that surfaced on CV gallery presets and made the LinkedIn / GitHub contact rows hijack each other's clicks (paragraph-level link path) or drift past their visible text (span-level link path through multi-space separators). No public API change — engine, DSL, themes, templates, and backend records all stay source-compatible with v1.6.2.
Engine
- Paragraph-level link annotations now hug rendered text.
PdfFixedLayoutBackendused to emit a paragraph'slinkOptionsas a single rectangle covering the entire fragment box (fragment.x()+fragment.width()), ignoringTextAlign.RIGHT/TextAlign.CENTER. Stacked right-aligned contact paragraphs (e.g. one per LinkedIn / GitHub icon row in Timeline Minimal / Sidebar Portrait / Monogram Sidebar) therefore produced full-column-wide rects that overlapped the empty alignment gap of neighbouring rows — hovering over GitHub clicked the LinkedIn row. The backend now emits one per-line rect tight toline.width()positioned at the alignment-awarelineX, matching how inline-span links already worked. Span-level link emission, table / shape / barcode payload links, and bookmark anchoring are unchanged. - Glyph sanitizer preserves all author whitespace.
PdfFont.sanitizeForRenderused to collapse any run of consecutive spaces into a single space, both for whitespace-only tokens (the" "halves of a" | "separator) and for inter-word gaps in spaced-caps strings (spacedUpper("ARTEM DEMCHYSHYN")produces"A R T E M D E M C H Y S H Y N"with deliberate triple-spaces between words). The collapse shrank the rendered glyph stream under measurement, drifting inline-link rectangles ~8pt per" | "separator past their visible labels and visually merging spaced-caps titles back into a single run ("A R T E M D E M C H Y S H Y N"— no word boundary). The sanitizer no longer collapses adjacent spaces; newlines / NBSP / non-tab control characters still resolve to a single space each, but author whitespace is now preserved verbatim so wrap geometry, link-rectangle emission, andshowText(...)all see the same string. Layout snapshot baselines for five CV presets and one nested-list document widened to reflect the recovered whitespace — the deliberate visual change is the bug fix.
Templates
- Boxed Sections projects render as title + indented description.
The "Projects" module now renders each bullet-list or
IndentedBlockitem as two stacked paragraphs — bullet plus bold project name (with an optional tech-stack chunk in parentheses) on the first line, then a hanging-indented description below aligned to the project name (not the bullet). The previous single-line rendering ran the project name and description together. Bullet marker, hanging-indent, and surrounding modules are unchanged. Example data inExampleDataFactory.sampleCvSpecV2andPresetVisualGalleryTestnow ships tech-stack chunks ("Java 21, PDFBox, Maven, JMH") so the gallery PDFs reflect the new layout.
Tests
- New regression in
PdfFixedLayoutBackendFeaturesTest—shouldTightlyHugRightAlignedParagraphLinkRectangles— stacks three right-aligned link paragraphs and asserts each clickable rect hugs its rendered label width (≤ 150pt), sits flush against the inner right margin, and does not overlap the Y-band of neighbouring rows. - New regression in
PdfFixedLayoutBackendFeaturesTest—shouldKeepCenteredInlineLinkRectanglesAlignedAcrossMultiSpaceSeparators— renders a centered contact line built with" | "separators and asserts the three resulting link rectangles preserve left-to-right order with non-overlapping X ranges and a sane per-separator gap (5..40pt), pinning the bug where collapsed whitespace pushed later rects past the line. - New regression in
PdfFontSanitizerTest—sanitizeForRender_preservesWhitespaceOnlyTokensVerbatim— pins the whitespace-only short-circuit so render width stays in lockstep withgetTextWidthfor tokenised contact-line separators.
v1.6.2 — 2026-05-20
Robustness patch. Closes four engine defects surfaced while building
the Noir corporate CV example: any Unicode glyph that the active PDF
font cannot encode used to crash the whole render, rounded human
input for page sizes hit a 1e-6 capacity check, a Row inside a
LayerStack content layer was rejected by the validator, and the
existing exceptions did not point at a fix. No public API change
— engine, DSL, themes, templates, and backend records all stay
source-compatible with v1.6.1.
Engine
- Glyph sanitizer on every PDF text render path. Any code point
the resolved font cannot encode (arrows
U+2192, bulletsU+25CF, emoji, custom unicode) is now substituted with?instead of throwingIllegalArgumentExceptiondeep inside PDFBoxshowText. NewPdfFont.sanitizeForRender(TextStyle, String)is the single entry point the paragraph / watermark / header-footer / table / block-text handlers route through; width measurement (PdfFont.getTextWidth) uses the same string so wrap geometry stays in lockstep with the bytes drawn. First substitution per unique(font, codePoint)emits a one-shot WARN through the newGlyphFallbackLogger(categorycom.demcha.compose.engine.render.pdf.glyph-fallback); subsequent substitutions are silent. - Page capacity rounding tolerance. The full-page check in
LayoutCompilernow uses a dedicatedCAPACITY_TOLERANCE = 0.5pt(≤ 0.18 mm — visually indistinguishable) instead of the floating-pointEPS = 1e-6. Authors who size content at the rounded842.0ptagainst the true A4 inner height841.88977ptno longer hitAtomicNodeTooLargeException; overflows of more than 1pt still throw.EPSstays as is for split / remaining- height decisions inside the splittable-leaf path. Rowallowed insideLayerStackcontent layer. New privateFixedSlotKind { ROW_SLOT, STACK_LAYER_SLOT }is threaded throughcompileNodeInFixedSlotand propagated down recursive calls. Validator at the row-in-fixed-slot guard now rejects nested horizontal rows only when the parent slot is a real row band; aRowdirectly inside aLayerStacklayer rectangle (or any vertical descendant thereof) is now a normal column-row and compiles cleanly. Row-in-row still throws — the relaxation does not leak into theROW_SLOTpath.- Exception messages now include action verbs. The five
engine-thrown exception messages
(
AtomicNodeTooLargeExceptionplus fourIllegalStateExceptions inLayoutCompiler) now say what to try, not just which rule fired: "Reduce the node height, split content into multiple atomic blocks, or increase the page size"; "Wrap the inner row in a LayerStack layer (allowed since v1.6.2), or stack horizontal content as sections inside a vertical column"; etc. Existing substring-based test assertions stay green.
Tests
- New
PdfFontSanitizerTestpins the sanitizer contract: substitution policy for unsupported glyphs, ASCII pass-through, single + collapsed spaces, empty / null input, width consistency between bullet input and?substitute, and the directsanitizeByFontescape hatch used by raw-PDFonthelpers. - New
LayerStackRowCompositionTestpins both ends of the R3 contract: three positive cases (row directly inside a layer, the Noir-CV shape with a dark band + sidebar/main row, row deep inside a layer through vertical sections) and one negative guard (row-inside-row still throws). PaginationEdgeCaseTestgainsatomicNodeHalfPointOverCapacityShouldFitWithinToleranceandatomicNodeClearlyOverCapacityShouldStillThrowboundary cases for the new capacity tolerance.- Three new visual regression demos write real PDFs under
target/visual-tests/for manual review:glyph-fallback/UnicodeFallback*.pdf(paragraph + table + watermark + header/footer all with unsupported glyphs),page-capacity/PageCapacityToleranceDemo.pdf(842pt shape on A4),layer-stack/LayerStackRowDemo.pdf(full Noir-CV shape). - New
DevelopTestscratch test class undersrc/test/java/com/demcha/testing/visual/renders a minimal document for manual API experimentation; output lives attarget/visual-tests/develop/Develop.pdf. - One layout snapshot baseline
(
document/nested_list_three_levels.json) updated as a deliberate consequence of the new font-aware width measurement:ListBuilderdefault markers for deep nesting (◦ U+25E6,▪ U+25AA) are outside Helvetica's WinAnsi coverage, so they now substitute to?and widen the list rectangle. Follow-up tracked: ship safer ASCII / font-aware list marker defaults in v1.7.
Documentation
README.mdgains a Companion projects section linking the experimentalgraphcompose-ai-flowsister repo (independent codebase, separate lifecycle, no dependency from this repo).- Maintainer email in
pom.xmlandCODE_OF_CONDUCT.mdcorrected from the non-existentdemchyshyn.artem@gmail.comto the real inboxdemchishynartem@gmail.com, so JitPack artifact metadata and CoC enforcement contact resolve.
v1.6.1 — 2026-05-09
Maintenance + compatibility patch. Drops the Java 21 source/target baseline to Java 17+ so the library can ship into older enterprise stacks without a fork, and refreshes test/build dependencies. No public API change — engine, DSL, themes, templates, and backend records all stay source-compatible with v1.6.0; existing v1.6.0 callers compile and behave unchanged.
Co-developed with external contributor @jottinger (#8, #10).
Toolchain
- Java 17 baseline.
<maven.compiler.release>flips from21to17acrosspom.xml,examples/pom.xml, andbenchmarks/pom.xml. Engine source loses the Java 21–only constructs (switch-with-type-patterns, switch-with-deconstruction,List.getFirst(),Thread.threadId()) in favour of Java 17 –compatible forms. CI runs against Temurin JDK 17. - Dependency refresh + CVE pass. Bumps Jackson
2.20.1 → 2.21.3, Logback1.5.18 → 1.5.32, Lombok1.18.38 → 1.18.46, POI5.4.0 → 5.5.1, SnakeYAML2.4 → 2.6, AssertJ3.27.3 → 3.27.6, JUnit5.12.2 → 5.14.4, Mockito5.20.0 → 5.23.0. Adds explicit ByteBuddy1.18.7so Mockito works on the Java 25+ access rules. Maven plugin bumps:maven-compiler-plugin 3.13 → 3.15,maven-surefire-plugin 3.2.5 → 3.5.5,exec-maven-plugin 3.5 → 3.6.2.
Looking ahead
Maven Central distribution (#7) remains on the v1.7.0 roadmap alongside the JMH benchmark migration; v1.6.1 stays on JitPack as a maintenance release.
v1.6.0 — 2026-05-07
The "expressive" release. Closes the remaining canonical-vs-legacy
parity gaps for advanced authoring without architectural rollback.
Every new primitive ships through DocumentNode + NodeDefinition + render handler. See docs/roadmaps/v1.6-roadmap.md
for the phased plan, verification gates, and ADRs.
Headline — "expressive"
- Nested list ergonomics (Phase A — landed).
ListBuilder.addItem(label, Consumer)for builder-callback child scopes; per-depth marker cascade; mixed flat / nested authoring preserves source order. ADR 0012. - Composed table cell content (Phase B — landed).
DocumentTableCell.node(DocumentNode)accepts any composable canonical node as cell content (paragraphs, lists, layer-stacks, sub-tables) with two-pass measurement. ADR 0013. - Controlled free-canvas placement (Phase C — landed).
CanvasLayerNode— pixel-precise(x, y)placement of children inside a fixed-size bounding box, withClipPolicyclipping and atomic pagination. ADR 0014. - Templates v2 preset library (committed). Canonical CV /
cover-letter / invoice / proposal surface rebuilt around four
layers (theme tokens → layout slots → components + blocks →
spec data); 14 CV presets and 14 paired cover-letter presets
with one-liner
create(BusinessTheme)factories, inline markdown, hyperlinks, and slot-based multi-column layouts. ADR 0011. - Architecture hardening (committed).
@InternalAPI stability marker, publicPdfFragmentRenderHandlerSPI,DocumentRenderingExceptionon the convenience render path, thread-safety contract documented. ADRs 0003 + 0004. - Verify gate: 819 / 0 / 0 / 0 (
mvnw verify). 26 runnable examples regenerate cleanly throughGenerateAllExamples.
Architecture hardening (committed in v1.6 line, develop)
The architecture lane closes the highest-severity findings from the
post-1.5 audit. None of these change author-facing behaviour for
unmodified v1.5 code; they sharpen the public-vs-internal boundary,
open extension points, and split the load-bearing files. See
docs/roadmaps/migration-v1-5-to-v1-6.md
for the user-facing summary.
@InternalAPI stability marker. Newcom.demcha.compose.document.api.Internalannotation (runtime-retained) marksdocument.layout.*and theBuiltInNodeDefinitionspayload records as implementation detail.InternalAnnotationCoverageTestenforces propagation. ADR 0003 records the boundary decision.DocumentRenderingExceptionwraps the convenience render path:buildPdf,writePdf,toPdfBytes, and the AutoCloseableclose()override no longer declarethrows Exception. Lower-level backend SPIs continue to declarethrows Exceptionon purpose.- Public PDF render handler SPI. The
PdfFragmentRenderHandlerJavadoc is rewritten as an extension point andPdfFixedLayoutBackend.Builder.addHandler(...)is the new registration path. Custom handlers replace built-in defaults bypayloadType(). ADR 0004 records the SPI shape. - Thread-safety contract documented on
document.api/package-info.javaanddocument.backend.fixed.pdf/package-info.java. - DSL polish.
DocumentDsl.text()andDocumentSession.builder()aliases are@Deprecated(forRemoval=true, since="1.6.0"); preferparagraph()anddsl()respectively. - PDF-typed chrome overloads on
DocumentSession—metadata,watermark,protect,header,footeracceptingPdf*Options— are@Deprecated(forRemoval=true, since="1.6.0"); the canonical backend-neutral overloads are unchanged. DocumentPalette.builder()replaces the positionalDocumentPalette.of(Color × 7)factory; the old factory is@Deprecated(forRemoval=true).BusinessTheme.classic()/modern()/executive()now use the builder.IllegalStateExceptionfrombuild()names every missing token in one message.- Targeted layout perf wins (none alter output bytes):
LayoutCompiler.compositeDecorationFragments/compositeOverlayFragmentsno longer wrap withList.copyOf,stableZIndexOrdershort-circuits when every layer reports the samezIndex,PdfRenderSessionkeeps page surfaces in aPDPageContentStream[](noInteger.valueOfautoboxing),PdfFontLoader.THREAD_LOCAL_TTF_CACHEis a bounded LRU (max 32 entries per thread). Duplicatecom.demcha.compose.font.Pdf_FontLoaderdeleted. - Layout invariant tests.
LayoutCompilerInvariantsTestpins four scenarios that previously had only transitive snapshot coverage: page-advance on overflow, layer source-order under uniformzIndex, explicitzIndexordering, equal-weight row slot distribution. BuiltInNodeDefinitionssplit (Phase E.1). All 15 built-inNodeDefinitionimplementations now live indocument.layout.definitions.*(one file per node type): PageBreak, Spacer, Shape, Line, Ellipse, Image, Barcode, Container, Section, Row, LayerStack, ShapeContainer, Table, Paragraph, List. Shared inline helpers (EPS, transform wrapping, decoration / table / measurement adapters) live inNodeDefinitionSupport; the paragraph / list text-flow cluster (wrapping, markdown tokenisation, inline-run layout, split slicing) lives in the newTextFlowSupporthelper.BuiltInNodeDefinitionsdrops from 3,037 to ~60 lines and now only exposesregisterDefaults(NodeRegistry)as the single registration entry point.PlacementContextstrategy interface (Phase E.4). A newPlacementContextsealed interface unifies the placement bookkeeping thatLayoutCompilerhelpers need (current page index, canvas, prepare/fragment contexts, target lists for placed nodes/fragments, and acanAdvancePage()/advancePage()/touchPage()strategy).FixedSlotPlacementContextpins the page for row slots, stacked layers, and atomic leaf placement;MutatingPlacementContextwraps the liveCompilerStatefor callers that drive top-down page flow. The previously private innerCompilerStateis lifted to a sibling package-private class.placeStackLayer,placeAtomicLeafFragments, andcompileNodeInFixedSlotnow takePlacementContextinstead of six explicit parameters each. Pure refactor — no public API change, no behaviour change.DocumentSessionslim (Phase E.3). NewSessionFontApifacade (session.fonts()) groupsregisterFontFamily(FontFamilyDefinition)andregisterNodeDefinition(NodeDefinition)alongside the existingchrome()andlayout()facades. Page-background composition moves from a private inner method to a newDocumentPageBackgroundsutility. The four convenience PDF methods (toPdfBytes,writePdf,buildPdf,buildPdf(Path)) share a singlewrapPdfRenderingexception-mapping helper instead of repeating the same try/catch four times. Javadoc on the deprecated PDF-typed chrome overloads is compacted to a single@deprecatedtag.DocumentSessiondrops from 1,024 to ~937 lines without changing any public method signatures.
Templates v2 restructure (committed in v1.6 line, develop)
The biggest change in v1.6 — the canonical template surface
(CV, cover letter, invoice, proposal) was rewritten from the ground
up. Old positional / cinematic-monolith composers (CvTemplateV1,
NordicCleanCvTemplate, InvoiceTemplateV2, etc.) replaced with a
four-layer architecture: **Theme tokens → Layout slots → Components
- Blocks → Spec data**, glued together by per-domain builders that preset classes wrap into one-liner factories. The result is a copy-and-tweak preset surface where adjusting one visual decision takes one method change rather than a fork of a 600-line composer.
New template package layout (replaces legacy templates/builtins,
templates/support/cv, templates/data/cv, templates/theme/CvTheme):
templates/
api/ DocumentTemplate<S>, SlotMap
themes/ Spacing, Typography (token records)
components/ Header, Module, MarkdownText
blocks/ sealed Block hierarchy:
ParagraphBlock, BulletListBlock, NumberedListBlock,
IndentedBlock, KeyValueBlock, MultiParagraphBlock
decorations/ Spacer, Divider, AccentStrip
cv/
layouts/ SingleColumn, TwoColumnSidebar, ThreeColumnMagazine
presets/ 14 flat copy-and-tweak preset classes
builder/ CvBuilder
spec/ CvSpec, CvHeader, CvModule
coverletter/
layouts/ LetterFormat
presets/ 14 paired letter presets (one per CV preset)
builder/ CoverLetterBuilder
spec/ CoverLetterSpec, CoverLetterHeader
invoice/
presets/ ModernInvoice (minimal v2 surface)
builder/ InvoiceBuilder
spec/ InvoiceSpec
proposal/
presets/ ModernProposal (minimal v2 surface)
builder/ ProposalBuilder
spec/ ProposalSpec
14 CV presets: ModernProfessional, NordicClean,
ClassicSerif, CompactMono, Executive, EngineeringResume (was
TechLeadCvTemplate), TimelineMinimal, BoxedSections,
CenteredHeadline, BlueBanner, EditorialBlue, Panel (was
ProductLeaderCvTemplate), SidebarPortrait, MonogramSidebar.
Each is one final class with one create(BusinessTheme) factory:
import com.demcha.compose.document.templates.cv.presets.ModernProfessional;
import com.demcha.compose.document.templates.cv.spec.CvSpec;
import com.demcha.compose.document.theme.BusinessTheme;
DocumentTemplate<CvSpec> template = ModernProfessional.create(BusinessTheme.modern());
template.compose(session, mySpec);
Inline markdown rich text — body strings carrying
**bold** and *italic* markers render with proper
{@code DocumentTextDecoration} via the new
templates.components.MarkdownText parser. Lets an LLM emit a
resume bullet like **Java 21**, SQL, Kotlin and the preset
renders Java 21 in bold without separate inline-run construction.
Active hyperlinks — header email + LinkedIn / GitHub labels
become clickable mailto: / https: hyperlinks via
DocumentLinkOptions on per-run inline runs.
Slot-based layouts — multi-column CV presets
(Panel, SidebarPortrait, MonogramSidebar) declare named
slots (MAIN, SIDEBAR); a custom preset can rearrange which
modules go into which slot via .place(slot, "Module Name", ...).
Layout snapshot tests lock the rendered tree of every preset
(28 baselines under
src/test/resources/layout-snapshots/canonical-templates/cv-v2/
and .../coverletter-v2/).
Examples — CvTemplateGalleryFileExample renders all 14 v2
CV presets to examples/target/generated-pdfs/cv-<id>.pdf; new
CoverLetterTemplateGalleryFileExample renders all 14 paired
letter presets to cover-letter-<id>.pdf.
Migration: legacy classes have been deleted, not
deprecated. Anyone on
new CvTemplateV1() / new NordicCleanCvTemplate() / etc. must
switch to the new factory:
| Old | New |
|---|---|
new CvTemplateV1() | ModernProfessional.create(BusinessTheme.modern()) |
new NordicCleanCvTemplate() | NordicClean.create(BusinessTheme.modern()) |
CvTheme.defaultTheme() | BusinessTheme.modern() + Spacing.compact() |
CvTemplate interface | DocumentTemplate<CvSpec> |
InvoiceTemplateV2 and ProposalTemplateV2 (cinematic) remain
in templates/builtins/ as the recommended path for fully-styled
output; the new ModernInvoice / ModernProposal v2 presets
provide the canonical builder seam, with cinematic feature parity
landing in a follow-up.
Phase E.1 reopen — visual parity recovery (May 2026)
The first Phase E.1 pass shipped visually-broken renders.
Every CV preset rendered as a teal-tinted single-column
ModernProfessional clone — NordicClean lost its sidebar and
soft-tinted PROFILE panel, BlueBanner lost its full-width
section banners, MonogramSidebar lost its monogram badge,
SidebarPortrait lost its portrait sidebar, ClassicSerif
lost its two-page editorial structure, and so on. The
ModernProfessionalVisualParityTest was a smoke test
(assertThat(output).exists()) and PresetLayoutSnapshotTest
recorded baselines from the new (broken) v2 renders without
comparing against V1, so the regressions sailed through CI.
Phase E.1 was reopened. All 14 CV preset renders + 14
cover-letter pair renders were rebuilt against the V1 visual
references (committed assets/readme/examples/cv-*.pdf for
the 7 presets that had a baseline; V1 source code under
docs/private/v1-reference/ — gitignored — for the rest).
The author-facing API stays stable; only the rendered output
changed.
- Adaptive sidebar fill —
SidebarPortraitandMonogramSidebarsize the trailing spacer dynamically fromcanvas().innerHeight()so the SIDEBAR_BG fill reaches the bottom of the page on A4 / Letter / smaller test fixtures without overflowing the row's page capacity. HeaderAPI gained three fluent overrides —withNameStyle(DocumentTextStyle),withContactStyle(DocumentTextStyle),withLinkStyle(DocumentTextStyle). Required for V1-parity palette (e.g. slate-blue name + royal-blue underlined links for ModernProfessional) — the unstyledHeader.rightAlignedrendered names with the activeBusinessTheme'sh1()colour instead.CvHeader.jobTitlefield added for the subtitle rendered under the name by presets that surface it (EditorialBlue, Panel, SidebarPortrait, MonogramSidebar). Falls back to a placeholder string when the spec leaves it empty.- Markdown rendering routed through
MarkdownText.parsein every CV / cover-letter preset paragraph body so spec-author bold / italic markers (**bold**/*italic*) carry through to the rendered runs — previously the paragraphs stripped markdown. - Sample data factory updated so
Education/ProjectsuseMultiParagraphBlockwith markdown bold prefixes (**MSc Computer Science** - University of Manchester | 2021) rather thanIndentedBlock's multi-line shape, andAdditional InformationcarriesKeyValueBlockentries (Languages / Work Eligibility) for bold-key + plain-value rendering. - Snapshot baselines regenerated — 28
*.jsonfiles undersrc/test/resources/layout-snapshots/canonical-templates/cv-v2/andcoverletter-v2/updated to lock the V1-parity render in place. - Pixel-diff visual parity gate landed.
PresetVisualParityTest(one for CV, one for cover letters) rasterises each preset's PDF page 0 (andclassic_serif's page 1) via PDFBoxPDFRendererand asserts per-pixel diff against a checked-in baseline PNG with budget 2500 mismatched pixels at per-channel tolerance 8 (pertemplates-restructure-plan.mdsec 6.2). 29 baselines undersrc/test/resources/visual-baselines/{cv-v2,coverletter-v2}/. Re-bless with-Dgraphcompose.visual.approve=true. ThePdfVisualRegressionharness was already built; the reopen plugged the 28 presets into it. The placeholderModernProfessionalVisualParityTestsmoke test is deleted.mvnw verify→ 792 / 0 / 0 / 0.
Tech debt (deferred to v1.7 as Phase E.4): 13 of the 14
v2 CV presets are implemented as hand-coded
DocumentTemplate subclasses driving the canonical
PageFlow DSL directly (≈ 400-700 LOC each) rather than thin
recipes through the slot-based CvBuilder. This was an
explicit trade-off during the reopen — restoring V1 visual
fidelity required components the v2 library hadn't grown
yet (Panel.softTinted, TwoColumnSidebar.tinted,
SectionStyle.uppercaseRule, WorkEntryRenderer). The
component library extension + preset refactor is tracked in
docs/private/templates-restructure-plan.md Phase E.4 and
docs/private/templates-v2-audit-remediation.md.
Feature scope
- Nested list ergonomics (Phase A — landed).
ListBuilder.addItem(String label, Consumer<ListBuilder> body)appends a nested item with a builder-callback child scope. NewListItemrecord carries(label, marker, children).ListNodegains anestedItemscomponent (record now has 12) and a back-compat 11-component constructor matching the v1.5 shape. Per-depth marker resolution: item-level marker wins, thenListBuilder.markerFor(int depth, ListMarker)overrides, then the built-in cascade (•→◦→▪→·). The internalusedNestedAuthoringflag preserves source order across mixed flat / nested entries — flat-only callers still get the v1.5 flatListNode. Layout flattens the tree depth-first into indent-prefixed paragraph fragments using non-breaking spaces (U+00A0) for the per-depth indent so the paragraph wrap pipeline preserves them. ADR 0012 records theListNode-extension-vs-new-NestedListNodedecision. Snapshot baseline:src/test/resources/layout-snapshots/document/nested_list_three_levels.json.mvnw verify→ 804 / 0 / 0 / 0. - Composed table cell content (Phase B — landed).
DocumentTableCell.node(DocumentNode)factory accepts any composable canonical node as cell content;DocumentTableCellgains a 5th componentDocumentNode contentwith explicit 4-arg / 3-arg / 2-arg back-compat constructors so v1.5 plain-text callers compile unchanged.TableLayoutSupportthreadsPrepareContextthroughresolveTableLayoutand prepares each composed cell's child against the cell's resolved inner width before row-height resolution; the prepared height feeds the existing two-pass row-height pass.FragmentContextgains a defaultemitChildFragments(PreparedNode, FragmentPlacement)method thatDocumentLayoutPassContextoverrides to dispatch through the registeredNodeDefinition— so any node type works inside a cell automatically (paragraph, list, layer-stack, sub-table). Pagination preserves row-by-row behaviour: a composed cell stays atomic on its row, andsliceTablePreparedNodesubsets the prepared-content map to the slice's row range while keeping repeat-header keys intact. The PDF table render handler is unchanged: it still iteratescell.lines()(empty for composed cells) and the child fragments render through their own already-registered handlers. ADR 0013 records the extend-vs-new-hierarchy and recursion-vs-special-case decisions. Snapshot baseline:src/test/resources/layout-snapshots/document/table_cell_with_paragraph.json.mvnw verify→ 810 / 0 / 0 / 0.
Feature scope (continued)
- Controlled free-canvas (Phase C — landed). New
CanvasLayerNodeatomic composite accepts children at explicit(x, y)pixel coordinates inside a fixed-size bounding box. Coordinates use the screen convention:(0, 0)is the canvas's top-left, positivexextends right, positiveyextends downward. NewCanvasChildrecord carries(node, x, y).CanvasLayerBuilderexposesposition(child, x, y),size(width, height),clipPolicy(...)and is plumbed throughAbstractFlowBuilder.addCanvas(width, height, Consumer).CanvasLayerDefinitionreuses the existingLayerStackNodeplacement plumbing — every child anchors atLayerAlign.TOP_LEFTand the canvas's(x, y)maps one-to-one onto the stack layout's(offsetX, offsetY). Pagination is atomic; clip policy defaults toClipPolicy.CLIP_BOUNDSand reuses theShapeContainerNodeclipping pipeline. The canvas's measured size is explicit (independent of children) so the surrounding flow reserves a deterministic rectangle. ADR 0014 records whyCanvasLayerNodeis a separate node and why absolute placement is rejected as a global policy onRowBuilder/SectionBuilder. Snapshot baseline:src/test/resources/layout-snapshots/document/canvas_layer_basic.json. Showcase:examples/.../CanvasLayerExample.java.mvnw verify→ 819 / 0 / 0 / 0.
Deferred to v1.7
These were on the v1.6 stretch list and did not land in time; they carry over to v1.7.
- Phase D — Real PPTX semantic export. Build out
PptxSemanticBackendfrom the existing manifest skeleton to a working POI-based exporter (paragraphs → text boxes, tables → PowerPoint tables, sections → slides). - Phase E — Maven Central distribution. Sonatype OSSRH + GPG
signing + automated deployment on tag push. Primary install
coordinates switch to
io.github.demchaav:graphcompose:1.7.0; JitPack stays documented as a fallback. - Phase F — Benchmark infrastructure modernisation. Replace the
custom warmup / measurement harness with
org.openjdk.jmhfor JIT-aware measurement, dead-code elimination protection, and proper statistical output. Move the benchmark suite (currently in test scope:CurrentSpeedBenchmark,ComparativeBenchmark,ScalabilityBenchmark,FullCvBenchmark,GraphComposeBenchmark) into a separatebenchmarks/Maven module mirroring theexamples/pattern, with a self-executing JMH jar built viamaven-shade-plugin. Add a standalonelayoutGraph()-only scenario so the README can publish a true Layout-vs-Render table backed by independently measured values rather than stage breakdown subtractions. CI Performance Smoke Check switches to the new JMH jar;scripts/run-benchmarks.ps1becomes a thin wrapper so the documented workflow keeps working.
Non-goals
- No revival of
GraphCompose.pdf(...)or publicEntityManager. - No nested rows or nested tables inside
RowBuilder(preserves pagination contract). - No DOCX path-clipping or transform support (Apache POI limit).
- No deprecation of v1.4 / v1.5 public records — back-compat constructors stay.
v1.5.1 — 2026-05-05
Dependencies
- PDFBox 3.0.7. Bumped from 3.0.5 to 3.0.7 (Apache PDFBox patch release with upstream rendering and security fixes). No public-API impact for GraphCompose consumers.
Tooling
ShapeContainerVisualRegressionTesttolerates the cross-platform PDF font-rendering drift that surfaces between Windows-rendered baselines and the Linux CI runner (~1-2% pixel diff), via a calibratedmismatchedPixelBudgetinstead of bit-exact comparison.DocumentationCoverageTestno longer pins to the structural section anchors that the v1.5.0 README slim removed; the guard now scans the whole README for canonical-DSL coverage and legacy-API leakage in one whole-file pass.
This is a maintenance patch release. There are no public API changes; v1.5.0 consumers can upgrade with no code changes.
v1.5.0 — 2026-05-04
Headline — "intuitive"
v1.5 keeps every v1.4 cinematic primitive and turns the canonical
authoring surface into a polished, theme-driven experience. Three new
visual feature pillars — shape-as-container with clip path,
transforms (rotate / scale) + per-layer z-index, and advanced
tables — combine with two new cinematic templates
(InvoiceTemplateV2, ProposalTemplateV2), a CvTheme ↔
BusinessTheme bridge (ADR 0002), six modernised CV templates,
and a documentation pass that covers every new primitive with a recipe
and a runnable example. Test count grew from 525 (v1.4.1) to 675 — an
extra +150 tests across the cinematic, transform, table, theme-bridge,
streaming, snapshot, CV-render, and Transformable-leaf-builder surfaces.
v1.5 is fully source-compatible with v1.4. Every public record
that grew a new field ships back-compat constructors that default the
new value, so v1.4 callers compile and behave unchanged. See
docs/roadmaps/migration-v1-4-to-v1-5.md.
Public API — visual primitives
- Shape-as-container. New
addCircle(diameter, fill, inside),addEllipse(w, h, fill, inside), andaddContainer(...)shortcuts onAbstractFlowBuilderbuild aShapeContainerNodewhose bounding box is dictated by aShapeOutline(Rectangle,RoundedRectangle,Ellipse, plus acircle(diameter)factory). Children are clipped via the newClipPolicyenum (CLIP_PATH— default — /CLIP_BOUNDS/OVERFLOW_VISIBLE). The PDF backend honours every clip policy via graphics-statesaveGraphicsState() + clip(path)markers; the DOCX backend renders layers inline without the outline frame and logs a one-timedocx.export.shape-container-fallbackcapability warning.ShapeContainerBuilderexposes the same nine-point alignment vocabulary asLayerStackBuilderplusposition(node, dx, dy, anchor)for screen-space nudges. - Transforms (rotate / scale). New
com.demcha.compose.document.style.DocumentTransformvalue type withrotate(deg),scale(uniform),scale(sx, sy)factories pluswithRotation(...)/withScale(...)axis-preserving copies and anisIdentity()helper. Newcom.demcha.compose.document.dsl.Transformable<T>mixin exposestransform(...),rotate(...),scale(...)as default methods. Every shape-shaped builder opts in:ShapeContainerBuilder,ShapeBuilder,LineBuilder,EllipseBuilder,ImageBuilder,BarcodeBuilder.rotate(...).scale(...)chain naturally and pivot around the placement centre. The PDF backend issuessaveGraphicsState() + cm(matrix)around each transformed leaf (rotation is negated on the way out so the engine's clockwise convention matches PDF native counter-clockwise). Identity transforms short-circuit and emit no markers, so layout snapshots for default-configured nodes are byte-identical to v1.4. - Per-layer z-index.
LayerStackNode.Layerand shape-container layers gainint zIndex(default0).LayerStackBuilder.layer(node, align, zIndex)/position(node, dx, dy, align, zIndex)and the matchingShapeContainerBuilderoverloads let a layer declared earlier draw on top of layers declared later. The layout compiler stable-sorts layers before render; equalzIndexkeeps source order.
Public API — advanced tables
DocumentTableCell.rowSpan(int)mirrors the existingcolSpan(int). Cells compose freely:DocumentTableCell.text("Tall").colSpan(2).rowSpan(3). The layout layer skips occupied grid positions when interpreting subsequent source rows; misalignments (missing cell, extra source cell, overlapping span, span exceeding remaining rows) raise precise diagnostics.TableBuilder.zebra(odd, even)paints alternating row fills. Available as(DocumentTableStyle, DocumentTableStyle)and as a(DocumentColor, DocumentColor)overload. Either argument may benullto skip painting that parity. Existing entries in therowStylesmap (headerStyle(...),rowStyle(idx, ...),totalRow(...)) always win over zebra alternation.TableBuilder.totalRow(values)adds a totals row with a default bold-on-grey-blue style;totalRow(style, values)is the customisable form.TableBuilder.repeatHeader()/repeatHeader(rowCount)re-emits the configured leading rows at the top of every continuation page when a table paginates. Default is0so existing tables paginate exactly as before.TableBuilder.headerRow(values)is a naming alias forheader(...)so authors writingheaderRow(...).row(...).totalRow(...)keep a parallel vocabulary.
Public API — templates and themes
InvoiceTemplateV2is the cinematic invoice counterpart toInvoiceTemplateV1. Two constructors: the no-arg form picksBusinessTheme.modern(), the one-argInvoiceTemplateV2(BusinessTheme)accepts any theme. HerosoftPanelcarrying invoice number / dates / inline rich-text status, a two-column row withFrom/Bill toparties, themed line-items table withheaderStyle/ zebra / totals /repeatHeader(), and a footer row withaccentLeftstrips on the notes / payment-terms columns.ProposalTemplateV2is the proposal counterpart, sharing the sameBusinessTheme-driven composition: hero panel rounded only on the right (via the newDocumentCornerRadius.right(...)form), themed executive-summary panel, sender / recipient parties row, sections rendered throughtheme.text().h2()headings, a timeline table (Phase / Duration / Details), and a pricing table (Item / Description / Amount) withrepeatHeader(), zebra rows, and a total-pricing row anchored at the bottom viatotalRow(...).CvTheme.fromBusinessTheme(BusinessTheme)static factory derives a CV theme from a business theme (ADR 0002). The bridge maps palette / text-scale slots intoprimaryColor/secondaryColor/bodyColor/accentColor/headerFont/bodyFont/ font sizes; CV-specific layout tokens (spacing,moduleMargin,spacingModuleName) keep the existing CV defaults. The ten existing CV templates andCvTemplateV1continue to work unchanged.- Six CV templates modernised to v1.5 idioms:
BlueBannerCvTemplate,BoxedSectionsCvTemplate,CenteredHeadlineCvTemplate,MonogramSidebarCvTemplate,SidebarPortraitCvTemplate,TimelineMinimalCvTemplate. Each gains a(CvTheme)constructor and keeps a no-arg one whose default theme matches the legacy palette/font choices, so default- constructed instances render identical-page-count PDFs to v1.4.accentTop/accentBottomreplace the oldaddLine(horizontal=innerWidth)separators around section banners, andsoftPanel(...)collapses thepadding(asymmetric) + fillColor(...)cascade. InvoiceTemplateV1andProposalTemplateV1continue to ship side-by-side. Authors who want the cinematic look opt in by switching the type.
Public API — DSL ergonomics (Phase A)
LayerStackBuilderexposes nine alignment shortcuts (topLeft,topCenter,topRight,centerLeft,center,centerRight,bottomLeft,bottomCenter,bottomRight) on top ofback/centerso authors do not need to remember the fullLayerAlignenum.LayerStackBuilder.position(node, offsetX, offsetY, anchor)nudges a layer from its anchor by an on-screen offset (positiveoffsetX= right, positiveoffsetY= down).AbstractFlowBuildergains five convenience overloads on top of the v1.4 surface:addShape(w, h, fill),addEllipse(diameter, fill),addEllipse(w, h, fill),addCircle(diameter, fill),addImage(data, w, h).RowBuilder.spacing(double)is the canonical name for horizontal child spacing;RowBuilder.gap(double)becomes a deprecated alias (@Deprecated(since = "1.5.0")) that delegates tospacing(...).RowBuilder.add(node)validates the child type eagerly and raisesIllegalArgumentExceptionfrom the offending call site instead of deferring tobuild()and raisingIllegalStateExceptionlater.DocumentDsl.richText(Consumer<RichText>)is a new callback entry point that builds aRichTextrun sequence in one fluent call.
Architecture
- New
NodeDefinition.emitOverlayFragments(...)hook complements the existingemitFragments(...). It exists for paired begin/end marker pairs (clip-begin/end, transform-begin/end) so the layout compiler can emit a single flat fragment sequence[transform-begin → outline → clip-begin → … layers … → clip-end → transform-end]in one pass. Most node types inherit the empty default and need no changes. - New marker payloads on
BuiltInNodeDefinitions:ShapeClipBeginPayload/ShapeClipEndPayload(carry outline + policy + owner path),TransformBeginPayload/TransformEndPayload. PDF render handlers ship alongside:PdfShapeClipBeginRenderHandler,PdfShapeClipEndRenderHandler,PdfTransformBeginRenderHandler,PdfTransformEndRenderHandler, registered inPdfFixedLayoutBackend.defaultHandlers(). - New
PaginationPolicy.SHAPE_ATOMICdistinguishes shape-clipped atomicity from bbox-onlyATOMICfor snapshots and render handlers. Oversized containers raise the existingAtomicNodeTooLargeExceptionwith the offending semantic name. TableLayoutSupportreplaces the per-rowcolSpan-sum check with a unified cell-grid pre-pass driven by an occupancy mask. The newbuildLogicalRows(node, columnCount)walks columns left-to-right, skipping positions covered by a prior row's spanning cell.LogicalCellcarries the cell's full(startRow, startColumn, colSpan, rowSpan, content)extent. Row-height resolution is two-pass: single-row first, then spanning cells distribute deficit equally across covered rows.TableResolvedCellgainsdouble yOffset(eighth field). Spanning cells use a NEGATIVE offset equal to the cumulative height of the rows below the starting row, so the cell's rectangle extends downward through the rows it merges instead of upward beyond the starting row. Both PDF row-render handlers honour the offset.TableNodegains a 12th fieldint repeatedHeaderRowCount(default0).TableDefinition.splithonours the field: the tail slice is built withprependHeaderRowCount = headerCountso each continuation carries the header at the top.LayoutCompiler.compileStackedLayerand the STACK branch ofcompileNodeInFixedSlotcompute a stableiterationOrderpermutation viastableZIndexOrder(...)before iterating the layer list. Stable on ties → equalzIndexkeeps source order.BuiltInNodeDefinitions.PreparedStackLayoutgains a fourth listzIndices: List<Integer>populated by bothShapeContainerDefinitionandLayerStackDefinition.- New ADR
docs/adr/0001-shape-as-container.mdrecords the "separate semantic type" decision (rejected: a clip flag on the existingLayerStackNoderecord). - New ADR
docs/adr/0002-theme-unification.mdrecords the phased approach toCvTheme↔BusinessTheme(rejected: a commonThemeinterface that loses CV-specific vocabulary).
Examples
The runnable examples/ module gains six new showcases hooked into
GenerateAllExamples:
ShapeContainerExample— circles, ellipses, rounded cards with clipped layers (ClipPolicy.CLIP_PATH).TransformsExample— three-circle rotate row (15° / -15° / no tilt), three-card scale row (scale(0.7),scale(1.1, 0.85), identity), and a z-swap stage where a RED square declared first withzIndex = 10draws on top of a TEAL square declared second.TableAdvancedExample— hero callout, a 3-row spanning side note, and a 36-row invoice with bold-on-teal repeating header, zebra body rows, and a gold totals row.CustomBusinessThemeExample— a hand-built "Studio Emerald"BusinessThemeconstructed from rawDocumentPalette/SpacingScale/TextScale/TablePresetrecords (no factory shortcut), feedingInvoiceTemplateV2.HttpStreamingExample—writePdf(OutputStream)for Servlet / S3 / GCS adopters. Includes a Spring Boot@RestControllersnippet in the Javadoc and aTrackingOutputStreamtest that proves the caller's stream is not closed.LayoutSnapshotRegressionExample— full compose →layoutSnapshot()→LayoutSnapshotJson.toJson(...)workflow with a copy-and-paste baseline / drift-report pattern, plus a pointer to the productionLayoutSnapshotAssertions.assertMatches(document, "...")helper for in-test usage.WeeklyScheduleFileExamplerewritten to delegate to a new reusableexamples/support/WeeklyScheduleRenderer. The renderer's typed surface —JobTitleenum,StaffMember/DayPlan/Shiftrecords, sealed-interfaceHalfandDayShifttypes with factory methods (DayShift.OFF,.acrossDay(start, end, ShiftType.STOCK),.shifts(lunchStart, lunchEnd, dinnerStart, dinnerEnd),.lunchOnly(...),.dinnerOnly(...),.halves(Half.shift(...), Half.STANDBY)) — replaces the cryptic string tokens used previously.Theme(withaurora()default and a per-ShiftStatuscolour map) andLayout(page size + margin + column widths) records keep every colour and dimension out of the renderer's static state, so re-skinning the schedule is a swap-one-record call. Auto-fills the seven day labels from aLocalDate weekStart, sorts staff byJobTitle.ordinal(), and emits a separator row at every job-title boundary so adding or removing aStaffMembernever requires updating positional indices. The example file shrinks from ~700 lines of literal data to ~180 lines of typed declarations.
Documentation
- README quick-start refreshed to open with a
BusinessTheme.modern()-driven hero (softPanel+accentLeft+theme.text().h1()); the plain-text DSL stays underneath for callers who do not want a theme. - New "v1.5 sample renders (PDF)" section links six committed PDFs
under
assets/readme/v1.5/so the README works without running anything. - New
examples/README.mdexamples gallery — every example listed with description, key code snippet, committed PDF preview, and source link, grouped by category (built-in templates / cinematic templates / v1.5 feature showcases / public- API surface / production patterns / operational documents). Committed PDF previews of all 22 examples live underassets/readme/examples/(whitelisted in.gitignore) so users can browse renders straight from GitHub without running anything. - New
docs/templates/v1-classic/authoring.md(~620 lines) — the canonical cheatsheet covering builder hierarchy, a per-builder one-liner cheatsheet, a style-types reference, the theme system in 60 seconds, six golden patterns, ten anti-patterns, a 40-lineStatusReportTemplateV1skeleton, and a "where to look next" map. - New recipes:
docs/recipes/shape-as-container.mddocs/recipes/transforms.mddocs/recipes/tables.md(row span / zebra / totals / repeating header)docs/recipes/shapes.md(filled cards, dividers, spacers, lines, ellipses, image fit, soft panels)docs/recipes/extending.md
docs/recipes.mdis now a pure index linking every topic-focused recipe page plus four 5-line "common DSL primitives" starter snippets.docs/architecture/canonical-legacy-parity.mdgains a "Shape-as-container (clipped)" row recording the DOCX fallback rule.- New
docs/roadmaps/migration-v1-4-to-v1-5.md— fresh migration guide for v1.4 consumers.
Performance — v1.5 baseline
CurrentSpeedBenchmark smoke profile (single-thread, 30 warmup +
100 measurement iterations per scenario) recorded on Java 21,
Windows 11. All five scenarios are well within healthy production
ranges.
| Scenario | Avg ms | p50 ms | p95 ms | Docs/sec | Peak MB |
|---|---|---|---|---|---|
engine-simple | 2.25 | 1.96 | 4.20 | 444.60 | 22 |
invoice-template (V1) | 13.39 | 13.12 | 17.55 | 74.67 | 182 |
cv-template (V1) | 6.94 | 6.58 | 10.18 | 144.02 | 78 |
proposal-template (V1) | 15.77 | 15.50 | 18.31 | 63.43 | 182 |
feature-rich | 36.80 | 32.06 | 35.51 | 27.18 | 94 |
Stage breakdown (median ms per stage):
| Scenario | Compose | Layout | Render | Total |
|---|---|---|---|---|
| invoice-template | 0.249 | 2.774 | 6.042 | 9.312 |
| cv-template | 0.173 | 2.343 | 1.544 | 4.087 |
| proposal-template | 0.256 | 8.715 | 5.345 | 14.563 |
The smoke profile is single-thread by design; throughput numbers reflect "one document at a time" latency, not concurrent throughput. The formal "no >5% regression" gate first activates between this baseline and the next snapshot.
Tests
- 675/675 green (was 525 on v1.4.1) — +150 new tests across:
- shape-clip-path fragment ordering and pagination invariants
(
ShapeContainerBuilderTest,ShapeContainerInvariantsTest) - transform mixin contract and CTM checks
(
DocumentTransformTest, theeveryTransformBeginInArbitraryDocumentHasMatchingEndOnSamePagearchitecture-guard test) - per-layer z-index ordering and stable-tie behaviour
(
ShapeContainerZIndexDemoTestplus the two zIndex cases onShapeContainerBuilderTest) - table row-span / zebra / totals / repeating-header invariants
(
TableBuilderRowSpanTest,TableBuilderZebraAndTotalsTest,TableBuilderRepeatHeaderTest) InvoiceTemplateV2/ProposalTemplateV2invariants and three- theme demo renders (InvoiceTemplateV2Test,InvoiceTemplateV2DemoTest,ProposalTemplateV2Test,ProposalTemplateV2DemoTest)- custom
BusinessThemeend-to-end (CustomBusinessThemeDemoTest) - HTTP streaming contract (
HttpStreamingDemoTest— no-close-on-caller invariant) - layout-snapshot determinism
(
LayoutSnapshotRegressionDemoTest) CvTheme.fromBusinessThememapping (CvThemeBusinessThemeAdapterTest)- six modernised CV templates rendered to file at expected page
counts (
CvTemplateRenderTest) Transformable<T>contract pinned for every leaf builder that opted in (TransformableLeafBuildersTest): default identity transform,rotate(...)/scale(...)propagation, identity short-circuit emits no markers, non-identity wraps the leaf payload with matching transform-begin / transform-end carrying the same owner path
- shape-clip-path fragment ordering and pagination invariants
(
Migration from v1.4.x
RowBuilder.gap(double)is deprecated in favour ofspacing(double). The deprecated alias still compiles; CV templates and runnable examples were migrated.RowBuilder.add(node)now throwsIllegalArgumentExceptioneagerly. Tests that asserted the deferredIllegalStateExceptioninbuild()must switch their expectation.- All other v1.4 record signatures stay backward-compatible:
LayerStackNode.Layer,ShapeContainerNode,TableNode,DocumentTableCell,TableResolvedCell, andBuiltInNodeDefinitions.PreparedStackLayoutship new canonical constructors and preserve every existing constructor as a back- compat shim that defaults the new fields.InvoiceTemplateV1andProposalTemplateV1ship side-by-side with the V2 templates; callers who want the cinematic look opt in by switching the type.
See docs/roadmaps/migration-v1-4-to-v1-5.md
for the full guide.
v1.4.1 - 2026-04-27
Documentation
- README rewrite for v1.4.0 dropped three structural sections (
## Table component,## Line primitive,## Architecture at a glance) that theDocumentationCoverageTestguards baseline. CI flagged the regression on themainbranch; v1.4.1 restores the sections (the table snippet now also points readers to the new column-span feature), keeps the canonical-DSL anti-patterns out of the snippets, and moves the architecture mermaid diagram back into its dedicated section.
Tooling
examples/src/main/java/com/demcha/examples/GenerateAllExamples.javanow wiresCinematicProposalFileExample.generate()into the orchestrator, so the runnable examples module produces all seven fixtures (includingproject-proposal-cinematic.pdf) used by the README visual previews.
This is a documentation-only patch release. There are no public API changes; v1.4.0 consumers can upgrade with no code changes.
v1.4.0 - 2026-04-27
Headline — "cinematic document engine"
v1.4 closes the visual-design gap that the previous releases left open. Tables can now span columns, layers can stack on top of each other, sections and pages carry semantic backgrounds, paragraphs accept fluent rich text, and the whole look-and-feel can be parametrised through a single BusinessTheme. The release also lands the visual-regression scaffolding required to keep README screenshots stable across refactors.
Public API — semantic primitives
DocumentTableCellis now a 3-field record (lines,style,colSpan). The newcolSpan(int)factory pluswithColSpan(...)onTableCellContentlet one cell occupy several columns; sum-of-spans-per-row is validated byTableLayoutSupport. Border ownership and natural-width distribution understand spans (extra width is shared acrossautocolumns inside the span; an all-fixed span throws when it cannot fit). Renderer code is unchanged — spanned cells emit a singleTableResolvedCellwith the merged width.- new
LayerStackNode+LayerAlignprimitive composes children inside the same bounding box, in source order (first behind, last in front). Each layer carries one of nine alignments (TOP_LEFT … BOTTOM_RIGHT). Pagination is atomic. Backed by a newAxis.STACKinCompositeLayoutSpecand acompileStackedLayerbranch inLayoutCompiler. DSL surface:LayerStackBuilderwithback(...),center(...),layer(node, align). DocumentSession.pageBackground(DocumentColor | Color)(and the matchingGraphCompose.DocumentBuildersetter) injects a full-canvasShapeFragmentPayloadat the start of every page. Combine withLayerStackNodefor cinematic hero pages without any backend changes.AbstractFlowBuildergains semantic shortcuts on every flow / section / module:band(color),softPanel(color)/softPanel(color, radius, padding), andaccentLeft / accentRight / accentTop / accentBottom(color, width). They reuse the existingfillColor,cornerRadius,padding, andDocumentBordersplumbing — the new methods are sugar for designer-style flows.RichTextfluent builder (document.dsl.RichText) plusParagraphBuilder.rich(...)/AbstractFlowBuilder.addRich(...)cover theStatus: Pendinglabel/value pattern in one expression:RichText.text("Status: ").bold("Pending").color("…", red).accent("…", brand). Includesplain / bold / italic / boldItalic / underline / strikethrough / color / accent / size / style / link / append.
Public API — design tokens
- new
com.demcha.compose.document.themepackage — entirely on top of public document-level types, no engine leaks.DocumentPalette— primary / accent / surface / surfaceMuted / textPrimary / textMuted / ruleSpacingScale— five-stepxs / sm / md / lg / xlwith monotonicity validation andinsetsXs() … insetsXl()helpersTextScale—h1 / h2 / h3 / body / caption / label / accentresolved stylesTablePreset—defaultCellStyle / headerStyle / totalRowStyle / zebraStyleBusinessTheme— composes the four scales plus an optional page background, with three built-in presets (classic(),modern()cream paper + teal,executive()slate panels with Times-Roman headings) and immutablewithName / withPageBackgroundforks
Testing infrastructure
com.demcha.testing.visual.ImageDiff— pixel-by-pixel comparison with per-channel tolerance and a red/grey diff image.com.demcha.testing.visual.PdfVisualRegression— renders PDF bytes to one PNG per page viaPdfRenderBridgeand compares against baselines undersrc/test/resources/visual-baselines. Approve mode (-Dgraphcompose.visual.approve=trueorGRAPHCOMPOSE_VISUAL_APPROVE=true) writes new baselines; comparison failures dropactual.pnganddiff.pngnext to the baseline for inspection.- 41 new tests across the cinematic surfaces (
TableColSpanIntegrationTest,TableBuilderColSpanTest,LayerStackBuilderTest,PageBackgroundTest,SectionPresetTest,RichTextTest,BusinessThemeTest,PdfVisualRegressionTest). Total green count: 525.
Architecture
CompositeLayoutSpec.Axis.STACKjoinsVERTICALandHORIZONTAL. The compiler dispatchesSTACKtocompileStackedLayer, which positions each child inside the stack box via per-layer alignment offsets and shares the samecompileNodeInFixedSlotplumbing rows already use.- table layout (
TableLayoutSupport, test-sideTableBuilder) was rewritten around a "logical cell" model: each authored cell is oneLogicalCell(startColumn, colSpan, content)resolved against astylesGrid[row][col]— the grid keeps existing border-ownership logic intact while letting render code keep emitting oneTableResolvedCellper logical cell. DocumentSession.layoutGraph()now wrapscompiler.compile(...)withwithPageBackgrounds(...)so backends never need to know about the page-background option — they just iterate fragments as usual.
Performance
- Cinematic features have negligible overhead: page-background injection is a single fragment per page; column spans, layer stacks, and themes do not change the number of emitted fragments. End-to-end template latency stays in the same envelope as v1.3 once JIT is warm.
- Full benchmark surface is now published in the README:
current-speed(full profile) latency + per-stage breakdown, parallel throughput on the invoice template (1→8 threads),scalabilitysuite (1→16 threads, 13.8× speedup at 16), 50-threadstresstest (5,000 docs, 0 errors), and thecomparativetable against iText 5 and JasperReports.
Documentation
- README rewritten around the cinematic v1.4 narrative: new sections for column spans, layer stacks, page background + section presets, rich text DSL, business themes, the visual-regression workflow, "Extending GraphCompose" guidance, and a refreshed Performance section sourced from
scripts/run-benchmarks.ps1.
v1.3.0 - 2026-04-27
Public API
DocumentSessionnow exposes ergonomic mutators for document-level PDF chrome:metadata(...),watermark(...),protect(...),header(...),footer(...), andclearHeadersAndFooters(). Convenience entrypoints (buildPdf,writePdf,toPdfBytes) honour these options without having to build aPdfFixedLayoutBackendmanually- new horizontal layout primitive:
addRow(...)on flows, sections, and modules creates aRowNodethat arranges atomic children left-to-right with optionalweights(...)andgap(...). Rows are atomic blocks from the paginator's perspective DocumentBordersvalue type plusborders(...)on flows, sections, modules, and rows let you describe per-side strokes (top / right / bottom / left). Per-side borders override the uniformstroke(...)settingParagraphBuilder.autoSize(maxSize, minSize)/autoSize(DocumentTextAutoSize)searches for the largest font size that fits the paragraph on a single line within the resolved inner width- new
addLink(text, uri)andaddLink(text, DocumentLinkOptions)shortcuts onAbstractFlowBuilderfor the common single-link case (was previously only available as a paragraph inline run) - backend-neutral output options under
com.demcha.compose.document.output(DocumentMetadata,DocumentWatermark,DocumentProtection,DocumentHeaderFooter, aggregated byDocumentOutputOptions). PDF and DOCX backends translate them; session-level metadata propagates to DOCX core properties as well as the PDF backend DocxSemanticBackendis now a functional Apache POI based backend that returns DOCX bytes (was a manifest-only skeleton); supports paragraphs, tables, images, spacers, page breaks, and document-level page geometry. Apache POI is declared optional, so consumers that only render PDFs do not pay the dependency cost
Performance
PageBreaker.paginationPrioritypre-computes(y, depth)keys and usesUUID.compareTofor tie-breaks (no per-compare string allocation). Old comparator allocated a 36-character UUID string for every priority queue compareEntity.getComponentandEntity.requireno longer issue per-call debug logging (even guardedisDebugEnabledcalls cost a volatile read on Logback)- table layout helpers (
resolveTableLayout,sliceTablePreparedNode, ~350 lines) extracted intoTableLayoutSupportfor clarity - end-to-end template rendering is 19–30 % faster than v1.2.0 on the canonical benchmark suite (
invoice-template: 25.77 → 10.77 ms avg;cv-template: 17.89 → 6.50 ms avg;proposal-template: 24.77 → 13.44 ms avg)
Benchmark methodology
CurrentSpeedBenchmarksmoke profile bumped from 2 / 5 → 30 / 100 warmup / measurement iterations so the JIT reaches a steady state and percentiles are statistically meaningful- percentile calculation now uses linear interpolation between order statistics (
rank = (n-1) * p) so p95 no longer collapses to max at small sample counts System.gc()plus a 50 ms sleep separates warmup from measurement, dropping run-to-run variance from 10–25 % to 2–5 %peakHeapMbreports the heap delta over the post-warmup baseline rather than absolute used heap- a per-stage breakdown table (`compose / layout / render / total$) \text{prints} \text{alongside} \text{the} \text{latency} \text{table} \text{so} \text{consumers} \text{can} \text{attribute} \text{regressions} \text{to} \text{engine} \text{layout} \text{vs} \text{PDFBox} \text{serialization}
- \text{smoke} \text{gate} \text{thresholds} \text{tightened} \text{from} 800–2600 \text{ms} (\text{effectively} \text{a} \text{no}-\text{op}) \text{to} 8–100 \text{ms} (~3 \times \text{the} \text{observed} \text{avg}) — \text{still} \text{safe} \text{against} \text{CI} \text{machine} \text{variance}, \text{now} \text{catches} ≥50 % \text{regressions}
- \text{the} $ComparativeBenchmark` console table no longer wraps when library names exceed 20 characters
Architecture
CompositeLayoutSpeccarries an explicitAxis(vertical / horizontal) and optional per-child weights; the layout compiler dispatches to a dedicated horizontal-row code path forRowNodeShapeFragmentPayloadcarries an optionalSideBorderspayload;PdfShapeFragmentRenderHandlerdraws each configured side stroke independently of the uniform rectangle strokeSemanticExportContextcarriesDocumentOutputOptionsso semantic backends (DOCX, future PPTX) can apply metadata / chrome configured at the session level- the unused engine
Buttonrenderable and theButtonBuildertest-support factory entry were removed - guide-line overlays now compute owner bounds across sub-fragments (e.g. table rows) and paint margin / padding once around the entire owning node instead of stacking dashed rectangles inside every row
Documentation
docs/architecture/canonical-legacy-parity.mdis updated to reflect the v1.3 capabilities (rows, per-side borders, auto-size text, DOCX export)docs/operations/benchmarks.mddocuments the new smoke profile defaults, the GC stabilization point, the linear-interpolation percentile rule, and the stage-breakdown tableCONTRIBUTING.mdrepository map and package list now describe the canonical functional layout (document.layout,document.backend,document.output) alongside the legacy ECS engine
v1.2.0 - 2026-04-25
Release identity
- the current canonical API cleanup is being released as v1.2.0 to match the project's early maturity while still making
GraphCompose.document(...) -> DocumentSession -> DocumentDslthe preferred authoring path - Maven coordinates are
io.github.demchaav:graphcompose:1.2.0; JitPack consumers continue to usecom.github.demchaav:GraphCompose:v1.2.0 - consumers on
v1.1.xshould adopt the canonicalGraphCompose.document(...)session-first path; the planneddocs/migration-v1-1-to-v1-2.mdwas never written and the canonical surface has stabilised since
Public API
DocumentSessionis now anAutoCloseablelifecycle owner:close()is idempotent, and authoring/rendering methods on a closed session fail fast withIllegalStateExceptioninstead of returning broken state- empty document rendering (
writePdf/toPdfBytes/buildPdf) now throws a domain-specificIllegalStateExceptioninstead of producing a zero-byte / zero-page PDF; add at least one root before rendering DocumentPageSizeis the public page-size value;GraphCompose.document(...).pageSize(PDRectangle)was removed from the canonical APIDocumentSession#margin(Margin)andGraphCompose.DocumentBuilder#margin(Margin)were removed from the canonical API; useDocumentInsetsormargin(top, right, bottom, left)to keep authoring renderer-neutral- PDF-specific metadata, protection, watermark, and header/footer options moved behind
PdfFixedLayoutBackend.builder()instead of the canonicalGraphCompose/DocumentSessionsurface GraphCompose.document(...).guideLines(true)andDocumentSession.guideLines(true)now enable debug guide-line overlays forbuildPdf,writePdf, andtoPdfBytesconvenience outputDocumentSession.layoutSnapshot()now returns public renderer-neutralcom.demcha.compose.document.snapshot.*DTOs instead of engine debug typesBoxConstraints.natural(width)is now the canonical natural-measurement factory;unboundedHeight(width)remains as a compatibility alias- the public font registry no longer exposes the unadvertised
getPdfFont(...)bridge; backend code resolves typed fonts throughgetFont(..., PdfFont.class)
Architecture guards
PublicApiNoEngineLeakTestbaselines the inventory ofcom.demcha.compose.engine.*imports allowed in the public API surface — any new leak fails the buildSemanticLayerNoPdfBoxDependencyTestkeepsdocument.node.*free of direct PDFBox imports and pins the remainingbackend.fixed.pdf.options.*references for Phase 3 cleanupPdfBackendIsolationGuardTestkeeps PDFBox out of canonical API, DSL, semantic nodes, layout, snapshots, and non-PDF backend contracts
Layout
PaginationEdgeCaseTestadds focused regressions for exact-fit content, near-boundary float handling, leading / trailing page breaks, oversized atomic images, too-tall table rows, module splits with PDF chrome, and nested sections that paginate while preserving margin and padding
Documentation
- new
docs/migration-v1-1-to-v1-2.mdoutlines the move from older v1.1 usage patterns to the canonical session-first API - new
docs/v1.2-roadmap.mdtracks the remaining stabilization work for the v1.2 release polish window docs/contributing/release-process.mdnow describes the current JitPack-first 1.x release flow and runnable examples verification- user-facing docs now describe debug guide-line overlays through
GraphCompose.document(...).guideLines(true)/DocumentSession.guideLines(true)and call out JitPack tag-cache handling during release verification
v1.1.0 - 2026-04-13
Highlights
- shifted the public built-in template narrative to
compose(DocumentSession, ...) - added document-level PDF features for richer real-world output
- moved the engine further away from PDF-centric internals through backend-neutral composition and render-handler seams
- strengthened architecture guard rails for template scene builders
- expanded visual testing and benchmark tooling for day-to-day development
Added
- canonical
DocumentSessioncontract as the primary composition seam - layout snapshot extraction and JSON-based regression coverage for resolved document geometry
- runnable
examples/module for CV, cover letter, invoice, proposal, and weekly schedule generation - new built-in business templates and data models for invoice, proposal, and weekly schedule documents
- barcode support with QR, Code 128, and EAN-13 builders
- watermark support
- configurable headers and footers with page numbers and separators
- PDF bookmarks / outline generation
- document metadata support
- PDF protection hooks
- explicit page-break and divider builders
- visual showcase render tests for barcodes, QR codes, pagination, and document chrome
- current-speed benchmark suite
- benchmark JSON/CSV export and diff tooling
- one-command benchmark runner: scripts/run-benchmarks.ps1
Changed
- bumped the library release to
v1.1.0 - updated README installation snippets to the new release version
- documented built-in templates as compose-first by default
- refreshed README visuals to show barcode/QR and compose-first template output
- added release-facing notes for the experimental live preview dev tool in test scope
- refreshed release documentation to point contributors at visual tests and benchmark workflows
Architecture and CI
- engine-side text measurement and rendering dispatch are now more explicitly decoupled from PDFBox-specific implementation details
- added template boundary guard coverage so
*SceneBuilderclasses stay free of backend-specific PDFBox types - split architecture/documentation guards into a dedicated CI job that can be required independently in branch protection
Compatibility notes
- older tagged JitPack releases remain usable as long as consumers pin a specific version such as
v1.0.3 - deprecated
render(...)template adapters remain available for compatibility, but new docs and examples now prefercompose(...)