Skip to content

GraphCompose 2.0

v1.5.0 Breaking

This release includes breaking changes for platform teams planning a safe upgrade.

Published 2mo Build & Package
✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

business-documents cv-templates declarative-api document-generation invoice-template java
+12 more
java-17 layout-engine maven pagination pdf pdf-generator pdf-lib pdf-library pdfbox3 report-generation snapshot-testing template-engine

Summary

AI summary

Broad release touches Architecture, Tests, Migration from v1.4.x, and docs/migration-v1-4-to-v1-5.md.

Full changelog

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/migration-v1-4-to-v1-5.md.

Public API — visual primitives

  • Shape-as-container. New addCircle(diameter, fill, inside),
    addEllipse(w, h, fill, inside), and addContainer(...) shortcuts
    on AbstractFlowBuilder build a ShapeContainerNode whose bounding
    box is dictated by a ShapeOutline (Rectangle,
    RoundedRectangle, Ellipse, plus a circle(diameter) factory).
    Children are clipped via the new ClipPolicy enum
    (CLIP_PATH — default — / CLIP_BOUNDS / OVERFLOW_VISIBLE). The
    PDF backend honours every clip policy via graphics-state
    saveGraphicsState() + clip(path) markers; the DOCX backend renders
    layers inline without the outline frame and logs a one-time
    docx.export.shape-container-fallback capability warning.
    ShapeContainerBuilder exposes the same nine-point alignment
    vocabulary as LayerStackBuilder plus position(node, dx, dy, anchor) for screen-space nudges.
  • Transforms (rotate / scale). New
    com.demcha.compose.document.style.DocumentTransform value type
    with rotate(deg), scale(uniform), scale(sx, sy) factories
    plus withRotation(...) / withScale(...) axis-preserving copies
    and an isIdentity() helper. New
    com.demcha.compose.document.dsl.Transformable<T> mixin exposes
    transform(...), 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 issues
    saveGraphicsState() + 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.Layer and shape-container
    layers gain int zIndex (default 0).
    LayerStackBuilder.layer(node, align, zIndex) /
    position(node, dx, dy, align, zIndex) and the matching
    ShapeContainerBuilder overloads let a layer declared earlier draw
    on top of layers declared later. The layout compiler stable-sorts
    layers before render; equal zIndex keeps source order.

Public API — advanced tables

  • DocumentTableCell.rowSpan(int) mirrors the existing
    colSpan(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 be
    null to skip painting that parity. Existing entries in the
    rowStyles map (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 is 0 so existing tables paginate
    exactly as before.
  • TableBuilder.headerRow(values) is a naming alias for
    header(...) so authors writing
    headerRow(...).row(...).totalRow(...) keep a parallel vocabulary.

Public API — templates and themes

  • InvoiceTemplateV2 is the cinematic invoice counterpart to
    InvoiceTemplateV1. Two constructors: the no-arg form picks
    BusinessTheme.modern(), the one-arg
    InvoiceTemplateV2(BusinessTheme) accepts any theme. Hero
    softPanel carrying invoice number / dates / inline rich-text
    status, a two-column row with From / Bill to parties, themed
    line-items table with headerStyle / zebra / totals /
    repeatHeader(), and a footer row with accentLeft strips on the
    notes / payment-terms columns.
  • ProposalTemplateV2 is the proposal counterpart, sharing the
    same BusinessTheme-driven composition: hero panel rounded only on
    the right (via the new DocumentCornerRadius.right(...) form),
    themed executive-summary panel, sender / recipient parties row,
    sections rendered through theme.text().h2() headings, a timeline
    table (Phase / Duration / Details), and a pricing table (Item /
    Description / Amount) with repeatHeader(), zebra rows, and a
    total-pricing row anchored at the bottom via totalRow(...).
  • CvTheme.fromBusinessTheme(BusinessTheme) static factory
    derives a CV theme from a business theme (ADR 0002). The bridge
    maps palette / text-scale slots into primaryColor /
    secondaryColor / bodyColor / accentColor / headerFont /
    bodyFont / font sizes; CV-specific layout tokens (spacing,
    moduleMargin, spacingModuleName) keep the existing CV defaults.
    The ten existing CV templates and CvTemplateV1 continue 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 / accentBottom replace the old
    addLine(horizontal=innerWidth) separators around section banners,
    and softPanel(...) collapses the
    padding(asymmetric) + fillColor(...) cascade.
  • InvoiceTemplateV1 and ProposalTemplateV1 continue to ship
    side-by-side. Authors who want the cinematic look opt in by
    switching the type.

Public API — DSL ergonomics (Phase A)

  • LayerStackBuilder exposes nine alignment shortcuts (topLeft,
    topCenter, topRight, centerLeft, center, centerRight,
    bottomLeft, bottomCenter, bottomRight) on top of back /
    center so authors do not need to remember the full LayerAlign
    enum.
  • LayerStackBuilder.position(node, offsetX, offsetY, anchor) nudges
    a layer from its anchor by an on-screen offset (positive offsetX
    = right, positive offsetY = down).
  • AbstractFlowBuilder gains 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 to spacing(...).
  • RowBuilder.add(node) validates the child type eagerly and
    raises IllegalArgumentException from the offending call site
    instead of deferring to build() and raising
    IllegalStateException later.
  • DocumentDsl.richText(Consumer<RichText>) is a new callback entry
    point that builds a RichText run sequence in one fluent call.

Architecture

  • New NodeDefinition.emitOverlayFragments(...) hook complements the
    existing emitFragments(...). 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 in PdfFixedLayoutBackend.defaultHandlers().
  • New PaginationPolicy.SHAPE_ATOMIC distinguishes shape-clipped
    atomicity from bbox-only ATOMIC for snapshots and render
    handlers. Oversized containers raise the existing
    AtomicNodeTooLargeException with the offending semantic name.
  • TableLayoutSupport replaces the per-row colSpan-sum check with
    a unified cell-grid pre-pass driven by an occupancy mask. The new
    buildLogicalRows(node, columnCount) walks columns left-to-right,
    skipping positions covered by a prior row's spanning cell.
    LogicalCell carries 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.
  • TableResolvedCell gains double 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.
  • TableNode gains a 12th field int repeatedHeaderRowCount
    (default 0). TableDefinition.split honours the field: the tail
    slice is built with prependHeaderRowCount = headerCount so each
    continuation carries the header at the top.
  • LayoutCompiler.compileStackedLayer and the STACK branch of
    compileNodeInFixedSlot compute a stable iterationOrder
    permutation via stableZIndexOrder(...) before iterating the
    layer list. Stable on ties → equal zIndex keeps source order.
  • BuiltInNodeDefinitions.PreparedStackLayout gains a fourth list
    zIndices: List<Integer> populated by both
    ShapeContainerDefinition and LayerStackDefinition.
  • New ADR docs/adr/0001-shape-as-container.md records the
    "separate semantic type" decision (rejected: a clip flag on the
    existing LayerStackNode record).
  • New ADR docs/adr/0002-theme-unification.md records the phased
    approach to CvThemeBusinessTheme (rejected: a common
    Theme interface 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
    with zIndex = 10 draws 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"
    BusinessTheme constructed from raw DocumentPalette /
    SpacingScale / TextScale / TablePreset records (no factory
    shortcut), feeding InvoiceTemplateV2.
  • HttpStreamingExamplewritePdf(OutputStream) for Servlet /
    S3 / GCS adopters. Includes a Spring Boot @RestController
    snippet in the Javadoc and a TrackingOutputStream test 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 production
    LayoutSnapshotAssertions.assertMatches(document, "...") helper
    for in-test usage.
  • WeeklyScheduleFileExample rewritten to delegate to a new reusable
    examples/support/WeeklyScheduleRenderer. The renderer's typed
    surface — JobTitle enum, StaffMember / DayPlan / Shift
    records, sealed-interface Half and DayShift types 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 (with aurora() default and
    a per-ShiftStatus colour map) and Layout (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 a
    LocalDate weekStart, sorts staff by JobTitle.ordinal(), and
    emits a separator row at every job-title boundary so adding or
    removing a StaffMember never 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.md examples 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 under
    assets/readme/examples/ (whitelisted in
    .gitignore) so users can browse renders straight from GitHub
    without running anything.
  • New docs/template-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-line StatusReportTemplateV1 skeleton, and a "where to look
    next" map.
  • New recipes:
  • docs/recipes.md is now a pure index linking
    every topic-focused recipe page plus four 5-line "common DSL
    primitives" starter snippets.
  • docs/canonical-legacy-parity.md
    gains a "Shape-as-container (clipped)" row recording the DOCX
    fallback rule.
  • New docs/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, the
      everyTransformBeginInArbitraryDocumentHasMatchingEndOnSamePage
      architecture-guard test)
    • per-layer z-index ordering and stable-tie behaviour
      (ShapeContainerZIndexDemoTest plus the two zIndex cases on
      ShapeContainerBuilderTest)
    • table row-span / zebra / totals / repeating-header invariants
      (TableBuilderRowSpanTest, TableBuilderZebraAndTotalsTest,
      TableBuilderRepeatHeaderTest)
    • InvoiceTemplateV2 / ProposalTemplateV2 invariants and three-
      theme demo renders
      (InvoiceTemplateV2Test, InvoiceTemplateV2DemoTest,
      ProposalTemplateV2Test, ProposalTemplateV2DemoTest)
    • custom BusinessTheme end-to-end
      (CustomBusinessThemeDemoTest)
    • HTTP streaming contract (HttpStreamingDemoTest
      no-close-on-caller invariant)
    • layout-snapshot determinism
      (LayoutSnapshotRegressionDemoTest)
    • CvTheme.fromBusinessTheme mapping
      (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

Migration from v1.4.x

  • RowBuilder.gap(double) is deprecated in favour of
    spacing(double). The deprecated alias still compiles; CV
    templates and runnable examples were migrated.
  • RowBuilder.add(node) now throws IllegalArgumentException
    eagerly. Tests that asserted the deferred IllegalStateException
    in build() must switch their expectation.
  • All other v1.4 record signatures stay backward-compatible:
    LayerStackNode.Layer, ShapeContainerNode, TableNode,
    DocumentTableCell, TableResolvedCell, and
    BuiltInNodeDefinitions.PreparedStackLayout ship new canonical
    constructors and preserve every existing constructor as a back-
    compat shim that defaults the new fields. InvoiceTemplateV1 and
    ProposalTemplateV1 ship side-by-side with the V2 templates;
    callers who want the cinematic look opt in by switching the type.

See docs/migration-v1-4-to-v1-5.md
for the full guide.


Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track GraphCompose 2.0

Get notified when new releases ship.

Sign up free

About GraphCompose 2.0

All releases →

Related context

Related tools

Earlier breaking changes

  • v2.0.0 Package rename: layered template packages dropped `.v2` suffix; update imports.
  • v2.0.0 Removed dormant ECS engine internals (EntityManager, SystemECS, Entity components, render pipeline).
  • v2.0.0 Removed linkOptions() accessor; use linkTarget() and ExternalLinkTarget.options().
  • v2.0.0 Removed PDF‑typed document‑chrome overloads on DocumentSession; use backend‑neutral metadata/watermark/protect/header/footer.
  • v2.0.0 Removed DSL name-aliases DocumentSession.builder() and DocumentDsl.text(); use builder() and paragraph().

Beta — feedback welcome: [email protected]