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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 10 additions & 12 deletions docs/operations/test-your-document.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.demcha.testing.visual;
package com.demcha.compose.testing.visual;

import java.awt.Color;
import java.awt.image.BufferedImage;
Expand All @@ -21,6 +21,7 @@
* can write the diff to disk for inspection.</p>
*
* @author Artem Demchyshyn
* @since 1.6.9
*/
public final class ImageDiff {

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
*
* <p>Each baseline lives at {@code src/test/resources/visual-baselines/&lt;name&gt;-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.</p>
* <p>The default baseline directory is {@code src/test/resources/visual-baselines}
* (override with {@link #baselineRoot(Path)}); each page is stored as
* {@code &lt;name&gt;-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.</p>
*
* <p>To re-bless baselines, run the test with the system property
* {@code -Dgraphcompose.visual.approve=true} (or environment variable
Expand All @@ -29,6 +36,7 @@
* assertion.</p>
*
* @author Artem Demchyshyn
* @since 1.6.9
*/
public final class PdfVisualRegression {

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -176,9 +188,10 @@ public void assertMatchesBaseline(String baselineName, byte[] pdfBytes) throws I
public List<BufferedImage> renderPages(byte[] pdfBytes) throws IOException {
Objects.requireNonNull(pdfBytes, "pdfBytes");
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
PDFRenderer renderer = new PDFRenderer(document);
List<BufferedImage> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Public test helpers for pixel-level visual regression of rendered PDFs.
*
* <p>Ownership: Owned by the testing support surface and intended for consumer regression tests.</p>
* <p>Extension rules: Extend with diff and render-harness utilities only; runtime engine code must not depend on this package.</p>
*
* @since 1.6.9
*/
package com.demcha.compose.testing.visual;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down