diff --git a/CHANGELOG.md b/CHANGELOG.md
index edfb181ae..289fed0c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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 —
diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx
index 56723b6e0..146b309d6 100644
Binary files a/assets/readme/examples/word-export-companion.docx and b/assets/readme/examples/word-export-companion.docx differ
diff --git a/assets/readme/examples/word-export-companion.pdf b/assets/readme/examples/word-export-companion.pdf
index 8b8ce609d..7d48382a8 100644
Binary files a/assets/readme/examples/word-export-companion.pdf and b/assets/readme/examples/word-export-companion.pdf differ
diff --git a/core/src/main/java/com/demcha/compose/document/layout/definitions/ChartDefinition.java b/core/src/main/java/com/demcha/compose/document/layout/definitions/ChartDefinition.java
index 4049258f0..761414806 100644
--- a/core/src/main/java/com/demcha/compose/document/layout/definitions/ChartDefinition.java
+++ b/core/src/main/java/com/demcha/compose/document/layout/definitions/ChartDefinition.java
@@ -36,7 +36,7 @@
*
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.)
*
* @author Artem Demchyshyn
diff --git a/core/src/main/java/com/demcha/compose/document/node/ChartNode.java b/core/src/main/java/com/demcha/compose/document/node/ChartNode.java
index 6e03b880d..175e914ce 100644
--- a/core/src/main/java/com/demcha/compose/document/node/ChartNode.java
+++ b/core/src/main/java/com/demcha/compose/document/node/ChartNode.java
@@ -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.
*
* 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
diff --git a/docs/recipes/charts.md b/docs/recipes/charts.md
index bd37e28cb..384975f9e 100644
--- a/docs/recipes/charts.md
+++ b/docs/recipes/charts.md
@@ -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
diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md
index 64bdb883f..2d24ad852 100644
--- a/docs/recipes/docx-export.md
+++ b/docs/recipes/docx-export.md
@@ -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
diff --git a/examples/src/main/java/com/demcha/examples/features/docx/WordExportExample.java b/examples/src/main/java/com/demcha/examples/features/docx/WordExportExample.java
index f97bfe870..c3eabd1fd 100644
--- a/examples/src/main/java/com/demcha/examples/features/docx/WordExportExample.java
+++ b/examples/src/main/java/com/demcha/examples/features/docx/WordExportExample.java
@@ -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)
diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
index b415a3f66..40326cbd7 100644
--- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
+++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java
@@ -226,6 +226,12 @@ public final class DocxSemanticBackend implements SemanticBackend {
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 chartsNotDrawn =
+ java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
// Geometry-only node kinds already warned about this export pass.
private final java.util.Set warnedNodeKinds =
java.util.concurrent.ConcurrentHashMap.newKeySet();
@@ -1071,6 +1077,7 @@ private byte[] write(List sections, Path outputFile) throws Exc
zonePartsReported.clear();
zoneLinesReported.clear();
separatorsReported.clear();
+ chartsNotDrawn.clear();
textFillsWritten = false;
closingBlocks.clear();
keepsUnheld.clear();
@@ -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
- * semantic 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 semantic 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 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());
}
@@ -13575,7 +13591,11 @@ private CellDrawing drawCellDrawing(TableNode table) {
boolean drew = false;
List skipped = new ArrayList<>();
List pending = new ArrayList<>();
+ List 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);
@@ -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.
+ *
+ * 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.
+ */
+ private List cellsBuiltFromAChart(TableNode table) {
+ List boxes = new ArrayList<>();
+ for (List row : TableGrid.resolve(table)) {
+ for (TableGrid.Placement placement : row) {
+ ChartNode chart = placement.cell() == null ? null : chartDrawnAlone(placement.cell().content());
+ if (chart != null) {
+ List 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> 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.
*
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxChartInACellTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxChartInACellTest.java
new file mode 100644
index 000000000..1a0a8a27f
--- /dev/null
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxChartInACellTest.java
@@ -0,0 +1,207 @@
+package com.demcha.compose.document.backend.semantic.docx;
+
+import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.api.DocumentSession;
+import com.demcha.compose.document.chart.ChartData;
+import com.demcha.compose.document.chart.ChartSpec;
+import com.demcha.compose.document.dsl.EllipseBuilder;
+import com.demcha.compose.document.dsl.PageFlowBuilder;
+import com.demcha.compose.document.dsl.SectionBuilder;
+import com.demcha.compose.document.dsl.TableBuilder;
+import com.demcha.compose.document.node.ChartNode;
+import com.demcha.compose.document.node.DocumentNode;
+import com.demcha.compose.document.style.DocumentColor;
+import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.table.DocumentTableCell;
+import com.demcha.compose.document.table.DocumentTableColumn;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A chart that is all a table cell draws is written there as its data table, as anywhere else, and
+ * none of its drawing is drawn over that table; one beside other drawings in its cell, or in a
+ * table composed in a cell, is drawn among the cell's drawings, and its note says so.
+ */
+class DocxChartInACellTest {
+
+ private static final ChartSpec BARS = ChartSpec.bar().data(ChartData.builder().categories("Q1", "Q2", "Q3")
+ .series("Sales", 3.0, 5.0, 4.0).series("Costs", 2.0, 2.5, 3.5).build()).build();
+ private static final String TABLE = "exported as its data table — a categories-by-series table in the chart's "
+ + "own value format — because the drawn chart is layout geometry";
+ private static final String DRAWN_TOO = TABLE + "; 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";
+
+ @Test
+ void aChartACellIsBuiltFromIsItsDataTableThereAndNothingIsDrawn() throws Exception {
+ Consumer content = beside(new ChartNode(BARS));
+ try (XWPFDocument document = DocxExports.withLayout(500, 400, 30, content)) {
+ XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(1);
+ assertThat(cell.getTables()).as("its data table").hasSize(1);
+ assertThat(cell.getTables().get(0).getRow(0).getCell(1).getText()).isEqualTo("Sales");
+ assertThat(drawings(document)).as("no bar or grid line drawn over it").isZero();
+ }
+ DocxExportReport report = laidOut(content);
+ assertThat(details(report, "chart")).containsExactly(TABLE);
+ assertThat(report.bySubject()).as("nothing drawn to name").doesNotContainKey("cell drawing");
+ }
+
+ @Test
+ void aChartWithOnlyTextBesideItOrInAWrapperThatPaintsNothingIsNotDrawn() throws Exception {
+ for (DocumentNode built : List.of(
+ new SectionBuilder().add(new ChartNode(BARS)).build(),
+ new SectionBuilder().addParagraph("By quarter").add(new ChartNode(BARS)).build())) {
+ Consumer content = beside(built);
+ try (XWPFDocument document = DocxExports.withLayout(500, 400, 30, content)) {
+ assertThat(drawings(document)).as("%s", built).isZero();
+ }
+ assertThat(details(laidOut(content), "chart")).containsExactly(TABLE);
+ }
+ }
+
+ @Test
+ void aChartBesideAnotherDrawingInItsCellIsDrawnTooAndItsNoteSaysSo() throws Exception {
+ Consumer content = beside(new SectionBuilder()
+ .add(new EllipseBuilder().circle(12).fillColor(DocumentColor.BLACK).build())
+ .add(new ChartNode(BARS)).build());
+ try (XWPFDocument document = DocxExports.withLayout(500, 400, 30, content)) {
+ assertThat(drawings(document)).as("the disc, and its bars and grid lines").isGreaterThan(1);
+ }
+ assertThat(details(laidOut(content), "chart")).containsExactly(DRAWN_TOO);
+ }
+
+ @Test
+ void aCellsOwnDrawingBesideAChartsCellIsStillDrawnThoughItReachesIntoIt() throws Exception {
+ // A tile in its own cell, and one its margin below zero widens from 20pt to 52pt in a 30pt
+ // column, reaching into the chart's cell, which does not hold it.
+ for (double right : new double[]{0, -30}) {
+ Consumer content = page -> page.addTable(t -> t
+ .columns(DocumentTableColumn.fixed(30), DocumentTableColumn.fixed(300))
+ .rowCells(DocumentTableCell.node(new com.demcha.compose.document.dsl.ShapeBuilder().size(20, 20)
+ .fillColor(DocumentColor.BLACK).margin(new DocumentInsets(0, right, 0, 0)).build()),
+ DocumentTableCell.node(new ChartNode(BARS))));
+ try (XWPFDocument document = DocxExports.withLayout(500, 400, 30, content)) {
+ assertThat(drawings(document)).as("the tile with a right margin of %spt, and nothing of the chart",
+ right).isEqualTo(1);
+ }
+ assertThat(details(laidOut(content), "chart")).containsExactly(TABLE);
+ }
+ }
+
+ @Test
+ void twoChartsInACellOrAChartInAPaintedWrapperAreDrawnAndNamed() throws Exception {
+ for (DocumentNode built : List.of(
+ new SectionBuilder().add(new ChartNode(BARS)).add(new ChartNode(BARS)).build(),
+ new SectionBuilder().fillColor(DocumentColor.rgb(240, 240, 240)).add(new ChartNode(BARS)).build())) {
+ Consumer content = beside(built);
+ try (XWPFDocument document = DocxExports.withLayout(500, 400, 30, content)) {
+ assertThat(drawings(document)).as("%s", built).isPositive();
+ }
+ assertThat(details(laidOut(content), "chart")).as("%s", built).isNotEmpty().allMatch(DRAWN_TOO::equals);
+ }
+ }
+
+ @Test
+ void aChartInARepeatedHeaderOrASpanningCellIsNotDrawnOnAnyPage() throws Exception {
+ Consumer header = page -> page.addTable(t -> {
+ t.columns(DocumentTableColumn.fixed(300)).repeatHeader(1)
+ .rowCells(DocumentTableCell.node(new ChartNode(BARS)));
+ for (int row = 0; row < 40; row++) {
+ t.row("Row " + row);
+ }
+ });
+ try (XWPFDocument document = DocxExports.withLayout(500, 400, 30, header)) {
+ assertThat(document.getTables().get(0).getRows()).as("a table over pages").hasSizeGreaterThan(20);
+ assertThat(drawings(document)).as("nothing of the chart on any page it repeats on").isZero();
+ }
+ assertThat(laidOut(header).bySubject()).doesNotContainKey("cell drawing");
+ Consumer spanning = page -> page.addTable(t -> t
+ .columns(DocumentTableColumn.fixed(150), DocumentTableColumn.fixed(150))
+ .rowCells(DocumentTableCell.node(new ChartNode(BARS)).colSpan(2))
+ .row("Left", "Right"));
+ try (XWPFDocument document = DocxExports.withLayout(500, 400, 30, spanning)) {
+ assertThat(drawings(document)).as("a chart across two columns").isZero();
+ }
+ assertThat(details(laidOut(spanning), "chart")).containsExactly(TABLE);
+ }
+
+ @Test
+ void aChartInATableComposedInACellIsDrawnTooAndItsNoteSaysSo() throws Exception {
+ assertThat(details(laidOut(page -> page.addTable(t -> t
+ .columns(DocumentTableColumn.fixed(420))
+ .rowCells(DocumentTableCell.node(new TableBuilder()
+ .columns(DocumentTableColumn.fixed(400))
+ .rowCells(DocumentTableCell.node(new ChartNode(BARS))).build())))), "chart"))
+ .containsExactly(DRAWN_TOO);
+ }
+
+ @Test
+ void aChartInTheFlowOrWithNoLayoutIsItsDataTableAlone() throws Exception {
+ assertThat(details(laidOut(page -> page.add(new ChartNode(BARS))), "chart")).containsExactly(TABLE);
+ DocxExportReport withoutLayout = DocxExports.reportWithoutLayout(500, 400, 30, beside(new SectionBuilder()
+ .add(new EllipseBuilder().circle(12).fillColor(DocumentColor.BLACK).build())
+ .add(new ChartNode(BARS)).build()));
+ assertThat(details(withoutLayout, "chart")).as("nothing is drawn without a layout").containsExactly(TABLE);
+ }
+
+ @Test
+ void aChartNotDrawnInOneExportIsNotTakenForOneInTheNext() throws Exception {
+ // One backend, two exports of one chart node: alone in a cell, then beside a disc.
+ ChartNode chart = new ChartNode(BARS);
+ AtomicReference report = new AtomicReference<>();
+ DocxSemanticBackend backend = new DocxSemanticBackend(report::set);
+ export(backend, beside(chart));
+ assertThat(details(report.get(), "chart")).containsExactly(TABLE);
+ export(backend, beside(new SectionBuilder()
+ .add(new EllipseBuilder().circle(12).fillColor(DocumentColor.BLACK).build()).add(chart).build()));
+ assertThat(details(report.get(), "chart")).containsExactly(DRAWN_TOO);
+ }
+
+ /** A table whose second cell is built from {@code content}, beside a cell of text. */
+ private static Consumer beside(DocumentNode content) {
+ return page -> page.addTable(t -> t
+ .columns(DocumentTableColumn.fixed(120), DocumentTableColumn.fixed(300))
+ .rowCells(DocumentTableCell.text("Quarterly"), DocumentTableCell.node(content)));
+ }
+
+ private static void export(DocxSemanticBackend backend, Consumer content) throws Exception {
+ try (DocumentSession session = GraphCompose.document().pageSize(500, 400).margin(DocumentInsets.of(30))
+ .create()) {
+ session.pageFlow(content::accept);
+ session.export(backend);
+ }
+ }
+
+ /** The report of an export the layout stands behind. */
+ private static DocxExportReport laidOut(Consumer content) throws Exception {
+ DocxExportReport report = DocxExports.report(500, 400, 30, content);
+ assertThat(report.bySubject()).as("laid out").doesNotContainKey("measured geometry");
+ return report;
+ }
+
+ private static List details(DocxExportReport report, String subject) {
+ return report.bySubject().getOrDefault(subject, List.of()).stream().map(DocxExportReport.Note::detail).toList();
+ }
+
+ /** The drawings in the document's body, its tables' cells included, floating or in a line. */
+ private static int drawings(XWPFDocument document) {
+ String xml = document.getDocument().xmlText();
+ return count(xml, "= 0; at = text.indexOf(of, at + 1)) {
+ count++;
+ }
+ return count;
+ }
+}