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
6 changes: 4 additions & 2 deletions LoopLibCore/sources/pdfinkcoverageprobe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ PDFInkCoverageProbeResult PDFInkCoverageProbe::probe(const PDFPage* page,
const QSizeF pageSizeMM = page->getRectMM(analysisBox).size();
const qreal pixelAreaMM2 = (pageSizeMM.width() * pageSizeMM.height()) / static_cast<qreal>(totalPixels);
result.overLimitAreaMM2 = static_cast<qreal>(overLimitPixels) * pixelAreaMM2;
result.pixelAreaMM2 = pixelAreaMM2;
result.minResolvableAreaMM2 = pixelAreaMM2 * static_cast<qreal>(PDFInkCoverageProbeSettings::MinResolvablePixels);

struct DeviceRegion
{
Expand Down Expand Up @@ -256,8 +258,8 @@ PDFInkCoverageProbeResult PDFInkCoverageProbe::probe(const PDFPage* page,
}
}

const qreal areaRatio = static_cast<qreal>(region.pixelCount) / static_cast<qreal>(totalPixels);
if (areaRatio < settings.minRegionAreaRatio)
const qreal regionAreaMM2 = static_cast<qreal>(region.pixelCount) * pixelAreaMM2;
if (regionAreaMM2 < settings.minRegionAreaMM2 || region.pixelCount < PDFInkCoverageProbeSettings::MinResolvablePixels)
{
continue;
}
Expand Down
16 changes: 14 additions & 2 deletions LoopLibCore/sources/pdfinkcoverageprobe.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,15 @@ struct LOOPLIBCORESHARED_EXPORT PDFInkCoverageProbeSettings
qreal maxInkCoverage = 3.0;
/// Rasterization resolution for the coverage probe.
int dpi = 150;
/// Regions smaller than this fraction of the page are ignored (antialiasing noise).
qreal minRegionAreaRatio = 0.0005;
/// Smallest over-limit region, in mm^2, that is reported. The floor is physical rather
/// than a fraction of the page: a 0.5 mm x 0.5 mm solid is about the smallest element a
/// press reproduces as a distinct area rather than as dot gain, so it is the same on an
/// A6 card and on an A0 poster.
qreal minRegionAreaMM2 = 0.25;
/// An over-limit region needs at least this many raster pixels to be told apart from
/// antialiasing, so the raster can only vouch for regions of MinResolvablePixels pixels
/// or more. When the floor is smaller than that, the probe result reports it.
static constexpr size_t MinResolvablePixels = 4;
/// Maximum number of regions reported per page; the largest are kept.
int maxRegionsPerPage = 20;
/// Maximum raster pixel count before the probe returns budgetExceeded.
Expand Down Expand Up @@ -81,6 +88,11 @@ struct LOOPLIBCORESHARED_EXPORT PDFInkCoverageProbeResult
PDFRenderDiagnostics diagnostics;
qreal peakInkCoverage = 0.0; // page-wide max TAC
qreal overLimitAreaMM2 = 0.0;
/// Area of one raster pixel in mm^2 at the probe resolution.
qreal pixelAreaMM2 = 0.0;
/// Smallest region area the raster can resolve (MinResolvablePixels pixels). A configured
/// floor below this cannot be honored, so a floor-sized element may go unseen.
qreal minResolvableAreaMM2 = 0.0;
std::vector<PDFInkCoverageRegion> regions; // sorted by areaMM2, descending
};

Expand Down
30 changes: 28 additions & 2 deletions LoopLibCore/sources/preflightengine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2327,7 +2327,7 @@ void runInkCoverageCheck(PDFDocumentSession* session,
QList<PreflightFinding>& warnings)
{
const QString analysisBox = check.restrictions.pageBox.value_or(check.inkCoverageAnalysisBox);
auto emitIncomplete = [&](int pageNumber, const QString& reason, bool budgetExceeded = false)
auto emitIncomplete = [&](int pageNumber, const QString& reason, bool budgetExceeded = false, const QJsonObject& extraEvidence = QJsonObject())
{
PreflightFinding finding;
finding.scope = pageNumber > 0
Expand All @@ -2343,6 +2343,10 @@ void runInkCoverageCheck(PDFDocumentSession* session,
{ QStringLiteral("analysis_box"), analysisBox },
{ QStringLiteral("max_raster_pixels"), check.maxRasterPixels }
};
for (auto it = extraEvidence.begin(); it != extraEvidence.end(); ++it)
{
finding.evidence.insert(it.key(), it.value());
}
finding.message = pageNumber > 0
? PDFTranslationContext::tr("Page %1 skipped: %2").arg(pageNumber).arg(reason)
: PDFTranslationContext::tr("Ink coverage skipped: %1").arg(reason);
Expand All @@ -2365,7 +2369,7 @@ void runInkCoverageCheck(PDFDocumentSession* session,
PDFInkCoverageProbeSettings probeSettings;
probeSettings.maxInkCoverage = check.maxInkPct / 100.0;
probeSettings.dpi = check.probeDpi;
probeSettings.minRegionAreaRatio = check.minRegionAreaPct / 100.0;
probeSettings.minRegionAreaMM2 = check.minRegionAreaMm2;
probeSettings.maxRegionsPerPage = check.maxRegionsPerPage;
probeSettings.maxRasterPixels = check.maxRasterPixels;
if (analysisBox == QStringLiteral("trim"))
Expand Down Expand Up @@ -2423,6 +2427,21 @@ void runInkCoverageCheck(PDFDocumentSession* session,
continue;
}

if (result.overLimitAreaMM2 > 0.0 && check.minRegionAreaMm2 < result.minResolvableAreaMM2)
{
emitIncomplete(int(pageIndex + 1),
PDFTranslationContext::tr("probe_dpi %1 cannot resolve min_region_area_mm2 %2 (the raster resolves %3 mm^2); raise probe_dpi")
.arg(check.probeDpi)
.arg(check.minRegionAreaMm2)
.arg(result.minResolvableAreaMM2),
false,
QJsonObject{
{ QStringLiteral("min_region_area_mm2"), check.minRegionAreaMm2 },
{ QStringLiteral("min_resolvable_area_mm2"), result.minResolvableAreaMM2 },
{ QStringLiteral("pixel_area_mm2"), result.pixelAreaMM2 },
{ QStringLiteral("probe_dpi"), check.probeDpi } });
}

int regionRank = 0;
for (const PDFInkCoverageRegion& region : result.regions)
{
Expand Down Expand Up @@ -7124,6 +7143,8 @@ bool PreflightEngine::parseProfile(const QJsonObject& profileObject, PreflightPr
const QJsonValue maxRasterPixelsValue = checkObject.value(QStringLiteral("max_raster_pixels"));
check.maxInkPct = maxInkValue.toDouble(0.0);
check.minRegionAreaPct = minRegionAreaValue.toDouble(0.05);
const QJsonValue minRegionAreaMm2Value = checkObject.value(QStringLiteral("min_region_area_mm2"));
check.minRegionAreaMm2 = minRegionAreaMm2Value.toDouble(0.25);
check.maxRegionsPerPage = maxRegionsValue.toInt(20);
check.maxRasterPixels = 250LL * 1000 * 1000;
if (check.id == QStringLiteral("ink-coverage") && (!maxInkValue.isDouble() || !std::isfinite(check.maxInkPct) || check.maxInkPct <= 0.0))
Expand All @@ -7144,6 +7165,11 @@ bool PreflightEngine::parseProfile(const QJsonObject& profileObject, PreflightPr
errorMessage = PDFTranslationContext::tr("Check '%1' requires min_region_area_pct between 0 and 100.").arg(check.id);
return false;
}
if (checkObject.contains(QStringLiteral("min_region_area_mm2")) && (!minRegionAreaMm2Value.isDouble() || !std::isfinite(check.minRegionAreaMm2) || check.minRegionAreaMm2 < 0.0))
{
errorMessage = PDFTranslationContext::tr("Check '%1' requires non-negative min_region_area_mm2.").arg(check.id);
return false;
}
if (checkObject.contains(QStringLiteral("max_regions_per_page")) && (!maxRegionsValue.isDouble() || std::floor(maxRegionsValue.toDouble()) != maxRegionsValue.toDouble() || check.maxRegionsPerPage < 0))
{
errorMessage = PDFTranslationContext::tr("Check '%1' requires non-negative integral max_regions_per_page.").arg(check.id);
Expand Down
2 changes: 2 additions & 0 deletions LoopLibCore/sources/preflightengine.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ struct LOOPLIBCORESHARED_EXPORT PreflightCheckConfig

// ink-coverage parameters.
qreal maxInkPct = 0.0;
/// Deprecated and no longer a suppression floor; accepted so existing profiles still load.
qreal minRegionAreaPct = 0.05;
qreal minRegionAreaMm2 = 0.25;
int maxRegionsPerPage = 20;
qint64 maxRasterPixels = 250LL * 1000 * 1000;
QString inkCoverageAnalysisBox = QStringLiteral("bleed");
Expand Down
81 changes: 81 additions & 0 deletions UnitTests/tst_preflightenginetest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ private slots:
void run_inkCoverage_emitsRegionalWarningForOverLimitFixture();
void run_inkCoverage_passesBelowLimitFixture();
void run_inkCoverage_budgetAbortIsIncomplete();
void run_inkCoverage_reportsIsolatedOverLimitRegionOnLargePage();
void run_inkCoverage_reportsFloorFinerThanProbeResolutionAsIncomplete();
void inkCoverageProbe_usesAnalysisBoxAndReportsBudget();
void run_downsampleFixupAdvertisedForHighDpiImage();
void run_imageResolutionBBoxMatchesCtmPlacement();
Expand Down Expand Up @@ -495,6 +497,8 @@ void PreflightEngineTest::parseProfile_rejectsInvalidInkCoverageParameters()
{ QStringLiteral("min_region_area_pct"), QJsonValue(-1) },
{ QStringLiteral("min_region_area_pct"), QJsonValue(101) },
{ QStringLiteral("min_region_area_pct"), QJsonValue(QStringLiteral("0.05")) },
{ QStringLiteral("min_region_area_mm2"), QJsonValue(-1) },
{ QStringLiteral("min_region_area_mm2"), QJsonValue(QStringLiteral("0.25")) },
{ QStringLiteral("max_regions_per_page"), QJsonValue(1.5) },
{ QStringLiteral("max_regions_per_page"), QJsonValue(QStringLiteral("20")) },
{ QStringLiteral("max_raster_pixels"), QJsonValue(0) },
Expand Down Expand Up @@ -1712,6 +1716,83 @@ void PreflightEngineTest::run_inkCoverage_budgetAbortIsIncomplete()
QVERIFY(!pdf::preflightAllowsCertification(result));
}

void PreflightEngineTest::run_inkCoverage_reportsIsolatedOverLimitRegionOnLargePage()
{
const QString fixturePath = QStringLiteral(LOOP_PREFLIGHT_SOURCE_DIR "/testdata/fixtures/ink-coverage-isolated-region.pdf");
QVERIFY(QFile::exists(fixturePath));

pdf::PDFDocumentReader reader(nullptr, [](bool*)
{ return QString(); }, true, false);
pdf::PDFDocument document = reader.readFromFile(fixturePath);
QCOMPARE(reader.getReadingResult(), pdf::PDFDocumentReader::Result::OK);

pdf::PDFDocumentSession session(&document);
pdf::PreflightEngine engine(&session);

const QJsonObject profile{
{ QStringLiteral("name"), QStringLiteral("Ink coverage isolated region") },
{ QStringLiteral("checks"), QJsonArray{
QJsonObject{
{ QStringLiteral("id"), QStringLiteral("ink-coverage") },
{ QStringLiteral("max_ink_pct"), 300 },
{ QStringLiteral("probe_dpi"), 150 },
{ QStringLiteral("severity"), QStringLiteral("warning") } } } }
};

const pdf::PreflightResult result = engine.run(profile);
QVERIFY(result.inspectionComplete);
QCOMPARE(result.warnings.size(), 1);
QCOMPARE(result.warnings.first().type, QStringLiteral("ink-coverage"));
const double areaMm2 = result.warnings.first().evidence.value(QStringLiteral("area_mm2")).toDouble();
QVERIFY2(areaMm2 > 25.0 && areaMm2 < 50.0, qPrintable(QString::number(areaMm2)));
QVERIFY(result.warnings.first().evidence.value(QStringLiteral("peak_ink_pct")).toDouble() > 390.0);

// The deprecated page-percentage floor no longer suppresses the region.
QJsonObject legacyCheck = profile.value(QStringLiteral("checks")).toArray().first().toObject();
legacyCheck.insert(QStringLiteral("min_region_area_pct"), 5.0);
const QJsonObject legacyProfile{ { QStringLiteral("name"), QStringLiteral("Ink coverage legacy floor") },
{ QStringLiteral("checks"), QJsonArray{ legacyCheck } } };
QCOMPARE(engine.run(legacyProfile).warnings.size(), 1);
}

void PreflightEngineTest::run_inkCoverage_reportsFloorFinerThanProbeResolutionAsIncomplete()
{
const QString fixturePath = QStringLiteral(LOOP_PREFLIGHT_SOURCE_DIR "/testdata/fixtures/ink-coverage-isolated-region.pdf");
QVERIFY(QFile::exists(fixturePath));

pdf::PDFDocumentReader reader(nullptr, [](bool*)
{ return QString(); }, true, false);
pdf::PDFDocument document = reader.readFromFile(fixturePath);
QCOMPARE(reader.getReadingResult(), pdf::PDFDocumentReader::Result::OK);

pdf::PDFDocumentSession session(&document);
pdf::PreflightEngine engine(&session);

// At 20 dpi one pixel is about 1.6 mm^2, so a 0.25 mm^2 floor cannot be honored.
const QJsonObject profile{
{ QStringLiteral("name"), QStringLiteral("Ink coverage coarse probe") },
{ QStringLiteral("checks"), QJsonArray{
QJsonObject{
{ QStringLiteral("id"), QStringLiteral("ink-coverage") },
{ QStringLiteral("max_ink_pct"), 300 },
{ QStringLiteral("probe_dpi"), 20 },
{ QStringLiteral("severity"), QStringLiteral("warning") } } } }
};

const pdf::PreflightResult result = engine.run(profile);
QVERIFY(!result.pass);
bool foundIncomplete = false;
for (const pdf::PreflightFinding& finding : result.warnings)
{
if (finding.type == QStringLiteral("check-incomplete"))
{
foundIncomplete = true;
QVERIFY(finding.evidence.value(QStringLiteral("min_resolvable_area_mm2")).toDouble() > 0.25);
}
}
QVERIFY(foundIncomplete);
}

void PreflightEngineTest::inkCoverageProbe_usesAnalysisBoxAndReportsBudget()
{
pdf::PDFDocumentBuilder builder;
Expand Down
30 changes: 30 additions & 0 deletions changes/cc-issue-116-ink-coverage-floor.evidence.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
format_version: 1
kind: evidence
claims:
- id: ink-isolated-region-regression-fixture
evidence:
- integration:UnitTestsPreflightCorpus
- architecture:loop-preflight/testdata/fixtures
- architecture:docs/generated/preflight-check-catalog.json
- architecture:docs/generated/architecture-catalog.json
- architecture:docs/generated/preflight-corpus-coverage.json
- id: ink-coverage-physical-area-floor
evidence:
- unit:agent-policy:core
- unit:agent-policy:preflight
- unit:agent-policy:schemas_contracts
- unit:UnitTestsPreflightChecks
- unit:UnitTestsPreflightEngine
- unit:UnitTestsPreflightInteraction
- unit:UnitTestsPreflightVerdict
- unit:UnitTestsPreflightProfileResolver
- unit:UnitTestsProfileIdentity
- unit:UnitTestsOperatorAcceptance
- integration:UnitTestsPreflightWorkflowAcceptance
- differential:UnitTestsStandardOracle
- differential:UnitTestsConversionOracle
- security:scripts/ci/check_source_integrity.py
unresolved:
- core:scripts/ci/check_independent_validation_gate.py
- Only UnitTestsPreflightEngine and UnitTestsPreflightCorpus were built and run locally; the other listed unit, integration and differential lanes are proven by the linux-build and windows-build CI lanes, not by this worktree.
- Residual limits - a floor finer than four raster pixels at probe_dpi reports incomplete; regions below the floor stay suppressed by design, and the probe is not a plate-ready separation TAC.
6 changes: 6 additions & 0 deletions changes/cc-issue-116-ink-coverage-floor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Report isolated over-limit ink regions (#116)

Category: fixed
Audience: prepress operators and preflight maintainers relying on ink-coverage
Breaking-Change: no
Summary: ink-coverage now reports every over-limit region above a physical floor, min_region_area_mm2 (default 0.25 mm^2), instead of suppressing regions below a percentage of the page, so a small rich-black element on a large page no longer passes clean; a probe_dpi too coarse to resolve the floor reports the page incomplete. min_region_area_pct is deprecated and ignored. Adds the ink-coverage-isolated-region regression fixture and lands the ink-coverage-raster-tac backlog row.
13 changes: 10 additions & 3 deletions docs/generated/preflight-check-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -522,10 +522,17 @@
"range": "> 0 (integer)",
"type": "integer"
},
{
"default": 0.25,
"id": "min_region_area_mm2",
"meaning": "Smallest over-limit connected region, in mm^2 of physical area, that is reported. The default is about a 0.5 mm square, the smallest solid a press reproduces as a distinct area. A floor finer than four raster pixels at probe_dpi cannot be honored and reports the page incomplete.",
"range": ">= 0",
"type": "number"
},
{
"default": 0.05,
"id": "min_region_area_pct",
"meaning": "Smallest over-limit connected region, as a percentage of the page, that is reported.",
"meaning": "Deprecated and ignored; a page-relative floor let an isolated over-limit element pass clean on a large page. Use min_region_area_mm2.",
"range": "0..100",
"type": "number"
},
Expand Down Expand Up @@ -560,12 +567,12 @@
],
"severity": [
{
"condition": "a connected region exceeds max_ink_pct over more than min_region_area_pct of the page. The profile's check severity (error|warning|info, default error) replaces it and warning/info route the finding to warnings.",
"condition": "a connected region of at least min_region_area_mm2 exceeds max_ink_pct. The profile's check severity (error|warning|info, default error) replaces it and warning/info route the finding to warnings.",
"finding_type": "ink-coverage",
"severity": "error"
},
{
"condition": "the page raster exceeded max_raster_pixels, was unavailable, or reported non-exact fidelity, so the page is reported incomplete rather than clean.",
"condition": "the page raster exceeded max_raster_pixels, was unavailable, reported non-exact fidelity, or probe_dpi is too coarse to resolve min_region_area_mm2, so the page is reported incomplete rather than clean.",
"finding_type": "check-incomplete",
"severity": "info"
}
Expand Down
3 changes: 2 additions & 1 deletion docs/generated/preflight-corpus-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"corpus_gap": null,
"coverage": "partial",
"finding_fixtures": [
"ink-coverage-isolated-region",
"ink-coverage-over"
],
"uninspected_fixtures": []
Expand Down Expand Up @@ -272,6 +273,6 @@
}
},
"snapshot_dir": "loop-preflight/testdata/snapshots",
"source_fixtures": 76,
"source_fixtures": 77,
"unattributed_findings": 1
}
6 changes: 3 additions & 3 deletions docs/generated/preflight-coverage-backlog.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,17 +322,17 @@
"state": "open"
},
{
"closed_by": "#116",
"closed_by": "ink-coverage",
"deferral": null,
"families": [
"sheetfed-offset",
"web-offset",
"newspaper"
],
"gap": "ink-coverage is a raster probe that suppresses regions below min_region_area_pct, so an isolated over-limit element passes clean",
"gap": "ink-coverage suppressed regions below min_region_area_pct, a fraction of the page, so an isolated over-limit element passed clean on a large page; the floor is now physical (min_region_area_mm2) and a probe_dpi too coarse to resolve it reports the page incomplete (filed as #116)",
"id": "ink-coverage-raster-tac",
"priority": "P2",
"state": "open"
"state": "landed"
},
{
"closed_by": "#117",
Expand Down
Loading
Loading