From 3b7a6eb6b1e9b13eb682d914c6542956ebd84de0 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Tue, 4 Aug 2026 19:46:01 +0100 Subject: [PATCH] feat(examples): the Maven Central banner carries the rest of the story MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was one slide: wordmark, coordinate, capability tags and a code to layout to document diagram. Everything a reader might do next — how the engine actually works, whether it is fast, how it behaves as documents grow — lived in a separate deck they had to be told about. Three pages now follow it in the same amber-on-navy language. The authoring pipeline and what each step guarantees; the measured comparison against iText 9 and JasperReports; how all three behave from 40 to 1000 rows. Every figure on the last two pages is read from the committed benchmark file at render time rather than typed into the layout, so refreshing the numbers is a file copy and the prose cannot drift away from them. Page 1 is unchanged and stays a canvas — it is a poster, and every element is placed by hand. The new pages are not: they carry a table and two charts that want a flow to measure them, so they are ordinary sections on the same night field the page background already paints. The .pptx gains the same three slides. The PNG preview stays the banner alone, which is what it is for. MavenBannerNativeShapeTest walks all four slides and holds the whole deck to a single rasterised element, the SVG checkmark. The table and both charts have to arrive as native shapes rather than an embedded image — the claim the last page makes, and the one a chart-rendering shortcut would quietly break. --- CHANGELOG.md | 11 + assets/readme/examples/maven-banner.pdf | Bin 10095 -> 16253 bytes assets/readme/examples/maven-banner.pptx | Bin 31234 -> 41009 bytes .../flagships/MavenBannerPptxExample.java | 323 ++++++++++++++++++ .../flagships/MavenBannerNativeShapeTest.java | 43 ++- 5 files changed, 365 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c316e5d9..e265feab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -241,6 +241,17 @@ follow semantic versioning; release dates are ISO 8601. ### Documentation +- **The Maven Central banner became a deck.** It was one 16:9 slide: wordmark, + coordinate, capability tags and a code → layout → document diagram. It now carries + three more pages in the same amber-on-navy language — the authoring pipeline and what + each step guarantees, the measured comparison against iText 9 and JasperReports, and + how all three behave as the report grows from 40 to 1000 rows. Every figure on the + last two pages is read from the committed benchmark file at render time rather than + typed into the layout. The `.pptx` gains the same three slides; the PNG preview stays + the banner alone, since that is what it is for. `MavenBannerNativeShapeTest` now walks + all four slides and holds the whole deck to one rasterised element — the SVG + checkmark — so the table and both charts have to arrive as native shapes rather than + as an embedded image, which is the claim the last page makes. - **The PowerShell commands run as written.** Two pages handed Windows readers a command that fails. PowerShell splits a `-D` flag whose property name contains a dot, passing the native command `-Dexec` and `.mainClass=…` as separate arguments, and diff --git a/assets/readme/examples/maven-banner.pdf b/assets/readme/examples/maven-banner.pdf index 97d307fcf54275bfb6ed9f02aa50aa9bc7fd6dd4..119c40d15322e1bf87997bddf0b85b3462bb99c6 100644 GIT binary patch delta 7350 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 be0vuG>W!CXq{QUrgYa>I7#O6KprHQ&L@oW| delta 1201 zcmexc_ug-UAEVhue|@ETBVz>vh5V!}E*l#zeV^34^pXq(a|;VD{gBFnR0Vziq^#hQ zTrPb-1tSA4eYeb@%uh)j>d?-U=3YW%`b{i9H(yYYtohUEW2F4|NYFVbG25K* zH+QaOb=sC{w=flt$rHPHB(qOU+B(U`Qc+{VsmzFUj7IaFx_+P`i%n>`GH$J7Q5Yi|LA+Zul4&E2?txJ*E>IK zFNx0Sc8(Ig9T6<~dY|sJ_AT@3k8XJPo-gy>l-Z(xL|(gwexCd+SLfG!|Auq#x~^79 zS~J*fJ6jWcEIqxXXwti9zpa>pb&}dwX@!)eD$L{F#a6o6QSB#>=i~2%aiWu-^{qSN zA@JYm_+nF=IO8=>wMrLB#UELBbBk)wje`^G%}e_$-1XMBvrIQXx@VR6gv>*8?tXnf zz4oHJhM8sB4VJrhIWO{5w0|7j_+rvD;i`+8VqeZ(Tk~peZp1C?+ne^ubEHiwwMz-s z`Filmnr{b9bbh3pF5evZC+76By)ojd=Q5j^JyrYesY6HOE| zN+PH2nWRwvE^vcTW?|E2-`H=V!NtjM&nsUm-u3FX?-G+3`<*k}KTAz#z2C{}n7KNh z#q)Jj@V$L!zUslcyG+jO}C4lGSK^w-Uzn}QMy*z2|q+^p8bvv_s%3gg#&Ue;R znJJ|=RVsVz!@p+U4gKD?|6z^v^xHSWQ$N;6{1<)r?qdA%+pFtUE@pMCixds*-R5%J zPDpUe^0s{eS@-V>mzZ}4^~mT87s{0J?XI8bXnT4>_ZPkmO59HAv#vh6bbRx!kTiwo zOP9M!qU;m&D6-*!qVB$#mLgc(9PV`)xyZg$;3{a7rT7TYuOijUkk*wFEB9#bs z;hrVuySS{lloxDz_eqL3s^o=X!jTq9?l6}ej}td)?LK{d#s0^G^~&UlhQgx7C5c5P T6-7XMEX|EfxKveL{oS|#_iO%< diff --git a/assets/readme/examples/maven-banner.pptx b/assets/readme/examples/maven-banner.pptx index 61e8386cc43477cfc4d61e793eda117a56dc1416..19d63c1a5fade4e33f25670125fe326520f4fcb8 100644 GIT binary patch delta 12323 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 J_(X_i{|4{0AK(B0 diff --git a/examples/src/main/java/com/demcha/examples/flagships/MavenBannerPptxExample.java b/examples/src/main/java/com/demcha/examples/flagships/MavenBannerPptxExample.java index fbb3b806..ea1fa979 100644 --- a/examples/src/main/java/com/demcha/examples/flagships/MavenBannerPptxExample.java +++ b/examples/src/main/java/com/demcha/examples/flagships/MavenBannerPptxExample.java @@ -3,11 +3,22 @@ import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentPageSize; import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.chart.AxisSpec; +import com.demcha.compose.document.chart.ChartData; +import com.demcha.compose.document.chart.ChartSize; +import com.demcha.compose.document.chart.ChartSpec; +import com.demcha.compose.document.chart.ChartStyle; +import com.demcha.compose.document.chart.LegendPosition; +import com.demcha.compose.document.chart.ValueLabelMode; import com.demcha.compose.document.dsl.EllipseBuilder; import com.demcha.compose.document.dsl.ParagraphBuilder; import com.demcha.compose.document.dsl.PathBuilder; +import com.demcha.compose.document.dsl.RowBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; import com.demcha.compose.document.dsl.ShapeBuilder; import com.demcha.compose.document.dsl.ShapeContainerBuilder; +import com.demcha.compose.document.table.DocumentTableColumn; +import com.demcha.compose.document.table.DocumentTableStyle; import com.demcha.compose.document.node.CanvasChild; import com.demcha.compose.document.node.CanvasLayerNode; import com.demcha.compose.document.node.DocumentNode; @@ -65,6 +76,29 @@ public final class MavenBannerPptxExample { private static final DocumentColor ORANGE = DocumentColor.rgb(233, 151, 45); private static final DocumentColor ORANGE_HI = DocumentColor.rgb(242, 168, 68); + /** + * Chart series colours for the two rivals. + * + *

Three series on one chart need three hues, not three greys. Slate and a + * near-slate violet were close enough that the iText and JasperReports lines + * were unreadable where they crossed, so the third series is green — far from + * both the amber GraphCompose line and the slate one, on a navy field.

+ */ + private static final DocumentColor SERIES_SLATE = DocumentColor.rgb(122, 133, 168); + private static final DocumentColor SERIES_GREEN = DocumentColor.rgb(93, 200, 143); + + /** + * Table banding. + * + *

Reusing the two card fills gave a stripe five units apart per channel — + * applied, measurable, and invisible. On a near-black field the eye needs a + * wider step than it does on paper, so these are set for the contrast rather + * than borrowed from the cards. The header sits above both.

+ */ + private static final DocumentColor HEADER_BG = DocumentColor.rgb(42, 53, 88); + private static final DocumentColor ROW_DARK = DocumentColor.rgb(19, 25, 45); + private static final DocumentColor ROW_LIGHT = DocumentColor.rgb(29, 38, 66); + // Text. private static final DocumentColor WHITE_TX = DocumentColor.rgb(247, 248, 252); private static final DocumentColor SUBTLE_TX = DocumentColor.rgb(206, 212, 230); @@ -163,10 +197,299 @@ static void compose(DocumentSession document) { .build()); document.pageFlow() .name("MavenBanner") + .spacing(0) .add(banner()) + .addPageBreak(b -> b.name("ToPipeline")) + .addSection("Pipeline", MavenBannerPptxExample::pipelinePage) + .addPageBreak(b -> b.name("ToMeasured")) + .addSection("Measured", MavenBannerPptxExample::measuredPage) + .addPageBreak(b -> b.name("ToScaling")) + .addSection("Scaling", MavenBannerPptxExample::scalingPage) .build(); } + // --------------------------------------------------------------------- + // Pages 2–4. + // + // Page 1 is a canvas: every element is placed by hand because the banner is + // a poster. These are not. They carry a table and four charts, which want a + // flow that measures them — so they are ordinary sections on the same night + // field the page background already paints, styled from the same tokens. + // --------------------------------------------------------------------- + + /** Page 2 — the authoring pipeline, four steps and what each one guarantees. */ + private static void pipelinePage(SectionBuilder page) { + page.padding(DocumentInsets.symmetric(38, 52)).spacing(0); + kicker(page, "HOW IT WORKS", "From one Java file to a finished document"); + lede(page, "You describe the document semantically — sections, rows, tables, charts, " + + "shapes, layers — and the engine resolves the rest. No manual coordinates, " + + "no XML templates, no per-format authoring."); + + page.addRow("Steps", row -> { + row.spacing(14).evenWeights().margin(DocumentInsets.top(AFTER_LEDE)); + stepCard(row, "1", "AUTHOR", "A fluent DSL describes intent, not geometry."); + stepCard(row, "2", "MEASURE", "Every node measured twice, deterministically."); + stepCard(row, "3", "PAGINATE", "The flow splits across pages row by row."); + stepCard(row, "4", "RENDER", "A backend writes the bytes — PDF or PPTX."); + }); + + page.addRow("Guarantees", row -> { + // Row to row, not lede to row: the card above brings its own bottom + // padding, so this reads as the same gap at a smaller number. + row.spacing(14).evenWeights().margin(DocumentInsets.top(22)); + proofCard(row, "Deterministic", + "The same input renders the same bytes on every machine. Layout is " + + "reproducible, not best-effort."); + proofCard(row, "Regression-tested", + "layoutSnapshot() captures the resolved geometry, so a layout change " + + "fails a test instead of surfacing in a release."); + proofCard(row, "One model, two formats", + "The same composition emits this PDF and an editable PPTX. Nothing is " + + "authored twice, so the two cannot drift."); + }); + } + + /** Page 3 — the measured comparison, drawn from the committed benchmark file. */ + private static void measuredPage(SectionBuilder page) { + EngineDeckData.BenchRun bench = EngineDeckData.loadBench(); + page.padding(DocumentInsets.symmetric(38, 52)).spacing(0); + kicker(page, "MEASURED, NOT CLAIMED", "GraphCompose against the field"); + lede(page, "The same documents through three engines — render time and peak heap, " + + "measured by this repository's own harness. Every figure on this page and the " + + "next is read from the result file at render time, not typed in."); + + page.addTable(table -> { + table.name("Comparison") + .columns(DocumentTableColumn.auto(), DocumentTableColumn.auto(), + DocumentTableColumn.auto(), DocumentTableColumn.auto()) + .headerStyle(cellStyle(sansBold(12, WHITE_TX), HEADER_BG)) + .defaultCellStyle(cellStyle(sans(12, SUBTLE_TX), CARD)) + // Full styles rather than the fill-only overload: a zebra style is + // registered as the row's style, so anything it leaves out is + // anything the row loses — including the text colour, which on this + // field is the difference between readable and invisible. + .zebra(cellStyle(sans(12, SUBTLE_TX), ROW_DARK), + cellStyle(sans(12, SUBTLE_TX), ROW_LIGHT)) + .headerRow("Report size", "GraphCompose", "iText 9", "JasperReports") + .row("1 page · 3 lines", + cell(bench.timeMs("GraphCompose Canonical"), bench.heapMb("GraphCompose Canonical")), + cell(bench.timeMs("iText 9"), bench.heapMb("iText 9")), + cell(bench.timeMs("JasperReports"), bench.heapMb("JasperReports"))); + for (int size : new int[]{40, 200, 1000}) { + table.row(size + " rows", + cell(bench.timeMs("GraphCompose", size), bench.heapMb("GraphCompose", size)), + cell(bench.timeMs("iText 9", size), bench.heapMb("iText 9", size)), + cell(bench.timeMs("JasperReports", size), bench.heapMb("JasperReports", size))); + } + table.margin(DocumentInsets.top(AFTER_LEDE)); + }); + + page.addRow("Headline", row -> { + row.spacing(14).evenWeights().margin(DocumentInsets.top(20)); + ratioCard(row, times(bench.timeMs("iText 9", 1000) / bench.timeMs("GraphCompose", 1000)), + "faster than iText 9 at 1000 rows", ORANGE); + ratioCard(row, times(bench.heapMb("iText 9", 1000) / bench.heapMb("GraphCompose", 1000)), + "lighter than iText 9", SERIES_SLATE); + ratioCard(row, times(bench.heapMb("JasperReports", 1000) / bench.heapMb("GraphCompose", 1000)), + "lighter than JasperReports", SERIES_GREEN); + }); + + page.addParagraph(p -> p + .text("Measured " + bench.timestamp() + " · " + bench.warmup() + " warmup / " + + bench.measure() + " measurement iterations on one machine. Read the " + + "ratios rather than the milliseconds: all three engines are measured " + + "in the same run, so their proportions survive a change of hardware " + + "while the timings do not.") + .textStyle(sans(9.5, FAINT_TX)) + .lineSpacing(3) + .margin(DocumentInsets.top(18))); + } + + /** Page 4 — how the three engines behave as the report grows. */ + private static void scalingPage(SectionBuilder page) { + EngineDeckData.BenchRun bench = EngineDeckData.loadBench(); + page.padding(DocumentInsets.symmetric(38, 52)).spacing(0); + kicker(page, "SCALING", "What changes as the report grows"); + lede(page, "From 40 to 1000 rows the time lead over iText widens; JasperReports closes " + + "to roughly the same render time at the top size, while GraphCompose stays " + + "markedly lighter on memory than both throughout. Every series reads from the " + + "same file as the table on the previous page."); + + page.addRow("ScalingCharts", row -> { + row.spacing(20).evenWeights().margin(DocumentInsets.top(AFTER_LEDE)); + chartCard(row, "Render time vs. report size — ms", "lower is faster", + seriesOf(bench, true)); + chartCard(row, "Peak heap vs. report size — MB", "lower is lighter", + seriesOf(bench, false)); + }); + + page.addParagraph(p -> p + .text("Both charts are native vector output: the engine compiles them to the same " + + "shapes, lines and text frames as everything else on the page — no " + + "image is embedded, and the numbers come from the file, not the caption.") + .textStyle(sans(9.5, FAINT_TX)) + .lineSpacing(3) + .margin(DocumentInsets.top(20))); + } + + // --------------------------------------------------------------------- + // Page furniture for 2–4. + // --------------------------------------------------------------------- + + /** + * Inner width of a card in a four-across / three-across row. + * + *

A section measures to its longest line, so a row of cards whose text + * happens to be shorter renders with ragged edges. A zero-height spacer of + * this width sets the floor and the row lines up.

+ */ + private static final double CONTENT_W = PAGE_W - 104; + private static final double STEP_INNER = (CONTENT_W - 3 * 14) / 4 - 32; + private static final double TRIPLE_INNER = (CONTENT_W - 2 * 14) / 3 - 32; + + /** + * Space between the lede and the panels under it. + * + *

Larger than the gap between two rows of cards, and deliberately so. A card + * carries its own bottom padding, so between two rows the eye sees that padding + * plus the margin; between the lede and the first row it sees the margin alone. + * Equal margins therefore read as unequal, and the text looked stuck to the + * cards. This is the row gap plus a card's padding, so both gaps land the same.

+ */ + private static final double AFTER_LEDE = 37; + + private static void kicker(SectionBuilder page, String eyebrow, String headline) { + page.addParagraph(p -> p.text(eyebrow).textStyle(monoBold(11, ORANGE))); + page.addParagraph(p -> p + .text(headline) + .textStyle(sansBold(30, WHITE_TX)) + .margin(DocumentInsets.top(6))); + page.addShape(sh -> sh.size(96, 4).fillColor(ORANGE).margin(DocumentInsets.top(12))); + } + + private static void lede(SectionBuilder page, String text) { + page.addParagraph(p -> p + .text(text) + .textStyle(sans(13, SUBTLE_TX)) + .lineSpacing(4) + .margin(DocumentInsets.top(16))); + } + + private static void stepCard(RowBuilder row, String number, String title, String desc) { + row.addSection("Step" + number, s -> s + .softPanel(CARD, 10, 16) + .stroke(DocumentStroke.of(BORDER, 1)) + .spacing(0) + .addSpacer(sp -> sp.size(STEP_INNER, 0)) + .addParagraph(p -> p.text(number).textStyle(monoBold(20, ORANGE))) + .addParagraph(p -> p.text(title) + .textStyle(sansBold(13, WHITE_TX)) + .margin(DocumentInsets.top(8))) + .addParagraph(p -> p.text(desc) + .textStyle(sans(11, MUTED_TX)) + .lineSpacing(3) + .margin(DocumentInsets.top(6)))); + } + + private static void proofCard(RowBuilder row, String title, String desc) { + row.addSection("Proof" + title, s -> s + .softPanel(CARD_2, 10, 16) + .accentTop(ORANGE, 3) + .spacing(0) + .addSpacer(sp -> sp.size(TRIPLE_INNER, 0)) + .addParagraph(p -> p.text(title).textStyle(sansBold(13, WHITE_TX))) + .addParagraph(p -> p.text(desc) + .textStyle(sans(11, MUTED_TX)) + .lineSpacing(3) + .margin(DocumentInsets.top(7)))); + } + + private static void ratioCard(RowBuilder row, String figure, String label, + DocumentColor accent) { + row.addSection("Ratio" + label, s -> s + .softPanel(CARD, 10, 16) + .stroke(DocumentStroke.of(BORDER, 1)) + .spacing(0) + .addSpacer(sp -> sp.size(TRIPLE_INNER, 0)) + .accentTop(accent, 3) + .addParagraph(p -> p.text(figure).textStyle(sansBold(28, accent))) + .addParagraph(p -> p.text(label) + .textStyle(sans(11, MUTED_TX)) + .lineSpacing(3) + .margin(DocumentInsets.top(4)))); + } + + private static void chartCard(RowBuilder row, String title, String note, ChartSpec spec) { + row.addSection("Chart" + title, s -> s + .softPanel(CARD, 10, 16) + .stroke(DocumentStroke.of(BORDER, 1)) + .spacing(0) + .addParagraph(p -> p.text(title).textStyle(sansBold(12, WHITE_TX))) + .addParagraph(p -> p.text(note) + .textStyle(sans(10, FAINT_TX)) + .margin(DocumentInsets.top(3))) + .chart(spec, chartStyle())); + } + + /** Time or heap for all three engines across the row-count sweep. */ + private static ChartSpec seriesOf(EngineDeckData.BenchRun bench, boolean time) { + ChartData.Builder data = ChartData.builder().categories("40", "200", "1000"); + for (String lib : new String[]{"GraphCompose", "iText 9", "JasperReports"}) { + data.series(lib, + time ? bench.timeMs(lib, 40) : bench.heapMb(lib, 40), + time ? bench.timeMs(lib, 200) : bench.heapMb(lib, 200), + time ? bench.timeMs(lib, 1000) : bench.heapMb(lib, 1000)); + } + return ChartSpec.line() + .data(data.build()) + .legend(LegendPosition.BOTTOM) + .size(ChartSize.aspectRatio(16, 7)) + .build(); + } + + /** + * The chart palette, matched to the banner: GraphCompose in amber, the two + * rivals in the muted greys the cards already use, so the reader's eye lands + * on the series the document is about without the chart shouting. + */ + private static ChartStyle chartStyle() { + return ChartStyle.builder() + .seriesPaint(0, DocumentPaint.solid(ORANGE)) + .seriesPaint(1, DocumentPaint.solid(SERIES_SLATE)) + .seriesPaint(2, DocumentPaint.solid(SERIES_GREEN)) + .barCornerRadius(DocumentCornerRadius.top(3)) + .barWidthRatio(0.5) + .lineWidth(2.0) + .axisTextStyle(mono(8.5, MUTED_TX)) + .legendTextStyle(sans(9, MUTED_TX)) + .valueLabelTextStyle(monoBold(10, ORANGE)) + // The labels sit over the card, not a white page: match the halo to + // the card fill so a gridline stops behind the digits instead of a + // white chip appearing on the navy. + .valueLabelHalo(DocumentPaint.solid(CARD)) + .grid(ChartStyle.GridStyle.horizontal(DocumentStroke.of(BORDER, 0.6))) + .build(); + } + + /** A table cell on the night field: the card fill behind the given text style. */ + private static DocumentTableStyle cellStyle(DocumentTextStyle text, DocumentColor fill) { + return DocumentTableStyle.builder() + .textStyle(text) + .fillColor(fill) + .stroke(DocumentStroke.of(BORDER, 0.8)) + .padding(DocumentInsets.symmetric(5, 10)) + .build(); + } + + /** {@code 40.0 ms · 19.3 MB} — one cell carrying both measurements. */ + private static String cell(double ms, double mb) { + return String.format(java.util.Locale.ROOT, "%.1f ms · %.1f MB", ms, mb); + } + + private static String times(double ratio) { + return String.format(java.util.Locale.ROOT, "%.1fx", ratio); + } + // --------------------------------------------------------------------- // Full-bleed banner scene. // --------------------------------------------------------------------- diff --git a/examples/src/test/java/com/demcha/examples/flagships/MavenBannerNativeShapeTest.java b/examples/src/test/java/com/demcha/examples/flagships/MavenBannerNativeShapeTest.java index 9c6e3800..c728cf4e 100644 --- a/examples/src/test/java/com/demcha/examples/flagships/MavenBannerNativeShapeTest.java +++ b/examples/src/test/java/com/demcha/examples/flagships/MavenBannerNativeShapeTest.java @@ -16,16 +16,24 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Guards the Maven-banner slide's editability promise at the PPTX structural - * level: the panels, tags, code, diagram and connectors land as native shapes - * and text, and the only rasterised element is the SVG checkmark in the "Maven - * Central" badge. A clip-safety or SVG-handling regression that flattened the - * panels into pictures would silently falsify that promise — this catches it. + * Guards the Maven deck's editability promise at the PPTX structural level: the + * panels, tags, code, diagram, connectors, table and charts land as native shapes + * and text, and the only rasterised element in the whole deck is the SVG checkmark + * in the "Maven Central" badge on slide 1. A clip-safety or SVG-handling regression + * that flattened any of it into pictures would silently falsify that promise. + * + *

The charts matter most here. They are compiled to the same primitives as + * everything else, so a deck claiming "native vector output" has to survive the + * round trip as shapes rather than as an embedded image — which is exactly what a + * chart-rendering shortcut would break, and nothing else would notice.

*/ class MavenBannerNativeShapeTest { + /** Banner, pipeline, measured, scaling. */ + private static final int SLIDES = 4; + @Test - void theSlideStaysNativeApartFromTheCheckIcon() throws Exception { + void everySlideStaysNativeApartFromTheCheckIcon() throws Exception { try (DocumentSession document = GraphCompose.document() .pageSize(DocumentPageSize.SLIDE_16_9) .pageBackground(DocumentColor.rgb(13, 17, 33)) @@ -34,14 +42,25 @@ void theSlideStaysNativeApartFromTheCheckIcon() throws Exception { MavenBannerPptxExample.compose(document); byte[] deck = document.toPptxBytes(); try (XMLSlideShow show = new XMLSlideShow(new ByteArrayInputStream(deck))) { - assertThat(show.getSlides()).hasSize(1); - List shapes = show.getSlides().get(0).getShapes(); - long pictures = shapes.stream().filter(XSLFPictureShape.class::isInstance).count(); + assertThat(show.getSlides()).hasSize(SLIDES); + + long pictures = 0; + for (int slide = 0; slide < SLIDES; slide++) { + List shapes = show.getSlides().get(slide).getShapes(); + pictures += shapes.stream().filter(XSLFPictureShape.class::isInstance).count(); + assertThat(shapes) + .as("slide %d carries native content", slide + 1) + .isNotEmpty(); + } + assertThat(pictures) - .as("only the SVG checkmark rasterises") + .as("across the whole deck only the SVG checkmark rasterises — the table " + + "and both charts have to arrive as shapes, not as an image") .isEqualTo(1); - assertThat(shapes.size() - pictures) - .as("the rest of the banner lands as native shapes") + + List banner = show.getSlides().get(0).getShapes(); + assertThat(banner.size() - 1) + .as("the banner itself lands as native shapes") .isGreaterThan(100); } }