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
11 changes: 11 additions & 0 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 DOCX export writes a chart in a table cell as its data table, and draws nothing of it over
that table.** Composed in a cell, a chart was written as its data table and drawn as well: its
bars and grid lines stood over the table in Word and LibreOffice on Windows and hid its numbers
— "Sales" read "Sa" — without its axis labels, while the report said only that it was exported
as its data table. Where the chart is all its cell draws — alone, or with text, lists and
wrappers that paint nothing beside it — nothing of its drawing is drawn now. One beside other
drawings in its cell, which the export cannot tell its own from, or in a table composed in a
cell, is still drawn, and its note now says so. The export's log line, the chart's Javadoc, the
charts recipe and the Word export example no longer say the export has no layout pass or draws
no vector geometry: it writes no Word chart. The example's committed DOCX and PDF are
re-rendered for its caption.
- **A DOCX export names a table whose columns it leaves to the editor.** A table with an auto
column whose widths the layout does not show — one composed in another table's cell, which the
layout lays out among that table's own rows, or one with a column edge no row of it shows —
Expand Down
Binary file modified assets/readme/examples/word-export-companion.docx
Binary file not shown.
Binary file modified assets/readme/examples/word-export-companion.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* <p>Because the output is ordinary geometry + text, every existing snapshot and
* visual-parity mechanism covers charts unchanged, any fixed-layout backend
* renders them with no chart-specific code, and determinism is structural, not
* promised. (Semantic exports bypass layout entirely — the DOCX backend falls
* promised. (The DOCX export writes no Word chart of this geometry and falls
* back to the chart's data table.)</p>
*
* @author Artem Demchyshyn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
* primitive nodes. No fixed-layout render handler ever sees a chart — that is
* what keeps charts deterministic and snapshot-testable for free, and what
* gives every fixed-layout backend (PDF today, others later) vector charts
* with no chart-specific code. Semantic exports have no layout pass, so they
* fall back to the chart's data: the DOCX backend writes a
* categories-by-series table.
* with no chart-specific code. The DOCX export writes no Word chart of that
* geometry and falls back to the chart's data: a categories-by-series table.
*
* <p>A chart is {@link com.demcha.compose.document.layout.PaginationPolicy#ATOMIC}:
* it places whole on a page or moves to the next, exactly like an inline image
Expand Down
6 changes: 4 additions & 2 deletions docs/recipes/charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,10 @@ The run's minimum maps to the bottom of the box and its maximum to the top;

Any fixed-layout backend (PDF today) renders charts for free — they are
ordinary primitives by the time rendering starts. The semantic DOCX export
does not draw vector geometry, so it writes the chart's **data table**
(categories × series) with a one-time capability warning.
writes no Word chart, so it writes the chart's **data table**
(categories × series) with a one-time capability warning — in a table cell
too, where a chart drawing alone in its cell is not drawn over that table
(see the [DOCX export recipe](docx-export.md)).

Unsupported combinations fail fast with `UnsupportedOperationException`
rather than rendering silently wrong; in the current release that is only
Expand Down
12 changes: 9 additions & 3 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -788,12 +788,18 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
where the layout puts it, has no place and is dropped, each with a note of its own saying so;
a rule is written as one still.

- **Charts → data table.** A chart compiles to vector geometry, which this
export does not draw. Its *semantic*
- **Charts → data table.** A chart compiles to vector geometry, of which this
export writes no Word chart. Its *semantic*
content is its data, so the backend writes a categories-by-series table
(values formatted with the chart's own axis format) and logs **one
capability warning per export**. The chart's margin and padding above, below
and on its left are not written round the table; its report note says so. See [charts.md](charts.md).
and on its left are not written round the table; its report note says so. A chart that is
all a table cell draws — alone, or with text, lists and wrappers that paint nothing beside
it — is its data table there too, and nothing of its drawing is drawn: drawn, its bars stood
over the table and hid its numbers, without its labels (measured in Word and LibreOffice on
Windows). One beside other drawings in its cell, whose drawings the export cannot tell its
own from, or in a table composed in a cell, is drawn as well, over its table, and its note
says so. See [charts.md](charts.md).
- **A canvas → its contents.** A canvas's drawings stand where it places them; what it
writes — text, pictures, tables — is written one block after another inside its margin and
padding. The places it gives them and the width its text wraps at are not carried, and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,9 @@ public static Path generate() throws Exception {
.addPageBreak(pb -> pb.name("ToPage2"))

.addParagraph("Charts fall back to their data", heading)
.addParagraph("The PDF renders the vector chart; Word has no layout pass, "
+ "so the same node exports as a categories-by-series table.", caption)
.addParagraph("The PDF renders the vector chart; the Word export writes no "
+ "chart, so the same node exports as a categories-by-series "
+ "table.", caption)
.addSection("Chart", s -> s.chart(ChartSpec.bar().data(quarters).build()))

.addParagraph("Geometry stays PDF-only by design", heading)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
private final AtomicBoolean chartWarned = new AtomicBoolean(false);
// Whether the table being written is a chart's data table, which the page does not draw.
private boolean writingAChartsTable;
// The charts that are all their table cell draws, whose drawings in that cell are not drawn:
// the chart is its data table there, as anywhere else (drawCellDrawing). By identity: one chart
// node placed twice, alone in one cell and beside other drawings in another, has the note of
// one not drawn in both.
private final java.util.Set<ChartNode> chartsNotDrawn =
java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
// Geometry-only node kinds already warned about this export pass.
private final java.util.Set<String> warnedNodeKinds =
java.util.concurrent.ConcurrentHashMap.newKeySet();
Expand Down Expand Up @@ -1071,6 +1077,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
zonePartsReported.clear();
zoneLinesReported.clear();
separatorsReported.clear();
chartsNotDrawn.clear();
textFillsWritten = false;
closingBlocks.clear();
keepsUnheld.clear();
Expand Down Expand Up @@ -6312,25 +6319,34 @@ private PictureReach writeInlineTextRuns(XWPFParagraph para, DocumentTextStyle s
}

/**
* Semantic chart fallback: the semantic export has no layout pass, so the
* chart's compiled vector geometry is unavailable here. The chart's
* <em>semantic</em> content is its data, so the fallback writes a
* categories-by-series table (values formatted with the chart's own axis
* format). Authors who need the rendered chart must use the PDF
* fixed-layout backend, where charts compile into ordinary primitives.
* Semantic chart fallback: the chart compiles into the layout's geometry — bars, lines and
* labels placed on the page — and this export writes no Word chart (DrawingML's
* {@code c:chart}) of it. The chart's <em>semantic</em> content is its data, so the fallback
* writes a categories-by-series table (values formatted with the chart's own axis format).
* Authors who need the rendered chart must use the PDF fixed-layout backend, where charts
* compile into ordinary primitives.
*/
private void writeChartFallback(XWPFDocument document, ChartNode node) throws Exception {
// The table is written as it is, set from the left with nothing owed round it for the
// chart's own insets, but where the page measures the space below it (insetSidesLost).
List<String> sides = insetSidesLost(node, true);
// A chart a cell's drawing is all of is not drawn there (drawCellDrawing); one the export
// does not find so — beside other drawings in its cell, which it cannot tell its own
// from, or in a table composed in a cell — is drawn among the table's drawings.
boolean drawnToo = composedInACell(node) && !chartsNotDrawn.contains(node);
report.add(DocxExportReport.Severity.APPROXIMATED, "chart", layout.pathOf(node),
"exported as its data table — a categories-by-series table in the chart's own "
+ "value format — because the drawn chart is layout geometry"
+ (drawnToo ? "; composed in a table cell where the export does not find it drawing alone — "
+ "beside other drawings, or in a table composed in a cell — its bars, lines and "
+ "slices are drawn as well, where the page draws them, over the table and "
+ "without its text"
: "")
+ (sides.isEmpty() ? "" : "; its margin and padding " + listed(sides)
+ " are not written round the table"));
if (chartWarned.compareAndSet(false, true)) {
LOG.warn("docx.export.chart-fallback kind={} — the semantic DOCX export has no "
+ "layout pass, so charts are exported as their data table. "
LOG.warn("docx.export.chart-fallback kind={} — a chart is exported as its data table: the "
+ "export writes no Word chart. "
+ "(One warning per export; use the PDF backend for the rendered chart.)",
node.spec().getClass().getSimpleName());
}
Expand Down Expand Up @@ -13575,7 +13591,11 @@ private CellDrawing drawCellDrawing(TableNode table) {
boolean drew = false;
List<com.demcha.compose.document.layout.PlacedFragment> skipped = new ArrayList<>();
List<CellFragment> pending = new ArrayList<>();
List<DocxLayoutMetrics.CellBox> charts = cellsBuiltFromAChart(table);
for (com.demcha.compose.document.layout.PlacedFragment fragment : fragments) {
if (charts.stream().anyMatch(cell -> cell.holds(fragment))) {
continue;
}
boolean frames = framesText(fragment, content);
if (frames && fragment.payload() instanceof com.demcha.compose.document.layout.payloads.ShapeFragmentPayload) {
skipped.add(fragment);
Expand Down Expand Up @@ -13603,6 +13623,75 @@ private CellDrawing drawCellDrawing(TableNode table) {
return new CellDrawing(drew, skipped, strayed, pending);
}

/**
* The boxes of a table's cells whose drawing is a chart's alone, which are not drawn: the chart
* is written there as its data table, as it is anywhere else.
*
* <p>Drawn too, its bars and grid lines stood over the table in both editors and hid its
* numbers — "Sales" read "Sa", 2.5 read "2." — and its axis labels, the page's text, were not
* drawn at all (measured in Word and LibreOffice on Windows). A cell's drawing is a chart's
* alone where the chart is the only thing in the cell that draws ({@link #chartDrawnAlone}):
* then every drawing the layout places in the cell's box is the chart's. One beside other
* drawings, which the export cannot tell its own from, or in a table composed in a cell, whose
* cells the layout gives no box, is drawn, and its note says so ({@link #writeChartFallback}).
* A drawing another cell pushes wholly into a chart's box, by a layer's offset or a margin
* below zero, is taken for the chart's.</p>
*/
private List<DocxLayoutMetrics.CellBox> cellsBuiltFromAChart(TableNode table) {
List<DocxLayoutMetrics.CellBox> boxes = new ArrayList<>();
for (List<TableGrid.Placement> row : TableGrid.resolve(table)) {
for (TableGrid.Placement placement : row) {
ChartNode chart = placement.cell() == null ? null : chartDrawnAlone(placement.cell().content());
if (chart != null) {
List<DocxLayoutMetrics.CellBox> cell = layout.cellBoxes(table, placement.row(), placement.column());
if (!cell.isEmpty()) {
boxes.addAll(cell);
chartsNotDrawn.add(chart);
}
}
}
}
return boxes;
}

/**
* The kinds that draw nothing in a cell beside a chart: a paragraph and a list, whose pictures
* stand in their lines, a spacer, a page break, and an alignment, which draws what it holds.
*/
private static final java.util.Set<Class<? extends DocumentNode>> DRAWING_NOTHING = java.util.Set.of(
ParagraphNode.class, com.demcha.compose.document.node.ListNode.class, SpacerNode.class,
PageBreakNode.class, com.demcha.compose.document.node.AlignNode.class);

/**
* The chart a cell's content draws alone, or {@code null}: one chart, and beside it nothing
* that draws — text, a list, a spacer, a wrapper that paints nothing — so every drawing the
* layout places in the cell is the chart's.
*
* @param content what the cell is built from, or {@code null}
*/
private static ChartNode chartDrawnAlone(DocumentNode content) {
ChartNode[] chart = new ChartNode[1];
return content != null && drawsAChartAlone(content, chart) ? chart[0] : null;
}

private static boolean drawsAChartAlone(DocumentNode node, ChartNode[] chart) {
if (node instanceof ChartNode found) {
boolean first = chart[0] == null;
chart[0] = found;
return first;
}
boolean wrapper = (node instanceof SectionNode || node instanceof ContainerNode) && paintOf(node).isEmpty();
if (!wrapper && !DRAWING_NOTHING.contains(node.getClass())) {
return false;
}
for (DocumentNode child : DocxWrittenTree.under(node)) {
if (!drawsAChartAlone(child, chart)) {
return false;
}
}
return true;
}

/**
* A drawing a table's composed cell paints, waiting to be anchored.
*
Expand Down
Loading
Loading