Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions docs/templates/v2-layered/contributor-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,65 @@ Minimum test coverage matching CV v2:
| `<Preset>SmokeTest` | `id()`, `displayName()`, default-factory render, custom-theme render |
| `SectionDispatcherTest` *(optional)* | Each sealed subtype routes correctly |
| `WidgetSmokeTest` | Each public widget variant renders without throwing |
| `<Family>V2VisualParityTest` | **Per-pixel diff** against a checked-in baseline PNG for each preset |

CI must be green before merge. Visual regression (PNG diff against
a reference render) is encouraged but optional today.
CI must be green before merge.

### Visual regression — pixel-diff parity gate

Each preset's visual signature is **frozen** in a checked-in
baseline PNG. A parameterised test renders the preset on A4 against
a canonical sample document, rasterises each page via PDFBox, and
asserts the per-pixel diff stays within a budget. Catches silent
visual breakage from theme / widget / renderer refactors.

**Workflow:**

```bash
# 1. After a deliberate visual change — refresh baselines:
./mvnw test -Dtest='<Family>V2VisualParityTest' -Dgraphcompose.visual.approve=true

# 2. Commit the updated PNGs in the same change:
git add src/test/resources/visual-baselines/<family>-v2-layered/*.png
git commit -m "test: refresh visual baselines after <reason>"

# 3. Normal run (defends against unintended drift):
./mvnw test -Dtest='<Family>V2VisualParityTest'
```

**Where baselines live:**
`src/test/resources/visual-baselines/<family>-v2-layered/<slug>-page-N.png`

One PNG per page per preset. Pages overflow naturally — a 2-page
preset gets `<slug>-page-0.png` and `<slug>-page-1.png`.

**Budget calibration** — mirror the CV v2 settings until you have
evidence your family needs different limits:

```java
private static final long PIXEL_DIFF_BUDGET = 50_000L; // max mismatched pixels per page
private static final int PER_PIXEL_TOLERANCE = 8; // per-channel tolerance
```

These are calibrated for cross-platform PDFBox font + colour
rendering drift between Windows-recorded baselines and Linux CI.
**Helvetica-based presets** (e.g. ModernProfessional) hit ~40k
mismatched pixels on the Linux CI; **PT-Serif-based presets**
(BoxedSections, MinimalUnderlined) stay under 10k. The 50k budget
covers both with margin.

If CI flakes on a specific preset above 50k, widen the budget for
that preset specifically (e.g. via a per-test `Map<String, Long>`
of overrides) rather than relaxing the global setting.

**Failure mode:** when the diff exceeds budget, the harness writes
`<slug>-page-N.actual.png` and `<slug>-page-N.diff.png` next to the
baseline so a reviewer can see exactly what changed before deciding
to re-bless or fix.

**Reference**: see
`src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java`
— a 200-line drop-in template you can copy for a new family.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package com.demcha.compose.document.templates.cv.v2.presets;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
import com.demcha.compose.document.templates.cv.v2.data.CvIdentity;
import com.demcha.compose.document.templates.cv.v2.data.EntriesSection;
import com.demcha.compose.document.templates.cv.v2.data.ParagraphSection;
import com.demcha.compose.document.templates.cv.v2.data.RowStyle;
import com.demcha.compose.document.templates.cv.v2.data.RowsSection;
import com.demcha.testing.visual.PdfVisualRegression;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.nio.file.Path;
import java.util.function.Supplier;
import java.util.stream.Stream;

/**
* Pixel-diff visual parity gate for the v2 layered CV presets.
*
* <p>Each preset renders the same canonical {@link CvDocument} on
* full A4 with the preset's {@code RECOMMENDED_MARGIN}; the resulting
* PDF is rasterised page-by-page and compared per-pixel against a
* checked-in baseline PNG. Failures write the actual render +
* diff image next to the baseline.</p>
*
* <p><strong>Re-blessing baselines</strong> — after a deliberate
* visual change, re-run with
* {@code -Dgraphcompose.visual.approve=true} (or environment variable
* {@code GRAPHCOMPOSE_VISUAL_APPROVE=true}) to overwrite the
* baselines with the current rendering. Commit the updated PNGs as
* part of the same change.</p>
*
* <p>Baselines live under
* {@code src/test/resources/visual-baselines/cv-v2-layered/}. Budget
* mirrors the v1 {@code PresetVisualParityTest} (20 000 mismatched
* pixels at per-channel tolerance 8) — calibrated for cross-platform
* PDFBox font + colour rendering drift between Windows-recorded
* baselines and Linux CI.</p>
*/
class CvV2VisualParityTest {

private static final Path BASELINE_ROOT = Path.of(
"src", "test", "resources", "visual-baselines", "cv-v2-layered");

// Calibrated against the worst observed cross-platform drift:
// ModernProfessional renders ~40k mismatched pixels on Linux CI
// vs Windows-recorded baseline because Helvetica is the only
// PDFBox built-in font where text glyph outlines + base colours
// differ noticeably between platforms (the PT-Serif presets hit
// ~5-10k). Budget sized to cover the MP case with margin —
// tighter per-preset overrides can be introduced later if drift
// patterns diverge.
private static final long PIXEL_DIFF_BUDGET = 50_000L;
private static final int PER_PIXEL_TOLERANCE = 8;

@ParameterizedTest(name = "{0}")
@MethodSource("presets")
void rendersWithinPixelDiffBudget(String slug,
double margin,
Supplier<DocumentTemplate<CvDocument>> factory)
throws Exception {
DocumentTemplate<CvDocument> template = factory.get();
float m = (float) margin;
byte[] pdfBytes;
try (DocumentSession document = GraphCompose.document()
.pageSize(DocumentPageSize.A4)
.margin(m, m, m, m)
.create()) {
template.compose(document, canonicalDocument());
pdfBytes = document.toPdfBytes();
}

PdfVisualRegression.standard()
.baselineRoot(BASELINE_ROOT)
.perPixelTolerance(PER_PIXEL_TOLERANCE)
.mismatchedPixelBudget(PIXEL_DIFF_BUDGET)
.assertMatchesBaseline(slug, pdfBytes);
}

private static Stream<Arguments> presets() {
return Stream.of(
Arguments.of("boxed_sections",
BoxedSections.RECOMMENDED_MARGIN,
(Supplier<DocumentTemplate<CvDocument>>) BoxedSections::create),
Arguments.of("minimal_underlined",
MinimalUnderlined.RECOMMENDED_MARGIN,
(Supplier<DocumentTemplate<CvDocument>>) MinimalUnderlined::create),
Arguments.of("modern_professional",
ModernProfessional.RECOMMENDED_MARGIN,
(Supplier<DocumentTemplate<CvDocument>>) ModernProfessional::create));
}

/**
* Canonical sample document — Jordan Rivera with every v2 section
* subtype exercised so the gate covers paragraph, all three
* row-styles, and timeline entries.
*
* <p>Kept inline (not pulled from the examples module) so the
* test depends only on main + main-test code.</p>
*/
private static CvDocument canonicalDocument() {
return CvDocument.builder()
.identity(CvIdentity.builder()
.name("Jordan", "Rivera")
.contact("+44 20 5555 1000",
"jordan.rivera@example.com",
"London, UK")
.link("LinkedIn", "https://linkedin.com/in/jordan-rivera-demo")
.link("GitHub", "https://github.com/jrivera-demo")
.build())
.section(new ParagraphSection("Professional Summary",
"Platform engineer with **10+ years** building resilient "
+ "document-generation pipelines, layout engines, and "
+ "developer-facing template systems. Specialised in "
+ "high-throughput PDF rendering, semantic authoring "
+ "DSLs, and turning brittle production-ops scripts "
+ "into typed, snapshot-tested libraries that scale."))
.section(RowsSection.builder("Technical Skills", RowStyle.BULLETED)
.row("Languages", "Java 21, Kotlin, Groovy, Python, SQL")
.row("Document & Print", "PDFBox, Apache POI (DOCX/XLSX), iText, "
+ "PostScript, ICC colour profiles, font metrics")
.row("Layout engines", "Custom DSL design, semantic layout trees, "
+ "pagination, snapshot testing, visual regression")
.row("Build & infrastructure", "Maven, Gradle, GitHub Actions, "
+ "JitPack, Docker, JMH benchmarking")
.row("Testing", "JUnit 5, AssertJ, PDFBox-based PNG diff, "
+ "layout-graph snapshots, mutation testing (Pitest)")
.row("Distribution", "Maven Central, Sonatype OSSRH, GPG signing, "
+ "JitPack, semantic versioning discipline")
.build())
.section(EntriesSection.builder("Education & Certifications")
.entry("MSc Computer Science",
"University of Manchester",
"2021",
"Distinction. Thesis: *Composable layout primitives "
+ "for deterministic document rendering*.")
.entry("BSc Software Engineering",
"Imperial College London",
"2019",
"First-class honours. Specialisation in compilers and "
+ "static analysis.")
.entry("Oracle Java Certification",
"Professional track",
"2023",
"Java 17 platform deep-dive: records, sealed types, "
+ "pattern matching, virtual threads.")
.build())
.section(RowsSection.builder("Projects", RowStyle.BULLETED_STACKED)
.row("GraphCompose (Java 21, PDFBox, Maven, JMH)",
"Declarative Java PDF layout engine. Semantic DSL, "
+ "slot-based templates, snapshot testing. Powers "
+ "production CV / invoice / proposal pipelines for "
+ "hiring tools and billing systems. *(Open source)*")
.row("Template Studio (Kotlin, Compose Desktop, PDFBox PNG diff)",
"Internal tool for evaluating CV, proposal, and "
+ "invoice output across 14 design presets. PNG "
+ "diffing, side-by-side layout, baseline freezing.")
.row("LayoutLint (Java 21, JavaParser, Spoon)",
"Static analyser that flags fragile authoring patterns "
+ "(deeply nested rows, untyped offsets, implicit "
+ "page breaks) before they ship to production.")
.row("ChromeForge (Java, GraphCompose, Pandoc bridge)",
"Editorial-magazine document toolkit built on "
+ "GraphCompose: cinematic covers, pull quotes, "
+ "multi-column flow, sidebar callouts.")
.build())
.section(EntriesSection.builder("Professional Experience")
.entry("Senior Platform Engineer",
"Northwind Systems",
"2024-Present",
"Led the reusable document-generation platform serving "
+ "billing, hiring, and reporting flows across "
+ "**8 product teams**. Reduced template maintenance "
+ "time by **70%** by retiring per-team PDF scripts "
+ "in favour of one canonical engine.")
.entry("Software Engineer",
"BrightLeaf Labs",
"2021-2024",
"Built backend services and production document rendering "
+ "pipelines processing **2M+ documents per month**. "
+ "Drove the migration from iText to a custom layout "
+ "engine, eliminating licensing risk and cutting "
+ "p99 render latency from 1.4s to 380ms.")
.entry("Backend Engineer",
"Helix Print Co",
"2019-2021",
"Maintained a high-volume invoice-printing service "
+ "(15M PDFs/year) and authored the compliance test "
+ "harness that gated every template change.")
.build())
.section(RowsSection.builder("Additional Information", RowStyle.PLAIN)
.row("Languages",
"English (Fluent), German (Intermediate), Spanish (Basic)")
.row("Work Eligibility",
"Eligible to work in the UK and the EU")
.row("Open Source",
"Maintainer of GraphCompose. Regular contributor to "
+ "PDFBox issue triage.")
.row("Speaking",
"JVM Summit 2024, Devoxx UK 2025 — both on declarative "
+ "document layout.")
.build())
.build();
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.