diff --git a/.github/workflows/WindowsInstall.yml b/.github/workflows/WindowsInstall.yml index 8e589fee..d4dfa288 100644 --- a/.github/workflows/WindowsInstall.yml +++ b/.github/workflows/WindowsInstall.yml @@ -317,7 +317,6 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SENTRY_ORG: berry-studios - SENTRY_PROJECT: loop-pdf SENTRY_URL: https://de.sentry.io run: | .\scripts\ci\upload_sentry_debug_files.ps1 -BuildDir "${env:GITHUB_WORKSPACE}\loop\build" diff --git a/.github/workflows/reusable-linux.yml b/.github/workflows/reusable-linux.yml index 9fc119e7..caedfb3a 100644 --- a/.github/workflows/reusable-linux.yml +++ b/.github/workflows/reusable-linux.yml @@ -76,7 +76,7 @@ jobs: working-directory: loop run: python3 scripts/ci/check_version_policy.py - name: Verify processing-budget exhaustion corpus - working-directory: loupe + working-directory: loop run: python3 scripts/budget_exhaustion/generate_corpus.py --check - name: Prepare vcpkg directories diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index a5e3e6bf..d45552f4 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -89,7 +89,7 @@ jobs: --page-count 256 --operations 256 --family pathological-vector - name: Verify processing-budget exhaustion corpus - working-directory: loupe + working-directory: loop shell: pwsh run: python scripts\budget_exhaustion\generate_corpus.py --check @@ -411,7 +411,6 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SENTRY_ORG: berry-studios - SENTRY_PROJECT: loop-pdf SENTRY_URL: https://de.sentry.io run: | .\scripts\ci\upload_sentry_debug_files.ps1 -BuildDir "${env:GITHUB_WORKSPACE}\loop\build" diff --git a/Desktop/io.github.mberrys.Loop-pdf.appdata.xml b/Desktop/io.github.mberrys.Loop-pdf.appdata.xml index f753da24..e8746b31 100644 --- a/Desktop/io.github.mberrys.Loop-pdf.appdata.xml +++ b/Desktop/io.github.mberrys.Loop-pdf.appdata.xml @@ -16,9 +16,9 @@ Utility Michael Berry - https://github.com/mberrys/Loop-pdf - https://github.com/mberrys/Loop-pdf/issues - https://github.com/mberrys/Loop-pdf + https://github.com/studio-berry/loop + https://github.com/studio-berry/loop/issues + https://github.com/studio-berry/loop io.github.mberrys.Loop-pdf.desktop diff --git a/LoopEditor/editorhost.cpp b/LoopEditor/editorhost.cpp index e0b0109e..dbfaca51 100644 --- a/LoopEditor/editorhost.cpp +++ b/LoopEditor/editorhost.cpp @@ -225,6 +225,13 @@ void EditorHost::goToPage(int pageIndex) bumpPresentation(); } +void EditorHost::goToOutlinePage(int pageIndex) +{ + // Outline navigation reuses the implemented viewport path; separate entry + // keeps QML from depending on an unimplemented goToOutlineIndex. + goToPage(pageIndex); +} + void EditorHost::acknowledgeWorkspaceRequest() { if (m_workspaceRequest < 0) @@ -236,6 +243,17 @@ void EditorHost::acknowledgeWorkspaceRequest() Q_EMIT presentationChanged(); } +void EditorHost::acknowledgeSearchPanel() +{ + if (!m_searchPanelVisible) + { + return; + } + + m_searchPanelVisible = false; + Q_EMIT presentationChanged(); +} + QString EditorHost::preflightStateName() const { return preflightStateToString(m_preflight.state()); diff --git a/LoopEditor/editorhost.h b/LoopEditor/editorhost.h index 0ed35526..794e3747 100644 --- a/LoopEditor/editorhost.h +++ b/LoopEditor/editorhost.h @@ -159,7 +159,9 @@ class EditorHost final : public QObject /// the document stays open. Q_INVOKABLE void toggleCurrentPageFidelity(); Q_INVOKABLE void goToPage(int pageIndex); + Q_INVOKABLE void goToOutlinePage(int pageIndex); Q_INVOKABLE void acknowledgeWorkspaceRequest(); + Q_INVOKABLE void acknowledgeSearchPanel(); Q_INVOKABLE QVariantList commandDescriptors() const; Q_INVOKABLE bool isCommandEnabled(const QString& commandId) const; diff --git a/LoopEditor/qml/DocumentPane.qml b/LoopEditor/qml/DocumentPane.qml index 6b80ef75..f03119c9 100644 --- a/LoopEditor/qml/DocumentPane.qml +++ b/LoopEditor/qml/DocumentPane.qml @@ -78,7 +78,9 @@ Item { width: outlineView.width text: model.display !== undefined ? model.display : title Accessible.name: text - onClicked: if (root.host && model.index !== undefined) root.host.goToOutlineIndex(model.index) + enabled: page >= 0 + onClicked: if (root.host && page >= 0) + root.host.goToOutlinePage(page) } Label { @@ -162,8 +164,10 @@ Item { Connections { target: root.host function onPresentationChanged() { - if (root.host && root.host.searchPanelVisible) + if (root.host && root.host.searchPanelVisible) { root.revealSearch() + root.host.acknowledgeSearchPanel() + } } } } diff --git a/LoopEditor/quickdocumentmodel.cpp b/LoopEditor/quickdocumentmodel.cpp index 08d15541..3145431f 100644 --- a/LoopEditor/quickdocumentmodel.cpp +++ b/LoopEditor/quickdocumentmodel.cpp @@ -1,6 +1,7 @@ // MIT License #include "quickdocumentmodel.h" +#include "pdfaction.h" #include "pdfcatalog.h" #include "pdfdocument.h" #include "pdfdocumentcontext.h" @@ -138,12 +139,29 @@ QVariant QuickOutlineModel::data(const QModelIndex& index, int role) const return node->item->getTitle(); if (role == HasChildrenRole) return !node->children.empty(); + if (role == PageRole) + { + const pdf::PDFAction* action = node->item->getAction(); + if (!action) + return -1; + if (action->getType() == pdf::ActionType::GoTo) + { + const auto* goTo = static_cast(action); + const pdf::PDFDestination& dest = goTo->getDestination(); + if (dest.isValid() && !dest.isNamedDestination()) + return static_cast(dest.getPageIndex()); + const pdf::PDFDestination& structDest = goTo->getStructureDestination(); + if (structDest.isValid() && !structDest.isNamedDestination()) + return static_cast(structDest.getPageIndex()); + } + return -1; + } return {}; } QHash QuickOutlineModel::roleNames() const { - return { { TitleRole, "title" }, { HasChildrenRole, "hasChildren" } }; + return { { TitleRole, "title" }, { HasChildrenRole, "hasChildren" }, { PageRole, "page" } }; } void QuickOutlineModel::build(Node* parent, const pdf::PDFOutlineItem* item) diff --git a/LoopEditor/quickdocumentmodel.h b/LoopEditor/quickdocumentmodel.h index b19e9962..6a39028d 100644 --- a/LoopEditor/quickdocumentmodel.h +++ b/LoopEditor/quickdocumentmodel.h @@ -66,6 +66,7 @@ class QuickOutlineModel final : public QAbstractItemModel { TitleRole = Qt::UserRole + 1, HasChildrenRole, + PageRole, }; explicit QuickOutlineModel(QObject* parent = nullptr); diff --git a/LoopLibCore/sources/pdfdocumentsearch.cpp b/LoopLibCore/sources/pdfdocumentsearch.cpp index 88a1813d..745b484f 100644 --- a/LoopLibCore/sources/pdfdocumentsearch.cpp +++ b/LoopLibCore/sources/pdfdocumentsearch.cpp @@ -33,7 +33,8 @@ PDFDocumentSearchResult searchDocumentText(PDFDocumentContext* context, const PDFPage* page = catalog->getPage(pageIndex); PDFTextLayoutGenerator generator(features, page, document, session->getFontCache(), session->getCMS(), - session->getOptionalContentActivity(), QTransform(), meshQuality); + session->getOptionalContentActivity(), QTransform(), meshQuality, + session->getProcessingBudget()); generator.processContents(); const PDFTextFlows flows = PDFTextFlow::createTextFlows( generator.createTextLayout(), diff --git a/LoopLibCore/sources/pdfrepairprimitives.cpp b/LoopLibCore/sources/pdfrepairprimitives.cpp index a6e0bea3..6e2a6810 100644 --- a/LoopLibCore/sources/pdfrepairprimitives.cpp +++ b/LoopLibCore/sources/pdfrepairprimitives.cpp @@ -465,6 +465,9 @@ PDFStandardConversionSettings standardConversionSettings(const QJsonObject& para ? parameters.value(QStringLiteral("normalize_color")).toBool() : (settings.target == PDFStandardTarget::PDFX1a2001 || settings.target == PDFStandardTarget::PDFX3_2002); settings.blackPointCompensation = parameters.value(QStringLiteral("black_point_compensation")).toBool(true); + settings.flattenTransparency = parameters.contains(QStringLiteral("flatten_transparency")) + ? parameters.value(QStringLiteral("flatten_transparency")).toBool() + : (settings.target == PDFStandardTarget::PDFX1a2001 || settings.target == PDFStandardTarget::PDFX3_2002); settings.independentValidatorProgram = parameters.value(QStringLiteral("validator_program")).toString(); const QJsonValue validatorArguments = parameters.value(QStringLiteral("validator_arguments")); if (validatorArguments.isArray()) @@ -496,6 +499,7 @@ QJsonObject standardConversionParameterSchema() { QStringLiteral("target_profile_name"), QJsonObject{ { QStringLiteral("type"), QStringLiteral("string") } } }, { QStringLiteral("normalize_color"), QJsonObject{ { QStringLiteral("type"), QStringLiteral("boolean") } } }, { QStringLiteral("black_point_compensation"), QJsonObject{ { QStringLiteral("type"), QStringLiteral("boolean") } } }, + { QStringLiteral("flatten_transparency"), QJsonObject{ { QStringLiteral("type"), QStringLiteral("boolean") } } }, { QStringLiteral("validator_program"), QJsonObject{ { QStringLiteral("type"), QStringLiteral("string") } } }, { QStringLiteral("validator_arguments"), QJsonObject{ { QStringLiteral("oneOf"), QJsonArray{ QJsonObject{ { QStringLiteral("type"), QStringLiteral("string") } }, QJsonObject{ { QStringLiteral("type"), QStringLiteral("array") }, { QStringLiteral("items"), QJsonObject{ { QStringLiteral("type"), QStringLiteral("string") } } } } } } } }, { QStringLiteral("validator_timeout_ms"), QJsonObject{ { QStringLiteral("type"), QStringLiteral("integer") }, { QStringLiteral("minimum"), 1000 }, { QStringLiteral("maximum"), 3600000 } } }, @@ -547,7 +551,8 @@ class PDFStandardConversionRepair final : public PDFRepairOperation plan->expectedChanges.outputIntent = true; plan->expectedChanges.pageBoxes = true; plan->expectedChanges.colorSpaces = settings.normalizeColor; - plan->expectedChanges.pageContent = settings.normalizeColor; + plan->expectedChanges.pageContent = settings.normalizeColor || settings.flattenTransparency; + plan->expectedChanges.images = settings.flattenTransparency; plan->validators = { PDFRepairValidatorKind::StructuralIntegrity, PDFRepairValidatorKind::OutputIntent, PDFRepairValidatorKind::NormalPreflight, diff --git a/LoopLibCore/sources/pdfstandardconversion.cpp b/LoopLibCore/sources/pdfstandardconversion.cpp index 72fff448..c7911f86 100644 --- a/LoopLibCore/sources/pdfstandardconversion.cpp +++ b/LoopLibCore/sources/pdfstandardconversion.cpp @@ -26,6 +26,7 @@ #include "pdfdocumentwriter.h" #include "pdfstreamfilters.h" #include "pdfrgbtocmykfixup.h" +#include "pdftransparencyflattener.h" #include "preflightengine.h" #include "pdfutils.h" #include "pdfworkloadenvelope.h" @@ -59,6 +60,13 @@ bool normalizesColorByDefault(PDFStandardTarget target) return target == PDFStandardTarget::PDFX1a2001 || target == PDFStandardTarget::PDFX3_2002; } +// PDF/X-1a and PDF/X-3 prohibit live transparency (see docs/PDFX_POLICY_MATRIX.md); +// PDF/X-4 and PDF/A-2b permit it, so flattening is opt-in there. +bool flattensTransparencyByDefault(PDFStandardTarget target) +{ + return target == PDFStandardTarget::PDFX1a2001 || target == PDFStandardTarget::PDFX3_2002; +} + QByteArray targetMarker(PDFStandardTarget target) { return pdfStandardTargetToString(target).toUtf8(); @@ -217,13 +225,14 @@ void collectPreflightBlockers(const PDFStandardConversionSettings& settings, } const bool normalizeColor = settings.normalizeColor || normalizesColorByDefault(settings.target); + const bool flattenTransparency = settings.flattenTransparency || flattensTransparencyByDefault(settings.target); for (const PDFXRuleResult& rule : result.pdfx->rules) { if (rule.state != PDFXRuleState::Failed && rule.state != PDFXRuleState::NotInspected) { continue; } - const bool fixable = rule.ruleId == QStringLiteral("pdfx.metadata.identification") || rule.ruleId == QStringLiteral("pdfx.output-intent.present") || rule.ruleId == QStringLiteral("pdfx.output-intent.identity") || rule.ruleId == QStringLiteral("pdfx.output-intent.subtype") || rule.ruleId == QStringLiteral("pdfx.output-intent.profile") || rule.ruleId == QStringLiteral("pdfx.output-intent.profile-space") || rule.ruleId == QStringLiteral("pdfx.page.trim-box") || rule.ruleId == QStringLiteral("pdfx.page.bleed-box") || rule.ruleId == QStringLiteral("pdfx.document.version") || (rule.ruleId == QStringLiteral("pdfx.color.device-rgb") && normalizeColor); + const bool fixable = rule.ruleId == QStringLiteral("pdfx.metadata.identification") || rule.ruleId == QStringLiteral("pdfx.output-intent.present") || rule.ruleId == QStringLiteral("pdfx.output-intent.identity") || rule.ruleId == QStringLiteral("pdfx.output-intent.subtype") || rule.ruleId == QStringLiteral("pdfx.output-intent.profile") || rule.ruleId == QStringLiteral("pdfx.output-intent.profile-space") || rule.ruleId == QStringLiteral("pdfx.page.trim-box") || rule.ruleId == QStringLiteral("pdfx.page.bleed-box") || rule.ruleId == QStringLiteral("pdfx.document.version") || (rule.ruleId == QStringLiteral("pdfx.color.device-rgb") && normalizeColor) || (rule.ruleId == QStringLiteral("pdfx.transparency.allowed") && flattenTransparency); if (!fixable) { report->blockers.append(rule.ruleId + QStringLiteral(": ") + rule.diagnostic); @@ -402,7 +411,8 @@ QJsonObject PDFStandardConversionReport::toJson() const { QStringLiteral("changes"), changesArray }, { QStringLiteral("blockers"), QJsonArray::fromStringList(blockers) }, { QStringLiteral("warnings"), QJsonArray::fromStringList(warnings) }, - { QStringLiteral("validator"), validator } + { QStringLiteral("validator"), validator }, + { QStringLiteral("transparency_flatten"), transparencyFlatten } }; } @@ -419,6 +429,7 @@ PDFOperationResult PDFStandardConversion::preview(const PDFDocument* document, report->blockers.clear(); report->warnings.clear(); report->preflightBefore = QJsonObject(); + report->transparencyFlatten = QJsonObject(); const PDFOperationResult profileResult = validateIcc(settings); if (!profileResult) @@ -453,6 +464,12 @@ PDFOperationResult PDFStandardConversion::preview(const PDFDocument* document, } } + const bool flattenTransparency = settings.flattenTransparency || flattensTransparencyByDefault(settings.target); + if (flattenTransparency && PDFTransparencyFlattener::hasLiveTransparency(document)) + { + report->changes.append({ QStringLiteral("transparency.flatten"), QStringLiteral("live transparency"), QStringLiteral("flattened to opaque raster content") }); + } + if (isPDFX(settings.target)) { PDFDocument copy = *document; @@ -501,6 +518,20 @@ PDFOperationResult PDFStandardConversion::apply(PDFDocument* document, } } + const bool flattenTransparency = settings.flattenTransparency || flattensTransparencyByDefault(settings.target); + if (flattenTransparency) + { + PDFTransparencyFlattenSettings transparencySettings = settings.transparencyFlattenSettings; + transparencySettings.analyzeOnly = false; + PDFTransparencyFlattenReport transparencyReport; + const PDFOperationResult transparencyResult = PDFTransparencyFlattener::apply(&candidate, transparencySettings, &transparencyReport); + report->transparencyFlatten = transparencyReport.toJson(); + if (!transparencyResult) + { + return transparencyResult; + } + } + PDFDocumentBuilder builder(&candidate); const PDFVersion version = minimumVersion(settings.target); addVersion(&builder, version); diff --git a/LoopLibCore/sources/pdfstandardconversion.h b/LoopLibCore/sources/pdfstandardconversion.h index 2946007d..38cfb58a 100644 --- a/LoopLibCore/sources/pdfstandardconversion.h +++ b/LoopLibCore/sources/pdfstandardconversion.h @@ -25,6 +25,7 @@ #include "pdfdocument.h" #include "pdfglobal.h" +#include "pdftransparencyflattener.h" #include "pdfutils.h" // PDFOperationResult, returned by preview()/apply() below #include @@ -45,7 +46,7 @@ enum class PDFStandardTarget LOOPLIBCORESHARED_EXPORT QString pdfStandardTargetToString(PDFStandardTarget target); LOOPLIBCORESHARED_EXPORT bool pdfStandardTargetFromString(const QString& value, - PDFStandardTarget* target); + PDFStandardTarget* target); LOOPLIBCORESHARED_EXPORT QStringList supportedPDFStandardTargets(); struct LOOPLIBCORESHARED_EXPORT PDFStandardConversionSettings @@ -56,6 +57,8 @@ struct LOOPLIBCORESHARED_EXPORT PDFStandardConversionSettings QString outputIntentName; bool normalizeColor = false; bool blackPointCompensation = true; + bool flattenTransparency = false; + PDFTransparencyFlattenSettings transparencyFlattenSettings; QString independentValidatorProgram; QStringList independentValidatorArguments; int independentValidatorTimeoutMs = 120000; @@ -83,6 +86,7 @@ struct LOOPLIBCORESHARED_EXPORT PDFStandardConversionReport QStringList blockers; QStringList warnings; QJsonObject validator; + QJsonObject transparencyFlatten; QJsonObject toJson() const; }; diff --git a/LoopLibCore/sources/pdftextlayoutgenerator.h b/LoopLibCore/sources/pdftextlayoutgenerator.h index 721948fd..3bdda43e 100644 --- a/LoopLibCore/sources/pdftextlayoutgenerator.h +++ b/LoopLibCore/sources/pdftextlayoutgenerator.h @@ -37,11 +37,11 @@ class LOOPLIBCORESHARED_EXPORT PDFTextLayoutGenerator : public PDFPageContentPro const PDFCMS* cms, const PDFOptionalContentActivity* optionalContentActivity, QTransform pagePointToDevicePointMatrix, - const PDFMeshQualitySettings& meshQualitySettings) : - BaseClass(page, document, fontCache, cms, optionalContentActivity, pagePointToDevicePointMatrix, meshQualitySettings), + const PDFMeshQualitySettings& meshQualitySettings, + PDFProcessingBudget* processingBudget = nullptr) : + BaseClass(page, document, fontCache, cms, optionalContentActivity, pagePointToDevicePointMatrix, meshQualitySettings, processingBudget), m_features(features) { - } /// Creates text layout from the text diff --git a/UnitTests/testdata/budget-exhaustion/manifest.json b/UnitTests/testdata/budget-exhaustion/manifest.json index 12642000..80f90486 100644 --- a/UnitTests/testdata/budget-exhaustion/manifest.json +++ b/UnitTests/testdata/budget-exhaustion/manifest.json @@ -187,6 +187,6 @@ } ], "generated_by": "scripts/budget_exhaustion/generate_corpus.py", - "schema_kind": "loupe-processing-budget-exhaustion-corpus", + "schema_kind": "loop-processing-budget-exhaustion-corpus", "schema_version": 2 } diff --git a/UnitTests/tst_budgetexhaustiontest.cpp b/UnitTests/tst_budgetexhaustiontest.cpp index b7334d86..04ea24f1 100644 --- a/UnitTests/tst_budgetexhaustiontest.cpp +++ b/UnitTests/tst_budgetexhaustiontest.cpp @@ -130,7 +130,7 @@ QList loadCorpus() } const QJsonObject root = document.object(); - if (root.value(QStringLiteral("schema_kind")).toString() != QLatin1String("loupe-processing-budget-exhaustion-corpus") || root.value(QStringLiteral("schema_version")).toInt() != 2) + if (root.value(QStringLiteral("schema_kind")).toString() != QLatin1String("loop-processing-budget-exhaustion-corpus") || root.value(QStringLiteral("schema_version")).toInt() != 2) { qFatal("Unexpected generated budget exhaustion corpus schema"); } diff --git a/UnitTests/tst_documentfacadetest.cpp b/UnitTests/tst_documentfacadetest.cpp index 43af532f..8d8a71f8 100644 --- a/UnitTests/tst_documentfacadetest.cpp +++ b/UnitTests/tst_documentfacadetest.cpp @@ -342,7 +342,7 @@ void DocumentFacadeTest::catalogLoadsTheWholeEditorActionSet() QVERIFY(descriptor.capability != pdfinteraction::CommandCapability::Unclassified); } } - QCOMPARE(implemented, 16); + QCOMPARE(implemented, 25); } void DocumentFacadeTest::catalogPublishesAvailabilityAtomically() diff --git a/changes/cc-pensive-cerf-kkvr3f.md b/changes/cc-pensive-cerf-kkvr3f.md new file mode 100644 index 00000000..6b30150d --- /dev/null +++ b/changes/cc-pensive-cerf-kkvr3f.md @@ -0,0 +1,20 @@ +Category: added +Audience: developers, print-production operators +Breaking-Change: no +Summary: Wire the existing PDFTransparencyFlattener operation (issue #164) into +the standards-convert Core operation (issue #167) so PDF/X-1a:2001 and +PDF/X-3:2002 conversion no longer treats live transparency as an unconditional, +unfixable blocker. Flattening runs by default for those two targets (which +prohibit live transparency) before the output-intent and page-box rewrite, is +reported as a real content change under a new transparency_flatten report +field (never a silent approximation), and remains skippable/enable-able via a +new flatten_transparency parameter surfaced identically through PdfTool's +repair command and PageMaster's export job (the one shared implementation). +PDF/X-4 and PDF/A-2b, which permit live transparency, do not flatten by +default. Also correct docs/STANDARD_CONVERSION.md's stale claim that an Editor +adapter can land "after the 0.1.1 GUI gate" — that gate is already complete +per docs/LOOP_SHELL_CONTRACT.md; Editor integration actually remains deferred +behind the still-closed S21/S22 product-GUI admission contracts, and +docs/REPO_MAP.md's LoopEditorPlugins/ module does not exist in the current +Qt-Quick-based tree, so a future Editor adapter belongs under +LoopLibInteraction/ + LoopEditor/qml/ instead. diff --git a/changes/cursor-fix-appstream-urls-8f37.md b/changes/cursor-fix-appstream-urls-8f37.md new file mode 100644 index 00000000..6580e274 --- /dev/null +++ b/changes/cursor-fix-appstream-urls-8f37.md @@ -0,0 +1,4 @@ +Category: fixed +Audience: developers +Breaking-Change: no +Summary: Point AppStream metainfo URLs at studio-berry/loop for Linux AppImage packaging, and resolve Windows Sentry debug-file upload from LOOP_SENTRY_DSN in CMakeLists.txt so MSI packaging is not blocked by a stale project slug. diff --git a/docs/CI.md b/docs/CI.md index c4972b24..8da773f9 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -111,7 +111,7 @@ the signing step refuses to run against an unpinned toolchain. Windows Release builds with `LOOP_ENABLE_SENTRY` emit PDBs (`/Zi` + `/DEBUG:FULL`) so crashpad minidumps can be symbolicated. After the Windows CI and MSI packaging jobs, `scripts/ci/upload_sentry_debug_files.ps1` -uploads Loop PDBs to `berry-studios/loop-pdf` on the EU region +uploads Loop PDBs to the `berry-studios` EU project id encoded in `LOOP_SENTRY_DSN` (`https://de.sentry.io`) using the pinned `sentryCli` binary. GitHub Actions cannot reference `secrets` in `if:` conditionals, so the workflow always runs the step; `upload_sentry_debug_files.ps1` no-ops when diff --git a/docs/PRODUCTION_RUNBOOK.md b/docs/PRODUCTION_RUNBOOK.md index 35b7c8b2..7cc4d52a 100644 --- a/docs/PRODUCTION_RUNBOOK.md +++ b/docs/PRODUCTION_RUNBOOK.md @@ -111,7 +111,7 @@ Debug Files — they are not Issues or traces. **Privacy:** Desktop sentry-native 0.15.x does not send default PII (`send_default_pii` is NX-only in that pin). Crashes may still include OS-level paths and PDF bytes in minidumps — set `SENTRY_DSN=off` in high-classification environments. CI sets `SENTRY_DSN=off` so test runs do not flood the project. -**Debug files:** Windows CI uploads Loop PDBs to `berry-studios/loop-pdf` (EU) when `SENTRY_AUTH_TOKEN` is set. Without those files, crash stacks stay unsymbolicated. Store the token as a GitHub Actions secret with `project:releases` (or broader) scope; do not commit it. +**Debug files:** Windows CI uploads Loop PDBs to the `berry-studios` EU project id encoded in `LOOP_SENTRY_DSN` when `SENTRY_AUTH_TOKEN` is set. Without those files, crash stacks stay unsymbolicated. Store the token as a GitHub Actions secret with `project:releases` (or broader) scope; do not commit it. **Verify (Windows, Sentry-enabled build):** diff --git a/docs/REPAIR_OPERATIONS.md b/docs/REPAIR_OPERATIONS.md index 71ee95fa..389cad35 100644 --- a/docs/REPAIR_OPERATIONS.md +++ b/docs/REPAIR_OPERATIONS.md @@ -39,9 +39,10 @@ The first adapters use the existing bounded Core fixups: `PDF/X-1a:2001`, `PDF/X-3:2002`, `PDF/X-4`, and `PDF/A-2b`. It produces a pre-conversion change report and refuses to commit unless an explicitly configured independent validator accepts the candidate. Validator arguments - must include `{input}`. Transparency flattening, font embedding, and - forbidden-action removal are not silently approximated; unsupported cases - fail closed. + must include `{input}`. Transparency flattening (via the shared + `PDFTransparencyFlattener` operation, default-on for X-1a/X-3) is a reported + content change, not a silent approximation; font embedding and + forbidden-action removal remain unsupported and fail closed. The preflight capability list is derived from the same registry: an operation is advertised only when its descriptor marks it as a preflight fixup. Profile diff --git a/docs/STANDARD_CONVERSION.md b/docs/STANDARD_CONVERSION.md index 23f1bbed..4d3c965d 100644 --- a/docs/STANDARD_CONVERSION.md +++ b/docs/STANDARD_CONVERSION.md @@ -2,18 +2,33 @@ Loop exposes standard conversion as the Core operation `standards-convert`. PdfTool's `repair` command and PageMaster's headless export job call this same -operation; an Editor adapter can be added after the 0.1.1 GUI gate without -creating a second conversion implementation. +operation. An Editor adapter is deferred until Loop's product GUI work clears +the S21 canvas / S22 Quick admission contracts described in +[`LOOP_SHELL_CONTRACT.md`](LOOP_SHELL_CONTRACT.md) — the 0.1.1 release gate +itself is already complete, so that document, not this one, is authoritative +on timing. No second conversion implementation is planned; PdfTool and +PageMaster already share the one Core implementation. Supported targets are explicit: `PDF/X-1a:2001`, `PDF/X-3:2002`, `PDF/X-4`, and `PDF/A-2b`. The selected target is recorded in the operation plan and report. The report lists metadata, PDF version, output-intent, page-box, and optional -color-normalization changes before mutation. +color-normalization and transparency-flattening changes before mutation. Conversion is fail-closed. A CMYK ICC profile is required for PDF/X-1a and -PDF/X-3 normalization. Loop does not claim that transparency was flattened, -fonts embedded, actions removed, or other unsupported constructs repaired when -the Core implementation cannot do so. Those findings remain blockers. +PDF/X-3 normalization. Loop does not claim that fonts were embedded, actions +removed, or other unsupported constructs repaired when the Core implementation +cannot do so. Those findings remain blockers. + +PDF/X-1a:2001 and PDF/X-3:2002 forbid live transparency. `standards-convert` +runs the shared `PDFTransparencyFlattener` operation (issue #164) against +those two targets by default before the output-intent and page-box rewrite, +so `pdfx.transparency.allowed` stops being an unconditional blocker; set the +`flatten_transparency` parameter explicitly to override the default (`false` +opts out for X-1a/X-3, `true` opts in for X-4, which otherwise permits live +transparency). Flattening rasterizes affected page content — it is a real +content change, reported under `transparency_flatten` in the conversion +report, not a silent approximation. PDF/X-4 and PDF/A-2b do not flatten by +default. Every non-dry-run conversion requires an independent validator command. The validator receives a temporary candidate through the `{input}` argument diff --git a/scripts/budget_exhaustion/generate_corpus.py b/scripts/budget_exhaustion/generate_corpus.py index 3ad577a6..03cfc314 100644 --- a/scripts/budget_exhaustion/generate_corpus.py +++ b/scripts/budget_exhaustion/generate_corpus.py @@ -11,7 +11,7 @@ DEFAULT_OUTPUT = Path(__file__).resolve().parents[2] / "UnitTests" / "testdata" / "budget-exhaustion" -SCHEMA_KIND = "loupe-processing-budget-exhaustion-corpus" +SCHEMA_KIND = "loop-processing-budget-exhaustion-corpus" SCHEMA_VERSION = 2 diff --git a/scripts/ci/upload_sentry_debug_files.ps1 b/scripts/ci/upload_sentry_debug_files.ps1 index 6f4ae1c2..bff0e017 100644 --- a/scripts/ci/upload_sentry_debug_files.ps1 +++ b/scripts/ci/upload_sentry_debug_files.ps1 @@ -34,9 +34,28 @@ if (-not $cli -or -not $cli.assetId -or -not $cli.sha256 -or -not $cli.upstream) } $org = if ($env:SENTRY_ORG) { $env:SENTRY_ORG } else { "berry-studios" } -$project = if ($env:SENTRY_PROJECT) { $env:SENTRY_PROJECT } else { "loop-pdf" } $url = if ($env:SENTRY_URL) { $env:SENTRY_URL } else { "https://de.sentry.io" } +function Resolve-SentryProject([string]$Root) { + if ($env:SENTRY_PROJECT) { + return [string]$env:SENTRY_PROJECT + } + $cmakePath = Join-Path $Root "CMakeLists.txt" + if (-not (Test-Path -LiteralPath $cmakePath)) { + throw "upload_sentry_debug_files.ps1: CMakeLists.txt not found; cannot resolve Sentry project id." + } + $dsnMatch = [regex]::Match( + (Get-Content -LiteralPath $cmakePath -Raw), + 'ingest\.de\.sentry\.io/(\d+)' + ) + if (-not $dsnMatch.Success) { + throw "upload_sentry_debug_files.ps1: LOOP_SENTRY_DSN in CMakeLists.txt has no ingest project id." + } + return $dsnMatch.Groups[1].Value +} + +$project = Resolve-SentryProject $repoRoot + $cliPath = Join-Path $env:RUNNER_TEMP "sentry-cli-Windows-x86_64.exe" if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { $cliPath = Join-Path ([System.IO.Path]::GetTempPath()) "sentry-cli-Windows-x86_64.exe"