Skip to content

Commit 736e599

Browse files
authored
docs(matrix): separate what DOCX cannot hold from what it does not do (#448)
The preamble said geometry rows were n/a for the semantic backend by design, while the table marked them with the symbol the legend defines as "no implementation, no current plan". The column therefore read as almost nothing supported, and gave no way to tell an inapplicable concept from a gap someone could close. Drawing nodes are not inapplicable: shape, line, ellipse, polygon, path and barcode all reach writeNode and are dropped there with a logged warning, and Word expresses floating shapes, pictures and gradient fills. Those stay ❌. What is genuinely n/a is the fixed-layout SPI — SemanticBackend carries only name() and export(), so renderSections, renderToImages and the raster-slide builder option are questions it cannot be asked. Three cells move. Three more were overstated. Image fit never reads fitMode or scale, so CONTAIN and COVER behave as STRETCH and an unsized node falls back to 100x100 pt. Tables write cell text but not colSpan, rowSpan, per-cell style or fill and border paint. Metadata hits the same OPC limitation the PPTX row already records.
1 parent 2099476 commit 736e599

3 files changed

Lines changed: 44 additions & 20 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ For a Spring Boot `@RestController` streaming the PDF straight to the response,
207207
| Format | Status | Notes |
208208
|---|---|---|
209209
| PDF | Production | Fixed-layout backend on PDFBox 3.0. Full DSL coverage. |
210-
| DOCX | Partial | Semantic export via Apache POI — paragraphs, block images, tables and metadata. Word owns the flow, so drawing nodes (`shape`, `line`, `ellipse`, `barcode`) are dropped silently. Beyond geometry, **hyperlinks, bookmarks, headers/footers and multi-section documents are not implemented** — see [render-docx](./render-docx/README.md#what-it-maps-and-what-it-does-not). |
210+
| DOCX | Partial | Semantic export via Apache POI — paragraphs, lists, block images, tables and metadata. Word owns the flow, so drawing nodes (`shape`, `line`, `ellipse`, `barcode`) are dropped, one logged warning per kind. **Hyperlinks, bookmarks and headers/footers are not implemented**, table `colSpan`/`rowSpan` is not applied, and image fit modes are ignored — see [render-docx](./render-docx/README.md#what-it-maps-and-what-it-does-not). |
211211
| PPTX | Beta | Fixed-layout export via Apache POI from the same resolved layout — one page per editable slide with native shapes and text frames; clipped regions land as pixel-exact pictures. First shipped in 2.1, marked `@Beta` while the API shape settles. |
212212

213213
### Text & internationalization

docs/architecture/backend-capability-matrix.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,19 @@ Backends:
3030
while feedback lands (see [../api-stability.md](../api-stability.md)).
3131
- **DOCX (semantic)**`graph-compose-render-docx`,
3232
`DocxSemanticBackend`. Walks the semantic node tree, deliberately
33-
ignores fixed-layout geometry; Word owns the flow. Geometry rows are
34-
n/a for it by design.
33+
ignores fixed-layout geometry; Word owns the flow.
34+
35+
Reading the DOCX column: a row is `n/a` only when the capability is
36+
declared on the fixed-layout SPI and can never reach a semantic backend —
37+
`SemanticBackend` carries just `name()` and `export(DocumentGraph,
38+
SemanticExportContext)`, so `renderSections`, `renderToImages` and the
39+
raster-slide builder option are not questions it can be asked. Drawing
40+
nodes are a different case: `ShapeNode`, `LineNode`, `EllipseNode`,
41+
`PolygonNode`, `PathNode` and `BarcodeNode` all reach
42+
`DocxSemanticBackend.writeNode` and are dropped there with a logged
43+
warning, and Word can express floating shapes, pictures and gradient
44+
fills. Those rows are ❌ — no implementation, no current plan — not
45+
`n/a`.
3546

3647
The PPTX *semantic* skeleton (`PptxSemanticBackend`, slide-safe node
3748
validation + manifest, writes no file) is out of scope for this matrix.
@@ -56,9 +67,9 @@ Payload records live in `core` under
5667
| Linear gradient fill (`DocumentPaint`) |`PdfShadingSupport` |`PptxGradientFill` (native `gradFill`; explicit-axis endpoints approximate to the angle) ||
5768
| Radial gradient fill (`DocumentPaint`) |`PdfShadingSupport` | ⚠️ `PptxGradientFill` (`circle` path shade — DrawingML cannot express radius-to-farthest-corner exactly) ||
5869
| Gradient strokes |`PdfPathPainter` (pattern stroking colour) |`PptxGradientFill` (native `ln`/`gradFill`) ||
59-
| Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) |`PdfImageFragmentRenderHandler` |`PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ✅ semantic images (`DocxSemanticBackend`) |
70+
| Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) |`PdfImageFragmentRenderHandler` |`PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ⚠️ `DocxSemanticBackend.writeImage` (the picture is embedded at the node's width/height; `fitMode` and `scale` are never read, so CONTAIN and COVER behave as STRETCH, a node with neither width nor height falls back to 100×100 pt, and every picture is declared `PICTURE_TYPE_PNG`) |
6071
| Barcode / QR (`BarcodeFragmentPayload`) |`PdfBarcodeFragmentRenderHandler` (ZXing raster) |`PptxBarcodeFragmentRenderHandler` (identical ZXing raster) ||
61-
| Table rows — resolved cells, row/col spans, two-pass fill/border paint (`TableRowFragmentPayload`) |`PdfTableRowFragmentRenderHandler` + row grouping in `PdfFixedLayoutBackend` |`PptxTableRowFragmentRenderHandler` + row grouping in `PptxFixedLayoutBackend` (positioned rectangles, edge lines, and text frames — never native PPTX tables, which re-lay-out content) | ✅ semantic tables (`DocxSemanticBackend`) |
72+
| Table rows — resolved cells, row/col spans, two-pass fill/border paint (`TableRowFragmentPayload`) |`PdfTableRowFragmentRenderHandler` + row grouping in `PdfFixedLayoutBackend` |`PptxTableRowFragmentRenderHandler` + row grouping in `PptxFixedLayoutBackend` (positioned rectangles, edge lines, and text frames — never native PPTX tables, which re-lay-out content) | ⚠️ `DocxSemanticBackend.writeTable` (cell text becomes a real Word table; `colSpan` / `rowSpan`, the per-cell `DocumentTableStyle`, and fill/border paint are not applied, and cell runs carry no text style) |
6273
| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning |
6374
| Transform open/close — rotate/scale about fragment centre (`TransformBegin/EndPayload`) |`PdfTransformBegin/EndRenderHandler` |`PptxTransformBegin/EndRenderHandler` (group shape; rotation and centre-pivot scaling via the exterior/interior frame ratio) | ⚠️ inline fallback + one-time capability warning |
6475
| Anchor markers (`AnchorMarkerPayload`) |`PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` |`PptxAnchorMarkerRenderHandler` + `PptxNavigationWriter` (slide-jump hyperlinks resolved after all fragments, so forward references work) ||
@@ -81,7 +92,7 @@ honour an option ignores it (documented contract).
8192

8293
| Capability | PDF (fixed) | PPTX (fixed) | DOCX (semantic) |
8394
|---|---|---|---|
84-
| Metadata (title, author, …) |`PdfDocumentPostProcessor` | ⚠️ `applyMetadata` in `PptxFixedLayoutBackend` (OPC core properties + extended `Application`; OPC has no producer field, so that value is not representable) | ✅ (`DocxSemanticBackend` via `SemanticExportContext`) |
95+
| Metadata (title, author, …) |`PdfDocumentPostProcessor` | ⚠️ `applyMetadata` in `PptxFixedLayoutBackend` (OPC core properties + extended `Application`; OPC has no producer field, so that value is not representable) | ⚠️ `applyOutputOptions` (OPC core properties — title, author as creator, subject, keywords; OPC has no producer field here either, so that value is not representable) |
8596
| Watermark (front/back layers) |`PdfWatermarkRenderer` |`PptxChromeRenderer` (per-slide shape at the PDF placement math; behind-content applies before fragments, so no z-order surgery) ||
8697
| Repeating headers / footers |`PdfHeaderFooterRenderer` |`PptxChromeRenderer` (positioned per-slide text boxes; `{page}` / `{pages}` / `{date}` tokens with the numbering window rules) ||
8798
| Protection / encryption |`PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) ||
@@ -93,12 +104,12 @@ honour an option ignores it (documented contract).
93104
| Capability | PDF (fixed) | PPTX (fixed) | DOCX (semantic) |
94105
|---|---|---|---|
95106
| Render to bytes / stream / file (`FixedLayoutRenderer`) |`PdfFixedLayoutBackend` |`PptxFixedLayoutBackend` |`DocxSemanticBackend` (`SemanticBackend<byte[]>`) |
96-
| Render to images (`renderToImages`) | ✅ PDFBox `PDFRenderer` | ❌ (throws with a pointer to the PDF backend — POI's slide rasterizer cannot honour embedded fonts, and the PDF raster of the same graph is the canonical image output) | |
97-
| Raster-slide mode — every page as one full-slide picture, pixel-exact to the PDF/PNG output (`Builder.rasterSlides(dpi)`) | n/a (the PDF raster is the source) |`PptxFixedLayoutBackend` | |
98-
| Multi-section documents (`renderSections`, per-section chrome, cross-section links) |`buildSectionsDocument` in `PdfFixedLayoutBackend` | ⚠️ `renderSections` in `PptxFixedLayoutBackend` (a deck carries one slide size, so every section must share the same page canvas — differing sizes throw) | |
107+
| Render to images (`renderToImages`) | ✅ PDFBox `PDFRenderer` | ❌ (throws with a pointer to the PDF backend — POI's slide rasterizer cannot honour embedded fonts, and the PDF raster of the same graph is the canonical image output) | n/a (a `FixedLayoutRenderer` surface; the semantic SPI has no raster output) |
108+
| Raster-slide mode — every page as one full-slide picture, pixel-exact to the PDF/PNG output (`Builder.rasterSlides(dpi)`) | n/a (the PDF raster is the source) |`PptxFixedLayoutBackend` | n/a (a fixed-layout backend builder option) |
109+
| Multi-section documents (`renderSections`, per-section chrome, cross-section links) |`buildSectionsDocument` in `PdfFixedLayoutBackend` | ⚠️ `renderSections` in `PptxFixedLayoutBackend` (a deck carries one slide size, so every section must share the same page canvas — differing sizes throw) | n/a (`renderSections` is declared on `FixedLayoutRenderer`; `MultiSectionDocument` drives fixed-layout backends only) |
99110
| Deterministic output (render twice → identical bytes) |`PdfDeterminismWriter` |`PptxDeterminismWriter` (pinned OPC created/modified + zip entry-time normalization) ||
100111
| ServiceLoader discovery (`FixedLayoutBackendProvider`) |`PdfFixedLayoutBackendProvider` (`format() == "pdf"`) |`PptxFixedLayoutBackendProvider` (`format() == "pptx"`) | n/a (semantic SPI: `SemanticBackend`) |
101-
| `DocumentSession` convenience methods |`buildPdf` / `writePdf` / `toPdfBytes` / `toImages` |`buildPptx` / `writePptx` / `toPptxBytes` (resolved via `BackendProviders.fixedLayout("pptx")`; session chrome applies; fails with `MissingBackendException` naming `graph-compose-render-pptx` when the backend is absent) | via `session.export(new DocxSemanticBackend(...))` |
112+
| `DocumentSession` convenience methods |`buildPdf` / `writePdf` / `toPdfBytes` / `toImages` |`buildPptx` / `writePptx` / `toPptxBytes` (resolved via `BackendProviders.fixedLayout("pptx")`; session chrome applies; fails with `MissingBackendException` naming `graph-compose-render-pptx` when the backend is absent) | `session.export(new DocxSemanticBackend(...), target)` — the semantic backend is named directly rather than discovered |
102113

103114
## Fidelity notes (PPTX)
104115

render-docx/README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,27 @@ try (var doc = GraphCompose.document().create()) {
3838
## What it maps, and what it does not
3939

4040
DOCX walks the **semantic node graph**, not the fixed PDF layout — Word owns the flow, so
41-
the page geometry the PDF backend resolves is deliberately ignored. Shapes, gradients,
42-
clips, transforms and alpha have no DOCX counterpart by design; a document that draws with
43-
them exports its text and tables, not its drawing.
41+
the page geometry the PDF backend resolves is deliberately ignored. Drawing nodes — shape,
42+
line, ellipse, polygon, path, barcode — do reach the backend, and are dropped there with one
43+
logged warning per kind; a clip or transform container renders its children inline, without
44+
the boundary and without the transform. A document that draws exports its text and tables,
45+
not its drawing.
4446

45-
What maps: paragraphs (runs, alignment), block images (`STRETCH` / `CONTAIN` / `COVER`),
46-
table rows including row and column spans, and document metadata. Run styling carries font
47-
family, size, colour, bold, italic and underline.
47+
What maps: paragraphs, lists, block images, tables, and document metadata (title, author,
48+
subject, keywords). Run styling carries font family, size, colour, bold, italic and
49+
underline.
4850

49-
Beyond geometry, these are **not implemented** even though Word itself can express them —
50-
check the list before you promise a `.docx` to a reader:
51+
What maps only in part:
52+
53+
- **Table cells keep their text, not their structure.** `colSpan` and `rowSpan` are not
54+
applied, so a table with merged cells exports with its columns misaligned. Per-cell
55+
style and fill/border paint are dropped, and cell text carries no run styling.
56+
- **Image fit is ignored.** The picture is embedded at the node's width and height;
57+
`CONTAIN` and `COVER` therefore behave as `STRETCH`, and an image sized only by `scale`
58+
falls back to 100 × 100 pt.
59+
60+
These are **not implemented** even though Word itself can express them — check the list
61+
before you promise a `.docx` to a reader:
5162

5263
- **Hyperlinks are dropped**, external and internal alike; the link text survives as plain text.
5364
- **No bookmarks and no navigation outline.**
@@ -57,10 +68,12 @@ check the list before you promise a `.docx` to a reader:
5768
- **Per-run styling in a mixed-style paragraph is flattened.** Every run in a `RichText`
5869
paragraph is written with the paragraph's style, so a bold or accent-coloured segment
5970
loses its own styling; the text itself is kept.
60-
- **Multi-section documents export through the fixed-layout backends only**
61-
`renderSections` is not part of the semantic path.
6271
- **Output is not byte-deterministic**: rendering twice does not produce identical files.
6372

73+
Multi-section documents are a separate case: `renderSections` is declared on the
74+
fixed-layout SPI, and `SemanticBackend` carries only `name()` and `export(...)`, so a
75+
multi-section export runs through the PDF or PPTX backend rather than this one.
76+
6477
Per-capability detail, with the implementing class for every supported cell:
6578
[backend capability matrix](../docs/architecture/backend-capability-matrix.md).
6679

0 commit comments

Comments
 (0)