From 30e30a94475dd19abe0ff08618256bc5f8bad6d8 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 25 Jul 2026 15:06:18 +0100 Subject: [PATCH 1/2] docs(changelog): rewrite the v2.1.0 section as release notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The section read as the journal it was written as: 44 bullets in commit order, 17 of them consecutive PPTX implementation slices, several superseded by later ones. It ships verbatim into the GitHub Release, so a reader met the backend's history rather than what it does. Restructure it around what a reader needs — Highlights, Public API, PPTX backend, Fixed, Templates, Documentation and examples, Build and compatibility, and a Known limitations section the notes previously had no equivalent of. 329 lines become 216 without dropping a shipped fact. Three statements were wrong rather than merely verbose, and are corrected rather than reworded: - Determinism was advertised as "renders byte-identical decks across runs" with no caveat. It is opt-in and off by default: the SPI provider that buildPptx(Path) resolves never enables it, so the convenience methods stream with live timestamps. The notes now say so, and Known limitations repeats it. - "First capability slice: rectangle shapes, ellipses, and lines" described an intermediate state that the following bullets already superseded with text, tables, images, paths and transforms. - A Tests bullet claimed a default-output-file failure mode whose test was deleted with the no-arg buildPptx() overload. Clipping was also split across a slice and its later correction; it is now one statement about the shipped behaviour. Every mechanism claim was verified against the code rather than carried over: the decoration offsets (0.10 / 0.28 / 0.05 em) against PdfTextDecorations, the once-per-family substitution warning against PptxRenderEnvironment, the duplicate-handler rejection against Builder.addHandler, the 38/24/10/4 capability counts against the matrix, the record component counts against the v2.0.0 tag, and every API name against source. The twin-output shape count (69 native, one picture) and the PDF/PPTX geometry match were measured from the emitted artifacts. Verified: full reactor clean verify green (1522 tests, 0 failures); the release workflow's own awk extraction yields the 216-line section; both linked documents resolve on disk. --- CHANGELOG.md | 515 ++++++++++++++++++++------------------------------- 1 file changed, 201 insertions(+), 314 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db02e65..8644e2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,332 +5,219 @@ follow semantic versioning; release dates are ISO 8601. ## v2.1.0 — Planned -### Public API +### 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.** - `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 - truncated — 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. -- **`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`. - PPTX output now always names its destination, which is also what lets one session - emit both formats. The PPTX surface is `@Beta` (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(format)` and the no-arg default previously took the - first `ServiceLoader` match, so a third-party provider declaring an existing format - silently decided the renderer by classpath order. Both entry points now throw - `IllegalStateException` naming the competing provider classes. +- **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](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 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 to the next page so the heading stays glued to its body. 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 a - page-spanning table or list. Distinct from `keepTogether()`, which relocates a - *whole* block — keep-with-next binds only the start of the following block, the right - tool for a boxed section title above a long, page-spanning body. Inert when nothing - follows (a trailing heading is never moved) and best-effort when the heading plus the - first slice cannot share a page. Default off, so layouts that do not opt in are - unchanged. -- `LineBuilder.keepWithNext()` — the line counterpart of - `SectionBuilder.keepWithNext()`, so a full-width header rule joins its banner's - keep-with-next run and the whole title block (rule + banner + rule) relocates - together instead of the banner stranding apart from its rules or its body. -- Keep-with-next surfaces on the node model as well: `DocumentNode.keepWithNext()` - is a new default method (existing implementations keep the `false` default and - need no change), 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 that defaults the flag to - `false` — but the canonical component count changed (`SectionNode` 13 → 14, + 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. -- **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: a standalone - header section binds to the following body section (a multi-node rule + banner + rule - group binds as one run), and a combined header+body module keeps the heading in a - nested keep-with-next group — including Panel's full-width Profile card, whose header - now stays with the summary if a long profile splits the card. Multi-column presets - place their sections in fixed columns that do not paginate, so no heading can strand - there. -- **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 across a page break — each - title now renders in its own keep-with-next section rather than a bare paragraph. -- **CV entry titles and subtitles accept inline `[text](url)` links.** In the layered - CV presets, a `[label](url)` in an experience/education entry title or subtitle now - renders as a clickable hyperlink — the same inline-Markdown convention already used - for project rows and body text. Upper-cased and letter-spaced preset titles keep the - link too, with the visible label styled and the URL preserved. Where a preset fuses a - line that cannot carry a clickable link — the single-line TimelineMinimal excerpt, and - the combined subtitle+date meta line of MintEditorial/SidebarPortrait experience - entries — the link's label text is shown instead. Titles without inline-Markdown - syntax render exactly as before, so the change is backward-compatible. -- **Project-card titles accept inline `[text](url)` links in the `EngineeringResume` - and `SidebarPortrait` presets.** Their hand-rolled project cards previously emitted - the title as flat styled text, so a `[label](url)` there stayed literal; it now runs - through the same link-aware path as project rows, entry titles, and body text, with - the trailing `" (stack)"` run preserved. Plain titles render exactly as before. -- The fixed-layout PPTX backend ships as `@Beta` (Experimental) in its first - release: the `document.backend.fixed.pptx` packages and the - `DocumentSession` PPTX convenience methods (`toPptxBytes`, `writePptx`, - `buildPptx`) carry the marker, so their API shape may still change in a - minor release while feedback lands. Geometry identity with the PDF backend - is a design invariant and is not subject to change; see - `docs/api-stability.md` for the policy. -- `BackendProviders.fixedLayout(String format)` selects a fixed-layout render - backend by its `FixedLayoutBackendProvider.format()` key (case-insensitive), - so several fixed-layout backends can coexist on one classpath. The no-arg - `BackendProviders.fixedLayout()` default is now deterministic regardless of - classpath order: the `"pdf"` provider wins when present, otherwise the - provider with the lexicographically smallest format. The PDF convenience - paths (`toPdfBytes`, `buildPdf`, multi-section rendering) resolve the PDF - backend explicitly by format. A missing format fails with a - `MissingBackendException` naming the artifact to add. -- `DocumentPageSize.SLIDE_16_9` (960 × 540 pt) and `DocumentPageSize.SLIDE_4_3` - (720 × 540 pt) presets matching the PowerPoint slide defaults. -- `graph-compose-render-pptx` gains a coordinate-exact fixed-layout backend: - `PptxFixedLayoutBackend` (with `PptxFixedLayoutBackendProvider`, - `format() == "pptx"`) consumes the same resolved `LayoutGraph` as the PDF - backend — one page becomes one identically-sized slide and fragments render - at identical coordinates. First capability slice: rectangle shapes (solid - fill, stroke, corner radii, per-side borders), ellipses, and lines, plus a - custom-handler seam (`PptxFragmentRenderHandler`, - `PptxFixedLayoutBackend.Builder#addHandler`). Unsupported payloads fail - with `UnsupportedNodeCapabilityException`; the live per-capability status - lives in `docs/architecture/backend-capability-matrix.md`. -- The fixed PPTX backend renders pre-wrapped paragraph lines as absolute, - wrap-disabled, no-autofit text frames with PDF-identical glyph sanitization, - standard-14 font-family mapping and rich run styles. External hyperlinks use - transparent measured-span hotspots so PowerPoint cannot replace the resolved - text colour and decoration with its hyperlink theme. - Inline chips, raster images, vector shapes and SVG paths retain their measured - baseline boxes. Simple SVG layers stay native; arbitrary clips, exact stroke - styles and off-viewBox art use a transparent PNG fallback. SVG gradients use - their primary colour. Document-local fonts preserve their registered - viewer-facing family name and embed into the deck as EOT-wrapped programs - when their license bits allow; restricted or unreadable fonts degrade to a - plain family-name reference with a one-time warning, and the run baseline - compensates the PDF-vs-viewer ascent difference so text sits on the - measured baseline either way. -- The fixed PPTX backend renders table rows: cell fills, border edge lines - (including the half-stroke internal join extension and the page-start top - border), and anchored cell text as positioned frames — never native - PowerPoint tables, which re-lay-out their content. Contiguous table rows - paint all fills before any border or text, the PDF backend's two-pass - discipline, and row-spanning cells grow downward through their merged rows. -- The fixed PPTX backend warns explicitly whenever a font cannot travel into - the deck identically — once per family per render: standard-14 families name - their metric-compatible replacement (Helvetica → Arial), families registered - without binary sources and unregistered names are flagged as name-only - references that viewers may substitute, and license-restricted embeds - already degrade with a warning. An embedded family stays silent. -- `PptxFixedLayoutBackend.Builder.rasterSlides(int dpi)` — raster-slide mode: - every page renders through the PDF backend and lands as one full-slide - picture, a pixel-exact copy of the PDF/PNG output for decks that must look - identical everywhere. Slides are not editable as text; the default stays - the editable vector mode. - -- The fixed PPTX backend completes the fragment matrix: images (STRETCH / - CONTAIN / COVER via the picture source crop, dimensions decoded from the - real bitmap), barcodes (the PDF handler's identical ZXing raster), polygons - and free paths as freeform geometry, native DrawingML gradient fills and - gradient strokes (radial centres and explicit linear axes approximate, with - a one-time note), and transform markers as rotated, centre-pivot-scaled - group shapes — every fragment between the markers is created inside the - group, PPTX's replacement for PDF's graphics-state matrix. Clip regions - render unclipped with a one-time warning (DrawingML has no graphics-state - clipping); anchor and bookmark markers record their destinations for the - navigation pass. - -- Clipped composites in the fixed PPTX backend are now pixel-exact: the clip - region renders through the PDF backend into one transparent picture placed - on the clip bounds (DrawingML has no graphics-state clipping), including any - enclosing rotation. The picture is not editable as shapes; - `PptxFixedLayoutBackend.Builder.clipRasterFallback(false)` restores the - previous unclipped vector rendering with its one-time warning. - -- The fixed PPTX backend renders document chrome and emits navigation. - Metadata lands in the OPC core properties (title, author, subject, keywords, - plus the extended application property; OPC has no producer field). - Watermarks render per slide with the PDF placement math — text rotated about - its baseline start, images with native alpha, tiling supported — and - behind-content watermarks apply before fragment rendering, so document order - produces the PDF's layering without z-order surgery. Repeating - headers/footers resolve `{page}` / `{pages}` / `{date}` tokens with the - numbering window rules (start-at, count-from, show-on-first-page, numeral - styles). Recorded link rectangles become transparent hotspots after every - fragment is placed: external targets as URL hyperlinks, internal targets as - slide-jump hyperlinks (forward references included; a dangling anchor is - skipped with a warning), and the first bookmark on a page names its slide. - Protection, viewer preferences, and debug overlays have no PPTX counterpart - and are skipped with a one-time warning. -- `PptxFixedLayoutBackend.renderSections` / `writeSections` concatenate - multi-section documents into one deck: each section keeps its own chrome - with section-local page numbering, navigation resolves across section - boundaries, and deck metadata follows the first section that declares it. - A deck carries one slide size, so sections with differing page canvases are - rejected. -- `PptxFixedLayoutBackend.Builder.deterministic(boolean | Instant)` renders - byte-identical decks across runs by pinning the OPC created/modified - properties and normalizing every zip entry timestamp (zone-independent). - -- Provably no-op clips stay editable in the fixed PPTX backend: when every - child of a clip region sits safely inside the clip outline (the common - defensive-clip case — a rounded card whose padded content never reaches the - corners), the raster fallback is skipped and the region renders as native, - editable shapes. Nested clip regions are judged by their confining clip box - and re-dispatch at their own granularity, so an unprovable icon inside a - card no longer rasterizes the whole card — only the icon. The proof is - conservative — unprovable outline kinds (polygons, free paths), transforms - inside the region, stroked polygons and paths (mitred joins), path segments - outside the unit box, table rows, broken nested pairings, and ink whose - stroke or side-border bleed nears the boundary all keep the pixel-exact - raster path. -- PPTX gradients pin their first and last stop offsets to the domain ends, - matching what the PDF shading functions actually paint — a partial-offset - ramp now shades identically in both formats. -- `PptxFixedLayoutBackend.Builder.addHandler` rejects a duplicate custom - handler for the same payload type, matching the PDF builder's contract, and - documents that custom handlers do not apply inside rasterized clip - composites. -- `DocumentSession.toPptxBytes()` / `writePptx(OutputStream)` / - `buildPptx(Path)` / `buildPptx(Path)` — the PPTX counterparts of the PDF - convenience trio. The backend resolves through the format-keyed provider - (`BackendProviders.fixedLayout("pptx")`), so the core stays free of a PPTX - dependency: with `graph-compose-render-pptx` on the classpath the session's - chrome (metadata, watermark, headers/footers) applies exactly as in the PDF - paths, and without it the render fails with a `MissingBackendException` - naming the artifact to add. Stream and default-output-file contracts match - the PDF trio. +- **`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](docs/architecture/backend-capability-matrix.md). +Of the 38 capabilities it tracks, 24 map to a native equivalent, 10 render natively +with an approximated styling detail, and 4 are unsupported. + +- **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 -- **A large clipped region in a PPTX deck is no longer downscaled.** The raster - fallback aims for a 2048-pixel long edge, and that ratio was only capped 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. The scale is now clamped - between native size and 4×. Decks whose clip regions fit a normal page are - unaffected: their scale already saturated at the upper cap. -- The PPTX backend now logs, once per render at `DEBUG` on - `com.demcha.compose.engine.render`, how many clip regions were rasterized and - their total megapixels. Each rasterized region costs a full sub-render through - the PDF backend and loses text editability inside its bounds, so a clip-heavy - deck's cost is visible rather than inferred from render time. Silent when - nothing was rasterized. -- The PDF backend now draws `DocumentTextDecoration.UNDERLINE` and - `STRIKETHROUGH` marks — previously the decoration flags 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 (underline 0.10 em below the baseline, - strikethrough 0.28 em above, thickness 0.05 em — the Type 1 underline - convention) in the run's colour, on paragraph runs, chips, and table cell - text alike. -- The PDF backend honours the alpha channel of `DocumentColor.rgba` on every - remaining surface — text runs, lines, side borders, and table fills, - borders, and cell text — matching the shape fills/strokes that already - carried it and the PPTX backend's native behaviour. Fully opaque documents - render byte-identically to before. -- `DocumentSession.toImages` / `toImage` rasterized documents that use binary - font families (any non-standard-14 file) with substitute glyphs — visually - garbled text with correct spacing. PDFBox writes embedded font subsets only - during `save()`, so rendering the unsaved in-memory document never saw the - real glyph programs. The PDF backend now saves to an in-memory buffer and - reloads before rasterizing; standard-14-only documents are unaffected apart - from the marginal serialization cost. - -### Internal - -- The PDF backend reuses one `PDExtendedGraphicsState` per distinct - (channel, alpha) pair for the whole render pass (each section of a - combined document runs its own pass) — `PdfRenderEnvironment` now owns - the shared states the alpha helpers emit. PDFBox maps repeated - writes of the same instance to one `/ExtGState` resource entry, 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. - -### Documentation - -- New `TwinOutputExample` flagship: a single 16:9 page written once and - emitted twice from the same session — `buildPdf()` and `buildPptx(Path)` - produce a print-ready PDF and a PowerPoint slide with identical geometry. - The README gains a dual-output section showing the PDF render next to - PowerPoint's own export of the generated slide, plus a screenshot of 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. -- New `MavenBannerPptxExample` flagship: a single 16:9 "Available on Maven - Central" brand slide — the `GraphCompose` wordmark, an - `io.github.demchaav:graph-compose` coordinate card, `JAVA 17+ / PDF / PPTX / - AUTO-PAGINATION` tags and a `code → layout → PDF/PPTX` diagram whose amber - connectors branch to both backends — composed as one full-bleed - `CanvasLayerNode` and emitted as an editable PowerPoint slide via - `buildPptx(Path)`. Registered in `GenerateAllExamples` and listed in the - examples gallery with committed PDF/PPTX previews; a native-shape guard - (`MavenBannerNativeShapeTest`) pins the slide to a single picture (the badge - checkmark) with the panels, tags, code and diagram all native. -- `docs/architecture/backend-capability-matrix.md` — a per-capability matrix - of what each render backend supports and which class implements it, - maintained as part of every capability-changing PR. -- The README's backend descriptions now present `graph-compose-render-pptx` - as the fixed-layout, geometry-identical PowerPoint backend (one page per - editable slide) instead of a semantic exporter, with its own - quick-start dependency row; the Engine Deck flagship example additionally - renders as a .pptx deck (`EngineDeckPptxExample`, part of - `GenerateAllExamples`). +- **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. -### Build - -- The examples CI job installs `graph-compose-render-pptx` before generating, - so the Engine Deck PPTX example resolves the backend the same way the DOCX - example resolves its own. - -### Tests +### Templates -- `BackendProvidersTest` (qa) pins format-keyed resolution and the - deterministic PDF default; `MissingBackendContractTest` (core, backend-free - classpath) pins the missing-format diagnostics; `DocumentPageSizeTest` pins - the slide presets. -- PPTX text tests re-read line, absolute-span and inline-graphic anchors through - POI; cover real wrapping, mixed font sizes, vertical seating, custom fonts, - rich styles, links, chips, SVG fallback and exact inline radii; and lock the - shared text-fidelity demo with a backend-neutral layout snapshot. -- PPTX table tests re-derive cell fills, border edge lines, and text frames - from the resolved layout graph across a page break with a repeated header - and a row-spanning cell, and pin the fills-before-ink paint order plus the - page-start top-border branch at the handler level. -- PPTX chrome, navigation, section, and determinism tests pin the OPC core - properties, the watermark's layer order and pass-through rotation, header/ - footer token resolution with numbering windows and roman styles, resolved - URL and slide-jump hotspots (a dangling anchor degrades to a warning, not a - failure), section-local footer restarts with a cross-section slide jump, the - differing-page-size rejection, and render-twice byte equality with pinned - zip entry times; a chrome-and-navigation parity demo renders the PDF/PPTX - pair with per-page PNG previews. -- Session-level PPTX convenience tests pin chrome flow-through (metadata and - footer tokens land in the deck), slide-count identity with the resolved - layout graph, the caller-owned-stream contract, file output, and the - default-output-file and empty-session failure modes; the backend-free - missing-format diagnostics were already pinned by the core's - `MissingBackendContractTest`. -- The qa module now guards the PPTX backend's boundaries at bytecode level: - POI stays a PPTX-backend implementation detail (the engine, the PDF backend, - and the templates must not touch it), the render backends stay - template-agnostic, and the PDF backend must not depend on the PPTX artifact - (the reverse dependency is by design). The `ShapeOutline` exhaustiveness - guard now renders every permit through both fixed-layout backends, so a new - outline kind cannot silently miss a PPTX dispatch branch. +- **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 + +- **`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](https://github.com/DemchaAV/GraphCompose/issues/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. +- `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. ## v2.0.0 — 2026-07-13 From d8f25ba5f7c0c0b2bee6e2a4b867982dd05bad3b Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 25 Jul 2026 15:16:48 +0100 Subject: [PATCH 2/2] docs(changelog): stop overstating what a partial PPTX capability costs The capability summary said 10 of the 38 tracked capabilities "render natively with an approximated styling detail". Reading all ten rows rather than the count shows only about half are that. The others lose something: inline SVG falls back to a transparent PNG for arbitrary clips and off-viewBox art, bookmarks after the first on a page are dropped because PPTX has no outline tree, and metadata's producer value has no OPC field at all. A reader with a bookmark-heavy document would have taken that sentence to mean the only cost was styling, shipped a deck, and silently lost the navigation. Split the sentence so the partial group is not characterised as one thing, and add the three content-losing cases to Known limitations, which previously listed only the cosmetic approximations. --- CHANGELOG.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8644e2da..f211cb67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,8 +79,11 @@ follow semantic versioning; release dates are ISO 8601. The per-capability status — what is native, what is approximated, what is unsupported — lives in [docs/architecture/backend-capability-matrix.md](docs/architecture/backend-capability-matrix.md). -Of the 38 capabilities it tracks, 24 map to a native equivalent, 10 render natively -with an approximated styling detail, and 4 are unsupported. +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 @@ -215,6 +218,14 @@ with an approximated styling detail, and 4 are unsupported. `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.