From 303d607a4c8dd45d120398499d7c25d4239d67de Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Wed, 3 Jun 2026 02:49:58 +0100 Subject: [PATCH] feat(testing): promote PdfVisualRegression + ImageDiff to public API (Track N N1, @since 1.6.9) Move the pixel-level visual-regression harness from the test source set to com.demcha.compose.testing.visual, sibling to the public com.demcha.compose.testing.layout.* snapshot helpers, so library consumers can run the same render-PDF -> diff-PNG baseline gate against their own presets. - PdfVisualRegression + ImageDiff + package-info public (@since 1.6.9) - inline PDFBox PDFRenderer in renderPages(), drop the test-only PdfRenderBridge dependency from the public surface - eager 0..255 guard on perPixelTolerance(int) for fail-fast symmetry with renderScale/mismatchedPixelBudget, plus negative test - rewire 7 in-repo callers to the new package - sync test-your-document.md + CHANGELOG (v1.6.9 Public API) Behavior-preserving: visual baselines unchanged. Full suite 1059/1059 green. --- CHANGELOG.md | 13 +++++++- docs/operations/test-your-document.md | 22 ++++++------- .../compose}/testing/visual/ImageDiff.java | 4 ++- .../testing/visual/PdfVisualRegression.java | 31 +++++++++++++------ .../compose/testing/visual/package-info.java | 9 ++++++ .../presets/PresetVisualParityTest.java | 2 +- .../CoverLetterV2VisualParityTest.java | 2 +- .../cv/presets/PresetVisualParityTest.java | 2 +- .../cv/v2/presets/CvV2VisualParityTest.java | 2 +- .../visual/PdfVisualRegressionTest.java | 12 +++++++ .../ShapeContainerVisualRegressionTest.java | 1 + .../testing/visual/TableRowSpanDemoTest.java | 1 + 12 files changed, 74 insertions(+), 27 deletions(-) rename src/{test/java/com/demcha => main/java/com/demcha/compose}/testing/visual/ImageDiff.java (98%) rename src/{test/java/com/demcha => main/java/com/demcha/compose}/testing/visual/PdfVisualRegression.java (84%) create mode 100644 src/main/java/com/demcha/compose/testing/visual/package-info.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 1693e204..880cf6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,18 @@ follow semantic versioning; release dates are ISO 8601. ## v1.6.9 — Planned -Next bug-fix / housekeeping cycle. Track open in `docs/private/` taskboard. +Housekeeping cycle plus the public pixel-level visual-regression API (Track N). + +### Public API + +- **Promoted the pixel-level visual-regression harness to public API.** + `com.demcha.compose.testing.visual.PdfVisualRegression` and + `com.demcha.compose.testing.visual.ImageDiff` (`@since 1.6.9`) move from the + test source set into `src/main/java`, alongside the existing + `com.demcha.compose.testing.layout.*` semantic snapshot helpers. Library + consumers can now run the same render-PDF → diff-PNG baseline gate against + their own presets and templates instead of copying the harness. Behaviour is + unchanged; the PDF→image step is inlined on PDFBox's `PDFRenderer`. ## v1.6.8 — 2026-06-01 diff --git a/docs/operations/test-your-document.md b/docs/operations/test-your-document.md index 0901a292..206e8400 100644 --- a/docs/operations/test-your-document.md +++ b/docs/operations/test-your-document.md @@ -221,17 +221,15 @@ shipped CV / cover-letter preset and for the engine showcase tests (see `CvV2VisualParityTest`, `CoverLetterV2VisualParityTest`, `TableRowSpanDemoTest` and friends). -The harness behind those tests -(`com.demcha.testing.visual.PdfVisualRegression` + -`ImageDiff`) is currently **test-only** inside the GraphCompose -build. Promoting it to a public `com.demcha.compose.testing.visual.*` -API so library consumers can adopt the same pixel-level gate against -their own presets is queued as **v1.6.8 / v1.7.0 Track N** — see the -release-readiness taskboard. Until that ships, the recommended -public path is layout snapshot above; for pixel-level work, copy -the pattern from `PdfVisualRegression` (it builds on the public -`com.demcha.compose.devtool.PdfRenderBridge` for PDF page → image -conversion). +The harness behind those tests is the public +`com.demcha.compose.testing.visual.PdfVisualRegression` + +`ImageDiff` API (`@since 1.6.9`), a sibling to the +`com.demcha.compose.testing.layout.*` snapshot helpers — library +consumers can adopt the same pixel-level gate against their own +presets. Start from `PdfVisualRegression.standard()`, point +`baselineRoot(...)` at your own baseline directory, and call +`assertMatchesBaseline(name, pdfBytes)`; run with +`-Dgraphcompose.visual.approve=true` to (re)bless baselines. --- @@ -241,7 +239,7 @@ conversion). |---|---| | The document compiles + renders at all | smoke (just call `buildPdf()` in a test) | | The semantic graph and resolved coordinates are stable across engine refactors | **layout snapshot** | -| The PDF visually looks identical, fonts/colours and all | pixel-level visual (Track N) | +| The PDF visually looks identical, fonts/colours and all | **pixel-level visual** (`PdfVisualRegression`) | | A specific layout math rule holds | a focused unit test | The advice scales: a flagship template or a preset you publish to diff --git a/src/test/java/com/demcha/testing/visual/ImageDiff.java b/src/main/java/com/demcha/compose/testing/visual/ImageDiff.java similarity index 98% rename from src/test/java/com/demcha/testing/visual/ImageDiff.java rename to src/main/java/com/demcha/compose/testing/visual/ImageDiff.java index 96e87f4d..a09fb738 100644 --- a/src/test/java/com/demcha/testing/visual/ImageDiff.java +++ b/src/main/java/com/demcha/compose/testing/visual/ImageDiff.java @@ -1,4 +1,4 @@ -package com.demcha.testing.visual; +package com.demcha.compose.testing.visual; import java.awt.Color; import java.awt.image.BufferedImage; @@ -21,6 +21,7 @@ * can write the diff to disk for inspection.

* * @author Artem Demchyshyn + * @since 1.6.9 */ public final class ImageDiff { @@ -105,6 +106,7 @@ private static int blue(int rgb) { * @param maxChannelDelta largest single-channel delta observed * @param summary human-readable summary line for failure messages * @param diffImage optional visualisation; {@code null} when sizes differed + * @since 1.6.9 */ public record Result( boolean differs, diff --git a/src/test/java/com/demcha/testing/visual/PdfVisualRegression.java b/src/main/java/com/demcha/compose/testing/visual/PdfVisualRegression.java similarity index 84% rename from src/test/java/com/demcha/testing/visual/PdfVisualRegression.java rename to src/main/java/com/demcha/compose/testing/visual/PdfVisualRegression.java index 7944614a..717da220 100644 --- a/src/test/java/com/demcha/testing/visual/PdfVisualRegression.java +++ b/src/main/java/com/demcha/compose/testing/visual/PdfVisualRegression.java @@ -1,8 +1,9 @@ -package com.demcha.testing.visual; +package com.demcha.compose.testing.visual; -import com.demcha.compose.devtool.PdfRenderBridge; 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; @@ -14,13 +15,19 @@ import java.util.Objects; /** - * Test harness for "render PDF → diff PNG" visual regression checks. + * Pixel-level visual-regression harness: renders a PDF and diffs each page + * against a stored PNG baseline. Public companion to the semantic + * {@code com.demcha.compose.testing.layout} snapshot layer — reach for this + * when byte-for-byte pixel fidelity matters, and for the snapshot layer when + * structural geometry is enough. * - *

Each baseline lives at {@code src/test/resources/visual-baselines/<name>-page-N.png}. - * In the default mode the harness renders the supplied PDF, converts each page - * to a {@link BufferedImage} via {@link PdfRenderBridge}, and compares against - * the baseline using {@link ImageDiff}. A failing comparison writes the actual - * render and the diff image next to the baseline for inspection.

+ *

The default baseline directory is {@code src/test/resources/visual-baselines} + * (override with {@link #baselineRoot(Path)}); each page is stored as + * {@code <name>-page-N.png}. In the default mode the harness renders the + * supplied PDF, converts each page to a {@link BufferedImage} with PDFBox's + * {@link PDFRenderer}, and compares against the baseline using {@link ImageDiff}. + * A failing comparison writes the actual render and the diff image next to the + * baseline for inspection.

* *

To re-bless baselines, run the test with the system property * {@code -Dgraphcompose.visual.approve=true} (or environment variable @@ -29,6 +36,7 @@ * assertion.

* * @author Artem Demchyshyn + * @since 1.6.9 */ public final class PdfVisualRegression { @@ -94,8 +102,12 @@ public PdfVisualRegression renderScale(float renderScale) { * * @param perPixelTolerance tolerance per channel * @return updated harness + * @throws IllegalArgumentException if {@code perPixelTolerance} is outside {@code 0..255} */ public PdfVisualRegression perPixelTolerance(int perPixelTolerance) { + if (perPixelTolerance < 0 || perPixelTolerance > 255) { + throw new IllegalArgumentException("perPixelTolerance must be 0..255, got " + perPixelTolerance); + } return new PdfVisualRegression(baselineRoot, renderScale, perPixelTolerance, mismatchedPixelBudget); } @@ -176,9 +188,10 @@ public void assertMatchesBaseline(String baselineName, byte[] pdfBytes) throws I public List renderPages(byte[] pdfBytes) throws IOException { Objects.requireNonNull(pdfBytes, "pdfBytes"); try (PDDocument document = Loader.loadPDF(pdfBytes)) { + PDFRenderer renderer = new PDFRenderer(document); List pages = new ArrayList<>(document.getNumberOfPages()); for (int i = 0; i < document.getNumberOfPages(); i++) { - pages.add(PdfRenderBridge.renderToImage(document, i, renderScale)); + pages.add(renderer.renderImage(i, renderScale, ImageType.RGB)); } return pages; } diff --git a/src/main/java/com/demcha/compose/testing/visual/package-info.java b/src/main/java/com/demcha/compose/testing/visual/package-info.java new file mode 100644 index 00000000..9ba88999 --- /dev/null +++ b/src/main/java/com/demcha/compose/testing/visual/package-info.java @@ -0,0 +1,9 @@ +/** + * Public test helpers for pixel-level visual regression of rendered PDFs. + * + *

Ownership: Owned by the testing support surface and intended for consumer regression tests.

+ *

Extension rules: Extend with diff and render-harness utilities only; runtime engine code must not depend on this package.

+ * + * @since 1.6.9 + */ +package com.demcha.compose.testing.visual; diff --git a/src/test/java/com/demcha/compose/document/templates/coverletter/presets/PresetVisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/coverletter/presets/PresetVisualParityTest.java index 7b3fda56..777a8c09 100644 --- a/src/test/java/com/demcha/compose/document/templates/coverletter/presets/PresetVisualParityTest.java +++ b/src/test/java/com/demcha/compose/document/templates/coverletter/presets/PresetVisualParityTest.java @@ -7,7 +7,7 @@ import com.demcha.compose.document.templates.coverletter.spec.CoverLetterHeader; import com.demcha.compose.document.templates.coverletter.spec.CoverLetterSpec; import com.demcha.compose.document.theme.BusinessTheme; -import com.demcha.testing.visual.PdfVisualRegression; +import com.demcha.compose.testing.visual.PdfVisualRegression; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; diff --git a/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2VisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2VisualParityTest.java index c61ba18c..b8cd501d 100644 --- a/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2VisualParityTest.java +++ b/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2VisualParityTest.java @@ -6,7 +6,7 @@ import com.demcha.compose.document.templates.api.DocumentTemplate; import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; -import com.demcha.testing.visual.PdfVisualRegression; +import com.demcha.compose.testing.visual.PdfVisualRegression; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; diff --git a/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualParityTest.java index 396b5abe..30d5bbde 100644 --- a/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualParityTest.java +++ b/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualParityTest.java @@ -13,7 +13,7 @@ import com.demcha.compose.document.templates.cv.spec.CvModule; import com.demcha.compose.document.templates.cv.spec.CvSpec; import com.demcha.compose.document.theme.BusinessTheme; -import com.demcha.testing.visual.PdfVisualRegression; +import com.demcha.compose.testing.visual.PdfVisualRegression; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; diff --git a/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java index 0b6220be..6e965866 100644 --- a/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java +++ b/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java @@ -12,7 +12,7 @@ import com.demcha.compose.document.templates.cv.v2.data.RowStyle; import com.demcha.compose.document.templates.cv.v2.data.RowsSection; import com.demcha.compose.document.templates.cv.v2.data.SkillsSection; -import com.demcha.testing.visual.PdfVisualRegression; +import com.demcha.compose.testing.visual.PdfVisualRegression; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; diff --git a/src/test/java/com/demcha/testing/visual/PdfVisualRegressionTest.java b/src/test/java/com/demcha/testing/visual/PdfVisualRegressionTest.java index cd41843b..8487ca5d 100644 --- a/src/test/java/com/demcha/testing/visual/PdfVisualRegressionTest.java +++ b/src/test/java/com/demcha/testing/visual/PdfVisualRegressionTest.java @@ -3,6 +3,8 @@ import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentSession; import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.testing.visual.ImageDiff; +import com.demcha.compose.testing.visual.PdfVisualRegression; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -71,6 +73,16 @@ void differentSizesAreReportedAsMaxDelta() { assertThat(diff.diffImage()).isNull(); } + @Test + void perPixelToleranceOutsideChannelRangeIsRejected() { + assertThatThrownBy(() -> PdfVisualRegression.standard().perPixelTolerance(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("perPixelTolerance"); + assertThatThrownBy(() -> PdfVisualRegression.standard().perPixelTolerance(256)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("perPixelTolerance"); + } + @Test void renderingTheSameDocumentTwiceProducesPixelIdenticalPages() throws Exception { byte[] first = renderSampleDocument(); diff --git a/src/test/java/com/demcha/testing/visual/ShapeContainerVisualRegressionTest.java b/src/test/java/com/demcha/testing/visual/ShapeContainerVisualRegressionTest.java index 5e230abb..d3f125d4 100644 --- a/src/test/java/com/demcha/testing/visual/ShapeContainerVisualRegressionTest.java +++ b/src/test/java/com/demcha/testing/visual/ShapeContainerVisualRegressionTest.java @@ -13,6 +13,7 @@ import com.demcha.compose.document.style.DocumentTextDecoration; import com.demcha.compose.document.style.DocumentTextStyle; import com.demcha.compose.font.FontName; +import com.demcha.compose.testing.visual.PdfVisualRegression; import org.junit.jupiter.api.Test; class ShapeContainerVisualRegressionTest { diff --git a/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java b/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java index 716b0522..fcf8e200 100644 --- a/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java +++ b/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java @@ -14,6 +14,7 @@ import com.demcha.compose.document.table.DocumentTableColumn; import com.demcha.compose.document.table.DocumentTableStyle; import com.demcha.compose.engine.components.content.table.TableResolvedCell; +import com.demcha.compose.testing.visual.PdfVisualRegression; import com.demcha.testing.VisualTestOutputs; import org.junit.jupiter.api.Test;