diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f78c907..e8fb8529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -202,6 +202,31 @@ to ship the same artefact. ### Documentation +- **New flagship example: `EngineShowcase`** + **regenerated + `assets/readme/repository_showcase_render.png` hero image** ahead of + the Maven Central debut. A presentation audit before v1.6.6 flagged + that the existing hero PDF was a dated single-page render and the + GitHub Pages showcase had 20 broken asset paths (CV v2 migration + added `-v2` suffixes that `docs/index.html` never picked up). Fixed + in three commits: (a) `docs/index.html` path repair so every CV / + cover-letter preview resolves; (b) new flagship + `examples/.../flagships/EngineShowcase.java` renders a single-page + cinematic brand promo — a navy + electric-orange composition with a + rounded clip-frame hero (semantic-graph → polished-PDFs visual + metaphor), a magazine-headline lockup ("Documents as code. / + Cinematic by default."), three KPI cards (Templates v2 · 1,033 + tests · v1.6.6 Maven Central), a three-column capability grid + (Semantic DSL · Deterministic Layout · Cinematic Themes), and a + footer brand stripe — exercising `ShapeContainerNode` + + `ClipPolicy.CLIP_PATH` for the hero frame, classpath-loaded image + embedding (`examples/src/main/resources/engine-hero.png`), + `softPanel(...)` + `accentLeft(...)` decorators on V2 sections, and + mixed serif/sans typography; (c) page 1 rasterised to + `assets/readme/repository_showcase_render.png` via the new persistent + helper `com.demcha.examples.support.PdfPageRasterizer` (PDFBox-based, + no external Ghostscript / ImageMagick dependency). The hero now + reads as the engine's brand register rather than a Lorem-ipsum + template render. - **`docs/architecture/package-map.md` updated** alongside H2. A new intro paragraph documents the stability-marker convention (Stable default; engine packages are package-level `@Internal`; individual diff --git a/assets/readme/repository_showcase_render.png b/assets/readme/repository_showcase_render.png index f7c1d5ae..4b581e71 100644 Binary files a/assets/readme/repository_showcase_render.png and b/assets/readme/repository_showcase_render.png differ diff --git a/examples/README.md b/examples/README.md index c7b1c21e..4e85a873 100644 --- a/examples/README.md +++ b/examples/README.md @@ -53,6 +53,7 @@ are with the canonical DSL, then jump to its detailed section below. | [Invoice — cinematic V2](#invoice-cinematic-v2) | `InvoiceTemplateV2 + BusinessTheme.modern()` — the recommended invoice path | [PDF](../assets/readme/examples/invoice-cinematic.pdf) · [Source](src/main/java/com/demcha/examples/templates/invoice/InvoiceCinematicFileExample.java) | | [Cover Letter](#cover-letter) | One-page `BusinessTheme.modern()` cover letter with section presets | [PDF](../assets/readme/examples/cover-letter.pdf) · [Source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterFileExample.java) | | [Module-first Profile](#module-first-profile) | Authoring directly against `DocumentSession.module(...).paragraph(...)` — DSL-direct, no template | [PDF](../assets/readme/examples/module-first-profile.pdf) · [Source](src/main/java/com/demcha/examples/flagships/ModuleFirstFileExample.java) | +| **Engine Showcase** | Single-page cinematic brand promo — semantic-graph → polished-PDFs visual metaphor with rounded clip frame, magazine headline lockup, KPI cards, capability columns; source of the README hero image | [Source](src/main/java/com/demcha/examples/flagships/EngineShowcase.java) | ### 🧱 Core DSL diff --git a/examples/src/main/java/com/demcha/examples/flagships/EngineShowcase.java b/examples/src/main/java/com/demcha/examples/flagships/EngineShowcase.java new file mode 100644 index 00000000..6c31a211 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/flagships/EngineShowcase.java @@ -0,0 +1,348 @@ +package com.demcha.examples.flagships; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.image.DocumentImageData; +import com.demcha.compose.document.image.DocumentImageFitMode; +import com.demcha.compose.document.node.TextAlign; +import com.demcha.compose.document.style.ClipPolicy; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentCornerRadius; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentStroke; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.font.FontName; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Objects; + +/** + * Single-page cinematic engine showcase used as the + * {@code assets/readme/repository_showcase_render.png} source. NOT a + * business document, NOT a CV — a brand promo page that demonstrates + * the visual register the engine can hit when an author cares about + * presentation as much as data: + * + * + * + *

The hero image lives in {@code examples/src/main/resources/engine-hero.png} + * and is loaded over the classpath so the example runs without any + * filesystem path assumptions.

+ * + *

Output: + * {@code examples/target/generated-pdfs/flagships/engine-showcase.pdf} + * — page 1 is rasterised by {@link com.demcha.examples.support.PdfPageRasterizer} + * into {@code assets/readme/repository_showcase_render.png}.

+ * + * @author Artem Demchyshyn + * @since 1.6.6 + */ +public final class EngineShowcase { + + // ── Theme palette — cinematic navy + electric accent ───────── + private static final DocumentColor PAPER = DocumentColor.rgb(248, 247, 242); + private static final DocumentColor NAVY = DocumentColor.rgb(18, 26, 56); + private static final DocumentColor NAVY_DARK = DocumentColor.rgb(10, 14, 36); + private static final DocumentColor ACCENT = DocumentColor.rgb(255, 138, 36); + private static final DocumentColor ACCENT_SOFT = DocumentColor.rgb(255, 198, 138); + private static final DocumentColor INK = DocumentColor.rgb(20, 24, 38); + private static final DocumentColor MUTED = DocumentColor.rgb(110, 116, 132); + private static final DocumentColor RULE = DocumentColor.rgb(220, 218, 210); + private static final DocumentColor CARD_RING = DocumentColor.rgb(232, 226, 212); + private static final DocumentColor PANEL_TINT = DocumentColor.rgb(232, 234, 246); + private static final DocumentColor PANEL_RING = DocumentColor.rgb(208, 214, 234); + + private EngineShowcase() { + } + + public static Path generate() throws Exception { + Path output = ExampleOutputPaths.prepare("flagships", "engine-showcase.pdf"); + DocumentImageData hero = loadHeroImage(); + + try (DocumentSession document = GraphCompose.document(output) + .pageSize(DocumentPageSize.A4) + .pageBackground(PAPER) + .margin(36, 38, 32, 38) + .create()) { + + document.pageFlow() + .name("EngineShowcase") + .spacing(14) + + // ── Marquee band: brand + version ────────────── + .addRow("Marquee", row -> row + .spacing(0) + .weights(1, 1) + .addSection("MarqueeLeft", section -> section + .addParagraph(p -> p + .text("GRAPHCOMPOSE") + .textStyle(bandLeft()) + .margin(DocumentInsets.zero()))) + .addSection("MarqueeRight", section -> section + .addParagraph(p -> p + .text("v1.6 · MAVEN CENTRAL") + .textStyle(bandRight()) + .align(TextAlign.RIGHT) + .margin(DocumentInsets.zero())))) + + // ── Thin orange rule ────────────────────────── + .addShape(s -> s.size(516, 1.2).fillColor(ACCENT) + .margin(DocumentInsets.zero())) + + // ── Hero frame: nested rounded shapes form a thick + // orange border around the brand artwork (avoids the + // stroke-on-rounded-path corner artifact at thick + // stroke widths). Outer = ACCENT fill, inner = NAVY + // fill clipped to round, image clipped inside that. + .addContainer(frame -> frame + .name("HeroFrame") + .roundedRect(519, 287, 16) + .fillColor(ACCENT) + .center(new com.demcha.compose.document.dsl.ShapeContainerBuilder() + .name("HeroInner") + .roundedRect(513, 281, 13) + .fillColor(NAVY_DARK) + .clipPolicy(ClipPolicy.CLIP_PATH) + .center(new com.demcha.compose.document.dsl.ImageBuilder() + .name("HeroImage") + .source(hero) + .size(509, 277) + .fitMode(DocumentImageFitMode.COVER) + .build()) + .build())) + + // ── Magazine-headline lockup ────────────────── + .addSection("Lockup", section -> section + .padding(new DocumentInsets(8, 4, 0, 4)) + .spacing(2) + .addParagraph(p -> p + .text("One engine.") + .textStyle(headline()) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text("Every PDF you ship.") + .textStyle(headlineAccent()) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text("A declarative Java engine for production PDFs. Semantic graph compiles to deterministic layout, paginates predictably, and renders through PDFBox — without leaking PDFBox into your app.") + .textStyle(tagline()) + .lineSpacing(1.35) + .margin(new DocumentInsets(6, 0, 0, 0)))) + + // ── Three KPI cards — engine capability counts ─ + .addRow("KpiRow", row -> row + .spacing(12) + .weights(1, 1, 1) + .addSection("KpiPresets", section -> kpiCard(section, + "31", "V2 Presets", + "Layered CV + cover-letter preset architecture — data · theme · components · widgets, swap one without rewriting the others.")) + .addSection("KpiTests", section -> kpiCard(section, + "1,033", "Tests · 0 failures", + "Snapshot baselines, japicmp binary-compat gate, parallel-session stress harness, JMH benchmark suite.")) + .addSection("KpiNodes", section -> kpiCard(section, + "17", "Semantic Primitives", + "Modules, sections, paragraphs, lists, tables, rows, shapes, images — every node is themable and snapshot-tested."))) + + // ── Capability columns ──────────────────────── + .addRow("Capabilities", row -> row + .spacing(14) + .weights(1, 1, 1) + .addSection("CapDsl", section -> capabilityColumn(section, + "DECLARATIVE BY DESIGN", + "Compose by intent — modules, sections, rows. Zero PDFBox imports in your application code.")) + .addSection("CapDeterm", section -> capabilityColumn(section, + "DETERMINISTIC RENDER", + "Identical input, identical PDF — byte-comparable. Layout snapshots, JMH-benchmarked hot paths.")) + .addSection("CapTheme", section -> capabilityColumn(section, + "CINEMATIC THEMING", + "BusinessTheme · CvTheme palettes, component-level tokens, v2 layered preset architecture."))) + + // ── Footer brand stripe ─────────────────────── + .addShape(s -> s.size(516, 0.6).fillColor(RULE) + .margin(new DocumentInsets(8, 0, 0, 0))) + .addRow("Footer", row -> row + .spacing(0) + .weights(2, 1) + .addSection("FooterLeft", section -> section + .addParagraph(p -> p + .text("graphcompose.dev · github.com/DemchaAV/GraphCompose") + .textStyle(footer()) + .margin(DocumentInsets.zero()))) + .addSection("FooterRight", section -> section + .addParagraph(p -> p + .text("Java · PDFBox · MIT") + .textStyle(footer()) + .align(TextAlign.RIGHT) + .margin(DocumentInsets.zero())))) + + .build(); + + document.buildPdf(); + } + return output; + } + + private static DocumentImageData loadHeroImage() throws Exception { + try (InputStream is = Objects.requireNonNull( + EngineShowcase.class.getResourceAsStream("/engine-hero.png"), + "engine-hero.png missing from examples/src/main/resources/")) { + return DocumentImageData.fromBytes(is.readAllBytes()); + } + } + + // ── Helpers ────────────────────────────────────────────────── + + private static void kpiCard(com.demcha.compose.document.dsl.SectionBuilder section, + String value, String label, String detail) { + section + .softPanel(DocumentColor.WHITE, 8, 12) + .stroke(DocumentStroke.of(CARD_RING, 0.5)) + .spacing(2) + .addParagraph(p -> p + .text(value) + .textStyle(kpiValue()) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text(label) + .textStyle(kpiLabel()) + .margin(new DocumentInsets(0, 0, 0, 0))) + .addParagraph(p -> p + .text(detail) + .textStyle(kpiDetail()) + .lineSpacing(1.25) + .margin(new DocumentInsets(4, 0, 0, 0))); + } + + private static void capabilityColumn(com.demcha.compose.document.dsl.SectionBuilder section, + String label, String prose) { + section + // Tab-on-left callout: round only the right corners so + // the accent rule on the left sits flush against a + // straight edge — square cap on the rule matches a + // square left side without crossing a rounded curve. + .softPanel(PANEL_TINT, DocumentCornerRadius.right(6), 0) + .accentLeft(ACCENT, 3.0f) + .padding(new DocumentInsets(12, 12, 12, 16)) + .spacing(3) + .addParagraph(p -> p + .text(label) + .textStyle(capLabel()) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text(prose) + .textStyle(capProse()) + .lineSpacing(1.3) + .margin(new DocumentInsets(2, 0, 0, 0))); + } + + // ── Text styles ────────────────────────────────────────────── + + private static DocumentTextStyle bandLeft() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA_BOLD) + .size(10) + .color(NAVY) + .build(); + } + + private static DocumentTextStyle bandRight() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .size(9) + .color(MUTED) + .build(); + } + + private static DocumentTextStyle headline() { + return DocumentTextStyle.builder() + .fontName(FontName.TIMES_BOLD) + .size(38) + .color(INK) + .build(); + } + + private static DocumentTextStyle headlineAccent() { + return DocumentTextStyle.builder() + .fontName(FontName.TIMES_BOLD_ITALIC) + .size(38) + .color(ACCENT) + .build(); + } + + private static DocumentTextStyle tagline() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .size(11) + .color(INK) + .build(); + } + + private static DocumentTextStyle kpiValue() { + return DocumentTextStyle.builder() + .fontName(FontName.TIMES_BOLD) + .size(26) + .color(NAVY) + .build(); + } + + private static DocumentTextStyle kpiLabel() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA_BOLD) + .size(9) + .color(ACCENT) + .build(); + } + + private static DocumentTextStyle kpiDetail() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .size(8) + .color(MUTED) + .build(); + } + + private static DocumentTextStyle capLabel() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA_BOLD) + .size(9) + .color(NAVY) + .build(); + } + + private static DocumentTextStyle capProse() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .size(9) + .color(INK) + .build(); + } + + private static DocumentTextStyle footer() { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .size(8) + .color(MUTED) + .build(); + } + + public static void main(String[] args) throws Exception { + System.out.println("Generated: " + generate()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/support/PdfPageRasterizer.java b/examples/src/main/java/com/demcha/examples/support/PdfPageRasterizer.java new file mode 100644 index 00000000..17ef1f5a --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/support/PdfPageRasterizer.java @@ -0,0 +1,73 @@ +package com.demcha.examples.support; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.ImageType; +import org.apache.pdfbox.rendering.PDFRenderer; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Rasterises one page of a PDF into a PNG at a chosen DPI. Tiny CLI built + * on top of PDFBox so this repo never has to depend on Ghostscript / + * pdftoppm / ImageMagick for README hero asset regeneration. Used during + * v1.6.6 release prep to produce the new {@code repository_showcase_render.png} + * (CV mint-editorial page 1 hero) without an external rasterizer. + * + *

Usage:

+ *
+ * ./mvnw -B -ntp -f examples/pom.xml -DskipTests exec:java \
+ *   -Dexec.mainClass=com.demcha.examples.support.PdfPageRasterizer \
+ *   -Dexec.args="<inputPdf> <outputPng> [pageIndex0=0] [dpi=200]"
+ * 
+ * + *

Defaults: page index 0 (first page), 200 DPI (~1654 × 2339 for A4 — + * comfortable for README hero quality).

+ * + * @author Artem Demchyshyn + * @since 1.6.6 + */ +public final class PdfPageRasterizer { + + private PdfPageRasterizer() { + } + + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.err.println("Usage: PdfPageRasterizer [pageIndex0=0] [dpi=200]"); + System.exit(2); + } + Path inputPdf = Paths.get(args[0]).toAbsolutePath(); + Path outputPng = Paths.get(args[1]).toAbsolutePath(); + int pageIndex = args.length >= 3 ? Integer.parseInt(args[2]) : 0; + float dpi = args.length >= 4 ? Float.parseFloat(args[3]) : 200f; + + if (!Files.isRegularFile(inputPdf)) { + System.err.println("Input PDF not found: " + inputPdf); + System.exit(3); + } + Files.createDirectories(outputPng.getParent()); + + try (PDDocument doc = Loader.loadPDF(inputPdf.toFile())) { + int totalPages = doc.getNumberOfPages(); + if (pageIndex < 0 || pageIndex >= totalPages) { + System.err.println("Page index " + pageIndex + " out of range (0.." + (totalPages - 1) + ")"); + System.exit(4); + } + PDFRenderer renderer = new PDFRenderer(doc); + BufferedImage image = renderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB); + if (!ImageIO.write(image, "png", outputPng.toFile())) { + System.err.println("Failed to write PNG to " + outputPng); + System.exit(5); + } + System.out.println("Rasterised page " + pageIndex + " of " + inputPdf + + " at " + dpi + " DPI -> " + outputPng + + " (" + image.getWidth() + " x " + image.getHeight() + " px)"); + } + } +} diff --git a/examples/src/main/resources/engine-hero.png b/examples/src/main/resources/engine-hero.png new file mode 100644 index 00000000..2589c559 Binary files /dev/null and b/examples/src/main/resources/engine-hero.png differ