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"