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
271 changes: 269 additions & 2 deletions LoopLibCore/sources/preflightengine.cpp

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions UnitTests/tst_preflightenginetest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include <QJsonObject>
#include <QPainter>
#include <QTemporaryDir>
#include <QTemporaryFile>

#include <algorithm>
#include <vector>
Expand Down Expand Up @@ -83,6 +84,8 @@ private slots:
void fontIntegrity_checkIsRegistered();
void run_fontIntegrity_keepsValidEmbeddedFixtureClean();
void hiddenContent_checksAreRegistered();
void run_hiddenLayers_reportsOcmdScreenPrintDivergence();
void run_hiddenLayers_reportsUnevaluableMembershipAsIncomplete();
void run_offPageContent_detectsMarksOutsideToleratedBox();
void run_includesProfileFixups();
void run_synthesizesAddBleedWhenGapAndNoProfileFixup();
Expand Down Expand Up @@ -910,6 +913,103 @@ void PreflightEngineTest::hiddenContent_checksAreRegistered()
QVERIFY(engine.hasCheck(QStringLiteral("obscured-content")));
}

void PreflightEngineTest::run_hiddenLayers_reportsOcmdScreenPrintDivergence()
{
const QString fixturePath = QStringLiteral(LOOP_PREFLIGHT_SOURCE_DIR "/testdata/fixtures/ocmd-print-divergence.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("Hidden layers") },
{ QStringLiteral("checks"), QJsonArray{
QJsonObject{ { QStringLiteral("id"), QStringLiteral("hidden-layers") },
{ QStringLiteral("severity"), QStringLiteral("warning") } } } }
};

const pdf::PreflightResult result = engine.run(profile);
QVERIFY(result.errors.isEmpty());
QCOMPARE(result.warnings.size(), 5);

QMap<QString, QString> directions; // policy -> "view/print"
for (const pdf::PreflightFinding& finding : result.warnings)
{
QCOMPARE(finding.checkId, QStringLiteral("hidden-layers"));
QCOMPARE(finding.page, 1);
QCOMPARE(finding.evidence.value(QStringLiteral("governor")).toString(), QStringLiteral("ocmd"));
QCOMPARE(finding.evidence.value(QStringLiteral("divergence")).toBool(), true);
QVERIFY(!finding.evidence.value(QStringLiteral("ocg_names")).toArray().isEmpty());
directions.insert(finding.evidence.value(QStringLiteral("policy")).toString(),
finding.evidence.value(QStringLiteral("view_state")).toString() + QLatin1Char('/') + finding.evidence.value(QStringLiteral("print_state")).toString());
}

QCOMPARE(directions.value(QStringLiteral("AllOn")), QStringLiteral("on/off"));
QCOMPARE(directions.value(QStringLiteral("AnyOn")), QStringLiteral("off/on"));
QCOMPARE(directions.value(QStringLiteral("AllOff")), QStringLiteral("off/on"));
QCOMPARE(directions.value(QStringLiteral("AnyOff")), QStringLiteral("off/on"));
QCOMPARE(directions.value(QStringLiteral("VE")), QStringLiteral("off/on"));
}

void PreflightEngineTest::run_hiddenLayers_reportsUnevaluableMembershipAsIncomplete()
{
// The visibility expression names an operator the specification does not define.
const QByteArray content("/OC /M BDC 0 0 0 rg 10 10 20 20 re f EMC\n");
const QList<QByteArray> objects{
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [ 5 0 R ] /D << /BaseState /ON >> >> >>",
"<< /Type /Pages /Kids [ 3 0 R ] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [ 0 0 100 100 ] /Contents 4 0 R "
"/Resources << /Properties << /M 6 0 R >> >> >>",
"<< /Length " + QByteArray::number(content.size()) + " >>\nstream\n" + content + "endstream",
"<< /Type /OCG /Name (Layer) >>",
"<< /Type /OCMD /VE [ /Xor 5 0 R 5 0 R ] >>"
};

QByteArray bytes("%PDF-1.7\n");
QList<int> offsets;
for (int index = 0; index < objects.size(); ++index)
{
offsets.append(bytes.size());
bytes += QByteArray::number(index + 1) + " 0 obj\n" + objects.at(index) + "\nendobj\n";
}
const int xref = bytes.size();
bytes += "xref\n0 " + QByteArray::number(objects.size() + 1) + "\n0000000000 65535 f \n";
for (int offset : offsets)
{
bytes += QByteArray::number(offset).rightJustified(10, '0') + " 00000 n \n";
}
bytes += "trailer\n<< /Size " + QByteArray::number(objects.size() + 1) + " /Root 1 0 R >>\nstartxref\n" +
QByteArray::number(xref) + "\n%%EOF\n";

QTemporaryFile file;
QVERIFY(file.open());
file.write(bytes);
file.flush();

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

pdf::PDFDocumentSession session(&document);
pdf::PreflightEngine engine(&session);
const QJsonObject profile{
{ QStringLiteral("name"), QStringLiteral("Hidden layers") },
{ QStringLiteral("checks"), QJsonArray{
QJsonObject{ { QStringLiteral("id"), QStringLiteral("hidden-layers") },
{ QStringLiteral("severity"), QStringLiteral("warning") } } } }
};

const pdf::PreflightResult result = engine.run(profile);
QVERIFY(!result.pass);
QCOMPARE(result.errors.size(), 1);
QCOMPARE(result.errors.first().evidence.value(QStringLiteral("inspection_complete")).toBool(), false);
}

void PreflightEngineTest::run_offPageContent_detectsMarksOutsideToleratedBox()
{
pdf::PDFDocumentBuilder builder;
Expand Down
29 changes: 29 additions & 0 deletions changes/cc-issue-115-ocmd-membership.evidence.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
format_version: 1
kind: evidence
claims:
- id: ocmd-print-divergence-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: ocmd-membership-print-evaluation
evidence:
- unit:agent-policy:core
- unit:agent-policy:preflight
- 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.
- Alternate optional-content configurations are not evaluated, and an /OC entry on a Form XObject or annotation that is never painted is not inspected.
6 changes: 6 additions & 0 deletions changes/cc-issue-115-ocmd-membership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Evaluate optional-content membership for hidden content (#115)

Category: fixed
Audience: prepress operators and preflight maintainers relying on hidden-layers
Breaking-Change: no
Summary: hidden-layers now evaluates optional-content membership dictionaries (every /P policy and /VE expression) on marked content, Form XObject /OC entries and annotation /OC entries under both the View and Print usages with the default configuration's /AS events, and reports content that is hidden in print or differs between screen and print; a governor it cannot evaluate is reported incomplete instead of clean. Adds the ocmd-print-divergence regression fixture and lands the hidden-layers-ocmd backlog row.
16 changes: 12 additions & 4 deletions docs/generated/preflight-check-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,22 @@
"message",
"check_id",
"evidence.confidence",
"evidence.ocg_name"
"evidence.ocg_name",
"evidence.governor",
"evidence.governor_source",
"evidence.policy",
"evidence.ocg_names",
"evidence.view_state",
"evidence.print_state",
"evidence.divergence",
"evidence.inspection_complete"
],
"families": [
"packaging"
],
"fixups": [],
"limitations": "Does not enumerate every OCMD combination.",
"measures": "Optional content groups that hide print content.",
"limitations": "Evaluates the default configuration under the View and Print usages with its /AS events; alternate configurations and content an /OC entry governs on Form XObjects or annotations that are never painted are not evaluated. A governor that cannot be evaluated is reported incomplete.",
"measures": "Optional content groups and membership dictionaries (/P AnyOn, AllOn, AnyOff, AllOff and /VE) that hide print content, and content whose visibility differs between screen and print.",
"parameters": [
{
"default": 9.0,
Expand All @@ -424,7 +432,7 @@
],
"severity": [
{
"condition": "an optional-content group referenced by print content is not printable by default. The profile's check severity (error|warning|info, default error) replaces it and warning/info route the finding to warnings.",
"condition": "an optional-content group referenced by print content is not printable by default, or a membership dictionary governing marked content, a Form XObject or an annotation is hidden in print or differs between screen and print; an optional-content governor that cannot be evaluated is forced to error with inspection_complete false. The profile's check severity (error|warning|info, default error) replaces the others and warning/info route the finding to warnings.",
"finding_type": "hidden-layers",
"severity": "error"
}
Expand Down
5 changes: 3 additions & 2 deletions docs/generated/preflight-corpus-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@
"corpus_gap": null,
"coverage": "partial",
"finding_fixtures": [
"hidden-layers"
"hidden-layers",
"ocmd-print-divergence"
],
"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 @@ -311,15 +311,15 @@
"state": "landed"
},
{
"closed_by": "#115",
"closed_by": "hidden-layers",
"deferral": null,
"families": [
"packaging"
],
"gap": "the shared hidden-content scan does not enumerate every optional-content membership-dictionary combination, so an unprintable configuration passes clean",
"gap": "membership dictionaries (every /P policy and /VE expressions) on marked content, Form XObjects and annotations were not evaluated for print, so an unprintable configuration passed clean; hidden-layers now evaluates them under the View and Print usages and reports screen-versus-print divergence (filed as #115)",
"id": "hidden-layers-ocmd",
"priority": "P2",
"state": "open"
"state": "landed"
},
{
"closed_by": "#116",
Expand Down
22 changes: 15 additions & 7 deletions docs/preflight-check-catalog-overlay.json
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,8 @@
"fixups": []
},
"hidden-layers": {
"measures": "Optional content groups that hide print content.",
"limitations": "Does not enumerate every OCMD combination.",
"measures": "Optional content groups and membership dictionaries (/P AnyOn, AllOn, AnyOff, AllOff and /VE) that hide print content, and content whose visibility differs between screen and print.",
"limitations": "Evaluates the default configuration under the View and Print usages with its /AS events; alternate configurations and content an /OC entry governs on Form XObjects or annotations that are never painted are not evaluated. A governor that cannot be evaluated is reported incomplete.",
"coverage": "partial",
"families": [
"packaging"
Expand All @@ -568,7 +568,7 @@
{
"finding_type": "hidden-layers",
"severity": "error",
"condition": "an optional-content group referenced by print content is not printable by default. The profile's check severity (error|warning|info, default error) replaces it and warning/info route the finding to warnings."
"condition": "an optional-content group referenced by print content is not printable by default, or a membership dictionary governing marked content, a Form XObject or an annotation is hidden in print or differs between screen and print; an optional-content governor that cannot be evaluated is forced to error with inspection_complete false. The profile's check severity (error|warning|info, default error) replaces the others and warning/info route the finding to warnings."
}
],
"evidence": [
Expand All @@ -578,7 +578,15 @@
"message",
"check_id",
"evidence.confidence",
"evidence.ocg_name"
"evidence.ocg_name",
"evidence.governor",
"evidence.governor_source",
"evidence.policy",
"evidence.ocg_names",
"evidence.view_state",
"evidence.print_state",
"evidence.divergence",
"evidence.inspection_complete"
],
"fixups": []
},
Expand Down Expand Up @@ -1494,12 +1502,12 @@
{
"id": "hidden-layers-ocmd",
"priority": "P2",
"gap": "the shared hidden-content scan does not enumerate every optional-content membership-dictionary combination, so an unprintable configuration passes clean",
"gap": "membership dictionaries (every /P policy and /VE expressions) on marked content, Form XObjects and annotations were not evaluated for print, so an unprintable configuration passed clean; hidden-layers now evaluates them under the View and Print usages and reports screen-versus-print divergence (filed as #115)",
"families": [
"packaging"
],
"state": "open",
"closed_by": "#115",
"state": "landed",
"closed_by": "hidden-layers",
"deferral": null
},
{
Expand Down
9 changes: 9 additions & 0 deletions loop-preflight/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ is observed; it does not claim full compositing equivalence. Off-page content
is tolerated inside the page BleedBox, or inside the configured allowance when
no BleedBox exists. These checks do not register corrective fixups.

`hidden-layers` evaluates every optional-content governor it meets under both
the View and the Print usage, with the default configuration's `/AS` events
applied: groups and membership dictionaries (`/P` AnyOn, AllOn, AnyOff, AllOff
and `/VE` expressions) on marked content, on Form XObject `/OC` entries and on
annotation `/OC` entries. It reports membership hidden in print and content
that differs between screen and print, in either direction, with
`evidence.view_state`, `evidence.print_state` and `evidence.policy`. A governor
that cannot be evaluated is reported with `inspection_complete: false`.

The `transparency-risk` check has no additional parameters; it observes
transparency groups, blend modes, and blend-space crossings.

Expand Down
11 changes: 11 additions & 0 deletions loop-preflight/testdata/fixtures/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,17 @@
"source": "hand-built",
"notes": "#668 corpus gap: optional-content group OFF by default with OC-marked content."
},
{
"id": "ocmd-print-divergence",
"pdf": "ocmd-print-divergence.pdf",
"profile": "testdata/profiles/test-hidden-layers.json",
"expect": {
"pass": true,
"check_ids": ["hidden-layers"]
},
"source": "hand-built",
"notes": "#115 regression: five OCMDs (AllOn, AnyOn, AllOff, AnyOff, VE) over groups with opposite View and Print usage, on Form XObjects and marked content. Screen and print show different content; the scan passed it clean before membership dictionaries were evaluated for both usages."
},
{
"id": "font-integrity-corrupt",
"pdf": "font-integrity-corrupt.pdf",
Expand Down
Binary file not shown.
Loading
Loading