From 095927b085a19460efe3200c1a57e9fe0dd3bf94 Mon Sep 17 00:00:00 2001 From: mberrys <212453881+mberrys@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:33:33 -0700 Subject: [PATCH 1/7] Add authoritative Quick print-to-PDF and document exports --- LoopEditor/CMakeLists.txt | 2 + LoopEditor/editorhost.cpp | 204 +++++++++++++++++ LoopEditor/editorhost.h | 22 ++ LoopEditor/qml/Main.qml | 107 +++++++++ LoopEditor/qml/MenuModel.qml | 2 + LoopEditor/quickoutput.cpp | 205 ++++++++++++++++++ LoopEditor/quickoutput.h | 44 ++++ .../sources/actionlistrunsubmitter.cpp | 4 + .../sources/actionlistrunsubmitter.h | 1 + UnitTests/phase4-tests.cmake | 10 + UnitTests/tst_editorhosttest.cpp | 187 ++++++++++++++++ agent-policy.json | 2 + ...issue-123-quick-print-export.evidence.yaml | 18 ++ changes/feat-issue-123-quick-print-export.md | 4 + docs/LOOP_SHELL_CONTRACT.md | 2 +- docs/QUICK_PDF_PARITY.md | 6 +- docs/QUICK_PRINT_EXPORT.md | 49 +++++ docs/loop-shell-actions.json | 75 ++++++- scripts/verify-command-catalog.py | 2 + .../CMakeLists.txt | 2 + .../qml/Main.qml | 107 +++++++++ .../qml/MenuModel.qml | 2 + 22 files changed, 1050 insertions(+), 7 deletions(-) create mode 100644 LoopEditor/quickoutput.cpp create mode 100644 LoopEditor/quickoutput.h create mode 100644 changes/feat-issue-123-quick-print-export.evidence.yaml create mode 100644 changes/feat-issue-123-quick-print-export.md create mode 100644 docs/QUICK_PRINT_EXPORT.md diff --git a/LoopEditor/CMakeLists.txt b/LoopEditor/CMakeLists.txt index c1a4d85d9..b160446e3 100644 --- a/LoopEditor/CMakeLists.txt +++ b/LoopEditor/CMakeLists.txt @@ -31,6 +31,8 @@ qt_add_executable(LoopEditor documentviewsession.cpp documentviewsession.h editorhost.cpp + quickoutput.cpp + quickoutput.h editorhost.h focusrestoration.cpp focusrestoration.h diff --git a/LoopEditor/editorhost.cpp b/LoopEditor/editorhost.cpp index f335a51fe..0da2988cf 100644 --- a/LoopEditor/editorhost.cpp +++ b/LoopEditor/editorhost.cpp @@ -416,6 +416,7 @@ EditorHost::EditorHost(QObject* parent) : finishRollbackJob(snapshot); finishPreflightJob(snapshot); finishActionListJob(snapshot); + finishDocumentOutput(snapshot); refreshCanvasTrace(); }); } @@ -426,6 +427,8 @@ EditorHost::~EditorHost() cancelPreflight(); cancelActionList(); QObject::disconnect(&m_session->scheduler(), nullptr, this, nullptr); + if (outputBusy()) + m_session->scheduler().cancel(m_outputJobId); unbindCanvas(); // The guard registration is global process state owned by the thread that @@ -3348,6 +3351,161 @@ void EditorHost::reopenDocument() } } +QString EditorHost::outputRevision() const +{ + return m_session->facade().currentRevision().toString(); +} + +bool EditorHost::exportDocumentFileUrl(const QUrl& url, const QString& format, int dpi, + const QString& revision, bool fidelityAcknowledged) +{ + if (!url.isValid() || !url.isLocalFile()) + return false; + return invokeCommand(format == QLatin1String("print-pdf") ? QStringLiteral("actionPrint") : QStringLiteral("actionExportDocument"), + { { QStringLiteral("path"), url.toLocalFile() }, { QStringLiteral("format"), format }, { QStringLiteral("dpi"), dpi }, { QStringLiteral("revision"), revision }, { QStringLiteral("fidelityAcknowledged"), fidelityAcknowledged } }) != pdfinteraction::InvalidCommandInvocation; +} + +void EditorHost::beginDocumentOutput(pdfinteraction::CommandInvocationId invocation, const QVariantMap& parameters) +{ + const auto refuse = [this, invocation](const QString& reason) + { + m_outputRecord = { { QStringLiteral("error"), reason }, { QStringLiteral("printProof"), false } }; + m_session->catalog().finishInvocation(invocation, pdfinteraction::CommandTerminalState::Failed, reason); + announceDocumentState(reason); + bumpPresentation(); + Q_EMIT documentOutputFinished(); + }; + if (!hasDocument() || outputBusy() || m_session->facade().operation().pending || + parameters.value(QStringLiteral("revision")).toString() != outputRevision()) + { + refuse(QStringLiteral("output/stale-or-busy-document")); + return; + } + loopeditor::OutputRequest request; + const QString format = parameters.value(QStringLiteral("format")).toString(); + if (format == QLatin1String("pdf")) + request.format = loopeditor::OutputFormat::PublishedPdf; + else if (format == QLatin1String("print-pdf")) + request.format = loopeditor::OutputFormat::PrintPdf; + else if (format == QLatin1String("png")) + request.format = loopeditor::OutputFormat::Png; + else if (format == QLatin1String("tiff")) + request.format = loopeditor::OutputFormat::Tiff; + else + { + refuse(QStringLiteral("output/invalid-format")); + return; + } + const bool printing = request.format == loopeditor::OutputFormat::PrintPdf; + request.dpi = parameters.value(QStringLiteral("dpi"), 300).toInt(); + if ((printing && (!m_documentModel.canPrint() || (!m_documentModel.canHighResolutionPrint() && request.dpi > 150))) || + (!printing && !m_documentModel.canCopy())) + { + refuse(QStringLiteral("output/permission-restricted")); + return; + } + request.fidelityAcknowledged = parameters.value(QStringLiteral("fidelityAcknowledged")).toBool(); + if (!pageFidelityIsExact() && !request.fidelityAcknowledged) + { + refuse(QStringLiteral("output/fidelity-acknowledgment-required")); + return; + } + request.revision = outputRevision(); + request.document = m_session->context().getDocumentPointer(); + request.sourcePath = m_session->facade().source().path; + request.page = currentPage(); + if (m_publicationRevision == request.revision) + { + request.publishedBytes = m_publishedOutputBytes; + request.publicationReceipt = m_publicationReceipt; + } + if (request.format == loopeditor::OutputFormat::PublishedPdf && request.publishedBytes.isEmpty() && + m_session->facade().facets().testFlag(pdfinteraction::DocumentFacet::Dirty)) + { + refuse(QStringLiteral("output/unpublished-revision")); + return; + } + const QString destination = parameters.value(QStringLiteral("path")).toString(); + const QFileInfo target(destination), source(request.sourcePath); + if (destination.isEmpty() || target.isDir() || + target.absoluteFilePath() == source.absoluteFilePath() || + (target.exists() && target.canonicalFilePath() == source.canonicalFilePath())) + { + refuse(QStringLiteral("output/invalid-or-source-destination")); + return; + } + m_outputRevision = request.revision; + m_outputDestination = destination; + m_outputInvocation = invocation; + m_outputCancelled = false; + m_outputJobId = QUuid::createUuid().toString(QUuid::WithoutBraces); + m_outputRecord = { { QStringLiteral("status"), QStringLiteral("preparing") } }; + m_outputResult = std::make_shared(); + pdf::PDFJobSpec spec; + spec.jobId = m_outputJobId; + spec.kind = pdf::PDFJobKind::Export; + spec.priority = pdf::PDFJobPriority::Operator; + spec.documentKey = m_session->revisionSource()->documentKey(); + spec.documentRevision = m_outputRevision; + spec.operationId = QStringLiteral("quick-output.%1").arg(format); + const auto result = m_outputResult; + m_session->scheduler().submit(spec, [request, result](pdf::PDFJobContext& job) + { loopeditor::prepareOutput(request, *result, job); }); + refreshFeatureAvailability(); + bumpPresentation(); +} + +void EditorHost::finishDocumentOutput(const pdf::PDFJobSnapshot& snapshot) +{ + if (snapshot.jobId != m_outputJobId || !m_outputResult) + return; + QString error = m_outputResult->error; + if (m_outputCancelled || snapshot.status != pdf::PDFJobStatus::Succeeded || !hasDocument() || + snapshot.kind != pdf::PDFJobKind::Export || snapshot.documentKey != m_session->revisionSource()->documentKey() || + snapshot.documentRevision != outputRevision() || snapshot.documentRevision != m_outputRevision || + !m_session->catalog().isPending(m_outputInvocation)) + { + error = QStringLiteral("output/cancelled-stale-or-failed"); + } + if (error.isEmpty()) + { + QFile staged(m_outputResult->stagedPath); + if (!staged.open(QIODevice::ReadOnly)) + error = QStringLiteral("output/staging-read-failed"); + else + { + const QString expectedDigest = snapshot.outputArtifact.sha256; + const auto written = pdf::PDFSafeFileWriter::writeDevice(m_outputDestination, [&staged, expectedDigest](QIODevice* output) + { + QCryptographicHash digest(QCryptographicHash::Sha256); + while (!staged.atEnd()) + { + const QByteArray chunk = staged.read(65536); + if (chunk.isEmpty() || output->write(chunk) != chunk.size()) return false; + digest.addData(chunk); + } + return staged.error() == QFile::NoError && QString::fromLatin1(digest.result().toHex()) == expectedDigest; }, pdf::PDFSafeFileWriter::OverwritePolicy::Overwrite); + if (!written) + error = QStringLiteral("output/commit-failed"); + } + } + const bool ok = error.isEmpty(); + m_outputRecord = m_outputResult->record.toVariantMap(); + m_outputRecord.insert(QStringLiteral("status"), ok ? QStringLiteral("saved") : QStringLiteral("failed")); + m_outputRecord.insert(QStringLiteral("error"), error); + m_outputRecord.insert(QStringLiteral("printProof"), false); + m_session->catalog().finishInvocation(m_outputInvocation, ok ? pdfinteraction::CommandTerminalState::Completed : snapshot.status == pdf::PDFJobStatus::Cancelled ? pdfinteraction::CommandTerminalState::Cancelled + : pdfinteraction::CommandTerminalState::Failed, + error); + m_outputJobId.clear(); + m_outputResult.reset(); + m_outputInvocation = pdfinteraction::InvalidCommandInvocation; + refreshFeatureAvailability(); + announceDocumentState(ok ? tr("Output saved. Artifact %1. This is not print proof.").arg(snapshot.outputArtifact.sha256.left(12)) : error); + bumpPresentation(); + Q_EMIT documentOutputFinished(); +} + void EditorHost::cancelPendingOperation() { if (m_session->facade().cancelPendingOperation()) @@ -3553,6 +3711,32 @@ void EditorHost::registerShellHandlers() void EditorHost::registerFeatureHandlers() { + for (const QString& id : { QStringLiteral("actionPrint"), QStringLiteral("actionExportDocument") }) + { + pdfinteraction::CommandCatalog::Handler output; + output.invoke = [this, id](pdfinteraction::CommandInvocationId invocation, const QVariantMap& parameters) + { + if (parameters.isEmpty()) + { + Q_EMIT documentOutputRequested(id == QLatin1String("actionPrint")); + m_session->catalog().finishInvocation(invocation, pdfinteraction::CommandTerminalState::Completed); + return; + } + QVariantMap request = parameters; + if (id == QLatin1String("actionPrint")) + request.insert(QStringLiteral("format"), QStringLiteral("print-pdf")); + beginDocumentOutput(invocation, request); + }; + output.cancel = [this](pdfinteraction::CommandInvocationId invocation) + { + if (invocation == m_outputInvocation) + { + m_outputCancelled = true; + m_session->scheduler().cancel(m_outputJobId); + } + }; + m_session->catalog().setHandler(id, std::move(output)); + } auto bind = [this](const QString& id, std::function action) { pdfinteraction::CommandCatalog::Handler handler; @@ -3616,6 +3800,8 @@ void EditorHost::refreshFeatureAvailability() availability.insert(QStringLiteral("actionFindPrevious"), hasSearchResults); availability.insert(QStringLiteral("actionFullscreenMode"), true); availability.insert(MoveSelectionCommandId, ready && m_session->facade().permissions().allowsCorrection()); + availability.insert(QStringLiteral("actionPrint"), ready && !outputBusy() && m_documentModel.canPrint()); + availability.insert(QStringLiteral("actionExportDocument"), ready && !outputBusy() && m_documentModel.canCopy()); m_session->catalog().setEnabledBatch(availability); } @@ -3713,6 +3899,12 @@ void EditorHost::syncDocumentLifecycle() void EditorHost::onDocumentGone() { + if (outputBusy()) + m_session->scheduler().cancel(m_outputJobId); + m_publishedOutputBytes.clear(); + m_publicationReceipt = {}; + m_publicationRevision.clear(); + m_outputRecord.clear(); cancelPreflight(); cancelActionList(); if (m_findingNavigator) @@ -3961,6 +4153,10 @@ void EditorHost::finishActionListJob(const pdf::PDFJobSnapshot& snapshot) m_actionListController.acceptExecution(snapshot.jobId, snapshot.documentRevision, outcome->executionResult)) { m_session->context().setDocument(outcome->candidate); + m_session->scheduler().setCurrentRevision(m_session->revisionSource()->documentKey(), m_session->facade().currentRevision().toString()); + m_publishedOutputBytes = outcome->publishedBytes; + m_publicationReceipt = outcome->executionResult.governed.value(QStringLiteral("receipt")).toObject(); + m_publicationRevision = m_session->facade().currentRevision().toString(); m_preflight.markProfileStale(); syncRevisionModels(); announceDocumentState(tr("Action List applied to the open document.")); @@ -4116,6 +4312,14 @@ void EditorHost::syncRevisionModels() const QString documentKey = m_session->revisionSource()->documentKey(); const QString documentRevision = m_session->facade().currentRevision().toString(); m_preflight.setCurrentRevision(documentKey, documentRevision); + if (outputBusy() && documentRevision != m_outputRevision) + m_session->scheduler().cancel(m_outputJobId); + if (documentRevision != m_publicationRevision) + { + m_publishedOutputBytes.clear(); + m_publicationReceipt = {}; + m_publicationRevision.clear(); + } m_actionListController.setCurrentRevision(documentKey, documentRevision); m_inspector.setCurrentRevision(documentKey, documentRevision); m_preview.setCurrentRevision(documentKey, documentRevision); diff --git a/LoopEditor/editorhost.h b/LoopEditor/editorhost.h index 9cef1bf44..4a6a0a10a 100644 --- a/LoopEditor/editorhost.h +++ b/LoopEditor/editorhost.h @@ -47,6 +47,7 @@ #include "focusrestoration.h" #include "documentviewsession.h" #include "quickdocumentmodel.h" +#include "quickoutput.h" #include "pdfdocumentcontext.h" #include "pdfjobscheduler.h" @@ -86,6 +87,8 @@ class EditorHost final : public QObject Q_PROPERTY(QString documentState READ documentState NOTIFY presentationChanged) Q_PROPERTY(bool hasDocument READ hasDocument NOTIFY presentationChanged) + Q_PROPERTY(bool outputBusy READ outputBusy NOTIFY presentationChanged) + Q_PROPERTY(QVariantMap outputRecord READ outputRecord NOTIFY presentationChanged) Q_PROPERTY(bool correctionsAllowed READ correctionsAllowed NOTIFY presentationChanged) Q_PROPERTY(QString displayTitle READ displayTitle NOTIFY presentationChanged) Q_PROPERTY(QString typedError READ typedError NOTIFY presentationChanged) @@ -193,6 +196,11 @@ class EditorHost final : public QObject QString documentState() const; bool hasDocument() const; + bool outputBusy() const { return !m_outputJobId.isEmpty(); } + QVariantMap outputRecord() const { return m_outputRecord; } + Q_INVOKABLE QString outputRevision() const; + Q_INVOKABLE bool exportDocumentFileUrl(const QUrl& url, const QString& format, int dpi, + const QString& revision, bool fidelityAcknowledged); bool correctionsAllowed() const; QString displayTitle() const; QString typedError() const; @@ -485,6 +493,8 @@ class EditorHost final : public QObject void preflightProfileDraftChanged(); void actionListRecipesChanged(); void preflightReportExportRequested(); + void documentOutputRequested(bool printing); + void documentOutputFinished(); void preflightProfileImportRequested(); void preflightProfileExportRequested(); void preflightProfileSaveRequested(); @@ -518,6 +528,8 @@ class EditorHost final : public QObject void finishPreflightJob(const pdf::PDFJobSnapshot& snapshot); void finishActionListJob(const pdf::PDFJobSnapshot& snapshot); void finishRollbackJob(const pdf::PDFJobSnapshot& snapshot); + void beginDocumentOutput(pdfinteraction::CommandInvocationId invocation, const QVariantMap& parameters); + void finishDocumentOutput(const pdf::PDFJobSnapshot& snapshot); bool submitActionListJob(pdfinteraction::ActionListRunPhase phase, pdfinteraction::ActionListController::State controllerState); void reloadActionListRecipes(); @@ -565,6 +577,16 @@ class EditorHost final : public QObject QPointer m_canvas; QHash m_activeAsyncJobs; + QString m_outputJobId; + QString m_outputRevision; + QString m_outputDestination; + bool m_outputCancelled = false; + pdfinteraction::CommandInvocationId m_outputInvocation = pdfinteraction::InvalidCommandInvocation; + std::shared_ptr m_outputResult; + QVariantMap m_outputRecord; + QByteArray m_publishedOutputBytes; + QJsonObject m_publicationReceipt; + QString m_publicationRevision; struct PreflightWorkerOutcome; QHash> m_preflightOutcomes; QHash> m_actionListOutcomes; diff --git a/LoopEditor/qml/Main.qml b/LoopEditor/qml/Main.qml index 25b34b815..641507d20 100644 --- a/LoopEditor/qml/Main.qml +++ b/LoopEditor/qml/Main.qml @@ -144,6 +144,113 @@ ApplicationWindow { onRejected: if (host && host.focusRestoration) host.focusRestoration.restore() } + Dialog { + id: outputDialog + objectName: "documentOutputDialog" + parent: Overlay.overlay + anchors.centerIn: parent + modal: true + title: printing ? qsTr("Print to PDF") : qsTr("Export document") + standardButtons: Dialog.Ok | Dialog.Cancel + property bool printing: false + property string revision: "" + Accessible.name: title + contentItem: ColumnLayout { + Label { + text: host ? host.previewFidelitySummary : "" + wrapMode: Text.WordWrap + Layout.preferredWidth: 440 + } + Label { + text: outputDialog.printing ? qsTr("Print every page to a PDF file using the authoritative renderer.") : + qsTr("PDF copies the exact document bytes. PNG and TIFF export the current page.") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + ComboBox { + id: outputFormat + visible: !outputDialog.printing + model: [qsTr("PDF"), qsTr("PNG"), qsTr("TIFF")] + Accessible.name: qsTr("Export format") + Layout.fillWidth: true + } + SpinBox { + id: outputResolution + from: 72 + to: 600 + value: 300 + editable: true + visible: outputDialog.printing || outputFormat.currentIndex !== 0 + Accessible.name: qsTr("Resolution in dots per inch") + } + CheckBox { + id: outputAcknowledgment + text: qsTr("I acknowledge fidelity warnings. This output is not print proof.") + Accessible.name: text + Layout.fillWidth: true + } + } + onOpened: printing ? outputResolution.forceActiveFocus() : outputFormat.forceActiveFocus() + onAccepted: outputFileDialog.open() + onRejected: if (host && host.focusRestoration) host.focusRestoration.restore() + } + + FileDialog { + id: outputFileDialog + title: outputDialog.title + fileMode: FileDialog.SaveFile + readonly property string format: outputDialog.printing ? "print-pdf" : ["pdf", "png", "tiff"][outputFormat.currentIndex] + nameFilters: format === "png" ? [qsTr("PNG files (*.png)")] : + format === "tiff" ? [qsTr("TIFF files (*.tiff *.tif)")] : [qsTr("PDF files (*.pdf)")] + onAccepted: { + if (host) host.exportDocumentFileUrl(selectedFile, format, outputResolution.value, outputDialog.revision, outputAcknowledgment.checked) + if (host && host.focusRestoration) host.focusRestoration.restore() + } + onRejected: if (host && host.focusRestoration) host.focusRestoration.restore() + } + + Connections { + target: host + function onDocumentOutputRequested(printing) { + if (host.focusRestoration) host.focusRestoration.remember(window.activeFocusItem) + outputDialog.printing = printing + outputDialog.revision = host.outputRevision() + outputAcknowledgment.checked = false + outputResolution.value = printing && !host.documentModel.canHighResolutionPrint ? 150 : 300 + outputDialog.open() + } + function onDocumentOutputFinished() { outputResultDialog.open() } + } + + Dialog { + id: outputResultDialog + objectName: "documentOutputResultDialog" + parent: Overlay.overlay + anchors.centerIn: parent + title: qsTr("Output result") + standardButtons: Dialog.Close + Accessible.name: title + contentItem: ColumnLayout { + Label { + Layout.preferredWidth: 500 + wrapMode: Text.WrapAnywhere + text: { + const record = host ? host.outputRecord : ({}) + if (record.error) return record.error + const artifact = record.artifact || ({}) + const receipt = record.publicationReceipt || ({}) + return qsTr("Saved artifact: %1\nRevision: %2\n%3\n%4\nThis output is not print proof.") + .arg(artifact.sha256 || "").arg(record.revision || "") + .arg(record.renderer || "") + .arg(receipt.plan_digest ? qsTr("Correction receipt plan: %1").arg(receipt.plan_digest) : + record.dpi ? qsTr("%1 dpi — %2").arg(record.dpi).arg(record.colorHandling) : qsTr("Original source bytes")) + } + Accessible.name: text + } + } + onClosed: if (host && host.focusRestoration) host.focusRestoration.restore() + } + FileDialog { id: saveAsDialog title: qsTr("Save PDF As") diff --git a/LoopEditor/qml/MenuModel.qml b/LoopEditor/qml/MenuModel.qml index 34069d28d..90456a6c2 100644 --- a/LoopEditor/qml/MenuModel.qml +++ b/LoopEditor/qml/MenuModel.qml @@ -78,6 +78,8 @@ QtObject { } function labelForEntry(entry) { + if (entry && entry.id === "actionPrint") return qsTr("Print to PDF…") + if (entry && entry.id === "actionExportDocument") return qsTr("Export document…") if (!entry || !entry.labelKey) { return entry && entry.id ? entry.id : "" } diff --git a/LoopEditor/quickoutput.cpp b/LoopEditor/quickoutput.cpp new file mode 100644 index 000000000..0e0a0e789 --- /dev/null +++ b/LoopEditor/quickoutput.cpp @@ -0,0 +1,205 @@ +// MIT License +#include "quickoutput.h" +#include "pdfartifactidentity.h" +#include "pdfdocumentcontext.h" +#include "pdfdocumentsession.h" +#include "pdfoptionalcontent.h" +#include "pdfpage.h" +#include "pdftransparencyrenderer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace loopeditor +{ +void prepareOutput(const OutputRequest& request, OutputResult& result, pdf::PDFJobContext& job) +{ + if (!request.document || request.revision.isEmpty() || request.dpi < 72 || request.dpi > 600) + { + result.error = QStringLiteral("output/invalid-request"); + return; + } + result.staging = std::make_shared(); + result.stagedPath = result.staging->filePath(QStringLiteral("output")); + if (!result.staging->isValid()) + { + result.error = QStringLiteral("output/staging-unavailable"); + return; + } + const auto hash = [](const QByteArray& bytes) + { return QString::fromLatin1(QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex()); }; + result.record = QJsonObject{ + { QStringLiteral("revision"), request.revision }, + { QStringLiteral("printProof"), false }, + { QStringLiteral("fidelityAcknowledged"), request.fidelityAcknowledged } + }; + if (job.isCancellationRequested()) + return; + + if (request.format == OutputFormat::PublishedPdf) + { + QByteArray bytes = request.publishedBytes; + if (bytes.isEmpty()) + { + QFile source(request.sourcePath); + if (!source.open(QIODevice::ReadOnly)) + { + result.error = QStringLiteral("output/source-unavailable"); + return; + } + bytes = source.readAll(); + if (source.error() != QFile::NoError || hash(bytes) != QString::fromLatin1(request.document->getSourceDataHash().toHex())) + { + result.error = QStringLiteral("output/source-changed"); + return; + } + } + else if (request.publicationReceipt.value(QStringLiteral("status")).toString() != QLatin1String("published") || + request.publicationReceipt.value(QStringLiteral("published_sha256")).toString() != hash(bytes)) + { + result.error = QStringLiteral("output/publication-identity-mismatch"); + return; + } + QFile output(result.stagedPath); + if (!output.open(QIODevice::WriteOnly) || output.write(bytes) != bytes.size() || !output.flush()) + { + result.error = QStringLiteral("output/staging-write-failed"); + return; + } + result.record.insert(QStringLiteral("publicationReceipt"), request.publicationReceipt); + result.record.insert(QStringLiteral("renderer"), QStringLiteral("none: exact PDF byte copy")); + } + else + { + const auto* catalog = request.document->getCatalog(); + const bool printing = request.format == OutputFormat::PrintPdf; + const int first = printing ? 0 : request.page; + const int last = printing ? int(catalog->getPageCount()) - 1 : request.page; + if (first < 0 || last >= int(catalog->getPageCount()) || first > last) + { + result.error = QStringLiteral("output/invalid-page"); + return; + } + pdf::PDFDocumentContext context(request.document); + auto* session = context.getSession(); + pdf::PDFOptionalContentActivity optionalContent(request.document.data(), printing ? pdf::OCUsage::Print : pdf::OCUsage::Export, nullptr); + pdf::PDFInkMapper inks(nullptr, request.document.data()); + inks.createSpotColors(true); + pdf::PDFTransparencyRendererSettings settings; + settings.renderPolicy = pdf::PDFRenderPolicy::forOutputPreview(); + settings.flags.setFlag(pdf::PDFTransparencyRendererSettings::SeparationSimulation, true); + settings.flags.setFlag(pdf::PDFTransparencyRendererSettings::SmoothImageTransformation, true); + pdf::PDFRenderDiagnostics diagnostics; + std::unique_ptr writer; + QPainter painter; + if (printing) + { + writer = std::make_unique(result.stagedPath); + writer->setResolution(request.dpi); + writer->setTitle(QStringLiteral("Loop print-to-PDF")); + writer->setCreator(QStringLiteral("Loop / PDFTransparencyRenderer")); + } + for (int index = first; index <= last; ++index) + { + if (job.isCancellationRequested()) + return; + const auto* page = catalog->getPage(size_t(index)); + if (!page->getAnnotations().empty()) + { + diagnostics.record(pdf::PDFRenderFidelity::Unsupported, QStringLiteral("Annotation appearances are not included in this page-content output.")); + } + const QSizeF points = page->getRotatedMediaBox().size(); + if (!points.isValid() || !std::isfinite(points.width()) || !std::isfinite(points.height()) || + points.width() * request.dpi / 72.0 > 20000 || points.height() * request.dpi / 72.0 > 20000) + { + result.error = QStringLiteral("output/invalid-page-size"); + return; + } + const QSize pixels(qCeil(points.width() * request.dpi / 72.0), qCeil(points.height() * request.dpi / 72.0)); + job.processingBudget().chargeRenderPixels(uint64_t(pixels.width()) * uint64_t(pixels.height()), QStringLiteral("Quick output")); + const QTransform matrix = pdf::PDFRenderer::createPagePointToDevicePointMatrix(page, QRectF(QPointF(), QSizeF(pixels))); + pdf::PDFTransparencyRenderer renderer(page, request.document.data(), session->getFontCache(), session->getCMS(), + &optionalContent, &inks, settings, matrix); + renderer.setOperationControl(job.operationControl()); + renderer.beginPaint(pixels); + renderer.processContents(); + renderer.endPaint(); + diagnostics.merge(renderer.getRenderDiagnostics()); + if (!diagnostics.isExact() && !request.fidelityAcknowledged) + { + result.error = QStringLiteral("output/fidelity-acknowledgment-required"); + return; + } + QImage image = renderer.toImage(false, true, pdf::PDFRGB{ 1.0f, 1.0f, 1.0f }); + if (image.isNull()) + { + result.error = QStringLiteral("output/render-failed"); + return; + } + image.setDotsPerMeterX(qRound(request.dpi / 0.0254)); + image.setDotsPerMeterY(qRound(request.dpi / 0.0254)); + if (printing) + { + writer->setPageSize(QPageSize(points, QPageSize::Point)); + writer->setPageMargins(QMarginsF(), QPageLayout::Point); + if ((index == first && !painter.begin(writer.get())) || (index != first && !writer->newPage())) + { + result.error = QStringLiteral("output/pdf-write-failed"); + return; + } + painter.drawImage(QRect(0, 0, writer->width(), writer->height()), image); + } + else + { + QImageWriter imageWriter(result.stagedPath, request.format == OutputFormat::Png ? QByteArray("png") : QByteArray("tiff")); + imageWriter.setText(QStringLiteral("Renderer"), QStringLiteral("Loop Core PDFTransparencyRenderer")); + imageWriter.setText(QStringLiteral("Color handling"), QStringLiteral("Core CMS; RGB composite; spot separation simulation; white paper")); + if (!imageWriter.write(image)) + { + result.error = QStringLiteral("output/image-encoder-unavailable-or-failed"); + return; + } + } + job.reportProgress((index - first + 1) * 90 / (last - first + 1)); + } + if (printing && !painter.end()) + { + result.error = QStringLiteral("output/pdf-write-failed"); + return; + } + writer.reset(); + result.record.insert(QStringLiteral("renderer"), QStringLiteral("Loop Core PDFTransparencyRenderer")); + result.record.insert(QStringLiteral("dpi"), request.dpi); + result.record.insert(QStringLiteral("firstPage"), first + 1); + result.record.insert(QStringLiteral("lastPage"), last + 1); + result.record.insert(QStringLiteral("colorHandling"), QStringLiteral("Core CMS; RGB composite; spot separation simulation; white paper")); + result.record.insert(QStringLiteral("fidelity"), diagnostics.isExact() ? QStringLiteral("exact-supported") : QStringLiteral("limited")); + result.record.insert(QStringLiteral("limitations"), QJsonArray::fromStringList(diagnostics.reasons)); + } + QFile file(result.stagedPath); + if (!file.open(QIODevice::ReadOnly)) + { + result.error = QStringLiteral("output/staging-read-failed"); + return; + } + QCryptographicHash digest(QCryptographicHash::Sha256); + if (!digest.addData(&file)) + { + result.error = QStringLiteral("output/staging-read-failed"); + return; + } + pdf::PDFArtifactIdentity artifact; + artifact.sha256 = QString::fromLatin1(digest.result().toHex()); + artifact.size = file.size(); + artifact.mediaType = request.format == OutputFormat::Png ? QStringLiteral("image/png") : request.format == OutputFormat::Tiff ? QStringLiteral("image/tiff") + : QStringLiteral("application/pdf"); + result.record.insert(QStringLiteral("artifact"), artifact.toJson()); + job.setOutputArtifact(artifact); +} +} diff --git a/LoopEditor/quickoutput.h b/LoopEditor/quickoutput.h new file mode 100644 index 000000000..225baad1d --- /dev/null +++ b/LoopEditor/quickoutput.h @@ -0,0 +1,44 @@ +// MIT License +#ifndef QUICKOUTPUT_H +#define QUICKOUTPUT_H + +#include "pdfdocument.h" +#include "pdfjobscheduler.h" +#include +#include +#include + +namespace loopeditor +{ +enum class OutputFormat +{ + PublishedPdf, + PrintPdf, + Png, + Tiff +}; + +struct OutputRequest +{ + OutputFormat format = OutputFormat::PublishedPdf; + pdf::PDFDocumentPointer document; + QString sourcePath; + QByteArray publishedBytes; + QJsonObject publicationReceipt; + QString revision; + int page = 0; + int dpi = 300; + bool fidelityAcknowledged = false; +}; + +struct OutputResult +{ + std::shared_ptr staging; + QString stagedPath; + QString error; + QJsonObject record; +}; + +void prepareOutput(const OutputRequest& request, OutputResult& result, pdf::PDFJobContext& job); +} +#endif diff --git a/LoopLibInteraction/sources/actionlistrunsubmitter.cpp b/LoopLibInteraction/sources/actionlistrunsubmitter.cpp index 82d971185..f43beed1b 100644 --- a/LoopLibInteraction/sources/actionlistrunsubmitter.cpp +++ b/LoopLibInteraction/sources/actionlistrunsubmitter.cpp @@ -277,6 +277,10 @@ ActionListRunWorker makeActionListRunWorker(ActionListRunPhase phase, mutation.operationControl = context.operationControl(); pdf::PDFGovernedMutationReceipt receipt; const pdf::PDFOperationResult governedResult = pdf::executeGovernedMutation(mutation, &receipt); + if (governedResult && receipt.isPublished()) + { + outcome->publishedBytes = candidateData; + } outcome->executionResult.governed = QJsonObject{ { QStringLiteral("approval"), approval.toJson() }, { QStringLiteral("revalidation"), receipt.revalidation.toJson() }, diff --git a/LoopLibInteraction/sources/actionlistrunsubmitter.h b/LoopLibInteraction/sources/actionlistrunsubmitter.h index eb9618399..844efcb6f 100644 --- a/LoopLibInteraction/sources/actionlistrunsubmitter.h +++ b/LoopLibInteraction/sources/actionlistrunsubmitter.h @@ -53,6 +53,7 @@ struct ActionListWorkerOutcome QVector validationSteps; pdf::PDFActionListExecutionResult executionResult; pdf::PDFDocumentPointer candidate; + QByteArray publishedBytes; }; using ActionListRunWorker = std::function; diff --git a/UnitTests/phase4-tests.cmake b/UnitTests/phase4-tests.cmake index cb963a85a..3ab2d0945 100644 --- a/UnitTests/phase4-tests.cmake +++ b/UnitTests/phase4-tests.cmake @@ -69,6 +69,8 @@ if(NOT LOOP_BUILD_ONLY_CORE_LIBRARY) tst_editorhosttest.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/app.qrc ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.h ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.h ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.h @@ -103,6 +105,8 @@ if(NOT LOOP_BUILD_ONLY_CORE_LIBRARY) add_executable(UnitTestsProductOperatorLoop tst_productoperatorloop.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.h ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.h ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.h @@ -307,6 +311,8 @@ if(NOT LOOP_BUILD_ONLY_CORE_LIBRARY) add_executable(UnitTestsShellKeyboard tst_shellkeyboardtest.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.h ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.h ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.h @@ -334,6 +340,8 @@ if(NOT LOOP_BUILD_ONLY_CORE_LIBRARY) ${CMAKE_SOURCE_DIR}/LoopEditor/operatorhelp.h ${CMAKE_SOURCE_DIR}/LoopEditor/app.qrc ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.h ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.h ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.h @@ -362,6 +370,8 @@ if(NOT LOOP_BUILD_ONLY_CORE_LIBRARY) add_executable(UnitTestsShellInspectorDispatch tst_shellinspectordispatch.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.cpp + ${CMAKE_SOURCE_DIR}/LoopEditor/quickoutput.h ${CMAKE_SOURCE_DIR}/LoopEditor/editorhost.h ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.cpp ${CMAKE_SOURCE_DIR}/LoopEditor/documentviewsession.h diff --git a/UnitTests/tst_editorhosttest.cpp b/UnitTests/tst_editorhosttest.cpp index 3bd15b0ea..f37344165 100644 --- a/UnitTests/tst_editorhosttest.cpp +++ b/UnitTests/tst_editorhosttest.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,10 @@ #include "pdfblockingthreadguard.h" #include "pdfdocumentbuilder.h" #include "pdfdocumentwriter.h" +#include "pdfdocumentreader.h" +#include "quickoutput.h" +#include +#include #include "pdfsecurityhandler.h" #include "pdfoperationhistorystore.h" #include "pdfsettings.h" @@ -269,6 +274,11 @@ class EditorHostTest : public QObject Q_OBJECT private slots: + void outputBytesAndFences(); + void outputPublishedPacket(); + void outputRenderFormats_data(); + void outputRenderFormats(); + void outputOverprintWarning(); void encryptedOpen_data(); void encryptedOpen(); void passwordFailureIsTypedAndRetryable(); @@ -1840,6 +1850,13 @@ void EditorHostTest::fixJourneyPublishesOnlyAnApprovedPlanBoundToTheDisplayedIde QVERIFY2(host.fixLifecycleSummary().contains(QStringLiteral("published")), qPrintable(host.fixLifecycleSummary())); + const QString exportPath = directory.filePath(QStringLiteral("accepted-correction.pdf")); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(exportPath), QStringLiteral("pdf"), 300, host.outputRevision(), true)); + QTRY_VERIFY_WITH_TIMEOUT(!host.outputBusy(), 10000); + QVERIFY2(host.outputRecord().value(QStringLiteral("status")).toString() == QLatin1String("saved"), qPrintable(host.outputRecord().value(QStringLiteral("error")).toString())); + QCOMPARE(QString::fromLatin1(fileDigest(exportPath)), publishedSha256); + QCOMPARE(host.outputRecord().value(QStringLiteral("publicationReceipt")).toMap().value(QStringLiteral("published_sha256")).toString(), publishedSha256); + // The as-received input is never the publication target. QCOMPARE(fileDigest(documentPath), sourceDigest); } @@ -2392,6 +2409,176 @@ void EditorHostTest::executeApprovedActionListPlanRefusesAnUnreviewedPlan() QVERIFY(host.executeApprovedActionListPlan()); } +void EditorHostTest::outputBytesAndFences() +{ + QTemporaryDir directory; + const QString source = directory.filePath(QStringLiteral("source.pdf")); + pdf::PDFDocumentBuilder builder; + builder.appendPage(QRectF(0, 0, 72, 144)); + const auto document = builder.build(); + QVERIFY(pdf::PDFDocumentWriter(nullptr).write(source, &document, true)); + QFile input(source); + QVERIFY(input.open(QIODevice::ReadOnly)); + const auto bytes = input.readAll(); + EditorHost host; + host.openFileUrl(QUrl::fromLocalFile(source)); + QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 10000); + const QString destination = directory.filePath(QStringLiteral("export.pdf")); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(destination), QStringLiteral("pdf"), 300, QStringLiteral("stale"), true)); + QVERIFY(!QFile::exists(destination)); + QCOMPARE(host.outputRecord().value(QStringLiteral("error")).toString(), QStringLiteral("output/stale-or-busy-document")); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(destination), QStringLiteral("pdf"), 300, host.outputRevision(), true)); + QTRY_VERIFY_WITH_TIMEOUT(!host.outputBusy(), 10000); + QCOMPARE(host.outputRecord().value(QStringLiteral("status")).toString(), QStringLiteral("saved")); + QFile exported(destination); + QVERIFY(exported.open(QIODevice::ReadOnly)); + QCOMPARE(exported.readAll(), bytes); + exported.close(); + QCOMPARE(host.outputRecord().value(QStringLiteral("artifact")).toMap().value(QStringLiteral("sha256")).toString(), + QString::fromLatin1(QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex())); + QVERIFY(!host.outputRecord().value(QStringLiteral("printProof")).toBool()); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(source), QStringLiteral("pdf"), 300, host.outputRevision(), true)); + QCOMPARE(host.outputRecord().value(QStringLiteral("error")).toString(), QStringLiteral("output/invalid-or-source-destination")); + + const QVariantMap parameters{ { QStringLiteral("path"), destination }, { QStringLiteral("format"), QStringLiteral("print-pdf") }, { QStringLiteral("dpi"), 72 }, { QStringLiteral("revision"), host.outputRevision() }, { QStringLiteral("fidelityAcknowledged"), true } }; + const auto invocation = host.invokeCommand(QStringLiteral("actionPrint"), parameters); + QVERIFY(invocation != pdfinteraction::InvalidCommandInvocation); + QVERIFY(host.cancelCommand(invocation)); + QTRY_VERIFY_WITH_TIMEOUT(!host.outputBusy(), 10000); + QVERIFY(exported.open(QIODevice::ReadOnly)); + QCOMPARE(exported.readAll(), bytes); + exported.close(); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(destination), QStringLiteral("print-pdf"), 72, host.outputRevision(), true)); + host.invokeCommand(QStringLiteral("actionClose")); + QTRY_VERIFY_WITH_TIMEOUT(!host.outputBusy(), 10000); + QVERIFY(exported.open(QIODevice::ReadOnly)); + QCOMPARE(exported.readAll(), bytes); +} + +void EditorHostTest::outputPublishedPacket() +{ + pdf::PDFDocumentBuilder builder; + builder.appendPage(QRectF(0, 0, 72, 144)); + loopeditor::OutputRequest request; + request.document = pdf::PDFDocumentPointer(new pdf::PDFDocument(builder.build())); + request.revision = QStringLiteral("accepted-revision"); + request.publishedBytes = QByteArray("%PDF-1.7\nexact published packet\n%%EOF"); + const QString digest = QString::fromLatin1(QCryptographicHash::hash(request.publishedBytes, QCryptographicHash::Sha256).toHex()); + request.publicationReceipt = { { QStringLiteral("status"), QStringLiteral("published") }, + { QStringLiteral("published_sha256"), digest }, + { QStringLiteral("plan_digest"), QStringLiteral("reviewed-plan") } }; + pdf::PDFJobContext job(std::make_shared(), pdf::PDFProcessingLimits::conservativeDefaults(), {}); + loopeditor::OutputResult result; + loopeditor::prepareOutput(request, result, job); + QVERIFY2(result.error.isEmpty(), qPrintable(result.error)); + QFile exported(result.stagedPath); + QVERIFY(exported.open(QIODevice::ReadOnly)); + QCOMPARE(exported.readAll(), request.publishedBytes); + QCOMPARE(result.record.value(QStringLiteral("publicationReceipt")).toObject(), request.publicationReceipt); + request.publicationReceipt.insert(QStringLiteral("published_sha256"), QString(64, QLatin1Char('0'))); + loopeditor::OutputResult refused; + loopeditor::prepareOutput(request, refused, job); + QCOMPARE(refused.error, QStringLiteral("output/publication-identity-mismatch")); + QVERIFY(!QFile::exists(refused.stagedPath)); +} + +void EditorHostTest::outputRenderFormats_data() +{ + QTest::addColumn("format"); + QTest::newRow("print-to-PDF") << QStringLiteral("print-pdf"); + QTest::newRow("PNG") << QStringLiteral("png"); + QTest::newRow("TIFF") << QStringLiteral("tiff"); +} + +void EditorHostTest::outputRenderFormats() +{ + QFETCH(QString, format); + if (format == QLatin1String("tiff") && !QImageWriter::supportedImageFormats().contains(QByteArray("tiff"))) + QSKIP("Qt TIFF encoder is unavailable; the product refuses TIFF output explicitly."); + QTemporaryDir directory; + const QString source = directory.filePath(QStringLiteral("source.pdf")); + pdf::PDFDocumentBuilder builder; + const auto firstPage = builder.appendPage(QRectF(0, 0, 72, 144)); + builder.appendPage(QRectF(0, 0, 144, 72)); + pdf::PDFPageContentStreamBuilder contents(&builder); + auto* painter = contents.begin(firstPage); + QVERIFY(painter); + painter->fillRect(QRectF(0, 0, 36, 144), Qt::black); + contents.end(painter); + const auto document = builder.build(); + QVERIFY(pdf::PDFDocumentWriter(nullptr).write(source, &document, true)); + EditorHost host; + host.openFileUrl(QUrl::fromLocalFile(source)); + QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 10000); + const QString destination = directory.filePath(QStringLiteral("output")); + host.setViewportGeometry(96.0 / 25.4, 1.0, 1024, 768); + host.goToPage(0); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(destination), format, 72, host.outputRevision(), true)); + QTRY_VERIFY_WITH_TIMEOUT(!host.outputBusy(), 20000); + const auto record = host.outputRecord(); + QVERIFY2(record.value(QStringLiteral("status")).toString() == QLatin1String("saved"), qPrintable(record.value(QStringLiteral("error")).toString())); + QCOMPARE(record.value(QStringLiteral("dpi")).toInt(), 72); + QCOMPARE(record.value(QStringLiteral("renderer")).toString(), QStringLiteral("Loop Core PDFTransparencyRenderer")); + QVERIFY(!record.value(QStringLiteral("colorHandling")).toString().isEmpty()); + QVERIFY(!record.value(QStringLiteral("printProof")).toBool()); + if (format == QLatin1String("print-pdf")) + { + pdf::PDFDocumentReader reader(nullptr, {}, true, false); + const auto printed = reader.readFromFile(destination); + QCOMPARE(reader.getReadingResult(), pdf::PDFDocumentReader::Result::OK); + QCOMPARE(printed.getCatalog()->getPageCount(), size_t(2)); + QCOMPARE(printed.getCatalog()->getPage(0)->getMediaBox().size(), QSizeF(72, 144)); + QCOMPARE(printed.getCatalog()->getPage(1)->getMediaBox().size(), QSizeF(144, 72)); + loopeditor::OutputRequest request; + request.format = loopeditor::OutputFormat::Png; + request.document = pdf::PDFDocumentPointer(new pdf::PDFDocument(printed)); + request.revision = QStringLiteral("printed-fixture"); + request.dpi = 72; + request.fidelityAcknowledged = true; + loopeditor::OutputResult result; + pdf::PDFJobContext job(std::make_shared(), pdf::PDFProcessingLimits::conservativeDefaults(), {}); + loopeditor::prepareOutput(request, result, job); + QVERIFY2(result.error.isEmpty(), qPrintable(result.error)); + const auto image = QImageReader(result.stagedPath).read(); + QCOMPARE(image.pixelColor(18, 72), QColor(Qt::black)); + QCOMPARE(image.pixelColor(54, 72), QColor(Qt::white)); + } + else + { + QImageReader reader(destination); + const auto image = reader.read(); + QCOMPARE(image.size(), QSize(72, 144)); + QCOMPARE(image.pixelColor(18, 72), QColor(Qt::black)); + QCOMPARE(image.pixelColor(54, 72), QColor(Qt::white)); + QVERIFY(qAbs(image.dotsPerMeterX() - qRound(72 / 0.0254)) <= 1); + } + QFile artifact(destination); + QVERIFY(artifact.open(QIODevice::ReadOnly)); + qInfo().noquote() << "quick-output-fixture" << format << "source_sha256=" + << QString::fromLatin1(host.sessionForTest()->context().getDocumentPointer()->getSourceDataHash().toHex()) + << "artifact_sha256=" << record.value(QStringLiteral("artifact")).toMap().value(QStringLiteral("sha256")).toString(); +} + +void EditorHostTest::outputOverprintWarning() +{ + EditorHost host; + host.openFileUrl(QUrl::fromLocalFile(preflightFixturesDir() + QStringLiteral("/overprint-cmyk-mode0-on.pdf"))); + QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 10000); + host.goToPage(0); + host.setViewportGeometry(96.0 / 25.4, 1.0, 1024, 768); + QTRY_VERIFY_WITH_TIMEOUT(!host.pageFidelityIsExact(), 10000); + QTemporaryDir directory; + const QString destination = directory.filePath(QStringLiteral("output.png")); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(destination), QStringLiteral("png"), 72, host.outputRevision(), false)); + QCOMPARE(host.outputRecord().value(QStringLiteral("error")).toString(), QStringLiteral("output/fidelity-acknowledgment-required")); + QVERIFY(!QFile::exists(destination)); + QVERIFY(host.exportDocumentFileUrl(QUrl::fromLocalFile(destination), QStringLiteral("png"), 72, host.outputRevision(), true)); + QTRY_VERIFY_WITH_TIMEOUT(!host.outputBusy(), 20000); + QCOMPARE(host.outputRecord().value(QStringLiteral("status")).toString(), QStringLiteral("saved")); + QVERIFY(host.outputRecord().value(QStringLiteral("fidelityAcknowledged")).toBool()); + QVERIFY(!host.outputRecord().value(QStringLiteral("printProof")).toBool()); +} + QTEST_GUILESS_MAIN(EditorHostTest) #include "tst_editorhosttest.moc" diff --git a/agent-policy.json b/agent-policy.json index 67c60b349..6a9501330 100644 --- a/agent-policy.json +++ b/agent-policy.json @@ -218,6 +218,8 @@ "LoopLibQuick/**", "LoopEditor/editorhost.cpp", "LoopEditor/editorhost.h", + "LoopEditor/quickoutput.cpp", + "LoopEditor/quickoutput.h", "LoopEditor/quickdocumentmodel.cpp", "LoopEditor/quickdocumentmodel.h", "UnitTests/tst_quickdocumentmodeltest.cpp", diff --git a/changes/feat-issue-123-quick-print-export.evidence.yaml b/changes/feat-issue-123-quick-print-export.evidence.yaml new file mode 100644 index 000000000..fcfefd87e --- /dev/null +++ b/changes/feat-issue-123-quick-print-export.evidence.yaml @@ -0,0 +1,18 @@ +format_version: 1 +kind: evidence +claims: + - id: quick-print-to-pdf-and-export + evidence: + - unit:agent-policy:quick + - unit:agent-policy:interaction + - unit:agent-policy:build_policy + - unit:agent-policy:schemas_contracts + - unit:scripts/agent/test_architecture_contracts.py + - architecture:scripts/agent/check-architecture.py + - architecture:scripts/agent/generate-adapters.py + - unit:UnitTests/tst_editorhosttest.cpp + - integration:tools/ProductQuickAccessibilitySmoke/main.cpp + - architecture:docs/QUICK_PRINT_EXPORT.md +unresolved: + - clean-machine-windows-print-to-file + - clean-machine-linux-print-to-file diff --git a/changes/feat-issue-123-quick-print-export.md b/changes/feat-issue-123-quick-print-export.md new file mode 100644 index 000000000..36f4dee99 --- /dev/null +++ b/changes/feat-issue-123-quick-print-export.md @@ -0,0 +1,4 @@ +Category: added +Audience: operators +Breaking-Change: no +Summary: Add authoritative print-to-PDF and PDF/PNG/TIFF export from Quick, with revision fencing, fidelity acknowledgment, artifact identity, and the correction publication receipt. diff --git a/docs/LOOP_SHELL_CONTRACT.md b/docs/LOOP_SHELL_CONTRACT.md index c575b3553..67135a3d6 100644 --- a/docs/LOOP_SHELL_CONTRACT.md +++ b/docs/LOOP_SHELL_CONTRACT.md @@ -43,7 +43,7 @@ workspace rail (7 IDs) | workspace stack | Document: pages rail | PDF canvas | c status: document state · production state · preflight state · page/zoom ``` -Manifest-driven menus group the 108 catalog actions by shell menu and +Manifest-driven menus group the 109 catalog actions by shell menu and workspace `target`, with `ADVANCED` actions gated behind the release-profile developer diagnostics flag. Compare is enabled: it presents the before/after artifact identities, technical finding delta, preserved attributes and diff --git a/docs/QUICK_PDF_PARITY.md b/docs/QUICK_PDF_PARITY.md index e227e92f2..1d302f688 100644 --- a/docs/QUICK_PDF_PARITY.md +++ b/docs/QUICK_PDF_PARITY.md @@ -29,10 +29,14 @@ slice is intentionally model-driven: The following remain deliberately declared or policy-excluded until their typed bridge and revision-fenced tests land: annotation/form overlays and -editing, attachments and metadata editing, print/export, undo/redo, encryption +editing, attachments and metadata editing, native printer spooling, undo/redo, encryption creation or changes, sanitization, optimization, signature verification, OCR, PageMaster, Compare, Redaction, signature creation, and deep inspection. +Print-to-PDF and PDF/PNG/TIFF exports use the revision-fenced Core output bridge +described in [Quick print and export](QUICK_PRINT_EXPORT.md). Corrected PDF +export copies the exact publication bytes and retains their Core receipt. + Search currently runs through the Core model on the host thread. It is a functional read-only bridge, but its next hardening step is to submit the same snapshot computation through `PDFJobScheduler` and admit the value on the diff --git a/docs/QUICK_PRINT_EXPORT.md b/docs/QUICK_PRINT_EXPORT.md new file mode 100644 index 000000000..b1cec1fa2 --- /dev/null +++ b/docs/QUICK_PRINT_EXPORT.md @@ -0,0 +1,49 @@ +# Quick print and export + +Issue [#123](https://github.com/studio-berry/loop/issues/123) adds File → Export +document and Document → Print to PDF. The approved scope for this PR is +print-to-PDF and PDF/PNG/TIFF exports. Native printer spooling remains outside +this slice; the product continues to exclude Qt PrintSupport and Widgets. + +PDF export copies bytes without rendering or reserializing. For an accepted +correction, the Editor retains the exact bytes passed through Core's governed +publication gateway, together with its existing mutation receipt. Export checks +the receipt's published digest and binds the packet to the accepted revision. +An unmodified input is copied only if its on-disk digest still matches the +loaded document. An unpublished modified revision is refused. + +Print to PDF renders every page through Core's `PDFTransparencyRenderer` and +`PDFRenderPolicy::forOutputPreview()`, then paints those new renders into a +QtGui PDF writer. PNG and TIFF render the current page through the same +compositor. These operations never read canvas pixels. They use Print or Export +optional-content usage respectively, preserve rotated media-page dimensions, +and offer 72–600 dpi. Restricted low-resolution printing is capped at 150 dpi. +The output is a color-managed RGB composite with separation simulation and +white paper; it is not a separations file. Annotation appearances are an +explicit fidelity limitation and require acknowledgment. + +The dialog shows the current page's fidelity summary. An active warning, or a +limitation discovered during output rendering, requires explicit acknowledgment. +The result records renderer, resolution, color handling, page range, artifact +SHA-256, document revision, and any correction receipt. Output never certifies +print safety. A missing TIFF encoder is an explicit failure. + +The scheduler prepares output in a private temporary directory. The owner +checks request identity, cancellation, lifecycle, and revision before Core's +atomic writer commits the selected destination. The writer verifies the staged +digest while copying. Cancelled, stale, failed, or refused requests preserve +an existing destination, and exporting over the input is refused. + +`UnitTestsEditorHost` covers exact input and publication-byte copying, receipt +identity mismatch, stale requests, cancellation/replacement, output geometry, +raster dimensions and resolution, and overprint acknowledgment. Its existing +governed correction journey also exports the accepted artifact and verifies its +digest against the Core sign-off and mutation receipt. + +Clean-machine print-to-file qualification on Windows and Linux is still a +separate acceptance lane. Record exact source SHA, fixture and artifact identity, +platform/toolchain, and hosted run before claiming that lane complete. + +The final diff review retains Core's existing renderer, safe writer, receipt, +and command catalog. It introduces no renderer, publication authority, stored +receipt schema, dependency, or protected build-contract change. diff --git a/docs/loop-shell-actions.json b/docs/loop-shell-actions.json index f18322e8b..bdc21bcff 100644 --- a/docs/loop-shell-actions.json +++ b/docs/loop-shell-actions.json @@ -2,7 +2,7 @@ "schema_version": 1, "issue": 193, "source_ui": "LoopEditor Quick shell (Widgets pdfeditormainwindow.ui deleted Issue 17)", - "expected_action_count": 108, + "expected_action_count": 109, "actions": [ { "id": "actionAbout", @@ -1041,10 +1041,36 @@ "shortcut": { "standard_key": "Print" }, - "parameters": [], - "capability": "unclassified", - "cancellable": false, - "availability": "declared" + "parameters": [ + { + "name": "path", + "type": "string", + "required": false + }, + { + "name": "format", + "type": "string", + "required": false + }, + { + "name": "dpi", + "type": "integer", + "required": false + }, + { + "name": "revision", + "type": "string", + "required": false + }, + { + "name": "fidelityAcknowledged", + "type": "boolean", + "required": false + } + ], + "capability": "document.read", + "cancellable": true, + "availability": "implemented" }, "menu_group": "File" }, @@ -1541,6 +1567,45 @@ "availability": "implemented" }, "menu_group": "View" + }, + { + "id": "actionExportDocument", + "disposition": "KEEP", + "target": "Document", + "command": { + "label_key": "command.actionExportDocument.label", + "parameters": [ + { + "name": "path", + "type": "string", + "required": false + }, + { + "name": "format", + "type": "string", + "required": false + }, + { + "name": "dpi", + "type": "integer", + "required": false + }, + { + "name": "revision", + "type": "string", + "required": false + }, + { + "name": "fidelityAcknowledged", + "type": "boolean", + "required": false + } + ], + "capability": "document.read", + "cancellable": true, + "availability": "implemented" + }, + "menu_group": "File" } ] } diff --git a/scripts/verify-command-catalog.py b/scripts/verify-command-catalog.py index e31cb6144..be0d98a97 100644 --- a/scripts/verify-command-catalog.py +++ b/scripts/verify-command-catalog.py @@ -69,6 +69,8 @@ "actionPageLayoutTwoColumns", "actionPageLayoutTwoPages", "actionProperties", + "actionPrint", + "actionExportDocument", } ) diff --git a/tools/ProductQuickAccessibilitySmoke/CMakeLists.txt b/tools/ProductQuickAccessibilitySmoke/CMakeLists.txt index c15ed93d6..37c83118c 100644 --- a/tools/ProductQuickAccessibilitySmoke/CMakeLists.txt +++ b/tools/ProductQuickAccessibilitySmoke/CMakeLists.txt @@ -3,6 +3,8 @@ qt_policy(SET QTP0001 NEW) qt_add_executable(ProductQuickAccessibilitySmoke main.cpp ../../LoopEditor/editorhost.cpp + ../../LoopEditor/quickoutput.cpp + ../../LoopEditor/quickoutput.h ../../LoopEditor/editorhost.h ../../LoopEditor/focusrestoration.cpp ../../LoopEditor/focusrestoration.h diff --git a/tools/ProductQuickAccessibilitySmoke/qml/Main.qml b/tools/ProductQuickAccessibilitySmoke/qml/Main.qml index 25b34b815..641507d20 100644 --- a/tools/ProductQuickAccessibilitySmoke/qml/Main.qml +++ b/tools/ProductQuickAccessibilitySmoke/qml/Main.qml @@ -144,6 +144,113 @@ ApplicationWindow { onRejected: if (host && host.focusRestoration) host.focusRestoration.restore() } + Dialog { + id: outputDialog + objectName: "documentOutputDialog" + parent: Overlay.overlay + anchors.centerIn: parent + modal: true + title: printing ? qsTr("Print to PDF") : qsTr("Export document") + standardButtons: Dialog.Ok | Dialog.Cancel + property bool printing: false + property string revision: "" + Accessible.name: title + contentItem: ColumnLayout { + Label { + text: host ? host.previewFidelitySummary : "" + wrapMode: Text.WordWrap + Layout.preferredWidth: 440 + } + Label { + text: outputDialog.printing ? qsTr("Print every page to a PDF file using the authoritative renderer.") : + qsTr("PDF copies the exact document bytes. PNG and TIFF export the current page.") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + ComboBox { + id: outputFormat + visible: !outputDialog.printing + model: [qsTr("PDF"), qsTr("PNG"), qsTr("TIFF")] + Accessible.name: qsTr("Export format") + Layout.fillWidth: true + } + SpinBox { + id: outputResolution + from: 72 + to: 600 + value: 300 + editable: true + visible: outputDialog.printing || outputFormat.currentIndex !== 0 + Accessible.name: qsTr("Resolution in dots per inch") + } + CheckBox { + id: outputAcknowledgment + text: qsTr("I acknowledge fidelity warnings. This output is not print proof.") + Accessible.name: text + Layout.fillWidth: true + } + } + onOpened: printing ? outputResolution.forceActiveFocus() : outputFormat.forceActiveFocus() + onAccepted: outputFileDialog.open() + onRejected: if (host && host.focusRestoration) host.focusRestoration.restore() + } + + FileDialog { + id: outputFileDialog + title: outputDialog.title + fileMode: FileDialog.SaveFile + readonly property string format: outputDialog.printing ? "print-pdf" : ["pdf", "png", "tiff"][outputFormat.currentIndex] + nameFilters: format === "png" ? [qsTr("PNG files (*.png)")] : + format === "tiff" ? [qsTr("TIFF files (*.tiff *.tif)")] : [qsTr("PDF files (*.pdf)")] + onAccepted: { + if (host) host.exportDocumentFileUrl(selectedFile, format, outputResolution.value, outputDialog.revision, outputAcknowledgment.checked) + if (host && host.focusRestoration) host.focusRestoration.restore() + } + onRejected: if (host && host.focusRestoration) host.focusRestoration.restore() + } + + Connections { + target: host + function onDocumentOutputRequested(printing) { + if (host.focusRestoration) host.focusRestoration.remember(window.activeFocusItem) + outputDialog.printing = printing + outputDialog.revision = host.outputRevision() + outputAcknowledgment.checked = false + outputResolution.value = printing && !host.documentModel.canHighResolutionPrint ? 150 : 300 + outputDialog.open() + } + function onDocumentOutputFinished() { outputResultDialog.open() } + } + + Dialog { + id: outputResultDialog + objectName: "documentOutputResultDialog" + parent: Overlay.overlay + anchors.centerIn: parent + title: qsTr("Output result") + standardButtons: Dialog.Close + Accessible.name: title + contentItem: ColumnLayout { + Label { + Layout.preferredWidth: 500 + wrapMode: Text.WrapAnywhere + text: { + const record = host ? host.outputRecord : ({}) + if (record.error) return record.error + const artifact = record.artifact || ({}) + const receipt = record.publicationReceipt || ({}) + return qsTr("Saved artifact: %1\nRevision: %2\n%3\n%4\nThis output is not print proof.") + .arg(artifact.sha256 || "").arg(record.revision || "") + .arg(record.renderer || "") + .arg(receipt.plan_digest ? qsTr("Correction receipt plan: %1").arg(receipt.plan_digest) : + record.dpi ? qsTr("%1 dpi — %2").arg(record.dpi).arg(record.colorHandling) : qsTr("Original source bytes")) + } + Accessible.name: text + } + } + onClosed: if (host && host.focusRestoration) host.focusRestoration.restore() + } + FileDialog { id: saveAsDialog title: qsTr("Save PDF As") diff --git a/tools/ProductQuickAccessibilitySmoke/qml/MenuModel.qml b/tools/ProductQuickAccessibilitySmoke/qml/MenuModel.qml index 34069d28d..90456a6c2 100644 --- a/tools/ProductQuickAccessibilitySmoke/qml/MenuModel.qml +++ b/tools/ProductQuickAccessibilitySmoke/qml/MenuModel.qml @@ -78,6 +78,8 @@ QtObject { } function labelForEntry(entry) { + if (entry && entry.id === "actionPrint") return qsTr("Print to PDF…") + if (entry && entry.id === "actionExportDocument") return qsTr("Export document…") if (!entry || !entry.labelKey) { return entry && entry.id ? entry.id : "" } From 09bce707b3059065ad4886ee69235e6110e26589 Mon Sep 17 00:00:00 2001 From: mberrys <212453881+mberrys@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:35:25 -0700 Subject: [PATCH 2/7] Refuse incomplete renders before output publication --- LoopEditor/quickoutput.cpp | 10 ++++++++++ UnitTests/tst_editorhosttest.cpp | 30 ++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/LoopEditor/quickoutput.cpp b/LoopEditor/quickoutput.cpp index 0e0a0e789..e22ec7601 100644 --- a/LoopEditor/quickoutput.cpp +++ b/LoopEditor/quickoutput.cpp @@ -130,6 +130,16 @@ void prepareOutput(const OutputRequest& request, OutputResult& result, pdf::PDFJ renderer.beginPaint(pixels); renderer.processContents(); renderer.endPaint(); + for (const auto& error : renderer.getRenderErrors()) + { + if (error.type == pdf::RenderErrorType::Error) + { + result.error = QStringLiteral("output/render-failed: %1").arg(error.message); + return; + } + if (error.type != pdf::RenderErrorType::Information) + diagnostics.record(pdf::PDFRenderFidelity::Unsupported, error.message); + } diagnostics.merge(renderer.getRenderDiagnostics()); if (!diagnostics.isExact() && !request.fidelityAcknowledged) { diff --git a/UnitTests/tst_editorhosttest.cpp b/UnitTests/tst_editorhosttest.cpp index f37344165..6f5469966 100644 --- a/UnitTests/tst_editorhosttest.cpp +++ b/UnitTests/tst_editorhosttest.cpp @@ -279,6 +279,7 @@ private slots: void outputRenderFormats_data(); void outputRenderFormats(); void outputOverprintWarning(); + void outputRenderFailure(); void encryptedOpen_data(); void encryptedOpen(); void passwordFailureIsTypedAndRetryable(); @@ -1826,10 +1827,6 @@ void EditorHostTest::fixJourneyPublishesOnlyAnApprovedPlanBoundToTheDisplayedIde QTRY_VERIFY_WITH_TIMEOUT(host.fixLifecycleStateName() != QStringLiteral("executing"), 120000); QCOMPARE(host.fixLifecycleStateName(), QStringLiteral("succeeded")); - // Completion presents the revalidated result and the artifact Core signed, distinct - // from the as-received input and bound to the plan the operator approved. The run - // presents the published bytes; materializing them as a file is the export slice, so - // this asserts the presentation rather than a filesystem write. const QVariantMap recheck = host.fixRecheck(); QVERIFY(recheck.value(QStringLiteral("available")).toBool()); const QString verdictState = recheck.value(QStringLiteral("verdictState")).toString(); @@ -2554,6 +2551,8 @@ void EditorHostTest::outputRenderFormats() } QFile artifact(destination); QVERIFY(artifact.open(QIODevice::ReadOnly)); + QCOMPARE(QString::fromLatin1(QCryptographicHash::hash(artifact.readAll(), QCryptographicHash::Sha256).toHex()), + record.value(QStringLiteral("artifact")).toMap().value(QStringLiteral("sha256")).toString()); qInfo().noquote() << "quick-output-fixture" << format << "source_sha256=" << QString::fromLatin1(host.sessionForTest()->context().getDocumentPointer()->getSourceDataHash().toHex()) << "artifact_sha256=" << record.value(QStringLiteral("artifact")).toMap().value(QStringLiteral("sha256")).toString(); @@ -2579,6 +2578,29 @@ void EditorHostTest::outputOverprintWarning() QVERIFY(!host.outputRecord().value(QStringLiteral("printProof")).toBool()); } +void EditorHostTest::outputRenderFailure() +{ + pdf::PDFDocumentBuilder builder; + const auto page = builder.appendPage(QRectF(0, 0, 72, 144)); + const auto stream = builder.addObject(pdf::PDFObject::createStream( + std::make_shared(pdf::PDFDictionary(), QByteArray("unknownOperator")))); + pdf::PDFDictionary dictionary; + dictionary.addEntry(QByteArray("Contents"), pdf::PDFObject::createReference(stream)); + builder.mergeTo(page, pdf::PDFObject::createDictionary(std::make_shared(std::move(dictionary)))); + loopeditor::OutputRequest request; + request.document = pdf::PDFDocumentPointer(new pdf::PDFDocument(builder.build())); + request.format = loopeditor::OutputFormat::Png; + request.revision = QStringLiteral("broken-content-fixture"); + request.dpi = 72; + request.fidelityAcknowledged = true; + loopeditor::OutputResult result; + pdf::PDFJobContext job(std::make_shared(), pdf::PDFProcessingLimits::conservativeDefaults(), {}); + loopeditor::prepareOutput(request, result, job); + QVERIFY2(result.error.startsWith(QLatin1String("output/render-failed")), qPrintable(result.error)); + QVERIFY(!QFile::exists(result.stagedPath)); + QVERIFY(!result.record.contains(QStringLiteral("artifact"))); +} + QTEST_GUILESS_MAIN(EditorHostTest) #include "tst_editorhosttest.moc" From 10d246fbc3089d465f49bf305bd525426fbcdcf1 Mon Sep 17 00:00:00 2001 From: mberrys <212453881+mberrys@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:47:38 -0700 Subject: [PATCH 3/7] Label export resolution and reflect restricted print permissions --- LoopEditor/qml/Main.qml | 6 +++++- docs/QUICK_PRINT_EXPORT.md | 2 +- tools/ProductQuickAccessibilitySmoke/qml/Main.qml | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/LoopEditor/qml/Main.qml b/LoopEditor/qml/Main.qml index 641507d20..d86c4b2c5 100644 --- a/LoopEditor/qml/Main.qml +++ b/LoopEditor/qml/Main.qml @@ -174,10 +174,14 @@ ApplicationWindow { Accessible.name: qsTr("Export format") Layout.fillWidth: true } + Label { + text: qsTr("Resolution (dpi)") + visible: outputResolution.visible + } SpinBox { id: outputResolution from: 72 - to: 600 + to: outputDialog.printing && host && !host.documentModel.canHighResolutionPrint ? 150 : 600 value: 300 editable: true visible: outputDialog.printing || outputFormat.currentIndex !== 0 diff --git a/docs/QUICK_PRINT_EXPORT.md b/docs/QUICK_PRINT_EXPORT.md index b1cec1fa2..4db809d26 100644 --- a/docs/QUICK_PRINT_EXPORT.md +++ b/docs/QUICK_PRINT_EXPORT.md @@ -1,7 +1,7 @@ # Quick print and export Issue [#123](https://github.com/studio-berry/loop/issues/123) adds File → Export -document and Document → Print to PDF. The approved scope for this PR is +document and File → Print to PDF. The approved scope for this PR is print-to-PDF and PDF/PNG/TIFF exports. Native printer spooling remains outside this slice; the product continues to exclude Qt PrintSupport and Widgets. diff --git a/tools/ProductQuickAccessibilitySmoke/qml/Main.qml b/tools/ProductQuickAccessibilitySmoke/qml/Main.qml index 641507d20..d86c4b2c5 100644 --- a/tools/ProductQuickAccessibilitySmoke/qml/Main.qml +++ b/tools/ProductQuickAccessibilitySmoke/qml/Main.qml @@ -174,10 +174,14 @@ ApplicationWindow { Accessible.name: qsTr("Export format") Layout.fillWidth: true } + Label { + text: qsTr("Resolution (dpi)") + visible: outputResolution.visible + } SpinBox { id: outputResolution from: 72 - to: 600 + to: outputDialog.printing && host && !host.documentModel.canHighResolutionPrint ? 150 : 600 value: 300 editable: true visible: outputDialog.printing || outputFormat.currentIndex !== 0 From 932e8418392567862eb2fbcbb639b022c66e77ce Mon Sep 17 00:00:00 2001 From: mberrys <212453881+mberrys@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:57:52 -0700 Subject: [PATCH 4/7] Read Core render diagnostics through a private output adapter --- LoopEditor/quickoutput.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/LoopEditor/quickoutput.cpp b/LoopEditor/quickoutput.cpp index e22ec7601..b3526aa46 100644 --- a/LoopEditor/quickoutput.cpp +++ b/LoopEditor/quickoutput.cpp @@ -18,6 +18,16 @@ namespace loopeditor { +namespace +{ +class OutputRenderer final : public pdf::PDFTransparencyRenderer +{ +public: + using pdf::PDFPageContentProcessor::getRenderErrors; + using pdf::PDFTransparencyRenderer::PDFTransparencyRenderer; +}; +} + void prepareOutput(const OutputRequest& request, OutputResult& result, pdf::PDFJobContext& job) { if (!request.document || request.revision.isEmpty() || request.dpi < 72 || request.dpi > 600) @@ -124,8 +134,8 @@ void prepareOutput(const OutputRequest& request, OutputResult& result, pdf::PDFJ const QSize pixels(qCeil(points.width() * request.dpi / 72.0), qCeil(points.height() * request.dpi / 72.0)); job.processingBudget().chargeRenderPixels(uint64_t(pixels.width()) * uint64_t(pixels.height()), QStringLiteral("Quick output")); const QTransform matrix = pdf::PDFRenderer::createPagePointToDevicePointMatrix(page, QRectF(QPointF(), QSizeF(pixels))); - pdf::PDFTransparencyRenderer renderer(page, request.document.data(), session->getFontCache(), session->getCMS(), - &optionalContent, &inks, settings, matrix); + OutputRenderer renderer(page, request.document.data(), session->getFontCache(), session->getCMS(), + &optionalContent, &inks, settings, matrix); renderer.setOperationControl(job.operationControl()); renderer.beginPaint(pixels); renderer.processContents(); From b9dccb3aa620820b6739542be2066f3d065314dd Mon Sep 17 00:00:00 2001 From: mberrys <212453881+mberrys@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:59:42 -0700 Subject: [PATCH 5/7] Update command catalog expectations and render-error fixture types --- UnitTests/tst_documentfacadetest.cpp | 4 ++-- UnitTests/tst_editorhosttest.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/UnitTests/tst_documentfacadetest.cpp b/UnitTests/tst_documentfacadetest.cpp index ed124cc98..82e31628c 100644 --- a/UnitTests/tst_documentfacadetest.cpp +++ b/UnitTests/tst_documentfacadetest.cpp @@ -404,7 +404,7 @@ void DocumentFacadeTest::catalogLoadsTheWholeEditorActionSet() // library that lost its resource would present an empty command set as a // working one, so this also pins the resource wiring. QVERIFY2(catalog.isLoaded(), qPrintable(catalog.loadError())); - QCOMPARE(catalog.descriptors().size(), 108); + QCOMPARE(catalog.descriptors().size(), 109); const pdfinteraction::CommandDescriptor* open = catalog.descriptor(pdfinteraction::DocumentFacade::OpenCommandId); @@ -436,7 +436,7 @@ void DocumentFacadeTest::catalogLoadsTheWholeEditorActionSet() QVERIFY(descriptor.capability != pdfinteraction::CommandCapability::Unclassified); } } - QCOMPARE(implemented, 26); + QCOMPARE(implemented, 28); } void DocumentFacadeTest::catalogPublishesAvailabilityAtomically() diff --git a/UnitTests/tst_editorhosttest.cpp b/UnitTests/tst_editorhosttest.cpp index 6f5469966..ab0842b93 100644 --- a/UnitTests/tst_editorhosttest.cpp +++ b/UnitTests/tst_editorhosttest.cpp @@ -2585,7 +2585,7 @@ void EditorHostTest::outputRenderFailure() const auto stream = builder.addObject(pdf::PDFObject::createStream( std::make_shared(pdf::PDFDictionary(), QByteArray("unknownOperator")))); pdf::PDFDictionary dictionary; - dictionary.addEntry(QByteArray("Contents"), pdf::PDFObject::createReference(stream)); + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("Contents"), pdf::PDFObject::createReference(stream)); builder.mergeTo(page, pdf::PDFObject::createDictionary(std::make_shared(std::move(dictionary)))); loopeditor::OutputRequest request; request.document = pdf::PDFDocumentPointer(new pdf::PDFDocument(builder.build())); From 0b89f0ab795563937434b9d5bf6aa1e1821b1d40 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 11:55:48 +0000 Subject: [PATCH 6/7] Pass the settings argument to every EditorHost constructor call The editor host constructor now takes the settings store first, then the parent and recipes directory. The merged tests still used the old two-argument form, which fails to compile. Pass a null settings argument at each recipe-path call. Co-Authored-By: Claude Haiku 5.5 Claude-Session: https://claude.ai/code/session_01Lqo8EDq3w2nU1zekEZDoZc --- UnitTests/tst_editorhosttest.cpp | 70 ++++++++++++++-------------- UnitTests/tst_shellworkspacetest.cpp | 16 +++---- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/UnitTests/tst_editorhosttest.cpp b/UnitTests/tst_editorhosttest.cpp index d5994a5da..382684aee 100644 --- a/UnitTests/tst_editorhosttest.cpp +++ b/UnitTests/tst_editorhosttest.cpp @@ -365,7 +365,7 @@ void EditorHostTest::encryptedOpen() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("encrypted.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::Algorithm(algorithm), QStringLiteral("fixture-user"), userPermissions)); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); const quint64 request = host.passwordRequestId(); @@ -402,7 +402,7 @@ void EditorHostTest::passwordFailureIsTypedAndRetryable() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("wrong-password-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); QVERIFY(host.submitPassword(host.passwordRequestId(), QStringLiteral("incorrect-fixture-password"))); @@ -421,7 +421,7 @@ void EditorHostTest::passwordCancelCloseAndSupersessionAreTerminal() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("cancelled-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); QVERIFY(host.cancelPassword(host.passwordRequestId())); @@ -455,7 +455,7 @@ void EditorHostTest::teardownWhilePasswordPromptIsPending() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("teardown-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - auto host = std::make_unique(nullptr, m_fixtureRecipes->path()); + auto host = std::make_unique(nullptr, nullptr, m_fixtureRecipes->path()); host->openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host->passwordRequestId() != 0, 10000); QElapsedTimer timer; @@ -476,7 +476,7 @@ void EditorHostTest::restrictedDocumentCannotPlanOrPublish() QVERIFY(source.open(QIODevice::ReadOnly)); const QByteArray original = source.readAll(); source.close(); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 10000); QCOMPARE(host.passwordRequestId(), quint64(0)); @@ -526,7 +526,7 @@ void EditorHostTest::encryptedInspectionReceiptContainsNoPassword() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("inspection-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); QVERIFY(host.submitPassword(host.passwordRequestId(), QStringLiteral("fixture-user"))); @@ -562,7 +562,7 @@ void EditorHostTest::teardownClearsTheInteractiveThreadRegistration() { QVERIFY(!pdf::PDFBlockingThreadGuard::isInteractiveThreadRegistered()); { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(pdf::PDFBlockingThreadGuard::isInteractiveThreadRegistered()); QVERIFY(pdf::PDFBlockingThreadGuard::isCurrentThreadInteractive()); } @@ -574,7 +574,7 @@ void EditorHostTest::teardownClearsTheInteractiveThreadRegistration() void EditorHostTest::startsWithNoDocument() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QCOMPARE(host.documentState(), QStringLiteral("empty")); QVERIFY(!host.hasDocument()); QCOMPARE(host.pageCount(), 0); @@ -593,7 +593,7 @@ void EditorHostTest::startsWithNoDocument() void EditorHostTest::exposesCatalogDescriptorsWithoutMutating() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); const QVariantList descriptors = host.commandDescriptors(); QVERIFY(descriptors.size() > 100); @@ -613,7 +613,7 @@ void EditorHostTest::exposesCatalogDescriptorsWithoutMutating() void EditorHostTest::navigationCommandsStayDisabledUntilOpen() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(!host.isCommandEnabled(QStringLiteral("actionGoToNextPage"))); QCOMPARE(host.invokeCommand(QStringLiteral("actionGoToNextPage")), quint64(0)); } @@ -662,7 +662,7 @@ void EditorHostTest::preflightRunsOffInteractiveThread() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); QVERIFY(host.runPreflight()); @@ -701,7 +701,7 @@ void EditorHostTest::preflightStateVisualIsNotCheckedBeforeARun() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -803,7 +803,7 @@ void EditorHostTest::exportedPreflightReportMatchesPdfToolForTheSameInputs() // 2. The GUI's report, through the export path the operator uses: // EditorHost::exportPreflightReportFileUrl -> PreflightController::serializedReport. - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); @@ -894,7 +894,7 @@ void EditorHostTest::restrictedPreflightReportMatchesPdfTool() QCOMPARE(cliScope.value(QStringLiteral("pages")).toArray(), QJsonArray{ 1 }); QCOMPARE(cliScope.value(QStringLiteral("object_classes")).toArray(), QJsonArray{ QStringLiteral("image") }); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(QUrl::fromLocalFile(profilePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); @@ -946,7 +946,7 @@ void EditorHostTest::importValidProfileAddsDigest() const QString expectedDigest = profile.value(QStringLiteral("digest")).toString(); QVERIFY(!expectedDigest.isEmpty()); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(QUrl::fromLocalFile(sourcePath))); const QString expectedId = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)) @@ -984,7 +984,7 @@ void EditorHostTest::importDigestMismatchProfileIsRejected() QVERIFY(sourceFile.open(QIODevice::WriteOnly)); sourceFile.write(QJsonDocument(profile).toJson(QJsonDocument::Compact)); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(!host.importPreflightProfileFileUrl(QUrl::fromLocalFile(sourcePath))); } @@ -993,7 +993,7 @@ void EditorHostTest::saveProfileForkRecordsDerivedFrom() QTemporaryDir temp; QVERIFY(temp.isValid()); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QString parentDigest; for (const QVariant& entry : host.preflightProfiles()) { @@ -1047,7 +1047,7 @@ void EditorHostTest::openLargeDocument() QElapsedTimer openTimer; openTimer.start(); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 90000); const qint64 openToFirstViewMs = openTimer.elapsed(); @@ -1238,7 +1238,7 @@ void EditorHostTest::openLargeDocument() void EditorHostTest::fixWorkspacePresentsIdleLifecycleAndRefusesToArm() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QCOMPARE(host.fixLifecycleStateName(), QStringLiteral("idle")); QVERIFY(!host.fixLifecycleSummary().trimmed().isEmpty()); @@ -1306,7 +1306,7 @@ void EditorHostTest::fixReviewBindsToThePlannedDigestAndTheCurrentRevision() recipe.close(); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); // The catalog identifies a recipe by the file that defines it, and importing selects it. QVERIFY(!host.selectedActionListRecipeId().isEmpty()); @@ -1419,7 +1419,7 @@ void EditorHostTest::moveSelectionProposesAPageBoxMoveInTheFixWorkspace() recipe.close(); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(!host.isCommandEnabled(commandId)); QCOMPARE(host.invokeCommand(commandId, moveParameters(QStringLiteral("PageBox"), 4.0, 5.0)), quint64(0)); @@ -1616,7 +1616,7 @@ bool writeSignatureAppendedCopy(const QString& sourcePath, const QString& destin void EditorHostTest::shippedRecipeIsListedButNeverSelectedByDefault() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); bool listed = false; for (const QVariant& entry : host.actionListRecipes()) { @@ -1651,7 +1651,7 @@ void EditorHostTest::shippedPageBoxRecipeRunsDefaultMoveThroughPlanApprovalAndRe const QByteArray sourceDigest = fileDigest(documentPath); QVERIFY(!sourceDigest.isEmpty()); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -1700,7 +1700,7 @@ void EditorHostTest::operatorRecipeWinsOverShippedRecipe() const QString recipePath = directory.filePath(QStringLiteral("operator-move.json")); QVERIFY(writeTranslatePageBoxRecipe(recipePath, QStringLiteral("operator-move"), 0)); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -1730,7 +1730,7 @@ void EditorHostTest::invalidOperatorRecipeStopsMoveWithoutShippedFallback() const QString brokenPath = QDir(m_fixtureRecipes->path()).filePath(QStringLiteral("broken-move.json")); QVERIFY(writeTranslatePageBoxRecipe(brokenPath, QStringLiteral("broken-move"), QStringLiteral("first"))); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -1772,7 +1772,7 @@ void EditorHostTest::completedDragOfARefusedKindIsReportedAndChangesNothing() QVERIFY(writer.write(documentPath, &document, true)); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); DocumentViewSession* session = host.sessionForTest(); @@ -1866,7 +1866,7 @@ void EditorHostTest::fixJourneyPublishesOnlyAnApprovedPlanBoundToTheDisplayedIde const QByteArray sourceDigest = fileDigest(documentPath); QVERIFY(!sourceDigest.isEmpty()); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2157,7 +2157,7 @@ void EditorHostTest::translatePageBoxRefusesAMoveThatIntroducesABlockingFinding( recipe.close(); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); @@ -2220,7 +2220,7 @@ void EditorHostTest::fixRollbackReturnsToARecordedRevision() QCOMPARE(process.exitCode(), 0); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(repairedPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2327,7 +2327,7 @@ void EditorHostTest::preflightFencesCompletionsThatLostTheirRequestIdentity() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2440,7 +2440,7 @@ void EditorHostTest::actionListFencesCompletionsThatLostTheirRequestIdentity() recipe.close(); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2729,7 +2729,7 @@ void EditorHostTest::previewFidelityNamesTheOriginAndSwitchesExplicitly() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); QCOMPARE(host.previewFidelityStateName(), QStringLiteral("unavailable")); @@ -2754,7 +2754,7 @@ void EditorHostTest::previewFidelityNamesTheOriginAndSwitchesExplicitly() preflightFixturesDir() + QStringLiteral("/overprint-cmyk-mode1-on.pdf"); QVERIFY2(QFileInfo::exists(overprintPath), qPrintable(overprintPath)); - EditorHost overprintHost(nullptr, m_fixtureRecipes->path()); + EditorHost overprintHost(nullptr, nullptr, m_fixtureRecipes->path()); overprintHost.openFileUrl(QUrl::fromLocalFile(overprintPath)); QTRY_VERIFY_WITH_TIMEOUT(overprintHost.hasDocument(), 30000); overprintHost.setViewportGeometry(96.0 / 25.4, 1.0, 1024, 768); @@ -2779,7 +2779,7 @@ void EditorHostTest::previewFidelityNamesTheOriginAndSwitchesExplicitly() void EditorHostTest::refusedRenderCannotBecomeAuthoritativeEvidence() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(preflightFixturesDir() + QStringLiteral("/overprint-cmyk-mode1-on.pdf"))); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); auto* surfaces = host.sessionForTest()->surfaces(); @@ -2845,7 +2845,7 @@ void EditorHostTest::executeApprovedActionListPlanRefusesAnUnreviewedPlan() const QByteArray sourceDigest = fileDigest(documentPath); QVERIFY(!sourceDigest.isEmpty()); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); diff --git a/UnitTests/tst_shellworkspacetest.cpp b/UnitTests/tst_shellworkspacetest.cpp index 39bb68fcf..0aa2b1f5b 100644 --- a/UnitTests/tst_shellworkspacetest.cpp +++ b/UnitTests/tst_shellworkspacetest.cpp @@ -77,7 +77,7 @@ void ShellWorkspaceTest::cleanup() void ShellWorkspaceTest::workspaceTransitionsPreserveClosedDocumentState() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QCOMPARE(host.workspace(), EditorHost::Document); QCOMPARE(host.documentShellStatus(), QStringLiteral("NO_DOCUMENT")); QCOMPARE(host.preflightStateName(), QStringLiteral("not-checked")); @@ -112,7 +112,7 @@ void ShellWorkspaceTest::workspaceTransitionsPreserveOpenDocumentAndPreflightSta const QString path = directory.filePath(QStringLiteral("shell.pdf")); QVERIFY(writer.write(path, &document, true)); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY(host.hasDocument()); @@ -140,7 +140,7 @@ void ShellWorkspaceTest::workspaceTransitionsPreserveOpenDocumentAndPreflightSta void ShellWorkspaceTest::compareWorkspaceIsReachable() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QCOMPARE(host.workspace(), EditorHost::Document); QVERIFY(host.isWorkspaceEnabled(EditorHost::Compare)); @@ -156,7 +156,7 @@ void ShellWorkspaceTest::compareWorkspaceIsReachable() void ShellWorkspaceTest::menuPolicyRoutesVisibleActions() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); const QVariantList descriptors = host.commandDescriptors(); QVERIFY(descriptors.size() > 100); @@ -194,7 +194,7 @@ void ShellWorkspaceTest::menuPolicyRoutesVisibleActions() void ShellWorkspaceTest::developerDiagnosticsFollowReleaseProfile() { - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); #ifdef LOOP_LOOP_DISTRIBUTION_BUILD QVERIFY(!host.allowDeveloperDiagnostics()); #else @@ -282,7 +282,7 @@ void ShellWorkspaceTest::samplesExerciseRealCoreOutcomes() const QVariantMap descriptor = job.toMap(); const QVariantMap sample = help.prepareSample(descriptor.value(QStringLiteral("id")).toString()); QVERIFY(sample.value(QStringLiteral("ok")).toBool()); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(sample.value(QStringLiteral("profile")).toUrl())); host.openFileUrl(sample.value(QStringLiteral("pdf")).toUrl()); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); @@ -305,7 +305,7 @@ void ShellWorkspaceTest::correctionSampleRunsGovernedCore() OperatorHelp help(settings, directory.filePath(QStringLiteral("samples"))); const QVariantMap sample = help.prepareSample(QStringLiteral("correction")); QVERIFY(sample.value(QStringLiteral("ok")).toBool()); - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(sample.value(QStringLiteral("profile")).toUrl())); QVERIFY(host.importActionListRecipe(sample.value(QStringLiteral("recipe")).toUrl())); host.openFileUrl(sample.value(QStringLiteral("pdf")).toUrl()); @@ -331,7 +331,7 @@ void ShellWorkspaceTest::helpDialogShowsCatalogLimitsAndWelcome() QSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); OperatorHelp help(settings, directory.filePath(QStringLiteral("samples"))); QQmlEngine engine; - EditorHost host(nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); QQmlComponent component(&engine, QUrl::fromLocalFile(QStringLiteral(LOOP_OPERATOR_SOURCE_DIR "/LoopEditor/qml/OperatorHelpDialog.qml"))); QTRY_VERIFY_WITH_TIMEOUT(component.status() != QQmlComponent::Loading, 10000); QVERIFY2(!component.isError(), qPrintable(component.errorString())); From cd1ba78c0069df6f55c7ae28813ec9eebbfc2080 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 12:42:51 +0000 Subject: [PATCH 7/7] Restore the two-argument EditorHost constructor calls on this branch The settings-first constructor exists only on the epic branch. This branch still builds against dev, where the constructor takes the parent and the recipes directory, so the test calls I changed here do not compile. Restore the calls to the dev form. Co-Authored-By: Claude Haiku 5.5 Claude-Session: https://claude.ai/code/session_01Lqo8EDq3w2nU1zekEZDoZc --- UnitTests/tst_editorhosttest.cpp | 70 ++++++++++++++-------------- UnitTests/tst_shellworkspacetest.cpp | 16 +++---- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/UnitTests/tst_editorhosttest.cpp b/UnitTests/tst_editorhosttest.cpp index 382684aee..d5994a5da 100644 --- a/UnitTests/tst_editorhosttest.cpp +++ b/UnitTests/tst_editorhosttest.cpp @@ -365,7 +365,7 @@ void EditorHostTest::encryptedOpen() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("encrypted.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::Algorithm(algorithm), QStringLiteral("fixture-user"), userPermissions)); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); const quint64 request = host.passwordRequestId(); @@ -402,7 +402,7 @@ void EditorHostTest::passwordFailureIsTypedAndRetryable() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("wrong-password-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); QVERIFY(host.submitPassword(host.passwordRequestId(), QStringLiteral("incorrect-fixture-password"))); @@ -421,7 +421,7 @@ void EditorHostTest::passwordCancelCloseAndSupersessionAreTerminal() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("cancelled-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); QVERIFY(host.cancelPassword(host.passwordRequestId())); @@ -455,7 +455,7 @@ void EditorHostTest::teardownWhilePasswordPromptIsPending() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("teardown-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - auto host = std::make_unique(nullptr, nullptr, m_fixtureRecipes->path()); + auto host = std::make_unique(nullptr, m_fixtureRecipes->path()); host->openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host->passwordRequestId() != 0, 10000); QElapsedTimer timer; @@ -476,7 +476,7 @@ void EditorHostTest::restrictedDocumentCannotPlanOrPublish() QVERIFY(source.open(QIODevice::ReadOnly)); const QByteArray original = source.readAll(); source.close(); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 10000); QCOMPARE(host.passwordRequestId(), quint64(0)); @@ -526,7 +526,7 @@ void EditorHostTest::encryptedInspectionReceiptContainsNoPassword() QTemporaryDir directory; const QString path = directory.filePath(QStringLiteral("inspection-AES-256.pdf")); QVERIFY(writeEncryptedFixture(path, pdf::PDFSecurityHandlerFactory::AES_256)); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.passwordRequestId() != 0, 10000); QVERIFY(host.submitPassword(host.passwordRequestId(), QStringLiteral("fixture-user"))); @@ -562,7 +562,7 @@ void EditorHostTest::teardownClearsTheInteractiveThreadRegistration() { QVERIFY(!pdf::PDFBlockingThreadGuard::isInteractiveThreadRegistered()); { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(pdf::PDFBlockingThreadGuard::isInteractiveThreadRegistered()); QVERIFY(pdf::PDFBlockingThreadGuard::isCurrentThreadInteractive()); } @@ -574,7 +574,7 @@ void EditorHostTest::teardownClearsTheInteractiveThreadRegistration() void EditorHostTest::startsWithNoDocument() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QCOMPARE(host.documentState(), QStringLiteral("empty")); QVERIFY(!host.hasDocument()); QCOMPARE(host.pageCount(), 0); @@ -593,7 +593,7 @@ void EditorHostTest::startsWithNoDocument() void EditorHostTest::exposesCatalogDescriptorsWithoutMutating() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); const QVariantList descriptors = host.commandDescriptors(); QVERIFY(descriptors.size() > 100); @@ -613,7 +613,7 @@ void EditorHostTest::exposesCatalogDescriptorsWithoutMutating() void EditorHostTest::navigationCommandsStayDisabledUntilOpen() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(!host.isCommandEnabled(QStringLiteral("actionGoToNextPage"))); QCOMPARE(host.invokeCommand(QStringLiteral("actionGoToNextPage")), quint64(0)); } @@ -662,7 +662,7 @@ void EditorHostTest::preflightRunsOffInteractiveThread() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); QVERIFY(host.runPreflight()); @@ -701,7 +701,7 @@ void EditorHostTest::preflightStateVisualIsNotCheckedBeforeARun() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -803,7 +803,7 @@ void EditorHostTest::exportedPreflightReportMatchesPdfToolForTheSameInputs() // 2. The GUI's report, through the export path the operator uses: // EditorHost::exportPreflightReportFileUrl -> PreflightController::serializedReport. - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); @@ -894,7 +894,7 @@ void EditorHostTest::restrictedPreflightReportMatchesPdfTool() QCOMPARE(cliScope.value(QStringLiteral("pages")).toArray(), QJsonArray{ 1 }); QCOMPARE(cliScope.value(QStringLiteral("object_classes")).toArray(), QJsonArray{ QStringLiteral("image") }); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(QUrl::fromLocalFile(profilePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); @@ -946,7 +946,7 @@ void EditorHostTest::importValidProfileAddsDigest() const QString expectedDigest = profile.value(QStringLiteral("digest")).toString(); QVERIFY(!expectedDigest.isEmpty()); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(QUrl::fromLocalFile(sourcePath))); const QString expectedId = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)) @@ -984,7 +984,7 @@ void EditorHostTest::importDigestMismatchProfileIsRejected() QVERIFY(sourceFile.open(QIODevice::WriteOnly)); sourceFile.write(QJsonDocument(profile).toJson(QJsonDocument::Compact)); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(!host.importPreflightProfileFileUrl(QUrl::fromLocalFile(sourcePath))); } @@ -993,7 +993,7 @@ void EditorHostTest::saveProfileForkRecordsDerivedFrom() QTemporaryDir temp; QVERIFY(temp.isValid()); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QString parentDigest; for (const QVariant& entry : host.preflightProfiles()) { @@ -1047,7 +1047,7 @@ void EditorHostTest::openLargeDocument() QElapsedTimer openTimer; openTimer.start(); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 90000); const qint64 openToFirstViewMs = openTimer.elapsed(); @@ -1238,7 +1238,7 @@ void EditorHostTest::openLargeDocument() void EditorHostTest::fixWorkspacePresentsIdleLifecycleAndRefusesToArm() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QCOMPARE(host.fixLifecycleStateName(), QStringLiteral("idle")); QVERIFY(!host.fixLifecycleSummary().trimmed().isEmpty()); @@ -1306,7 +1306,7 @@ void EditorHostTest::fixReviewBindsToThePlannedDigestAndTheCurrentRevision() recipe.close(); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); // The catalog identifies a recipe by the file that defines it, and importing selects it. QVERIFY(!host.selectedActionListRecipeId().isEmpty()); @@ -1419,7 +1419,7 @@ void EditorHostTest::moveSelectionProposesAPageBoxMoveInTheFixWorkspace() recipe.close(); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(!host.isCommandEnabled(commandId)); QCOMPARE(host.invokeCommand(commandId, moveParameters(QStringLiteral("PageBox"), 4.0, 5.0)), quint64(0)); @@ -1616,7 +1616,7 @@ bool writeSignatureAppendedCopy(const QString& sourcePath, const QString& destin void EditorHostTest::shippedRecipeIsListedButNeverSelectedByDefault() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); bool listed = false; for (const QVariant& entry : host.actionListRecipes()) { @@ -1651,7 +1651,7 @@ void EditorHostTest::shippedPageBoxRecipeRunsDefaultMoveThroughPlanApprovalAndRe const QByteArray sourceDigest = fileDigest(documentPath); QVERIFY(!sourceDigest.isEmpty()); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -1700,7 +1700,7 @@ void EditorHostTest::operatorRecipeWinsOverShippedRecipe() const QString recipePath = directory.filePath(QStringLiteral("operator-move.json")); QVERIFY(writeTranslatePageBoxRecipe(recipePath, QStringLiteral("operator-move"), 0)); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -1730,7 +1730,7 @@ void EditorHostTest::invalidOperatorRecipeStopsMoveWithoutShippedFallback() const QString brokenPath = QDir(m_fixtureRecipes->path()).filePath(QStringLiteral("broken-move.json")); QVERIFY(writeTranslatePageBoxRecipe(brokenPath, QStringLiteral("broken-move"), QStringLiteral("first"))); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -1772,7 +1772,7 @@ void EditorHostTest::completedDragOfARefusedKindIsReportedAndChangesNothing() QVERIFY(writer.write(documentPath, &document, true)); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); DocumentViewSession* session = host.sessionForTest(); @@ -1866,7 +1866,7 @@ void EditorHostTest::fixJourneyPublishesOnlyAnApprovedPlanBoundToTheDisplayedIde const QByteArray sourceDigest = fileDigest(documentPath); QVERIFY(!sourceDigest.isEmpty()); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2157,7 +2157,7 @@ void EditorHostTest::translatePageBoxRefusesAMoveThatIntroducesABlockingFinding( recipe.close(); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); @@ -2220,7 +2220,7 @@ void EditorHostTest::fixRollbackReturnsToARecordedRevision() QCOMPARE(process.exitCode(), 0); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(repairedPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2327,7 +2327,7 @@ void EditorHostTest::preflightFencesCompletionsThatLostTheirRequestIdentity() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2440,7 +2440,7 @@ void EditorHostTest::actionListFencesCompletionsThatLostTheirRequestIdentity() recipe.close(); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); @@ -2729,7 +2729,7 @@ void EditorHostTest::previewFidelityNamesTheOriginAndSwitchesExplicitly() QVERIFY(writer.write(path, &document, true)); } - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); QCOMPARE(host.previewFidelityStateName(), QStringLiteral("unavailable")); @@ -2754,7 +2754,7 @@ void EditorHostTest::previewFidelityNamesTheOriginAndSwitchesExplicitly() preflightFixturesDir() + QStringLiteral("/overprint-cmyk-mode1-on.pdf"); QVERIFY2(QFileInfo::exists(overprintPath), qPrintable(overprintPath)); - EditorHost overprintHost(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost overprintHost(nullptr, m_fixtureRecipes->path()); overprintHost.openFileUrl(QUrl::fromLocalFile(overprintPath)); QTRY_VERIFY_WITH_TIMEOUT(overprintHost.hasDocument(), 30000); overprintHost.setViewportGeometry(96.0 / 25.4, 1.0, 1024, 768); @@ -2779,7 +2779,7 @@ void EditorHostTest::previewFidelityNamesTheOriginAndSwitchesExplicitly() void EditorHostTest::refusedRenderCannotBecomeAuthoritativeEvidence() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(preflightFixturesDir() + QStringLiteral("/overprint-cmyk-mode1-on.pdf"))); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); auto* surfaces = host.sessionForTest()->surfaces(); @@ -2845,7 +2845,7 @@ void EditorHostTest::executeApprovedActionListPlanRefusesAnUnreviewedPlan() const QByteArray sourceDigest = fileDigest(documentPath); QVERIFY(!sourceDigest.isEmpty()); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importActionListRecipe(QUrl::fromLocalFile(recipePath))); host.openFileUrl(QUrl::fromLocalFile(documentPath)); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 15000); diff --git a/UnitTests/tst_shellworkspacetest.cpp b/UnitTests/tst_shellworkspacetest.cpp index 0aa2b1f5b..39bb68fcf 100644 --- a/UnitTests/tst_shellworkspacetest.cpp +++ b/UnitTests/tst_shellworkspacetest.cpp @@ -77,7 +77,7 @@ void ShellWorkspaceTest::cleanup() void ShellWorkspaceTest::workspaceTransitionsPreserveClosedDocumentState() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QCOMPARE(host.workspace(), EditorHost::Document); QCOMPARE(host.documentShellStatus(), QStringLiteral("NO_DOCUMENT")); QCOMPARE(host.preflightStateName(), QStringLiteral("not-checked")); @@ -112,7 +112,7 @@ void ShellWorkspaceTest::workspaceTransitionsPreserveOpenDocumentAndPreflightSta const QString path = directory.filePath(QStringLiteral("shell.pdf")); QVERIFY(writer.write(path, &document, true)); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); host.openFileUrl(QUrl::fromLocalFile(path)); QTRY_VERIFY(host.hasDocument()); @@ -140,7 +140,7 @@ void ShellWorkspaceTest::workspaceTransitionsPreserveOpenDocumentAndPreflightSta void ShellWorkspaceTest::compareWorkspaceIsReachable() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QCOMPARE(host.workspace(), EditorHost::Document); QVERIFY(host.isWorkspaceEnabled(EditorHost::Compare)); @@ -156,7 +156,7 @@ void ShellWorkspaceTest::compareWorkspaceIsReachable() void ShellWorkspaceTest::menuPolicyRoutesVisibleActions() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); const QVariantList descriptors = host.commandDescriptors(); QVERIFY(descriptors.size() > 100); @@ -194,7 +194,7 @@ void ShellWorkspaceTest::menuPolicyRoutesVisibleActions() void ShellWorkspaceTest::developerDiagnosticsFollowReleaseProfile() { - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); #ifdef LOOP_LOOP_DISTRIBUTION_BUILD QVERIFY(!host.allowDeveloperDiagnostics()); #else @@ -282,7 +282,7 @@ void ShellWorkspaceTest::samplesExerciseRealCoreOutcomes() const QVariantMap descriptor = job.toMap(); const QVariantMap sample = help.prepareSample(descriptor.value(QStringLiteral("id")).toString()); QVERIFY(sample.value(QStringLiteral("ok")).toBool()); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(sample.value(QStringLiteral("profile")).toUrl())); host.openFileUrl(sample.value(QStringLiteral("pdf")).toUrl()); QTRY_VERIFY_WITH_TIMEOUT(host.hasDocument(), 30000); @@ -305,7 +305,7 @@ void ShellWorkspaceTest::correctionSampleRunsGovernedCore() OperatorHelp help(settings, directory.filePath(QStringLiteral("samples"))); const QVariantMap sample = help.prepareSample(QStringLiteral("correction")); QVERIFY(sample.value(QStringLiteral("ok")).toBool()); - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QVERIFY(host.importPreflightProfileFileUrl(sample.value(QStringLiteral("profile")).toUrl())); QVERIFY(host.importActionListRecipe(sample.value(QStringLiteral("recipe")).toUrl())); host.openFileUrl(sample.value(QStringLiteral("pdf")).toUrl()); @@ -331,7 +331,7 @@ void ShellWorkspaceTest::helpDialogShowsCatalogLimitsAndWelcome() QSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); OperatorHelp help(settings, directory.filePath(QStringLiteral("samples"))); QQmlEngine engine; - EditorHost host(nullptr, nullptr, m_fixtureRecipes->path()); + EditorHost host(nullptr, m_fixtureRecipes->path()); QQmlComponent component(&engine, QUrl::fromLocalFile(QStringLiteral(LOOP_OPERATOR_SOURCE_DIR "/LoopEditor/qml/OperatorHelpDialog.qml"))); QTRY_VERIFY_WITH_TIMEOUT(component.status() != QQmlComponent::Loading, 10000); QVERIFY2(!component.isError(), qPrintable(component.errorString()));