Skip to content

GraphCompose v1.8.0

Latest

Choose a tag to compare

@github-actions github-actions released this 18 Jun 12:33
· 8 commits to main since this release

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
    LineInterpolation enum 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
    with ChartSpec.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.Series rejects non-finite values. A NaN / ±∞ 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. null entries
    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.
    New AlignNode + HorizontalAlign (LEFT / CENTER / RIGHT) seat any node
    across the available width: flow.addAligned(HorizontalAlign.CENTER, node)
    and the icon sugar flow.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). New com.demcha.compose.document.chart
    package with a layered, serialization-friendly API: ChartData (categories +
    series, type/colour-agnostic), sealed ChartSpec (bar() / line() with
    axis, legend, value-label, and sizing knobs), ChartStyle (nullable-field
    cascade merged over ChartTheme tokens, 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) via ChartDefinition — 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). Declarative NumberFormatSpec
    keeps specs JSON-serializable. The one unsupported combination
    (ValueLabelMode.INSIDE) fails fast with UnsupportedOperationException
    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 — one PathNode per 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.TOP and RIGHT now lay out as a top
    strip / right column for every chart kind, including pie. The chart
    resolver is split per kind (BarChartLayout / LineChartLayout /
    PieChartLayout over a shared ChartLayoutSupport).
  • Axis / grid / label visibility toggles. AxisSpec.showTickLabels(false)
    hides the numeric axis and collapses its gutter; showGridLines(false) and
    ChartStyle.GridStyle control 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 adds sliceStroke (separator),
    sliceGapDegrees (pad angle), and donutCenterTextStyle. Sectors compile
    into the new general-purpose PolygonNode (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). New PathNode — the open-path,
    curve-capable sibling of PolygonNode: normalized DocumentPathSegments
    (moveTo / lineTo / cubic cubicTo / 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
    DocumentDashPattern contract as lines — the pattern follows the curve.
  • Path-outline clipper (@since 1.8.0). ShapeOutline.Path joins the
    sealed outline family as the curve-capable sibling of Polygon, so a
    shape container can clip its children to — and fill / stroke along — an
    arbitrary native-curve silhouette. ShapeContainerBuilder.path(w, h, segments) takes raw DocumentPathSegments; path(w, h, svgPath) (beta)
    clips to an imported SVG path, turning any icon or logo into a content
    mask under ClipPolicy.CLIP_PATH. The outline rides the existing
    vector-path fragment pipeline (one source of truth for native curves) and
    the clip handler emits the same addPathSegments geometry, so fill, clip,
    and addPath(...) all agree. The new Path permit is additive and keeps
    the artifact binary-compatible (the japicmp gate stays green); only
    consumer code that exhaustively switches over ShapeOutline would need a
    new branch, and the canonical authoring surface exposes no such switch.
  • SVG path import (@since 1.8.0, beta — annotated @Beta while
    the surface hardens against real-world exporter output). SvgPath.parse(d) /
    parse(d, viewBox...) in the new document.svg package lowers the full
    SVG 1.1 path grammar — absolute/relative M 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-flipped DocumentPathSegments.
    PathBuilder.svg(svgPath) drops the result straight into addPath(...):
    any icon's d string 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 / polygon lowered to
    path data, <g> nesting with translate / scale / rotate / matrix
    transforms (affine maps are exact on Bézier control points), and
    fill / stroke / stroke-width styling with SVG inheritance and
    defaults — into ordered layers, and addSvgIcon(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 inside ShapeContainer / LayerStack nine-point grids (and
    rows now accept ShapeContainerNode children directly — it is the same
    atomic overlay composite as the already-allowed LayerStackNode).
    Gradients render natively: linearGradient / radialGradient
    referenced via url(#id) — on fills and strokes — map to PDF axial /
    radial shadings with exact endpoints (userSpaceOnUse and
    objectBoundingBox units, gradientTransform, percentage offsets,
    multi-stop stitching, one href hop for split definitions); gradient
    strokes ride a shading-pattern stroking colour. Underneath,
    DocumentPaint gains endpoint-exact LinearAxis / RadialCircle forms
    and PathNode / PathBuilder grow fill(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 PDF J / j
    operators via new DocumentLineCap / DocumentLineJoin, also on
    PathBuilder.lineCap() / lineJoin()) and stroke-dasharray, the full
    CSS named-colour table (147 keywords), rgb() / rgba() with numbers or
    percentages, #rgb / #rgba / #rrggbb / #rrggbbaa hex, 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-pad spreadMethod and
    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 as in <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. PointMarker draws 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
    cascading valueLabelTextStyle, 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). DocumentPaint graduates to
    com.demcha.compose.document.style as the shared paint vocabulary, and
    gradients now actually render: ShapeNode gains an optional fillPaint
    (ShapeBuilder.fill(paint)) that wins over fillColor. The PDF backend
    paints DocumentPaint.linear as a native axial shading (0° = left→right,
    90° = bottom→top; two stops exponential, more stops stitched) and
    DocumentPaint.radial as 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 to primaryColor() by contract. The
    flagship BusinessReportExample hero 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)
    and withOpacity(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, and TimelineBuilder (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). The com.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-yaml dependency (ConfigLoader was its only consumer)
    and the YAML entry in the NoClassDefFoundError troubleshooting 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-neutral DocumentDebugOptions (guides + node labels +
    label-text mode, in document.output next to the other neutral output
    options) configures fixed-layout rendering via
    GraphCompose.document(...).debug(...), DocumentSession.debug(...), or
    PdfFixedLayoutBackend.builder().debug(...). With nodeLabels() 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_PATH prints the whole ancestry. Label text
    degrades through the shared WinAnsi fallback (accents like é survive,
    anything outside WinAnsi becomes ? with a glyph.missing log). 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 the graph-compose jar, so an engine upgrade never
    re-downloads them and the engine artifact drops from ~40 MB to a few MB. The
    public FontName constants and the DefaultFonts catalog are unchanged
    (source- and binary-compatible), and the classpath layout fonts/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 -tests classifier jar (it stays a local-only build aid for the
    benchmarks module), and with the fonts gone the -sources.jar no longer
    carries font binaries either. The published artifact set is now just the
    engine bytecode plus the small template assets.
  • graph-compose-fonts releases on its own fonts-v* tag via a dedicated
    publish workflow, so the font set ships only when it actually changes,
    independent of the engine's v* 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 at SvgIcon#node(...) (an empty
    PathNode) and a moveto+close rendered blank. SvgIconReader now 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(...) or baselineAtZero(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.paintForSeries rejects a negative series index with a
    value-naming IllegalArgumentException instead of leaking a bare
    IndexOutOfBoundsException from 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 PdfShadingSupport dropped 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 refuses stop-opacity,
    and reaches the DocumentPaint.linear(from, to) sugar too. Opaque gradients are
    unaffected.
  • SVG path reader no longer hangs on malformed d data. A Z/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 / SvgIcon reader. The scanner now fails fast with the
    usual position-carrying IllegalArgumentException when an iteration
    consumes neither a command nor an operand.
  • BEHIND_CONTENT watermarks 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's resetContext only
    isolates appended streams, so the watermark alpha leaked into the entire
    page and every element rendered nearly invisible. The watermark now wraps
    its drawing in q/Q, keeping page content at full strength. This
    affected every document using the default DocumentWatermark layer.
  • DOCX export no longer drops lists. DocxSemanticBackend had no branch
    for ListNode, so addList(...) 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 uses ListMarker.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 JDK NumberFormatException
    ("For input string: …") as the reason. SvgGradients now 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 claims RIGHT/TOP are "reserved and rejected by validation" —
    all four placements are laid out for every chart kind, as the resolver and
    its tests already prove. DocumentPaint documents why the Linear/Radial
    (angle/corner-reaching) and LinearAxis/RadialCircle (exact endpoint/radius)
    forms coexist. ShapeContainerBuilder's missing-outline error and class
    Javadoc now name the full set of outline setters (including path).
    PathBuilder.dashed(double...) documents the IllegalArgumentException it
    throws eagerly, and SvgIcon documents that a gradient href inherits 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 beta SvgIcon tile 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 use keepTogether(), so a snippet is never orphaned
    from its result.
  • Landscape capability deck on real benchmark data. New flagship
    EngineDeckExample renders 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
    an EngineDeckData data layer; an EngineDeckLayoutSnapshotTest locks 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 same DocumentSession as a
    fixed-layout PDF and an editable Word file via DocxSemanticBackend,
    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 under assets/readme/examples/
    (word-export-companion.pdf / .docx); the examples module adds the
    optional poi-ooxml dependency exactly like a consuming project would.
  • BusinessReportExample chart 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 uses ChartSpec.bar() with a
    ChartStyle palette 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 committed assets/readme/chart-showcase.png hero
    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 SvgGlyph helper flattens an icon's
    filled layers into one outline that the presets fill with each template's
    own accent colour via rich.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 bundled templates/cv assets from ~717 KB to ~133 KB (the
    431 KB portrait.png alone 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
    duplicating CurrentSpeedBenchmark's engine-simple scenario), and
    ScalabilityBenchmark (its thread-scaling sweep folded into
    CurrentSpeedBenchmark's full-profile throughput run, now 1,2,4,8,16).
    Dropped the matching run-benchmarks.ps1 steps 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,
    @Param 8/32/128); and MixedShowcaseJmhBenchmark (one document combining
    prose, inline sparklines, bar + pie charts, SVG icons and a gradient path).
    Shared SvgBenchmarkFixtures / ChartBenchmarkFixtures hold the inputs so
    each bench and its probe measure identical data.
  • Current-speed report carries a stage breakdown and a run summary (not
    shipped).
    CurrentSpeedBenchmark persists a per-scenario compose / layout /
    render split (stages[], median ms) to the JSON and a stages CSV, and
    writes a readable summary.md. BenchmarkDiffTool consumes stages[],
    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-token scenario previously had no SMOKE threshold and
    silently escaped the gate; it now has one, and CurrentSpeedScenarioGateTest
    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, with ImageCacheGateTest pinning PdfImageCache reuse), a
    single-shot cold-start render bench (ColdStartJmhBenchmark), a report-scaling
    sweep in ComparativeBenchmark (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-scale LargeTableJmhBenchmark, 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-smoke job 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. A vector-rich scenario (charts + SVG icons +
    gradient) joins the gated current-speed harness; BenchmarkMedianTool carries the
    stage breakdown into its aggregate; and the smoke gate's GC-noisy peakHeapMb
    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 engine PdfRenderingSystemECS
    imported both wildcards, leaving List resolvable from either
    java.awt.List or java.util.List — sound today only because java.awt.List
    was never referenced. Each used only java.awt.Color, so the wildcard is now
    an explicit import 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 preset Template
    classes converted to records, constructor copy-loops replaced with
    Collections.addAll, explicit imports collapsed to wildcards, and five
    presets' explicit section == null guards folded into
    SectionLookup.hasContent's null tolerance (now documented on the method
    and pinned by SectionLookupTest). 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
    (withOpacity range + NaN rejection, boundary alpha rounding, rgba
    alpha), ShapeOutline.Path cases in ShapeOutlineTest (segment-count /
    MoveTo-first / null guards, defensive copy), and PathBuilder dashed-pattern
    rejection plus the documented build()-snapshot contract. Extended
    PublicApiNoEngineLeakTest to cover the new public document.svg package
    (it is engine-clean today; the guard now keeps it that way).
  • Chart geometry pinned without rendering: NiceScaleTest golden tables and
    ChartLayoutResolverTest exact-position assertions on a font-independent
    text-metrics fake; ChartLayoutSnapshotTest layout snapshots + a
    fragment-lowering assertion; SectionKeepTogetherTest covers 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-negative NiceScale
    ranges.
  • Monotone interpolation pinned in ChartLayoutResolverTest: the MONOTONE
    curve's bounding box stays within the LINEAR data range (ground truth)
    while SMOOTH overshoots it, plus a one-native-Bézier-run assertion and a
    charts/line_monotone layout snapshot.