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-pptxis on the classpath; a missing backend fails with
MissingBackendExceptionnaming the artifact.DocumentPageSize.SLIDE_16_9
(960 × 540 pt) andSLIDE_4_3(720 × 540 pt) match the PowerPoint defaults.
The PPTX surface — thedocument.backend.fixed.pptxpackages 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-sectionbuildPdf(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 getsrw-r--r--when it is new. Replacing the destination entry replaces
a symlink rather than writing through it. - Keep a heading with its content —
SectionBuilder.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 fromkeepTogether(), 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 thefalsedefault), and
SectionNodeandLineNodeeach gain a trailingkeepWithNextrecord component.
Constructor calls are unaffected — both records keep an overload at the previous
argument count — but the canonical component count changed (SectionNode13 → 14,
LineNode17 → 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; usebuildPptx(Path). The
session has a single configured output path, shared withbuildPdf(), 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 firstServiceLoadermatch — letting classpath order decide the
renderer — an ambiguous format now throwsIllegalStateExceptionnaming the
competing provider classes. MarkdownInlinegainsappendTransformed,appendUpperCasedand
appendIfPresent, andPdfRenderEnvironmentgainsfillAlphaState/
strokeAlphaStatefor 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 withUnsupportedNodeCapabilityException; custom handlers
plug in throughPptxFragmentRenderHandlerandBuilder.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-Italicand 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.UNDERLINEandSTRIKETHROUGH.
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.rgbaeverywhere —
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/toImageno longer rasterize binary fonts with substitute glyphs.
PDFBox writes embedded font subsets only duringsave(), 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
DEBUGon
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
EngineeringResumeand
SidebarPortraitpresets, 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.composeentirely, 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-pptxdocuments 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-bleedCanvasLayerNode, 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
andgraph-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-pptxbefore generating. - The PDF backend reuses one
PDExtendedGraphicsStateper 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/ExtGStateresources at all. graph-compose-render-pptxdepends ongraph-compose-render-pdfat 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;
renderToImagesis 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-docxneedsgraph-compose-render-pdfon the classpath as
well: opening a session resolves a font-metrics provider and the PDF backend is the
only artifact that publishes one.