diff --git a/LoopEditor/editorhost.cpp b/LoopEditor/editorhost.cpp index f142644c..ec8f0611 100644 --- a/LoopEditor/editorhost.cpp +++ b/LoopEditor/editorhost.cpp @@ -1064,6 +1064,82 @@ QString EditorHost::previewStaleReason() const return QString(); } +QString EditorHost::previewFidelityStateName() const +{ + return pdfquick::tokens::classifyPreviewFidelityState(hasDocument(), + !previewStaleReason().isEmpty(), + pageFidelityIsAuthoritative(), + pageFidelityIsExact()); +} + +QVariantMap EditorHost::previewFidelityVisual() const +{ + const pdfquick::tokens::LoopStateVisual visual = + pdfquick::tokens::resolvePreviewFidelityStateVisual(previewFidelityStateName()); + + QVariantMap result; + result.insert(QStringLiteral("kind"), pdfquick::tokens::stateKindName(visual.kind)); + result.insert(QStringLiteral("colorRole"), pdfquick::tokens::colorRoleName(visual.colorRole)); + result.insert(QStringLiteral("icon"), pdfquick::tokens::stateIconName(visual.icon)); + result.insert(QStringLiteral("accessibleName"), visual.accessibleName); + return result; +} + +QColor EditorHost::previewFidelityColor() const +{ + const pdfquick::tokens::LoopStateVisual visual = + pdfquick::tokens::resolvePreviewFidelityStateVisual(previewFidelityStateName()); + const pdfquick::tokens::LoopTheme theme = + highContrast() ? pdfquick::tokens::LoopTheme::HighContrast : pdfquick::tokens::LoopTheme::Dark; + return pdfquick::tokens::color(visual.colorRole, theme); +} + +QString EditorHost::previewFidelityOriginName() const +{ + return pdfquick::tokens::previewFidelityOriginName(previewFidelityStateName()); +} + +bool EditorHost::previewRequiresAuthoritative() const +{ + return previewFidelityStateName() == QLatin1String("approximate"); +} + +QString EditorHost::previewFidelitySummary() const +{ + const QString state = previewFidelityStateName(); + + if (state == QLatin1String("unavailable")) + { + return tr("No document is open, so there is no preview to describe."); + } + if (state == QLatin1String("stale")) + { + return tr("Not current evidence: %1").arg(previewStaleReason()); + } + if (state == QLatin1String("authoritative") && !pageFidelityIsExact()) + { + return tr("Fidelity authoritative but still approximate, origin output-preview: this page is " + "rendered with the overprint-accurate compositor, yet its diagnostics report an " + "approximation (%1). Do not read it as an exact render.") + .arg(pageFidelityReason()); + } + if (state == QLatin1String("authoritative")) + { + return tr("Fidelity authoritative, origin output-preview: this page is rendered with the " + "overprint-accurate compositor. Read it with the plates and separations; it does " + "not by itself certify publication safety."); + } + if (state == QLatin1String("approximate")) + { + return tr("Fidelity approximate, origin fast-canvas: overprint is not simulated (%1). These " + "canvas pixels cannot stand as proof of print-safe output; switch this page to the " + "authoritative overprint render before any print claim.") + .arg(pageFidelityReason()); + } + return tr("Fidelity exact, origin fast-canvas: this page reports no overprint-sensitive content. " + "The interactive preview still does not certify print-safe output."); +} + QString EditorHost::productionStateName() const { if (!hasDocument()) @@ -1338,6 +1414,18 @@ void EditorHost::toggleCurrentPageFidelity() bumpPresentation(); } +bool EditorHost::ensureAuthoritativePreview() +{ + if (!hasDocument() || !previewRequiresAuthoritative()) + { + return false; + } + + m_session->surfaces()->setPageAuthoritativeOverprint(currentPage(), true); + bumpPresentation(); + return true; +} + void EditorHost::selectFinding(const QString& findingId) { if (!m_session->revisionSource() || findingId.isEmpty()) diff --git a/LoopEditor/editorhost.h b/LoopEditor/editorhost.h index 5d075833..cb124c07 100644 --- a/LoopEditor/editorhost.h +++ b/LoopEditor/editorhost.h @@ -162,6 +162,12 @@ class EditorHost final : public QObject Q_PROPERTY(QString fixRollbackSummary READ fixRollbackSummary NOTIFY presentationChanged) Q_PROPERTY(QVariantMap previewIdentity READ previewIdentity NOTIFY presentationChanged) Q_PROPERTY(QString previewStaleReason READ previewStaleReason NOTIFY presentationChanged) + Q_PROPERTY(QString previewFidelityStateName READ previewFidelityStateName NOTIFY presentationChanged) + Q_PROPERTY(QVariantMap previewFidelityVisual READ previewFidelityVisual NOTIFY presentationChanged) + Q_PROPERTY(QColor previewFidelityColor READ previewFidelityColor NOTIFY presentationChanged) + Q_PROPERTY(QString previewFidelityOriginName READ previewFidelityOriginName NOTIFY presentationChanged) + Q_PROPERTY(QString previewFidelitySummary READ previewFidelitySummary NOTIFY presentationChanged) + Q_PROPERTY(bool previewRequiresAuthoritative READ previewRequiresAuthoritative NOTIFY presentationChanged) public: enum LoopWorkspace @@ -293,6 +299,22 @@ class EditorHost final : public QObject QVariantMap previewIdentity() const; QString previewStaleReason() const; + /// Render fidelity and origin of the interactive preview (#28), the same fact on the + /// ordinary canvas and in the Production Preview. `previewFidelityStateName` is one of + /// `unavailable`, `stale`, `exact`, `approximate` or `authoritative`, derived by + /// `pdfquick::tokens::classifyPreviewFidelityState()` from the preview state and the + /// current page's render diagnostics; `previewFidelityOriginName` names the render path + /// (`none`, `fast-canvas`, `output-preview`). `previewRequiresAuthoritative` is true for + /// the overprint-sensitive fast render (`approximate`), whose pixels are not proof of + /// print-safe output. The interactive preview never certifies publication safety: only + /// `authoritative` names the output-preview origin, and no state reaches a pass. + QString previewFidelityStateName() const; + QVariantMap previewFidelityVisual() const; + QColor previewFidelityColor() const; + QString previewFidelityOriginName() const; + QString previewFidelitySummary() const; + bool previewRequiresAuthoritative() const; + /// Overprint render fidelity for the currently displayed page (issue #49). /// True (and pageFidelityReason empty) when the page has no overprint /// content, or none is known yet. Separate from the document-wide @@ -364,6 +386,13 @@ class EditorHost final : public QObject /// authoritative overprint-accurate one. Re-renders only that page; /// the document stays open. Q_INVOKABLE void toggleCurrentPageFidelity(); + + /// The explicit switch the preview surfaces call before a page is presented as proof: + /// when the current page is the overprint-sensitive fast render, it moves that page to + /// the authoritative output-preview render and returns true. A page that is already + /// authoritative, is exact, or has no document returns false and is left alone. + Q_INVOKABLE bool ensureAuthoritativePreview(); + Q_INVOKABLE void goToPage(int pageIndex); Q_INVOKABLE void goToOutlinePage(int pageIndex); Q_INVOKABLE void setWorkspace(LoopWorkspace workspace); diff --git a/LoopEditor/qml/CanvasPane.qml b/LoopEditor/qml/CanvasPane.qml index 8b7a1c25..0de392c8 100644 --- a/LoopEditor/qml/CanvasPane.qml +++ b/LoopEditor/qml/CanvasPane.qml @@ -53,11 +53,14 @@ Item { Accessible.description: qsTr("The selected finding's registered evidence mode.") } - // Persistent, non-modal render-fidelity indicator (issue #49). Unlike a - // toast, this stays up for as long as the current page is approximated so - // an operator cannot miss overprinted artwork that will drop out on - // press. Mirrors Main.qml's stateBanner: a Pane + Label status bar, shown - // only when there is something to say. + // Persistent, non-modal render-fidelity indicator (issue #49, #28). Unlike a + // toast, this stays up for as long as the page is not an exact fast render so an operator + // cannot miss overprinted artwork that will drop out on press. It names + // both the fidelity and the render origin (fast canvas path vs the + // authoritative output-preview path) via EditorHost's projection, and lets + // the operator escalate the current page. The words come from + // previewFidelitySummary; QML derives nothing. Mirrors Main.qml's + // stateBanner: a Pane + Label status bar. Pane { id: fidelityBanner objectName: "renderFidelityBanner" @@ -65,11 +68,13 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom padding: 8 - visible: root.host && root.host.hasDocument - && (!root.host.pageFidelityIsExact || root.host.pageFidelityIsAuthoritative) + visible: root.host && root.host.hasDocument && root.host.previewFidelityStateName !== "exact" Accessible.role: Accessible.StatusBar Accessible.name: qsTr("Render fidelity status") + Accessible.description: root.host && root.host.previewFidelityOriginName === "output-preview" + ? qsTr("Authoritative output-preview render") + : qsTr("Fast canvas render") RowLayout { anchors.fill: parent @@ -79,18 +84,7 @@ Item { id: fidelityLabel Layout.fillWidth: true wrapMode: Text.WordWrap - text: { - if (!root.host) { - return "" - } - if (root.host.pageFidelityIsAuthoritative && root.host.pageFidelityIsExact) { - return qsTr("Accurate render") - } - const reason = root.host.pageFidelityReason - return reason.length > 0 - ? qsTr("Approximate render: %1").arg(reason) - : qsTr("Approximate render") - } + text: root.host ? root.host.previewFidelitySummary : "" Accessible.name: qsTr("Render fidelity message") } diff --git a/LoopEditor/qml/ProductionPreviewPane.qml b/LoopEditor/qml/ProductionPreviewPane.qml index d9a68afc..52239eab 100644 --- a/LoopEditor/qml/ProductionPreviewPane.qml +++ b/LoopEditor/qml/ProductionPreviewPane.qml @@ -90,10 +90,22 @@ Pane { wrapMode: Text.WordWrap text: !root.host ? "" - : (root.host.pageFidelityIsExact - ? qsTr("This page is rendered with exact overprint fidelity.") - : qsTr("This page is approximate: %1").arg(root.host.pageFidelityReason)) - Accessible.name: qsTr("Page render fidelity") + : qsTr("Fidelity %1, origin %2.") + .arg(root.host.previewFidelityStateName) + .arg(root.host.previewFidelityOriginName) + Accessible.name: qsTr("Page render fidelity and origin") + } + + // The same fidelity-and-origin fact the ordinary canvas banner shows, on the + // surface where a production/print claim is read. An approximate overprint + // render states that its pixels are not proof of print-safe output. + StateBadge { + objectName: "productionPreviewFidelityBadge" + Layout.fillWidth: true + visual: root.host ? root.host.previewFidelityVisual : null + stateColor: root.host ? root.host.previewFidelityColor : "transparent" + labelText: root.host ? root.host.previewFidelitySummary : "" + badgePrefix: "productionPreviewFidelityBadge" } Label { @@ -107,12 +119,26 @@ Pane { RowLayout { Layout.fillWidth: true + // The explicit switch to the authoritative render the acceptance requires + // before a print claim. Enabled only while the page is still the + // overprint-sensitive fast render. + Button { + objectName: "productionPreviewProveButton" + text: qsTr("Proof this page (authoritative render)") + enabled: root.host && root.host.previewRequiresAuthoritative + onClicked: if (root.host) root.host.ensureAuthoritativePreview() + Accessible.name: qsTr("Switch this page to the authoritative overprint render") + Accessible.description: qsTr("Moves the current page to the output-preview render before any print claim. The document stays open.") + } + Button { objectName: "productionPreviewFidelityToggle" - text: qsTr("Use exact overprint render") + text: root.host && root.host.pageFidelityIsAuthoritative + ? qsTr("Return to the fast preview") + : qsTr("Use exact overprint render") enabled: root.host && root.host.hasDocument onClicked: if (root.host) root.host.toggleCurrentPageFidelity() - Accessible.name: qsTr("Switch this page to the exact overprint render") + Accessible.name: qsTr("Change this page's render fidelity") Accessible.description: qsTr("Re-renders the current page only; the document stays open.") } } diff --git a/LoopLibQuick/sources/loopstatevisual.cpp b/LoopLibQuick/sources/loopstatevisual.cpp index 47d73f52..7ca2a195 100644 --- a/LoopLibQuick/sources/loopstatevisual.cpp +++ b/LoopLibQuick/sources/loopstatevisual.cpp @@ -267,4 +267,76 @@ LoopStateVisual resolveFixLifecycleStateVisual(const QString& stateName) QStringLiteral("No correction planned") }; } +QString classifyPreviewFidelityState(bool hasDocument, bool stale, bool authoritative, bool exact) +{ + // The host owns these four facts; this orders them and names the result. An open + // document whose preview belongs to an earlier revision is stale before it is + // anything else, and an authoritative render is authoritative before it is judged + // on its (ascertained later) diagnostics. + if (!hasDocument) + { + return QStringLiteral("unavailable"); + } + if (stale) + { + return QStringLiteral("stale"); + } + if (authoritative) + { + return QStringLiteral("authoritative"); + } + if (!exact) + { + // The fast canvas path on overprint-sensitive content: overprint is not simulated. + return QStringLiteral("approximate"); + } + return QStringLiteral("exact"); +} + +QString previewFidelityOriginName(const QString& stateName) +{ + const QString normalized = stateName.trimmed().toLower(); + + if (normalized == QLatin1String("authoritative")) + { + return QStringLiteral("output-preview"); + } + if (normalized == QLatin1String("approximate") || normalized == QLatin1String("exact")) + { + return QStringLiteral("fast-canvas"); + } + return QStringLiteral("none"); +} + +LoopStateVisual resolvePreviewFidelityStateVisual(const QString& stateName) +{ + const QString normalized = stateName.trimmed().toLower(); + + if (normalized == QLatin1String("authoritative")) + { + return { StateKind::Info, ColorRole::SeverityInfo, StateIcon::BadgeOverlay, + QStringLiteral("Authoritative output-preview render") }; + } + if (normalized == QLatin1String("approximate")) + { + // The overprint-sensitive fast render: never a pass, never a proof of print-safe output. + return { StateKind::Warning, ColorRole::SeverityWarning, StateIcon::Cross, + QStringLiteral("Approximate render, overprint is not simulated") }; + } + if (normalized == QLatin1String("exact")) + { + return { StateKind::Info, ColorRole::SeverityInfo, StateIcon::FilledSquare, + QStringLiteral("Fast preview path, no overprint-sensitive content") }; + } + if (normalized == QLatin1String("stale")) + { + return { StateKind::Incomplete, ColorRole::StateIncomplete, StateIcon::Hatched, + QStringLiteral("Preview is stale for the current revision") }; + } + + // "unavailable", an empty string and anything unrecognised: no preview to trust. + return { StateKind::NotChecked, ColorRole::StateNotChecked, StateIcon::Outline, + QStringLiteral("Preview fidelity unavailable") }; +} + } // namespace pdfquick::tokens diff --git a/LoopLibQuick/sources/loopstatevisual.h b/LoopLibQuick/sources/loopstatevisual.h index 7069ec55..1a6b1972 100644 --- a/LoopLibQuick/sources/loopstatevisual.h +++ b/LoopLibQuick/sources/loopstatevisual.h @@ -120,6 +120,27 @@ LOOPLIBQUICK_EXPORT LoopStateVisual resolvePreflightStateVisual(const QString& s /// every state carries a shape no other state uses, so the state survives without colour. LOOPLIBQUICK_EXPORT LoopStateVisual resolveFixLifecycleStateVisual(const QString& stateName); +/// Render fidelity and origin of the interactive preview (#28). The state name is derived +/// from four facts the host already holds: whether a document is open, whether the preview +/// is stale for the open revision, whether the current page is showing the authoritative +/// output-preview render, and whether that page's render is diagnostic-exact. +/// +/// `approximate` is the overprint-sensitive page on the fast canvas path: overprint is not +/// simulated there, so those pixels are never proof of print-safe output. `authoritative` +/// is the only state that names the output-preview origin. No preview state reaches a pass +/// treatment: the interactive preview proves a page's render path, never publication safety. +LOOPLIBQUICK_EXPORT QString classifyPreviewFidelityState(bool hasDocument, bool stale, bool authoritative, bool exact); + +/// Which render path produced the preview's pixels, for a state name above: `none`, +/// `fast-canvas` or `output-preview`. Rendering only, no analysis. +LOOPLIBQUICK_EXPORT QString previewFidelityOriginName(const QString& stateName); + +/// Canonical treatment for a preview fidelity state name. Each state takes a shape no other +/// preview state uses, and `approximate` (the fast canvas path on overprint-sensitive +/// content) can never be read as proof of print-safe output: no preview state carries a pass +/// colour or a check. +LOOPLIBQUICK_EXPORT LoopStateVisual resolvePreviewFidelityStateVisual(const QString& stateName); + } // namespace pdfquick::tokens #endif // LOOPSTATEVISUAL_H diff --git a/UnitTests/tst_editorhosttest.cpp b/UnitTests/tst_editorhosttest.cpp index bfadc2b1..e8257d04 100644 --- a/UnitTests/tst_editorhosttest.cpp +++ b/UnitTests/tst_editorhosttest.cpp @@ -257,6 +257,7 @@ private slots: void moveSelectionProposesAPageBoxMoveInTheFixWorkspace(); void fixJourneyPublishesOnlyAnApprovedPlanBoundToTheDisplayedIdentity(); void fixRollbackReturnsToARecordedRevision(); + void previewFidelityNamesTheOriginAndSwitchesExplicitly(); }; void EditorHostTest::teardownClearsTheInteractiveThreadRegistration() @@ -1613,6 +1614,73 @@ void EditorHostTest::actionListFencesCompletionsThatLostTheirRequestIdentity() QVERIFY(!host.fixPlanIdentity().value(QStringLiteral("planDigest")).toString().isEmpty()); } +void EditorHostTest::previewFidelityNamesTheOriginAndSwitchesExplicitly() +{ + // #28 acceptance: the preview names render fidelity AND origin on the ordinary canvas + // and in the Production Preview, an overprint-sensitive page switches to the + // authoritative render explicitly, and approximate canvas pixels are never presented + // as print-safe evidence. + + // A synthetic document with no overprint content: the fast canvas path renders it + // exactly, and the host still refuses to certify it - the interactive preview proves a + // render path, never publication safety. + QTemporaryDir directory; + QVERIFY(directory.isValid()); + + pdf::PDFDocumentBuilder builder; + builder.appendPage(QRectF(0, 0, 612, 792)); + const QString path = directory.filePath(QStringLiteral("preview-fidelity.pdf")); + { + const pdf::PDFDocument document = builder.build(); + pdf::PDFDocumentWriter writer(nullptr); + QVERIFY(writer.write(path, &document, true)); + } + + EditorHost host; + host.openFileUrl(QUrl::fromLocalFile(path)); + QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); + host.setViewportGeometry(96.0 / 25.4, 1.0, 1024, 768); + + QCOMPARE(host.previewFidelityStateName(), QStringLiteral("exact")); + QCOMPARE(host.previewFidelityOriginName(), QStringLiteral("fast-canvas")); + QVERIFY(!host.previewRequiresAuthoritative()); + QVERIFY(!host.ensureAuthoritativePreview()); + QVERIFY(!host.previewFidelitySummary().trimmed().isEmpty()); + + // The explicit configuration switch moves this page to the output-preview origin; the + // projected state and origin follow the coordinator immediately. + host.toggleCurrentPageFidelity(); + QCOMPARE(host.previewFidelityStateName(), QStringLiteral("authoritative")); + QCOMPARE(host.previewFidelityOriginName(), QStringLiteral("output-preview")); + QVERIFY(!host.previewRequiresAuthoritative()); + + // A real overprint fixture: the fast canvas path is the overprint-sensitive + // approximation. The host must say so, must name the fast canvas origin, must expose the + // explicit switch, and must never dress the approximation as a pass. + const QString overprintPath = + preflightFixturesDir() + QStringLiteral("/overprint-cmyk-mode1-on.pdf"); + QVERIFY2(QFileInfo::exists(overprintPath), qPrintable(overprintPath)); + + EditorHost overprintHost; + overprintHost.openFileUrl(QUrl::fromLocalFile(overprintPath)); + QTRY_VERIFY_WITH_TIMEOUT(overprintHost.hasDocument(), 30000); + overprintHost.setViewportGeometry(96.0 / 25.4, 1.0, 1024, 768); + QTRY_VERIFY_WITH_TIMEOUT(overprintHost.previewFidelityStateName() == QStringLiteral("approximate"), 30000); + QCOMPARE(overprintHost.previewFidelityOriginName(), QStringLiteral("fast-canvas")); + QVERIFY(overprintHost.previewRequiresAuthoritative()); + QVERIFY(overprintHost.previewFidelitySummary().contains(QStringLiteral("print-safe output"))); + + const QVariantMap approximate = overprintHost.previewFidelityVisual(); + QVERIFY(approximate.value(QStringLiteral("kind")).toString() != QStringLiteral("Passed")); + QVERIFY(approximate.value(QStringLiteral("colorRole")).toString() != QStringLiteral("Success")); + + QVERIFY(overprintHost.ensureAuthoritativePreview()); + QCOMPARE(overprintHost.previewFidelityStateName(), QStringLiteral("authoritative")); + QCOMPARE(overprintHost.previewFidelityOriginName(), QStringLiteral("output-preview")); + QVERIFY(!overprintHost.previewRequiresAuthoritative()); + QVERIFY(!overprintHost.ensureAuthoritativePreview()); +} + QTEST_GUILESS_MAIN(EditorHostTest) #include "tst_editorhosttest.moc" diff --git a/UnitTests/tst_loopstatevisualtest.cpp b/UnitTests/tst_loopstatevisualtest.cpp index 2d54c752..007f797d 100644 --- a/UnitTests/tst_loopstatevisualtest.cpp +++ b/UnitTests/tst_loopstatevisualtest.cpp @@ -29,6 +29,7 @@ #include #include +using pdfquick::tokens::classifyPreviewFidelityState; using pdfquick::tokens::ColorRole; using pdfquick::tokens::FocusOutlineOffsetPx; using pdfquick::tokens::FocusOutlineWidthPx; @@ -36,7 +37,9 @@ using pdfquick::tokens::LoopStateVisual; using pdfquick::tokens::LoopTheme; using pdfquick::tokens::MinimumKeyboardTargetPx; using pdfquick::tokens::MinimumPointerTargetPx; +using pdfquick::tokens::previewFidelityOriginName; using pdfquick::tokens::resolveFixLifecycleStateVisual; +using pdfquick::tokens::resolvePreviewFidelityStateVisual; using pdfquick::tokens::resolveStateVisual; using pdfquick::tokens::SpaceL; using pdfquick::tokens::SpaceM; @@ -235,6 +238,14 @@ private slots: void fixLifecycleStatesStayDistinctWithoutColour(); void fixLifecycleNeverClaimsAPass_data(); void fixLifecycleNeverClaimsAPass(); + + void previewFidelityClassification_data(); + void previewFidelityClassification(); + void previewFidelityOrigin_data(); + void previewFidelityOrigin(); + void previewFidelityStatesStayDistinctWithoutColour(); + void previewFidelityNeverClaimsAPass_data(); + void previewFidelityNeverClaimsAPass(); }; void LoopStateVisualTest::severityMapping_data() @@ -801,6 +812,101 @@ void LoopStateVisualTest::fixLifecycleNeverClaimsAPass() QVERIFY(visual.colorRole != ColorRole::Success); } +void LoopStateVisualTest::previewFidelityClassification_data() +{ + QTest::addColumn("hasDocument"); + QTest::addColumn("stale"); + QTest::addColumn("authoritative"); + QTest::addColumn("exact"); + QTest::addColumn("expectedName"); + + QTest::newRow("no document") << false << false << false << true << QStringLiteral("unavailable"); + QTest::newRow("stale outranks every render fact") << true << true << true << true << QStringLiteral("stale"); + QTest::newRow("authoritative origin outranks pending diagnostics") << true << false << true << false << QStringLiteral("authoritative"); + QTest::newRow("overprint content on the fast canvas") << true << false << false << false << QStringLiteral("approximate"); + QTest::newRow("fast canvas with no overprint content") << true << false << false << true << QStringLiteral("exact"); +} + +void LoopStateVisualTest::previewFidelityClassification() +{ + QFETCH(bool, hasDocument); + QFETCH(bool, stale); + QFETCH(bool, authoritative); + QFETCH(bool, exact); + QFETCH(QString, expectedName); + + QCOMPARE(classifyPreviewFidelityState(hasDocument, stale, authoritative, exact), expectedName); +} + +void LoopStateVisualTest::previewFidelityOrigin_data() +{ + QTest::addColumn("stateName"); + QTest::addColumn("expectedOrigin"); + + QTest::newRow("authoritative") << QStringLiteral("authoritative") << QStringLiteral("output-preview"); + QTest::newRow("approximate") << QStringLiteral("approximate") << QStringLiteral("fast-canvas"); + QTest::newRow("exact") << QStringLiteral("exact") << QStringLiteral("fast-canvas"); + QTest::newRow("stale") << QStringLiteral("stale") << QStringLiteral("none"); + QTest::newRow("unavailable") << QStringLiteral("unavailable") << QStringLiteral("none"); + QTest::newRow("unrecognised") << QStringLiteral("garbage") << QStringLiteral("none"); +} + +void LoopStateVisualTest::previewFidelityOrigin() +{ + QFETCH(QString, stateName); + QFETCH(QString, expectedOrigin); + + QCOMPARE(previewFidelityOriginName(stateName), expectedOrigin); +} + +/// The fidelity-and-origin states the preview surfaces promise to keep visibly and semantically +/// distinct (#28): each carries its own shape and its own spoken name, so no two states are told +/// apart by colour alone. +void LoopStateVisualTest::previewFidelityStatesStayDistinctWithoutColour() +{ + const QStringList states = { QStringLiteral("unavailable"), QStringLiteral("stale"), + QStringLiteral("exact"), QStringLiteral("approximate"), + QStringLiteral("authoritative") }; + + QSet icons; + QSet names; + for (const QString& state : states) + { + const LoopStateVisual visual = resolvePreviewFidelityStateVisual(state); + icons.insert(static_cast(visual.icon)); + names.insert(visual.accessibleName); + QVERIFY2(!visual.accessibleName.trimmed().isEmpty(), qPrintable(state)); + } + + QCOMPARE(icons.size(), states.size()); + QCOMPARE(names.size(), states.size()); +} + +void LoopStateVisualTest::previewFidelityNeverClaimsAPass_data() +{ + QTest::addColumn("stateName"); + + for (const QString& state : { QStringLiteral("unavailable"), QStringLiteral("stale"), + QStringLiteral("exact"), QStringLiteral("approximate"), + QStringLiteral("authoritative"), QString() }) + { + QTest::newRow(qPrintable(state.isEmpty() ? QStringLiteral("empty") : state)) << state; + } +} + +void LoopStateVisualTest::previewFidelityNeverClaimsAPass() +{ + QFETCH(QString, stateName); + + const LoopStateVisual visual = resolvePreviewFidelityStateVisual(stateName); + + // The interactive preview proves a render path, never publication safety, so no + // fidelity-and-origin state may read as a pass. This is the failure case of #28: + // approximate canvas pixels can never be cited as proof of print-safe output. + QVERIFY(visual.kind != StateKind::Passed); + QVERIFY(visual.colorRole != ColorRole::Success); +} + QTEST_APPLESS_MAIN(LoopStateVisualTest) #include "tst_loopstatevisualtest.moc" diff --git a/UnitTests/tst_pagesurfacetest.cpp b/UnitTests/tst_pagesurfacetest.cpp index e5fcfac7..8d108a43 100644 --- a/UnitTests/tst_pagesurfacetest.cpp +++ b/UnitTests/tst_pagesurfacetest.cpp @@ -364,6 +364,7 @@ private slots: void inexactSurfacesStandInDuringZoom(); void sessionRendererSerializesAndRendersARealPage(); void sessionRendererEscalatesToAuthoritativeOverprintMatchingGoldenBaseline(); + void fastCanvasOverprintRenderIsNotTheAuthoritativeGolden(); }; void PageSurfaceTest::initTestCase() @@ -966,6 +967,60 @@ void PageSurfaceTest::sessionRendererEscalatesToAuthoritativeOverprintMatchingGo "Authoritative canvas escalation does not match the committed overprint-cmyk-mode1-on.png baseline"); } +void PageSurfaceTest::fastCanvasOverprintRenderIsNotTheAuthoritativeGolden() +{ + // #28 failure case, against the same committed baseline the escalation slot above + // trusts: the fast canvas path is the overprint approximation, so its pixels are NOT + // the authoritative output-preview render and cannot be cited as proof of print-safe + // output. Same page, same key fields; the only difference is the authoritative marker. + const QString fixturePath = overprintFixturesDirectory() + QStringLiteral("/overprint-cmyk-mode1-on.pdf"); + pdf::PDFDocumentReader reader(nullptr, [](bool*) + { return QString(); }, true, false); + pdf::PDFDocument document = reader.readFromFile(fixturePath); + QCOMPARE(reader.getReadingResult(), pdf::PDFDocumentReader::Result::OK); + + pdf::PDFDocumentContext context(&document); + pdfinteraction::PDFSessionPageSurfaceRenderer renderer(context); + + const QImage baseline(overprintRendersDirectory() + QStringLiteral("/overprint-cmyk-mode1-on.png")); + QVERIFY2(!baseline.isNull(), "Missing committed baseline overprint-cmyk-mode1-on.png"); + + auto token = std::make_shared(); + pdf::PDFJobContext jobContext(token, pdf::PDFProcessingLimits::conservativeDefaults(), [](int) {}); + + const auto renderWith = [&](const QString& colorOutputIdentity) + { + pdfinteraction::PageSurfaceRequest request; + request.key = pdfinteraction::makePageSurfaceKey(context.getRevision(), + 0, + pdf::PageRotation::None, + pdf::PDFRenderer::getDefaultFeatures(), + colorOutputIdentity, + 1.0, + QSize(128, 128), + 1.0); + request.token = pdfinteraction::RevisionFencedToken{ 1, context.getRevision() }; + return renderer.render(request, jobContext); + }; + + const pdfinteraction::PageSurfaceResult fast = renderWith(QStringLiteral("srgb")); + QCOMPARE(fast.state, pdfinteraction::SurfaceTerminalState::Complete); + QVERIFY(fast.pixels); + // The fast path discloses its approximation rather than claiming exact overprint fidelity. + QVERIFY2(!fast.diagnostics.isExact(), + "The fast canvas path reported an exact overprint render for an overprint fixture"); + + const pdfinteraction::PageSurfaceResult authoritative = + renderWith(pdfinteraction::withAuthoritativeOverprintMarker(QStringLiteral("srgb"))); + QCOMPARE(authoritative.state, pdfinteraction::SurfaceTerminalState::Complete); + QVERIFY(authoritative.pixels); + + QVERIFY2(imagesMatchWithinTolerance(authoritative.pixels->image, baseline), + "Authoritative render does not match the committed overprint-cmyk-mode1-on.png baseline"); + QVERIFY2(!imagesMatchWithinTolerance(fast.pixels->image, baseline), + "The fast canvas render matched the authoritative golden; the approximation is not being disclosed"); +} + QTEST_GUILESS_MAIN(PageSurfaceTest) #include "tst_pagesurfacetest.moc" diff --git a/changes/codex-issue-28-production-preview.evidence.yaml b/changes/codex-issue-28-production-preview.evidence.yaml new file mode 100644 index 00000000..945d89b3 --- /dev/null +++ b/changes/codex-issue-28-production-preview.evidence.yaml @@ -0,0 +1,16 @@ +format_version: 1 +kind: evidence +claims: + - id: preview-fidelity-and-origin-on-both-surfaces + evidence: + - unit:agent-policy:quick + - unit:agent-policy:interaction + - unit:UnitTests/tst_loopstatevisualtest.cpp + - unit:UnitTests/tst_editorhosttest.cpp + - differential:UnitTestsOverprintRender + - id: approximate-canvas-pixels-are-not-the-authoritative-golden + evidence: + - unit:UnitTests/tst_pagesurfacetest.cpp + - differential:UnitTestsOverprintRender +unresolved: + - Only UnitTestsLoopStateVisual, UnitTestsEditorHost and UnitTestsPageSurface were built and run locally; the other mapped quick/interaction suites are proven by the linux-build and windows-build CI lanes, not by this worktree. diff --git a/changes/codex-issue-28-production-preview.md b/changes/codex-issue-28-production-preview.md new file mode 100644 index 00000000..2eebe2a1 --- /dev/null +++ b/changes/codex-issue-28-production-preview.md @@ -0,0 +1,6 @@ +# Separate Production Preview from the fast canvas (#28) + +Category: changed +Audience: prepress operators and reviewers reading render fidelity on the canvas and in the Production Preview +Breaking-Change: no +Summary: The ordinary canvas banner (now hidden for an exact fast-canvas page, with a plain-language accessible description) and the Production Preview now render one shared preview fidelity-and-origin projection. EditorHost exposes previewFidelityStateName / previewFidelityOriginName / previewFidelityVisual / previewFidelityColor / previewFidelitySummary, derived by pdfquick::tokens::classifyPreviewFidelityState from the preview state and the current page's render diagnostics, naming both the fidelity (unavailable / stale / exact / approximate / authoritative) and the render origin (none / fast-canvas / output-preview). An overprint-sensitive page on the fast canvas path is the `approximate` state; previewRequiresAuthoritative() is true exactly then, its summary states that those pixels are not proof of print-safe output, and EditorHost::ensureAuthoritativePreview() is the explicit switch the preview surfaces call before a page is presented as proof. No preview fidelity state resolves to a pass treatment. Proven by UnitTestsLoopStateVisual (classification, origin, distinct shapes, never a pass), UnitTestsEditorHost (the projection and the explicit switch on a real overprint fixture) and UnitTestsPageSurface::fastCanvasOverprintRenderIsNotTheAuthoritativeGolden (the fast render does not match overprint-cmyk-mode1-on.png while the authoritative render does). diff --git a/docs/RENDERER_DIFFERENTIALS.md b/docs/RENDERER_DIFFERENTIALS.md index e0e5f9f8..38ab6630 100644 --- a/docs/RENDERER_DIFFERENTIALS.md +++ b/docs/RENDERER_DIFFERENTIALS.md @@ -44,6 +44,34 @@ asserts that escalated render matches `overprint-cmyk-mode1-on.png`, the same baseline `UnitTestsOverprintRender` checks — so canvas escalation and this measurement renderer are proven to agree, not just independently plausible. +## Fidelity and origin on the two preview surfaces (#28) + +The ordinary canvas banner and the Production Preview both render the same +projection, `EditorHost::previewFidelityStateName()` / +`previewFidelityOriginName()` / `previewFidelityVisual()` / +`previewFidelitySummary()`, so neither surface presents the fast canvas pixels +as the authoritative render. The state vocabulary is `unavailable`, `stale`, +`exact`, `approximate` and `authoritative`; the origin is `none`, `fast-canvas` +or `output-preview`. `approximate` is the overprint-sensitive page on the fast +canvas path, and `previewRequiresAuthoritative()` is true exactly then: its +`previewFidelitySummary()` states that those pixels cannot stand as proof of +print-safe output, and no preview state resolves to a pass treatment. +`EditorHost::ensureAuthoritativePreview()` is the explicit switch the preview +surfaces call before a page is presented as proof; it moves the current page to +the output-preview render and returns false when the page is already +authoritative or exact. + +`UnitTestsPageSurface::fastCanvasOverprintRenderIsNotTheAuthoritativeGolden` +proves the distinction against the same committed baseline: for +`overprint-cmyk-mode1-on.pdf` the marked (authoritative) render matches +`overprint-cmyk-mode1-on.png` while the unmarked fast render does not, and the +fast render's own diagnostics report the approximation rather than exact +overprint fidelity. `UnitTestsEditorHost::previewFidelityNamesTheOriginAndSwitchesExplicitly` +proves the host projection and the explicit switch, and +`UnitTestsLoopStateVisual::previewFidelityNeverClaimsAPass` pins that no +fidelity state reads as a pass. The interactive preview still certifies nothing +about publication safety. + ## Independent measurement scope With `LOOP_INDEPENDENT_RENDER_DIR` set, `UnitTestsOverprintRender` additionally diff --git a/tools/ProductQuickAccessibilitySmoke/qml/CanvasPane.qml b/tools/ProductQuickAccessibilitySmoke/qml/CanvasPane.qml index 8b7a1c25..0de392c8 100644 --- a/tools/ProductQuickAccessibilitySmoke/qml/CanvasPane.qml +++ b/tools/ProductQuickAccessibilitySmoke/qml/CanvasPane.qml @@ -53,11 +53,14 @@ Item { Accessible.description: qsTr("The selected finding's registered evidence mode.") } - // Persistent, non-modal render-fidelity indicator (issue #49). Unlike a - // toast, this stays up for as long as the current page is approximated so - // an operator cannot miss overprinted artwork that will drop out on - // press. Mirrors Main.qml's stateBanner: a Pane + Label status bar, shown - // only when there is something to say. + // Persistent, non-modal render-fidelity indicator (issue #49, #28). Unlike a + // toast, this stays up for as long as the page is not an exact fast render so an operator + // cannot miss overprinted artwork that will drop out on press. It names + // both the fidelity and the render origin (fast canvas path vs the + // authoritative output-preview path) via EditorHost's projection, and lets + // the operator escalate the current page. The words come from + // previewFidelitySummary; QML derives nothing. Mirrors Main.qml's + // stateBanner: a Pane + Label status bar. Pane { id: fidelityBanner objectName: "renderFidelityBanner" @@ -65,11 +68,13 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom padding: 8 - visible: root.host && root.host.hasDocument - && (!root.host.pageFidelityIsExact || root.host.pageFidelityIsAuthoritative) + visible: root.host && root.host.hasDocument && root.host.previewFidelityStateName !== "exact" Accessible.role: Accessible.StatusBar Accessible.name: qsTr("Render fidelity status") + Accessible.description: root.host && root.host.previewFidelityOriginName === "output-preview" + ? qsTr("Authoritative output-preview render") + : qsTr("Fast canvas render") RowLayout { anchors.fill: parent @@ -79,18 +84,7 @@ Item { id: fidelityLabel Layout.fillWidth: true wrapMode: Text.WordWrap - text: { - if (!root.host) { - return "" - } - if (root.host.pageFidelityIsAuthoritative && root.host.pageFidelityIsExact) { - return qsTr("Accurate render") - } - const reason = root.host.pageFidelityReason - return reason.length > 0 - ? qsTr("Approximate render: %1").arg(reason) - : qsTr("Approximate render") - } + text: root.host ? root.host.previewFidelitySummary : "" Accessible.name: qsTr("Render fidelity message") } diff --git a/tools/ProductQuickAccessibilitySmoke/qml/ProductionPreviewPane.qml b/tools/ProductQuickAccessibilitySmoke/qml/ProductionPreviewPane.qml index d9a68afc..52239eab 100644 --- a/tools/ProductQuickAccessibilitySmoke/qml/ProductionPreviewPane.qml +++ b/tools/ProductQuickAccessibilitySmoke/qml/ProductionPreviewPane.qml @@ -90,10 +90,22 @@ Pane { wrapMode: Text.WordWrap text: !root.host ? "" - : (root.host.pageFidelityIsExact - ? qsTr("This page is rendered with exact overprint fidelity.") - : qsTr("This page is approximate: %1").arg(root.host.pageFidelityReason)) - Accessible.name: qsTr("Page render fidelity") + : qsTr("Fidelity %1, origin %2.") + .arg(root.host.previewFidelityStateName) + .arg(root.host.previewFidelityOriginName) + Accessible.name: qsTr("Page render fidelity and origin") + } + + // The same fidelity-and-origin fact the ordinary canvas banner shows, on the + // surface where a production/print claim is read. An approximate overprint + // render states that its pixels are not proof of print-safe output. + StateBadge { + objectName: "productionPreviewFidelityBadge" + Layout.fillWidth: true + visual: root.host ? root.host.previewFidelityVisual : null + stateColor: root.host ? root.host.previewFidelityColor : "transparent" + labelText: root.host ? root.host.previewFidelitySummary : "" + badgePrefix: "productionPreviewFidelityBadge" } Label { @@ -107,12 +119,26 @@ Pane { RowLayout { Layout.fillWidth: true + // The explicit switch to the authoritative render the acceptance requires + // before a print claim. Enabled only while the page is still the + // overprint-sensitive fast render. + Button { + objectName: "productionPreviewProveButton" + text: qsTr("Proof this page (authoritative render)") + enabled: root.host && root.host.previewRequiresAuthoritative + onClicked: if (root.host) root.host.ensureAuthoritativePreview() + Accessible.name: qsTr("Switch this page to the authoritative overprint render") + Accessible.description: qsTr("Moves the current page to the output-preview render before any print claim. The document stays open.") + } + Button { objectName: "productionPreviewFidelityToggle" - text: qsTr("Use exact overprint render") + text: root.host && root.host.pageFidelityIsAuthoritative + ? qsTr("Return to the fast preview") + : qsTr("Use exact overprint render") enabled: root.host && root.host.hasDocument onClicked: if (root.host) root.host.toggleCurrentPageFidelity() - Accessible.name: qsTr("Switch this page to the exact overprint render") + Accessible.name: qsTr("Change this page's render fidelity") Accessible.description: qsTr("Re-renders the current page only; the document stays open.") } }