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; + } +}