Skip to content

GraphCompose v2.1.0

Latest

Choose a tag to compare

@github-actions github-actions released this 26 Jul 12:33
58ad29c

v2.1.0 — 2026-07-26

Highlights

  • The same document now also prints to PowerPoint. graph-compose-render-pptx
    gains a fixed-layout backend that consumes the same resolved layout graph as the
    PDF backend, so one page becomes one identically-sized slide and every element keeps
    its position by construction — as native, editable shapes rather than a picture of a
    page. Ships as @Beta (Experimental) in its first release.
  • A failed render no longer destroys the document it was replacing.
  • Headings stay with their content across a page break, in the engine and in the
    CV and proposal presets.
  • CV entry, subtitle and project-card titles accept inline [text](url) links.

Public API

  • Editable PowerPoint output. buildPptx(Path), writePptx(OutputStream) and
    toPptxBytes() render the current session through the PPTX backend when
    graph-compose-render-pptx is on the classpath; a missing backend fails with
    MissingBackendException naming the artifact. DocumentPageSize.SLIDE_16_9
    (960 × 540 pt) and SLIDE_4_3 (720 × 540 pt) match the PowerPoint defaults.
    The PPTX surface — the document.backend.fixed.pptx packages and these
    convenience methods — carries @Beta, so its shape may still change in a minor
    release; the geometry relationship with the PDF backend is a design invariant and
    is not subject to change. See docs/api-stability.md.
  • A failed render no longer truncates the destination. buildPdf(Path),
    buildPptx(Path) and multi-section buildPdf(Path) render into a scratch file in
    the destination's own directory and move it onto the destination only after the
    render returns. Previously the destination was opened — and therefore emptied —
    before the backend produced a byte, so an oversized node, a missing backend or a
    full disk left an empty file where a published document used to be. The move is
    atomic where the filesystem supports it, so a concurrent reader never observes a
    half-written document; on POSIX the destination keeps the permissions it already
    had, or gets rw-r--r-- when it is new. Replacing the destination entry replaces
    a symlink rather than writing through it.
  • Keep a heading with its contentSectionBuilder.keepWithNext(). A section
    marked keep-with-next is never left stranded as the last block on a page apart from
    the content it introduces: when the section plus the first slice of the following
    block would overflow the remaining page space but fit on a fresh page, the section
    relocates. The first slice is a paragraph's first line, a table's repeated header
    rows plus first body row, or a list's first item, so the rule holds whether the
    following block is atomic or page-spanning. Distinct from keepTogether(), which
    relocates a whole block. Inert when nothing follows, best-effort when the heading
    plus the first slice cannot share a page, and off by default.
    LineBuilder.keepWithNext() is the line counterpart, so a full-width header rule
    joins its banner's run and the whole title block moves together.
  • Keep-with-next also surfaces on the node model: DocumentNode.keepWithNext() is a
    new default method (existing implementations keep the false default), and
    SectionNode and LineNode each gain a trailing keepWithNext record component.
    Constructor calls are unaffected — both records keep an overload at the previous
    argument count — but the canonical component count changed (SectionNode 13 → 14,
    LineNode 17 → 18), so a record deconstruction pattern written against the
    2.0.0 component list must add the new binding to compile.
  • DocumentSession.buildPptx() (no-arg) is removed; use buildPptx(Path). The
    session has a single configured output path, shared with buildPdf(), so the no-arg
    form wrote deck bytes into whatever that path was — including a file named .pdf.
    Naming the destination is also what lets one session emit both formats. The PPTX
    surface is Experimental and was never published, so no released code can depend on
    the removed overload.
  • Two backends registered for one format now fail loudly.
    BackendProviders.fixedLayout(String format) selects a fixed-layout backend by its
    FixedLayoutBackendProvider.format() key (case-insensitive), so several backends can
    coexist on one classpath; the no-arg default resolves the "pdf" provider when
    present, otherwise the lexicographically smallest format. Where both entry points
    previously took the first ServiceLoader match — letting classpath order decide the
    renderer — an ambiguous format now throws IllegalStateException naming the
    competing provider classes.
  • MarkdownInline gains appendTransformed, appendUpperCased and
    appendIfPresent, and PdfRenderEnvironment gains fillAlphaState /
    strokeAlphaState for handlers that need a shared alpha graphics state.

PPTX backend

The per-capability status — what is native, what is approximated, what is unsupported
— lives in
docs/architecture/backend-capability-matrix.md.
Of the 38 capabilities it tracks, 24 map to a native equivalent and 4 are unsupported.
The remaining 10 are partial, and not all in the same way: some render natively with an
approximated styling detail (distinct per-corner radii collapse to one value, numeric
dash arrays map to the nearest preset, a radial gradient uses the closest DrawingML
shade), while others lose something the format cannot carry — see Known limitations.

  • Content. Paragraphs render as absolute, wrap-disabled text frames seated on the
    measured baselines with PDF-identical glyph sanitization, carrying rich runs, inline
    code chips, inline images, shapes and SVG. Tables render as row fills, border edge
    lines and per-cell text frames at graph coordinates, across page breaks with repeated
    headers and row spans. Shapes, ellipses, lines, polygons, free paths (with gradient
    fills and strokes), images, barcodes and transform groups all render natively.
    Unsupported payloads fail with UnsupportedNodeCapabilityException; custom handlers
    plug in through PptxFragmentRenderHandler and Builder.addHandler, which rejects a
    duplicate registration for one payload type.
  • Chrome and navigation. Metadata maps onto OPC core properties, watermarks and
    repeating headers/footers render per slide with token resolution, and hyperlinks,
    internal slide jumps and bookmark slide names are emitted. Multi-section documents
    concatenate into one deck; every section must share the slide size.
  • Fonts. Families are embedded where the licensing bits allow it and the backend
    warns once per family whenever a font is substituted, so a deck that will render
    differently on another machine says so at build time.
  • Clipping. DrawingML cannot express graphics-state clipping, so a clip region that
    can actually cut ink renders through the PDF backend into one transparent picture on
    the clip bounds — pixel-exact, but not editable as shapes, and run-level link
    hotspots inside it are not emitted. A clip that provably cannot remove ink (a rounded
    card whose padded content never reaches the corners) skips the fallback and stays
    native. Builder.clipRasterFallback(false) restores unclipped vectors with a
    one-time warning. The raster targets a 2048-pixel long edge, clamped between native
    size and 4×.
  • Raster-slide mode. Builder.rasterSlides(int dpi) renders each page through the
    PDF backend and places it as one full-slide picture — a pixel-exact copy for decks
    that must not be edited.
  • Reproducible output is opt-in. Builder.deterministic(true) or
    deterministic(Instant) pins the OPC created/modified properties and normalizes
    every zip entry timestamp, so the same document renders to byte-identical bytes
    across runs. The default path does not: buildPptx(Path) and the other
    convenience methods stream the deck with live timestamps, matching the PDF backend's
    opt-in convention.

Fixed

  • PPTX text no longer overruns the frame the engine measured for it. A span that
    named a standard-14 style variant — Helvetica-Bold, Times-Italic and their
    siblings — travelled to PowerPoint as a bold or italic run flag. Those names are
    family aliases: the engine resolves each to its regular base and takes the real face
    from the span's decoration, so the layout had measured the regular metrics. The viewer
    then drew a face about 6% wider than its slot, which pushed a chip label past its card
    and closed the gap between two words of a rich-text heading until they read as one.
    Run flags now follow the decoration for those families, so a deck renders the same
    face the PDF does; a binary family still carries its face in its own name.
  • A large clipped region in a PPTX deck is no longer downscaled. The raster scale
    was capped only from above, so a clip box wider than 2048 pt rasterized below
    native size — an A0-scale composite landed near 44 DPI and read as visibly blurry,
    since the picture is anchored at the full clip size regardless. Decks whose clip
    regions fit a normal page are unaffected; their scale already saturated at the upper
    cap. A very large region now costs transient memory proportional to its size.
  • The PDF backend draws DocumentTextDecoration.UNDERLINE and STRIKETHROUGH.
    The flags previously resolved only to font faces, which alias to the regular program,
    so decorated text rendered as plain glyphs while the PPTX backend already drew real
    marks. Marks are em-proportional filled bands in the run's colour — underline 0.10 em
    below the baseline, strikethrough 0.28 em above, thickness 0.05 em, the Type 1
    convention — on paragraph runs, chips and table cell text alike.
  • The PDF backend honours the alpha channel of DocumentColor.rgba everywhere
    text runs, lines, side borders, and table fills, borders and cell text — matching the
    shape fills and strokes that already carried it. Fully opaque documents render
    byte-identically to before.
  • toImages / toImage no longer rasterize binary fonts with substitute glyphs.
    PDFBox writes embedded font subsets only during save(), so rendering the unsaved
    in-memory document never saw the real glyph programs and produced visually garbled
    text with correct spacing. The backend now saves to an in-memory buffer and reloads
    before rasterizing; standard-14-only documents are unaffected apart from the marginal
    serialization cost.
  • The PPTX backend logs, once per render at DEBUG on
    com.demcha.compose.engine.render, how many clip regions were rasterized and their
    total megapixels — each one costs a full sub-render through the PDF backend and loses
    text editability inside its bounds. Silent when nothing was rasterized.

Templates

  • Single-column CV presets no longer orphan a section title. Every preset whose
    sections flow down the page — BoxedSections, MinimalUnderlined, ModernProfessional,
    Executive, CenteredHeadline, BlueBanner, EditorialBlue and ClassicSerif — keeps a
    section heading with the first line of its body across a page break, including
    Panel's full-width Profile card whose header now stays with the summary. Multi-column
    presets place their sections in fixed columns that do not paginate.
  • The Modern Proposal template no longer orphans a section heading. Its flowing
    section bodies, the Timeline and Investment tables, and the Acceptance terms each
    keep their title with the first line of the block it introduces.
  • CV entry titles and subtitles accept inline [text](url) links. A [label](url)
    in an experience or education entry title or subtitle renders as a clickable
    hyperlink — the convention already used for project rows and body text — and
    upper-cased or letter-spaced preset titles keep the link with the visible label
    styled. Where a preset fuses a line that cannot carry a link (the single-line
    TimelineMinimal excerpt, and the combined subtitle+date meta line of MintEditorial
    and SidebarPortrait experience entries) the label text is shown instead. Titles
    without inline-Markdown syntax render exactly as before.
  • Project-card titles accept the same links in the EngineeringResume and
    SidebarPortrait presets
    , whose hand-rolled cards previously emitted the title as
    flat styled text; the trailing " (stack)" run is preserved.

Documentation and examples

  • The examples now show the engine's warnings. Their logging config silenced
    com.demcha.compose entirely, so a reader running a deck example never saw which
    fonts the viewer would substitute or which capability had degraded — the warnings
    fired into a muted logger. The output stays short because every warning is
    deduplicated before it is logged: a font substitution once per family per render, a
    capability note once per kind for the process.
  • render-pptx documents how to get a glyph-identical deck. Geometry matches the
    PDF by construction, but glyphs are drawn by the viewer; the page now states the
    three cases — a standard-14 name travels as a metric-compatible viewer font with
    identical widths and different letterforms, an embeddable binary family travels as
    itself, and a name-only family depends on the viewer having it installed.
  • TwinOutputExample — a single 16:9 page written once and emitted twice from the
    same session. The README gains a dual-output section showing the PDF render beside
    PowerPoint's own export of the generated slide, plus the deck open in PowerPoint with
    the headline text frame selected; the committed artifacts land 69 native shapes with
    only the clip-masked logo as a picture.
  • MavenBannerPptxExample — a single 16:9 "Available on Maven Central" brand slide
    composed as one full-bleed CanvasLayerNode, with a native-shape guard pinning it to
    a single picture (the badge checkmark).
  • PDF→PPTX twins for the business report, financial report and master showcase, and
    the Engine Deck flagship also renders as a deck. The showcase site publishes the deck
    beside the PDF on the same card.
  • docs/architecture/backend-capability-matrix.md — the per-capability matrix of
    what each backend supports and which class implements it, maintained as part of every
    capability-changing PR. The PPTX documentation, the architecture pages and the
    release runbook were rewritten to describe the shipped backend and the 2.x layout.

Build and compatibility

  • The consumer release-smoke harness gains scenarios for graph-compose-render-pptx
    and graph-compose-render-docx, so a broken or unpublished backend artifact can no
    longer pass the release gate unnoticed. Its default version under test now follows
    the release instead of staying pinned to the previous one.
  • The examples CI job installs graph-compose-render-pptx before generating.
  • The PDF backend reuses one PDExtendedGraphicsState per distinct (channel, alpha)
    pair for a render pass, so a page's resource dictionary stays bounded by the number
    of distinct alpha values instead of growing with every translucent draw. Fully opaque
    documents still carry no /ExtGState resources at all.
  • graph-compose-render-pptx depends on graph-compose-render-pdf at compile scope:
    the backend reuses the PDF measurement font library so glyph widths stay aligned with
    the widths that produced the layout graph, and re-renders through it for the clip
    raster fallback. A PPTX-only consumer therefore also resolves the PDF stack.

Known limitations

  • Clipped regions that can cut ink become a picture: text inside them is not editable
    and run-level link hotspots are not emitted. True vector clipping is tracked in
    #413.
  • Glyphs are rasterized by the viewing application, so text rendering depends on the
    fonts installed there. Frames and positions are fixed by the layout graph.
  • Byte-identical PPTX output requires opting in through Builder.deterministic(...).
  • Document protection, viewer preferences and the debug guide-line overlays are PDF
    concepts and are ignored by the PPTX backend with a one-time warning;
    renderToImages is not implemented on it and throws.
  • Distinct per-corner radii collapse to a single value and numeric dash arrays map to
    the nearest preset in PPTX.
  • PPTX has no outline tree: the first bookmark on a page names its slide and further
    bookmarks on that page are dropped
    , so a bookmark-heavy document does not survive
    the round trip as navigation.
  • Inline SVG stays native for simple layers, but arbitrary clips, exact dash/cap/join
    styles and off-viewBox art fall back to a transparent PNG, and gradient paints use
    their primary colour.
  • OPC has no producer field, so document metadata's producer value is not representable
    in a deck.
  • graph-compose-render-docx needs graph-compose-render-pdf on the classpath as
    well: opening a session resolves a font-metrics provider and the PDF backend is the
    only artifact that publishes one.