Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/WindowsInstall.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reusable-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/reusable-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
6 changes: 3 additions & 3 deletions Desktop/io.github.mberrys.Loop-pdf.appdata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
<category>Utility</category>
</categories>
<developer_name>Michael Berry</developer_name>
<url type="homepage">https://github.com/mberrys/Loop-pdf</url>
<url type="bugtracker">https://github.com/mberrys/Loop-pdf/issues</url>
<url type="help">https://github.com/mberrys/Loop-pdf</url>
<url type="homepage">https://github.com/studio-berry/loop</url>
<url type="bugtracker">https://github.com/studio-berry/loop/issues</url>
<url type="help">https://github.com/studio-berry/loop</url>
<content_rating type="oars-1.1"/>
<launchable type="desktop-id">io.github.mberrys.Loop-pdf.desktop</launchable>
<releases>
Expand Down
18 changes: 18 additions & 0 deletions LoopEditor/editorhost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions LoopEditor/editorhost.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions LoopEditor/qml/DocumentPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
}
}
}
20 changes: 19 additions & 1 deletion LoopEditor/quickdocumentmodel.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// MIT License
#include "quickdocumentmodel.h"

#include "pdfaction.h"
#include "pdfcatalog.h"
#include "pdfdocument.h"
#include "pdfdocumentcontext.h"
Expand Down Expand Up @@ -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<const pdf::PDFActionGoTo*>(action);
const pdf::PDFDestination& dest = goTo->getDestination();
if (dest.isValid() && !dest.isNamedDestination())
return static_cast<int>(dest.getPageIndex());
const pdf::PDFDestination& structDest = goTo->getStructureDestination();
if (structDest.isValid() && !structDest.isNamedDestination())
return static_cast<int>(structDest.getPageIndex());
}
return -1;
}
return {};
}

QHash<int, QByteArray> QuickOutlineModel::roleNames() const
{
return { { TitleRole, "title" }, { HasChildrenRole, "hasChildren" } };
return { { TitleRole, "title" }, { HasChildrenRole, "hasChildren" }, { PageRole, "page" } };
}

void QuickOutlineModel::build(Node* parent, const pdf::PDFOutlineItem* item)
Expand Down
1 change: 1 addition & 0 deletions LoopEditor/quickdocumentmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class QuickOutlineModel final : public QAbstractItemModel
{
TitleRole = Qt::UserRole + 1,
HasChildrenRole,
PageRole,
};

explicit QuickOutlineModel(QObject* parent = nullptr);
Expand Down
3 changes: 2 additions & 1 deletion LoopLibCore/sources/pdfdocumentsearch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
7 changes: 6 additions & 1 deletion LoopLibCore/sources/pdfrepairprimitives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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 } } },
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 33 additions & 2 deletions LoopLibCore/sources/pdfstandardconversion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 }
};
}

Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion LoopLibCore/sources/pdfstandardconversion.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#include "pdfdocument.h"
#include "pdfglobal.h"
#include "pdftransparencyflattener.h"
#include "pdfutils.h" // PDFOperationResult, returned by preview()/apply() below

#include <QByteArray>
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -83,6 +86,7 @@ struct LOOPLIBCORESHARED_EXPORT PDFStandardConversionReport
QStringList blockers;
QStringList warnings;
QJsonObject validator;
QJsonObject transparencyFlatten;

QJsonObject toJson() const;
};
Expand Down
6 changes: 3 additions & 3 deletions LoopLibCore/sources/pdftextlayoutgenerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion UnitTests/testdata/budget-exhaustion/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion UnitTests/tst_budgetexhaustiontest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ QList<CorpusFixture> 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");
}
Expand Down
2 changes: 1 addition & 1 deletion UnitTests/tst_documentfacadetest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ void DocumentFacadeTest::catalogLoadsTheWholeEditorActionSet()
QVERIFY(descriptor.capability != pdfinteraction::CommandCapability::Unclassified);
}
}
QCOMPARE(implemented, 16);
QCOMPARE(implemented, 25);
}

void DocumentFacadeTest::catalogPublishesAvailabilityAtomically()
Expand Down
20 changes: 20 additions & 0 deletions changes/cc-pensive-cerf-kkvr3f.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading