Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A barcode reaches Word, and scans.** The DOCX export dropped a `BarcodeNode` with the
geometry-only nodes, so a receipt or a shipping label lost the code a reader scans. It is
now a PNG picture of the same ZXing matrix the PDF and PPTX backends draw
(`BarcodeMatrices`), one pixel a cell in the symbol's two colours with their alpha, encoded
in memory, at the node's size, with its margin as the space around it, its `anchor` as a
bookmark and its data as the picture's description. Decoded back from the picture, a QR
code and a Code 128 return their data. The data is part of the picture rather than
editable in Word, and the report says so (`APPROXIMATED`), naming a link or a transform
on the barcode as not carried. Outside the body — in a page zone — a barcode is still
skipped. The PDF backend comes in at compile scope for this; see Packaging.

- **A table cell's alignment reaches Word.** The DOCX export read neither half of a cell's
`textAnchor`: every column a template right-aligns or centres — amounts, quantities,
totals — came out flush left, and since Word's default is the top of the cell while the
Expand Down Expand Up @@ -591,9 +602,9 @@ follow semantic versioning; release dates are ISO 8601.
publishes one — but the DOCX module declared it at test scope, so an application depending
on `graph-compose-core` + `graph-compose-render-docx` failed at `create()` with
`MissingBackendException` before it could export anything, and the class javadoc said the
opposite. The PDF backend is now a `runtime` dependency of the DOCX module, the way the
PPTX module already brings it: nothing compiles against it through this module, and an
application that declared it by hand resolves the same version. Checked with three
opposite. The PDF backend is now a dependency of the DOCX module at compile scope, the way
the PPTX module already brings it — the DOCX module draws a barcode with its matrix encoder
— and an application that declared it by hand resolves the same version. Checked with three
consumer applications built outside the reactor against the installed artifacts — DOCX
alone, PDF + DOCX, PDF + PPTX + DOCX: each exports, and each resolves one Apache POI
(5.5.1, `poi-ooxml-lite`, never `-full`), one XMLBeans (5.3.0) and one PDFBox (3.0.8), so
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Committed internal direction for the post-2.0 line: refactors, scale work, and t
Not committed. Reflects current thinking; priorities may shift based on user feedback and adoption signals.

- **PPTX beyond beta.** Graduate the fixed-layout PPTX backend from `@Beta` to stable and close the remaining fidelity gaps — true vector clipping instead of the raster fallback ([#413](https://github.com/DemchaAV/GraphCompose/issues/413)), exact numeric dash arrays, and distinct per-corner radii.
- **A strict mode for DOCX.** Dropping a node the format cannot carry (`shape`, `line`, `ellipse`, `barcode`) now warns, so the loss is at least visible in the log. What is missing is the option to refuse: a flag that fails the export instead of silently producing a document with content gone.
- **A strict mode for DOCX.** Dropping a node the format cannot carry (`shape`, `line`, `ellipse`) now warns, so the loss is at least visible in the log. What is missing is the option to refuse: a flag that fails the export instead of silently producing a document with content gone.
- **Block-level alignment for fixed-size flow children.** Paths, images, layer stacks, shape containers and barcodes currently left-align in a flow; centring one means wrapping it in a full-width `ShapeContainer` just to use its CENTER anchor. Add a per-node horizontal align (left / centre / right — the `margin: auto` / `align(center)` analogue) so a fixed box can place itself in the flow directly. Surfaced by the v1.8 SVG icon-gallery and feature-catalog work.
- **Backend-neutral layout measurement.** Decouple measurement from PDFBox-specific resources so non-PDF backends do not pull PDFBox into the dependency graph.
- **DOCX maturity.** Either expand DOCX coverage toward PDF parity, or move DOCX behind an explicitly experimental flag.
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ SemanticExportContext)` and `exportSections(List<SemanticSection>)`, so
`renderToImages` and the raster-slide builder option are not questions it
can be asked. Drawing
nodes are a different case: `ShapeNode`, `LineNode`, `EllipseNode`,
`PolygonNode`, `PathNode` and `BarcodeNode` all reach
`PolygonNode` and `PathNode` all reach
`DocxSemanticBackend.writeNode` and are dropped there with a logged
warning, and Word can express floating shapes, pictures and gradient
fills. Those rows are ❌ — no implementation, no current plan — not
`n/a`.
`n/a`. A `BarcodeNode` reaches it too and is written as a picture.

The PPTX *semantic* skeleton (`PptxSemanticBackend`, slide-safe node
validation + manifest, writes no file) is out of scope for this matrix.
Expand Down Expand Up @@ -71,7 +71,7 @@ Payload records live in `core` under
| Radial gradient fill (`DocumentPaint`) | ✅ `PdfShadingSupport` | ⚠️ `PptxGradientFill` (`circle` path shade — DrawingML cannot express radius-to-farthest-corner exactly) | ❌ |
| Gradient strokes | ✅ `PdfPathPainter` (pattern stroking colour) | ✅ `PptxGradientFill` (native `ln`/`gradFill`) | ❌ |
| Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) | ✅ `PdfImageFragmentRenderHandler` | ✅ `PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ✅ `DocxSemanticBackend.writeImage` (the box comes from `NodeDefinitionSupport.resolveImageDimensions`, the same rule layout applies to `width` / `height` / `scale` and the content-width clamp; CONTAIN is embedded at its fitted size, COVER via the picture source crop as in PPTX, and the picture type is read from the bytes) |
| Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (vector: the ZXing bit matrix filled as merged rectangles) | ✅ `PptxBarcodeFragmentRenderHandler` (native freeforms: the same ZXing bit matrix as merged rectangles) | ❌ |
| Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (vector: the ZXing bit matrix filled as merged rectangles) | ✅ `PptxBarcodeFragmentRenderHandler` (native freeforms: the same ZXing bit matrix as merged rectangles) | ⚠️ `DocxSemanticBackend.writeBarcode` (a PNG picture of the same ZXing bit matrix through `BarcodeMatrices`, one pixel a cell, in the symbol's two colours with their alpha and at the node's size, its data as the picture's description; it scans, but its data is part of the picture rather than editable, reported `APPROXIMATED`, which also names a link or a transform on it as not carried; an `anchor` is a bookmark on its paragraph; in a page zone it is skipped) |
| 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` (a real Word table on the grid `TableGrid` resolves: `colSpan` maps to `w:gridSpan`, `rowSpan` to `w:vMerge`, and the cascaded `DocumentTableStyle` text style reaches the cell's runs; the cell's fill maps to `w:shd` and its stroke to `w:tcBorders`; the cascaded `textAnchor` maps to `w:vAlign` on every cell and to `w:jc` on a text cell's paragraph, with the engine's default — the vertical middle, on the left, or on the right for a right-to-left cell — and `DEFAULT` at the bottom left, as the renderer draws it; a composed cell is written by the same writers that write its node anywhere, so one built from an image, a list or a table carries it — a nested table is a real `w:tbl` taking the width of the column it sits in, which is the column's rather than the one the page gives it, since the layout reports a composed cell's content under the owner's path; a fill's opacity is dropped since `w:shd` is opaque; Word re-paginates, so the export states where the layout breaks: every row the layout placed is `w:cantSplit`, `repeatHeader(n)` rows are `w:tblHeader` and keep with the row under them, and a row of blocks is kept whole the same way) |
| 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 |
| Timeline rail — one logical connector line resolved from marker and entry anchors after layout (`ShapeFragmentPayload` per page) | ✅ `PdfShapeFragmentRenderHandler` — one fragment per page, spliced beneath the markers | ✅ `PptxShapeFragmentRenderHandler` — same payload, same per-page fragments | ⚠️ omitted: the rail is resolved fixed-layout geometry and `DocxSemanticBackend` consumes the semantic tree, never a `LayoutGraph`. A timeline's **content** exports in full — entries, titles, meta and bodies — and the export does not throw; only the drawn line is absent. Marker shapes are geometry and may be omitted for the same reason |
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/package-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ships them differs.
| `graph-compose-core` | The lean engine: `com.demcha.compose`, the canonical `document.*` authoring surface (`api` / `dsl` / `node` / `style` / `table` / `snapshot`), `document.showcase` (`FontShowcase`), the `document.backend.fixed` SPI seam, the public `document.backend.fixed.pdf.options` records, `document.layout`, `font.*`, and the internal `engine.*` foundation. |
| `graph-compose-render-pdf` | The PDFBox backend: `document.backend.fixed.pdf.**` (the `PdfFixedLayoutBackend` impl + handlers) and the `engine.render.pdf.**` render tree. Registers the PDF `FixedLayoutBackendProvider` / `FontMetricsProvider`. |
| `graph-compose-render-pptx` | The POI XSLF backend: `document.backend.fixed.pptx.**` (the `PptxFixedLayoutBackend` impl + handlers), registering the `"pptx"` `FixedLayoutBackendProvider`. Also carries the older `document.backend.semantic.pptx` manifest exporter. Depends on `graph-compose-render-pdf` for shared font measurement and the clip raster pass. |
| `graph-compose-render-docx` | The POI semantic exporter — `document.backend.semantic.docx`. Brings `graph-compose-render-pdf` at runtime scope, because opening a session needs its font-metrics provider. |
| `graph-compose-render-docx` | The POI semantic exporter — `document.backend.semantic.docx`. Brings `graph-compose-render-pdf` at compile scope, because opening a session needs its font-metrics provider and a barcode is drawn with its matrix encoder. |
| `graph-compose-templates` | The built-in preset families — `document.templates.**`. |
| `graph-compose-testing` | Consumer test support — `com.demcha.compose.testing.**`. |
| `graph-compose` | Back-compat wrapper: an empty jar over `graph-compose-core` + `graph-compose-render-pdf`. |
Expand Down
10 changes: 8 additions & 2 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,14 @@ tint it was flattened to. Recorded, like the other two.

## What is skipped

Lines, ellipses, standalone shapes, and barcodes are **silently skipped**
— they are pure fixed-layout geometry with no semantic equivalent.
Lines, ellipses and standalone shapes are **skipped**, and the report names
each one — they are pure fixed-layout geometry with no semantic equivalent.

A barcode in the body is not skipped: it exports as a picture of the symbol at
its size, the same matrix the PDF draws, so it scans, with its data as the
picture's description. Its data is part of the picture — changing it means
exporting again — and the report says so, and names a link or a transform on it
as not carried. In a page zone a barcode is still skipped.
The text header and footer slots, watermarks, and protection options are
also ignored by the current exporter.

Expand Down
10 changes: 6 additions & 4 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ bullet `•` (U+2022) **is** in WinAnsi, but the larger black circle `●`

Full detail and the coverage table: [font coverage and glyph fallback](font-coverage.md).

## DOCX export is missing shapes, lines, ellipses, or barcodes
## DOCX export is missing shapes, lines, or ellipses

**Cause.** The DOCX backend (`DocxSemanticBackend`, Apache POI) is a
**semantic** exporter. POI cannot express fixed-layout graphics, so
`shape`, `line`, `ellipse`, and `barcode` nodes are **dropped silently**,
and `ShapeContainerNode` clipping / `DocumentTransform` rotation + scale
fall back to inline content with a one-time capability warning.
`shape`, `line`, and `ellipse` nodes are **dropped** — logged once per kind and
named in the export report — and `ShapeContainerNode` clipping /
`DocumentTransform` rotation + scale fall back to inline content with a
one-time capability warning. A `barcode` is not dropped: it is written as a
picture of the symbol, which scans but whose data is not editable in Word.

**Fix.** Export those documents to **PDF** (the full-fidelity backend).
Use DOCX only for paragraph / list / table / image / section content.
Expand Down
7 changes: 4 additions & 3 deletions render-docx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ Add it (at compile scope) only when you export `.docx`. It is **not** included b

**From 2.5.0 it is sufficient on its own.** Opening a `DocumentSession` resolves a
`FontMetricsProvider` so text can be measured, and `graph-compose-render-pdf` is the only
artifact that publishes one — so this module brings it at `runtime` scope. A classpath of
`graph-compose-core` + `graph-compose-render-docx` opens a session and exports `.docx`;
you never compile against the PDF backend through it.
artifact that publishes one — so this module brings it, at compile scope, as the PPTX
module does: a barcode is written as a picture of the same matrix the PDF backend draws,
through that module's encoder. A classpath of `graph-compose-core` +
`graph-compose-render-docx` opens a session and exports `.docx`.

**On 2.4.x and earlier it was not.** Those versions declared the PDF backend at test scope
only, and core + render-docx failed at `create()` with `MissingBackendException` before any
Expand Down
12 changes: 6 additions & 6 deletions render-docx/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- Runtime, not test. Opening a DocumentSession resolves a FontMetricsProvider,
and graph-compose-render-pdf is the only artifact that publishes one, so a
<!-- Not test. Opening a DocumentSession resolves a FontMetricsProvider, and
graph-compose-render-pdf is the only artifact that publishes one, so a
classpath of core + render-docx alone failed at create() before any export —
there is no working DOCX export without it. Carried at runtime scope so a
consumer never compiles against the PDF backend through this module; the PPTX
backend already brings it the same way. -->
there is no working DOCX export without it. Compile scope, as the PPTX backend
carries it: a barcode is written as a picture of the same ZXing matrix the PDF
and PPTX backends draw, through render-pdf's BarcodeMatrices, so the three
cannot encode one symbol differently. -->
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-render-pdf</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>

<!-- Test dependencies -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.demcha.compose.document.backend.semantic.docx;

import com.demcha.compose.document.backend.fixed.pdf.handlers.BarcodeMatrices;
import com.demcha.compose.engine.components.content.barcode.BarcodeData;
import com.google.zxing.common.BitMatrix;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
* A barcode as the picture Word can hold.
*
* <p>Word has no barcode, and drawing the symbol from shapes would make every bar a separate
* object a reader can knock out of place. So the symbol is a picture: the same ZXing matrix
* the PDF and PPTX backends draw ({@link BarcodeMatrices}), one pixel per cell — the matrix is
* already at least two cells per point and two hundred a side — in two colours, the foreground
* for a dark cell and the background for the rest, each with its own alpha. The data is not
* editable in Word; the picture scans as the page's does.</p>
*
* <p>The PNG is encoded in memory. {@code ImageIO.write} to a stream caches through a temporary
* file by default, which is the per-barcode cost the PDF backend's barcodes were measured
* paying and stopped paying when they became vectors.</p>
*/
final class DocxBarcodePictures {

private DocxBarcodePictures() {
}

/**
* Encodes a barcode as a PNG sized for a box of the given size.
*
* @param data the symbol, its colours and its quiet zone
* @param width the box width in points
* @param height the box height in points
* @return the PNG bytes, one pixel per matrix cell
* @throws IOException when ZXing cannot encode the content in that symbology
*/
static byte[] png(BarcodeData data, double width, double height) throws IOException {
BitMatrix matrix = BarcodeMatrices.encode(data, (int) width, (int) height);
BufferedImage image = new BufferedImage(matrix.getWidth(), matrix.getHeight(),
BufferedImage.TYPE_BYTE_BINARY, twoColours(data.getBackground(), data.getForeground()));
WritableRaster raster = image.getRaster();
int[] row = new int[matrix.getWidth()];
for (int y = 0; y < matrix.getHeight(); y++) {
for (int x = 0; x < row.length; x++) {
row[x] = matrix.get(x, y) ? 1 : 0;
}
raster.setPixels(0, y, row.length, 1, row);
}
return encode(image);
}

/** Index 0 is the background, index 1 the foreground, alpha included. */
private static IndexColorModel twoColours(Color background, Color foreground) {
return new IndexColorModel(1, 2,
new byte[]{(byte) background.getRed(), (byte) foreground.getRed()},
new byte[]{(byte) background.getGreen(), (byte) foreground.getGreen()},
new byte[]{(byte) background.getBlue(), (byte) foreground.getBlue()},
new byte[]{(byte) background.getAlpha(), (byte) foreground.getAlpha()});
}

private static byte[] encode(BufferedImage image) throws IOException {
ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
MemoryCacheImageOutputStream out = new MemoryCacheImageOutputStream(bytes)) {
writer.setOutput(out);
writer.write(image);
out.flush();
return bytes.toByteArray();
} finally {
writer.dispose();
}
}
}
Loading
Loading