v1.8.0 — 2026-06-18
Open cycle — the chart subsystem and the keep-together pagination control.
Entries land here as they merge.
Public API
- Line-chart interpolation modes (
@since 1.8.0). New
LineInterpolationenum 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 new
MONOTONE(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.chart
package 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), and
DocumentPaint(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). DeclarativeNumberFormatSpec
keeps specs JSON-serializable. The one unsupported combination
(ValueLabelMode.INSIDE) fails fast withUnsupportedOperationException
instead 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)and
ChartStyle.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, and
dashed(on, off, ...)makes the stroke dashed with the same
DocumentDashPatterncontract 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>plus
rect/circle/ellipse/line/polyline/polygonlowered to
path data,<g>nesting withtranslate/scale/rotate/matrix
transforms (affine maps are exact on Bézier control points), and
fill/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/radialGradient
referenced viaurl(#id)— on fills and strokes — map to PDF axial /
radial shadings with exact endpoints (userSpaceOnUseand
objectBoundingBoxunits,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 honours
stroke-linecap/stroke-linejoin(rendered as native PDFJ/j
operators via newDocumentLineCap/DocumentLineJoin, also on
PathBuilder.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, and
sparklineLine(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 configurable
ChartStyle.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 to
com.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) and
DocumentPaint.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 on
SectionBuilder,ModuleBuilder, andTimelineBuilder(plus
keepEntriesTogether()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.ConfigLoader
YAML/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 via
GraphCompose.document(...).debug(...),DocumentSession.debug(...), or
PdfFixedLayoutBackend.builder().debug(...). WithnodeLabels()enabled,
every rendered node prints its stable semantic path — the same path
layoutSnapshot()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, add
io.github.demchaav:graph-compose-fonts(its own version line, starting at
1.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 empty
PathNode) 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 withvalueAxis().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 bare
IndexOutOfBoundsExceptionfrom 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, soPdfShadingSupportdropped a stop colour's alpha and a
translucent stop rendered fully opaque with no diagnostic.DocumentPaint.Stop
now 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/z
close 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@Beta
SvgPath.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 concatenatedListMarker.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)and
ListMarker.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.
LegendPosition
no 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
FeatureCatalogExample
renders 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 a
ChartStylepalette 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.
ChartShowcaseExample
gains 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 internalSvgGlyphhelper 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 JMH
TemplateCvJmhBenchmark),GraphComposeBenchmark(early-engine relic
duplicatingCurrentSpeedBenchmark'sengine-simplescenario), and
ScalabilityBenchmark(its thread-scaling sweep folded into
CurrentSpeedBenchmark'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 a
SvgParseAllocProbe;ChartJmhBenchmark(bar + line + pie render) plus a
ChartAllocProbe(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). Thelong-tokenscenario previously had no SMOKE threshold and
silently escaped the gate; it now has one, andCurrentSpeedScenarioGateTest
fails 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; theperf-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-noisypeakHeapMb
check 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 enginePdfRenderingSystemECS
imported both wildcards, leavingListresolvable from either
java.awt.Listorjava.util.List— sound today only becausejava.awt.List
was 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 presetTemplate
classes converted to records, constructor copy-loops replaced with
Collections.addAll, explicit imports collapsed to wildcards, and five
presets' explicitsection == nullguards folded into
SectionLookup.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,rgba
alpha),ShapeOutline.Pathcases inShapeOutlineTest(segment-count /
MoveTo-first / null guards, defensive copy), andPathBuilderdashed-pattern
rejection plus the documentedbuild()-snapshot contract. Extended
PublicApiNoEngineLeakTestto cover the new publicdocument.svgpackage
(it is engine-clean today; the guard now keeps it that way). - Chart geometry pinned without rendering:
NiceScaleTestgolden tables and
ChartLayoutResolverTestexact-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-negativeNiceScale
ranges. - Monotone interpolation pinned in
ChartLayoutResolverTest: theMONOTONE
curve's bounding box stays within theLINEARdata range (ground truth)
whileSMOOTHovershoots it, plus a one-native-Bézier-run assertion and a
charts/line_monotonelayout snapshot.