diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d9176750..84bf9f78 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -79,6 +79,9 @@ jobs:
- '**/pom.xml'
- '**/src/main/resources/**'
- '**/src/test/resources/**'
+ # The examples module renders figures from the committed perf
+ # baseline, so a baseline-only refresh changes generated output.
+ - 'baselines/**'
- '.mvn/**'
- 'mvnw'
- 'mvnw.cmd'
diff --git a/assets/readme/examples/master-showcase.pdf b/assets/readme/examples/master-showcase.pdf
index 536bc433..0ca75ac5 100644
Binary files a/assets/readme/examples/master-showcase.pdf and b/assets/readme/examples/master-showcase.pdf differ
diff --git a/assets/readme/examples/master-showcase.pptx b/assets/readme/examples/master-showcase.pptx
index 6d78b8bd..56a8347a 100644
Binary files a/assets/readme/examples/master-showcase.pptx and b/assets/readme/examples/master-showcase.pptx differ
diff --git a/examples/pom.xml b/examples/pom.xml
index 9c01df16..532f66fe 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -146,6 +146,20 @@
The rendered PDF reads like a fictional "GraphCompose Q2 Sample - * Report" — three pages of designed content meant to look at a glance + * Report" — designed content meant to look at a glance * like the kind of business document GraphCompose was built to * generate, not a feature checklist. Use it as a reference when * composing your own multi-page documents.
+ * + *The narrative is deliberately undated. This file is regenerated and + * re-committed on every release, so anything that names a version, a date or a + * measurement would go stale between one release and the next — as it did. What + * survives is either sample copy that cannot age or a figure read at render time + * from {@link com.demcha.examples.support.PerfBaseline} and + * {@link com.demcha.examples.support.ExampleVersion}.
*/ public final class MasterShowcaseExample { private static final BusinessTheme THEME = BusinessTheme.modern(); @@ -90,7 +100,7 @@ static void compose(DocumentSession document) { document.metadata(DocumentMetadata.builder() .title("GraphCompose master showcase") .author("Jordan Rivera") - .subject("Comprehensive end-to-end demo of the v1.5 canonical surface") + .subject("Comprehensive end-to-end demo of the canonical document surface") .keywords("graphcompose, showcase, business, theme, rich-text, table, shape, transform, barcode") .creator("GraphCompose Examples") .producer("GraphCompose") @@ -109,7 +119,7 @@ static void compose(DocumentSession document) { document.footer(DocumentHeaderFooter.builder() .zone(DocumentHeaderFooterZone.FOOTER) - .leftText("v1.5 — \"intuitive\" release") + .leftText("GraphCompose " + ExampleVersion.currentLine() + " — sample report") .rightText("Page {page} of {pages}") .fontSize(9f) .textColor(MUTED) @@ -165,16 +175,17 @@ static void compose(DocumentSession document) { .plain("Status: ") .bold("On track") .plain(" — ") - .accent("675 / 675 tests green", BRAND) - .plain(" — Q2 release window confirmed for ") - .underline("June 2026") + .accent("every gate green", BRAND) + .plain(" — release window confirmed for the ") + .underline("current quarter") .plain(".")) .addRich(rich -> rich .plain("Performance: ") .color("invoice-template", BRAND_DEEP) - .plain(" 13.4 ms avg, 75 docs/sec; ") + .plain(" " + speed("invoice-template") + "; ") .color("feature-rich", BRAND_DEEP) - .plain(" 36.8 ms avg, 27 docs/sec."))) + .plain(" " + speed("feature-rich") + ". One machine, " + + PerfBaseline.get().capturedOn() + "."))) .addSection("Seal", section -> section .padding(DocumentInsets.of(2)) .addCircle(118, BRAND, circle -> circle @@ -187,7 +198,7 @@ static void compose(DocumentSession document) { style(FontName.HELVETICA_BOLD, 13, DocumentTextDecoration.BOLD, DocumentColor.WHITE))) - .position(label("Q2 / 2026", + .position(label("SAMPLE", style(FontName.HELVETICA_BOLD, 7.5, DocumentTextDecoration.BOLD, SOFT_GOLD)), @@ -222,7 +233,7 @@ static void compose(DocumentSession document) { .textStyle(THEME.text().h2()) .margin(DocumentInsets.zero())) .addRich(rich -> rich - .plain("v1.5 lands the cinematic features that turn GraphCompose from \"tidy PDF layouter\" into a designed-document engine: ") + .plain("The features below are what turn GraphCompose from a \"tidy PDF layouter\" into a designed-document engine: ") .bold("shape-as-container with clip path") .plain(", ") .bold("rotate / scale + per-layer z-index") @@ -314,7 +325,7 @@ static void compose(DocumentSession document) { .addSection("Card3", section -> highlightCard(section, "Transformable mixin", "rotate / scale on every shape", - "v1.5 extends TransformableRestates the committed baseline rather than a literal, so the figure + * moves when the measurement does instead of when somebody remembers.
+ * + * @param scenario benchmark scenario name + * @return e.g. {@code "6.6 ms avg, 152 docs/sec"}, or a plain note when the + * baseline does not carry the scenario + */ + private static String speed(String scenario) { + return PerfBaseline.get().scenario(scenario) + .map(measured -> String.format(Locale.ROOT, "%.1f ms avg, %.0f docs/sec", + measured.avgMillis(), measured.docsPerSecond())) + .orElse("not measured"); + } + private static DocumentTextStyle label() { return DocumentTextStyle.builder() .fontName(FontName.HELVETICA_BOLD) diff --git a/examples/src/main/java/com/demcha/examples/support/ExampleVersion.java b/examples/src/main/java/com/demcha/examples/support/ExampleVersion.java new file mode 100644 index 00000000..76003677 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/support/ExampleVersion.java @@ -0,0 +1,71 @@ +package com.demcha.examples.support; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The reactor version, for examples that print it. + * + *Read from the filtered {@code banner.properties} rather than written out, + * because example documents are regenerated on every release: a literal keeps + * announcing whichever line it was typed on. Shared so the value cannot drift + * between the documents that show it.
+ */ +public final class ExampleVersion { + + private static final Pattern MAJOR_MINOR = Pattern.compile("^(\\d+)\\.(\\d+)"); + + private static final String CURRENT = load(); + + private ExampleVersion() { + } + + /** + * The full reactor version, or {@code "dev"} when the resource is absent or + * unfiltered (running straight from sources). + * + * @return version string + */ + public static String current() { + return CURRENT; + } + + /** + * The {@code major.minor} of {@link #current()}, for prose that names a line + * rather than a patch. + * + * @return the leading {@code major.minor}, or the version unchanged when it + * carries no dotted numeric prefix + */ + public static String currentLine() { + return majorMinor(CURRENT); + } + + /** + * Reduces a version to its {@code major.minor} prefix. + * + * @param version version string to reduce + * @return the leading {@code major.minor}, or {@code version} unchanged when + * it carries no dotted numeric prefix + */ + public static String majorMinor(String version) { + Matcher matcher = MAJOR_MINOR.matcher(version); + return matcher.find() ? matcher.group(1) + "." + matcher.group(2) : version; + } + + private static String load() { + Properties banner = new Properties(); + try (InputStream in = ExampleVersion.class.getResourceAsStream("/banner.properties")) { + if (in != null) { + banner.load(in); + } + } catch (IOException ignored) { + // Fall through to the development label below. + } + String value = banner.getProperty("version"); + return value == null || value.isBlank() || value.startsWith("@") ? "dev" : value.trim(); + } +} diff --git a/examples/src/main/java/com/demcha/examples/support/PerfBaseline.java b/examples/src/main/java/com/demcha/examples/support/PerfBaseline.java new file mode 100644 index 00000000..3e1da248 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/support/PerfBaseline.java @@ -0,0 +1,117 @@ +package com.demcha.examples.support; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * The committed current-speed baseline, for examples that quote engine speed. + * + *Reads {@code baselines/current-speed-full.json} — the same file the local + * perf gate scores against, put on the classpath by the examples module rather + * than copied, so there is one number and not two that drift. An example that + * quotes a figure therefore restates measured data instead of asserting a + * literal somebody typed once.
+ * + *The figures are one machine's median over several runs. Absolute + * milliseconds do not travel between machines, so anything rendering them + * should show {@link #capturedOn()} beside them and let the reader judge.
+ * + *Loading never throws: a resource that is missing, unreadable or malformed + * yields an empty baseline, and callers render a plain "not measured" rather + * than a number nobody measured. {@code PerfBaselineTest} keeps that fallback + * from becoming the normal case by asserting the resource really is on the + * classpath with the scenarios the examples quote.
+ */ +public final class PerfBaseline { + + private static final String RESOURCE = "/baselines/current-speed-full.json"; + + private static final PerfBaseline EMPTY = new PerfBaseline("undated", Map.of()); + + private static final PerfBaseline INSTANCE = load(); + + private final String capturedOn; + private final Map{@link PerfBaseline} degrades to "not measured" when its resource is + * absent, which is the right behaviour at render time and the wrong thing to + * discover in a published PDF. The baseline reaches the classpath through a + * resource directory in {@code examples/pom.xml} that points outside the + * module; rename the file, move the directory, or drop that pom entry and every + * figure in the master showcase silently turns into a placeholder while the + * build stays green. This is the test that would go red instead.
+ */ +class PerfBaselineTest { + + @Test + void theCommittedBaselineIsOnTheClasspath() { + assertThat(PerfBaseline.get().capturedOn()) + .describedAs("baseline resource missing from the examples classpath - " + + "check the baselines resource entry in examples/pom.xml") + .matches("\\d{4}-\\d{2}-\\d{2}"); + } + + @Test + void itCarriesTheScenariosTheShowcaseQuotes() { + for (String scenario : new String[] {"invoice-template", "feature-rich"}) { + assertThat(PerfBaseline.get().scenario(scenario)) + .describedAs("MasterShowcaseExample renders '%s', so the baseline must carry it", + scenario) + .hasValueSatisfying(measured -> { + assertThat(measured.avgMillis()).isPositive(); + assertThat(measured.docsPerSecond()).isPositive(); + }); + } + } + + @Test + void anUnknownScenarioIsAbsentRatherThanZero() { + assertThat(PerfBaseline.get().scenario("no-such-scenario")) + .describedAs("an unknown scenario must be empty so the caller can say " + + "'not measured' instead of rendering 0.0 ms") + .isEmpty(); + } +} diff --git a/web/showcase/pdf/flagships/default/master-showcase.pdf b/web/showcase/pdf/flagships/default/master-showcase.pdf index 0c1fbbeb..0ca75ac5 100644 Binary files a/web/showcase/pdf/flagships/default/master-showcase.pdf and b/web/showcase/pdf/flagships/default/master-showcase.pdf differ diff --git a/web/showcase/pptx/flagships/default/master-showcase.pptx b/web/showcase/pptx/flagships/default/master-showcase.pptx index 0be32967..56a8347a 100644 Binary files a/web/showcase/pptx/flagships/default/master-showcase.pptx and b/web/showcase/pptx/flagships/default/master-showcase.pptx differ diff --git a/web/showcase/screenshots/flagships/default/master-showcase.png b/web/showcase/screenshots/flagships/default/master-showcase.png index cb73361f..cef27d63 100644 Binary files a/web/showcase/screenshots/flagships/default/master-showcase.png and b/web/showcase/screenshots/flagships/default/master-showcase.png differ