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
91 changes: 90 additions & 1 deletion UnitTests/tst_overprintrendertest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <QFile>
Expand Down Expand Up @@ -84,12 +92,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();
}
Expand Down Expand Up @@ -127,6 +144,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)));
Expand Down Expand Up @@ -220,6 +262,8 @@ private slots:
void renderSubstituteFont_data();
void renderSubstituteFont();
void rendererDifferentialDoesNotDriftBeyondTolerance();
void flattenThenRender_data();
void flattenThenRender();
};

void OverprintRenderTest::render_data()
Expand Down Expand Up @@ -280,5 +324,50 @@ void OverprintRenderTest::rendererDifferentialDoesNotDriftBeyondTolerance()
compareRender(name + QStringLiteral(".png"), actual, expected);
}

void OverprintRenderTest::flattenThenRender_data()
{
QTest::addColumn<QString>("fixture");
QTest::addColumn<QString>("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(), 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.

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"
1 change: 1 addition & 0 deletions agent-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"UnitTests/tst_jbig2decodertest.cpp",
"UnitTests/tst_lifecycletest.cpp",
"UnitTests/tst_overprinttest.cpp",
"UnitTests/tst_overprintrendertest.cpp",
"UnitTests/tst_pageboxcorpustest.cpp",
"UnitTests/tst_repairdifftest.cpp",
"UnitTests/tst_repairoperationtest.cpp",
Expand Down
4 changes: 4 additions & 0 deletions changes/cursor-transparency-flatten-pixel-golden-9148.md
Original file line number Diff line number Diff line change
@@ -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, 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.
15 changes: 14 additions & 1 deletion docs/RENDERER_DIFFERENTIALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,20 @@ cannot catch silent substitute-font rendering regressions.
- 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
Expand Down
7 changes: 7 additions & 0 deletions docs/TRANSPARENCY_FLATTENING_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 5 additions & 0 deletions loop-preflight/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"width": 128,
"height": 128,
"max_channel_delta_budget": 2,
"differing_pixel_budget": 64
}
Loading