From b89d0882fc17f452435001295f3f26bf22575c73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 20:52:20 +0000 Subject: [PATCH 1/2] test: add transparency flatten-then-render pixel golden Close the silent-blank flatten hole by flattening transparency-normal-cmyk through PDFTransparencyFlattener::apply(), re-rendering at 128x128, and comparing to a committed PNG plus measurement budgets on UnitTestsOverprintRender. Co-authored-by: michael berry --- UnitTests/tst_overprintrendertest.cpp | 89 +++++++++++++++++- ...-transparency-flatten-pixel-golden-9148.md | 4 + docs/RENDERER_DIFFERENTIALS.md | 15 ++- docs/TRANSPARENCY_FLATTENING_PLAN.md | 7 ++ loop-preflight/README.md | 5 + .../flatten-transparency-normal-cmyk.png | Bin 0 -> 716 bytes ...sparency-normal-cmyk.png.measurements.json | 6 ++ 7 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 changes/cursor-transparency-flatten-pixel-golden-9148.md create mode 100644 loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png create mode 100644 loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png.measurements.json diff --git a/UnitTests/tst_overprintrendertest.cpp b/UnitTests/tst_overprintrendertest.cpp index b9183033c..5a1426f79 100644 --- a/UnitTests/tst_overprintrendertest.cpp +++ b/UnitTests/tst_overprintrendertest.cpp @@ -24,12 +24,20 @@ // intentionally tolerant of small Qt/platform rasterization differences while // still requiring a bounded number of differing pixels. Mismatch images are // written beside the committed baselines for CI artifact inspection. +// +// Flatten-then-render goldens (`flatten-*.png`) run the real +// PDFTransparencyFlattener::apply() path before the same 128x128 measurement +// renderer. Structural flatten tests can report a full-page region and +// fullyOpaque while the replacement raster is blank; these goldens fail that +// case. Linux CI is the source of truth; Windows uses the same PNGs with the +// shared channel-delta / differing-pixel budgets. #include "pdfcms.h" #include "pdfdocument.h" #include "pdfdocumentreader.h" #include "pdfoptionalcontent.h" #include "pdfrenderer.h" +#include "pdftransparencyflattener.h" #include "pdftransparencyrenderer.h" #include @@ -72,12 +80,21 @@ bool updateSnapshotsRequested() return qEnvironmentVariableIntValue("LOOP_UPDATE_SNAPSHOTS") == 1; } -QImage renderFixture(const QString& fixturePath, bool separationSimulation) +pdf::PDFDocument loadFixtureDocument(const QString& fixturePath) { pdf::PDFDocumentReader reader(nullptr, [](bool*) { return QString(); }, true, false); pdf::PDFDocument document = reader.readFromFile(fixturePath); if (reader.getReadingResult() != pdf::PDFDocumentReader::Result::OK) + { + return pdf::PDFDocument(); + } + return document; +} + +QImage renderDocumentPage(pdf::PDFDocument& document, bool separationSimulation) +{ + if (!document.getCatalog()) { return QImage(); } @@ -115,6 +132,31 @@ QImage renderFixture(const QString& fixturePath, bool separationSimulation) return renderer.toImage(false, true, pdf::PDFRGB{ 1.0f, 1.0f, 1.0f }); } +QImage renderFixture(const QString& fixturePath, bool separationSimulation) +{ + pdf::PDFDocument document = loadFixtureDocument(fixturePath); + return renderDocumentPage(document, separationSimulation); +} + +int nonWhitePixelCount(const QImage& image) +{ + const QImage rgba = image.convertToFormat(QImage::Format_RGBA8888); + int count = 0; + for (int y = 0; y < rgba.height(); ++y) + { + const uchar* line = rgba.constScanLine(y); + for (int x = 0; x < rgba.width(); ++x) + { + const int offset = x * 4; + if (line[offset] < 250 || line[offset + 1] < 250 || line[offset + 2] < 250) + { + ++count; + } + } + } + return count; +} + void compareRender(const QString& name, const QImage& actual, const QImage& expected) { QVERIFY2(!actual.isNull(), qPrintable(QStringLiteral("Renderer returned no image for %1").arg(name))); @@ -206,6 +248,8 @@ private slots: void render_data(); void render(); void rendererDifferentialDoesNotDriftBeyondTolerance(); + void flattenThenRender_data(); + void flattenThenRender(); }; void OverprintRenderTest::render_data() @@ -243,5 +287,48 @@ void OverprintRenderTest::rendererDifferentialDoesNotDriftBeyondTolerance() compareRender(name + QStringLiteral(".png"), actual, expected); } +void OverprintRenderTest::flattenThenRender_data() +{ + QTest::addColumn("fixture"); + QTest::addColumn("baseline"); + + QTest::newRow("transparency-normal-cmyk") << QStringLiteral("transparency-normal-cmyk.pdf") + << QStringLiteral("flatten-transparency-normal-cmyk.png"); +} + +void OverprintRenderTest::flattenThenRender() +{ + QFETCH(QString, fixture); + QFETCH(QString, baseline); + + pdf::PDFDocument document = loadFixtureDocument(fixturesDirectory() + QLatin1Char('/') + fixture); + QVERIFY2(document.getCatalog()->getPage(0), qPrintable(QStringLiteral("Could not load flatten fixture %1").arg(fixture))); + + // Flatten rasterizes every selected page even when the structural live-transparency + // walk misses a nested Normal group. The pixel golden is the paint proof. + + pdf::PDFTransparencyFlattenSettings settings; + settings.rasterizationDpi = 72; + settings.maxRasterPixels = 100000; + pdf::PDFTransparencyFlattenReport report; + const pdf::PDFOperationResult result = pdf::PDFTransparencyFlattener::apply(&document, settings, &report); + QVERIFY2(result, qPrintable(result.getErrorMessage())); + QVERIFY(report.changed); + QVERIFY(report.fullyOpaque); + QVERIFY(!pdf::PDFTransparencyFlattener::hasLiveTransparency(&document)); + + const QImage actual = renderDocumentPage(document, false); + QVERIFY2(!actual.isNull(), qPrintable(QStringLiteral("Flattened renderer returned no image for %1").arg(fixture))); + constexpr int minNonWhitePixels = 256; + const int paintedPixels = nonWhitePixelCount(actual); + QVERIFY2(paintedPixels >= minNonWhitePixels, + qPrintable(QStringLiteral("%1 flattened to a blank raster (%2 non-white pixels); structural flatten success is not paint proof") + .arg(fixture) + .arg(paintedPixels))); + + const QImage expected = QImage(rendersDirectory() + QLatin1Char('/') + baseline); + compareRender(baseline, actual, expected); +} + QTEST_APPLESS_MAIN(OverprintRenderTest) #include "tst_overprintrendertest.moc" diff --git a/changes/cursor-transparency-flatten-pixel-golden-9148.md b/changes/cursor-transparency-flatten-pixel-golden-9148.md new file mode 100644 index 000000000..ed14f88d1 --- /dev/null +++ b/changes/cursor-transparency-flatten-pixel-golden-9148.md @@ -0,0 +1,4 @@ +Category: added +Audience: developers +Breaking-Change: no +Summary: Add a flatten-then-render pixel golden for transparency-normal-cmyk on UnitTestsOverprintRender so a silent blank flatten fails CI instead of only structural region and dry-run checks. diff --git a/docs/RENDERER_DIFFERENTIALS.md b/docs/RENDERER_DIFFERENTIALS.md index a1e1d0e10..8947db42b 100644 --- a/docs/RENDERER_DIFFERENTIALS.md +++ b/docs/RENDERER_DIFFERENTIALS.md @@ -13,7 +13,20 @@ measurements in sibling `*.measurements.json` files: - declared budgets (max channel delta 2, 64 differing pixels) A drift beyond those budgets fails the named test. Refresh goldens only with -`LOOP_UPDATE_SNAPSHOTS=1`. +`LOOP_UPDATE_SNAPSHOTS=1` (Linux is the source of truth; Windows uses the same +PNGs with those budgets): + +```bash +LOOP_UPDATE_SNAPSHOTS=1 ctest --test-dir build -R UnitTestsOverprintRender +``` + +The same target also flattens `transparency-normal-cmyk.pdf` through +`PDFTransparencyFlattener::apply()` at 72 DPI, re-renders the opaque page at +128×128, and compares it to `flatten-transparency-normal-cmyk.png`. The slot +fails closed if flatten reports success but the raster is blank (fewer than +256 non-white pixels) or drifts beyond the shared budgets. Structural flatten +tests in `UnitTestsTransparencyFlattener` only check region reports and dry-run +identity; they cannot catch a silent blank paint. **Disclosed limitation:** page-view overprint (the ordinary viewer paint path) is not this measurement renderer and must not be cited as proof of separation diff --git a/docs/TRANSPARENCY_FLATTENING_PLAN.md b/docs/TRANSPARENCY_FLATTENING_PLAN.md index 2e968f462..79ad712e2 100644 --- a/docs/TRANSPARENCY_FLATTENING_PLAN.md +++ b/docs/TRANSPARENCY_FLATTENING_PLAN.md @@ -31,3 +31,10 @@ source document. The current implementation intentionally reports the entire page as the rasterized region. A later vector-preserving balance mode can use the existing settings and report contract without introducing a second pipeline. + +Paint proof is `UnitTestsOverprintRender::flattenThenRender`, which flattens +`transparency-normal-cmyk.pdf` and compares the re-rendered page to +`loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png`. Refresh +that golden with `LOOP_UPDATE_SNAPSHOTS=1` as documented in +`docs/RENDERER_DIFFERENTIALS.md`. A flatten that still reports a full-page +region and `fullyOpaque` but writes a blank raster fails that slot. diff --git a/loop-preflight/README.md b/loop-preflight/README.md index 50db72638..c19e78a8a 100644 --- a/loop-preflight/README.md +++ b/loop-preflight/README.md @@ -516,6 +516,11 @@ passes, and only the target check is exercised. | `transparency-cmyk-group-spot.pdf` | test-transparency-risk | warning | `transparency-blend-space` | | `transparency-annotation-appearance.pdf` | test-transparency-risk | warning | annotation `/AP` `transparency-blend-space` | +Flatten-then-render paint proof for `transparency-normal-cmyk.pdf` lives beside the +overprint PNG goldens in `testdata/renders/flatten-transparency-normal-cmyk.png` +and is executed by `UnitTestsOverprintRender`. Refresh it with +`LOOP_UPDATE_SNAPSHOTS=1` as documented in `docs/RENDERER_DIFFERENTIALS.md`. + The `bleed-*` pair above covers the `bleed` check, so every Loop Default custom check has at least one known-pass and one known-fail (or warning) case. diff --git a/loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png b/loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png new file mode 100644 index 0000000000000000000000000000000000000000..e12647464d86a124eddc7644aeaf8b80c0b6dae0 GIT binary patch literal 716 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9Gmi6G3l!hU%s0|Qf? zr;B4q#hkY{H)cI{5OI50zwU{e=C{A{#o|FM-sRgSbT5fiF+12;Y!~n_X-Q+X&dG{1 zmgy~_=kGPlJI(Nbhv6Isp~v=NUWt0~X&?2?`_sdBrF~15yKOjs&-}Pual0<;Q(HW# zcG>k)Mpm7IHg`R&rl!vKylS}o>77g2j8|;9cK`Ld9z882P5Qv*-c==5-uu;V&rkMC zzaIC~ykObol~2M}=BBBATz}-bbpPV(6Yp40J!kQ=WWR_*m4B8>vZuY?s`-h}K029y zUy>g5d>-TVI41kKS3dsNk$+ZL^rO?B`6D*>4qLHzQ4D+Qe;Dj9Q~%{J|LkL?-~VlK zyIgupv?ATJX3sQio}XaTS1aqO6*{-D=Oe=o9j&(zhJIiUGrTxGE zpS<*?H*L9_`e~mu`C_Zc12c+$Nw9x@&7ICrz{Y?^d>8j)uDffV9PbBAo(!I@elF{r G5}E+uNGYcP literal 0 HcmV?d00001 diff --git a/loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png.measurements.json b/loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png.measurements.json new file mode 100644 index 000000000..93e56cc70 --- /dev/null +++ b/loop-preflight/testdata/renders/flatten-transparency-normal-cmyk.png.measurements.json @@ -0,0 +1,6 @@ +{ + "width": 128, + "height": 128, + "max_channel_delta_budget": 2, + "differing_pixel_budget": 64 +} From 76997c3ac7bc224d400024f43d944f171e426cb5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 23:21:59 +0000 Subject: [PATCH 2/2] test: wire flatten golden into PR CI and fail closed on missing fixture - Map UnitTests/tst_overprintrendertest.cpp into the core agent-policy path set so edits select UnitTestsOverprintRender in PR CI. - Assert catalog presence before dereferencing a missing flatten fixture. Co-authored-by: michael berry --- UnitTests/tst_overprintrendertest.cpp | 4 +++- agent-policy.json | 1 + changes/cursor-transparency-flatten-pixel-golden-9148.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/UnitTests/tst_overprintrendertest.cpp b/UnitTests/tst_overprintrendertest.cpp index 5a1426f79..4a6078500 100644 --- a/UnitTests/tst_overprintrendertest.cpp +++ b/UnitTests/tst_overprintrendertest.cpp @@ -302,7 +302,9 @@ void OverprintRenderTest::flattenThenRender() QFETCH(QString, baseline); pdf::PDFDocument document = loadFixtureDocument(fixturesDirectory() + QLatin1Char('/') + fixture); - QVERIFY2(document.getCatalog()->getPage(0), qPrintable(QStringLiteral("Could not load flatten fixture %1").arg(fixture))); + QVERIFY2(document.getCatalog(), qPrintable(QStringLiteral("Could not load flatten fixture %1").arg(fixture))); + QVERIFY2(document.getCatalog()->getPage(0), + qPrintable(QStringLiteral("Flatten fixture %1 has no first page").arg(fixture))); // Flatten rasterizes every selected page even when the structural live-transparency // walk misses a nested Normal group. The pixel golden is the paint proof. diff --git a/agent-policy.json b/agent-policy.json index 66bf5afe8..bf5d09765 100644 --- a/agent-policy.json +++ b/agent-policy.json @@ -92,6 +92,7 @@ "UnitTests/tst_jbig2decodertest.cpp", "UnitTests/tst_lifecycletest.cpp", "UnitTests/tst_overprinttest.cpp", + "UnitTests/tst_overprintrendertest.cpp", "UnitTests/tst_revisionstresstest.cpp", "UnitTests/tst_safefilewritertest.cpp" ], diff --git a/changes/cursor-transparency-flatten-pixel-golden-9148.md b/changes/cursor-transparency-flatten-pixel-golden-9148.md index ed14f88d1..96f7f1bcd 100644 --- a/changes/cursor-transparency-flatten-pixel-golden-9148.md +++ b/changes/cursor-transparency-flatten-pixel-golden-9148.md @@ -1,4 +1,4 @@ Category: added Audience: developers Breaking-Change: no -Summary: Add a flatten-then-render pixel golden for transparency-normal-cmyk on UnitTestsOverprintRender so a silent blank flatten fails CI instead of only structural region and dry-run checks. +Summary: Add a flatten-then-render pixel golden for transparency-normal-cmyk on UnitTestsOverprintRender so a silent blank flatten fails CI instead of only structural region and dry-run checks, map tst_overprintrendertest.cpp into the core agent-policy path set so PR CI executes the target, and assert catalog presence before dereferencing a missing flatten fixture.