diff --git a/LoopEditor/CMakeLists.txt b/LoopEditor/CMakeLists.txt
index 4be682549..3d95b92aa 100644
--- a/LoopEditor/CMakeLists.txt
+++ b/LoopEditor/CMakeLists.txt
@@ -47,6 +47,10 @@ qt_add_qml_module(LoopEditor
qml/CanvasPane.qml
qml/PreflightPane.qml
qml/InspectorPane.qml
+ qml/WorkspacePlaceholderPane.qml
+ qml/ShellToolBar.qml
+ qml/ShellMenuBar.qml
+ qml/MenuModel.qml
)
target_link_libraries(LoopEditor
@@ -78,6 +82,10 @@ if(WIN32 AND LOOP_LOOP_DISTRIBUTION)
set(_loop_editor_win32_executable OFF)
endif()
+if(LOOP_LOOP_DISTRIBUTION)
+ target_compile_definitions(LoopEditor PRIVATE LOOP_LOOP_DISTRIBUTION_BUILD=1)
+endif()
+
set_target_properties(LoopEditor PROPERTIES
WIN32_EXECUTABLE ${_loop_editor_win32_executable}
MACOSX_BUNDLE ON
diff --git a/LoopEditor/app.qrc b/LoopEditor/app.qrc
index 937ba9215..2449fb301 100644
--- a/LoopEditor/app.qrc
+++ b/LoopEditor/app.qrc
@@ -1,5 +1,6 @@
app-icon.svg
+ ../loop-preflight/profiles/loop-default.json
diff --git a/LoopEditor/editorhost.cpp b/LoopEditor/editorhost.cpp
index dbfaca51d..05cb97765 100644
--- a/LoopEditor/editorhost.cpp
+++ b/LoopEditor/editorhost.cpp
@@ -28,10 +28,21 @@
#include "interactionstate.h"
#include "interactiontarget.h"
#include "loopcanvasitem.h"
+#include "loopstatevisual.h"
+#include "looptokens.h"
#include "pagesurfacecoordinator.h"
#include "preflightcontroller.h"
+#include "preflightclirun.h"
+#include "preflightengine.h"
+#include "preflightprofileresolver.h"
#include "previewstatemodel.h"
+#include "productionmodel.h"
+#include "interactiontarget.h"
+
+#include "pdfdocumentsession.h"
+#include "pdfsafefilewriter.h"
+#include "pdfblockingthreadguard.h"
#include "pdfpage.h"
#include "pdftransparencyrenderer.h"
@@ -39,19 +50,29 @@
#include
#include
#include
+#include
+#include
+#include
+#include
#include
+#include
#include
#include
#include
+#include
+#include
#include
+#include
+#include
#include
+#include
+#include
namespace
{
const QString QuitCommandId = QStringLiteral("actionQuit");
-
int rotationToDegrees(pdf::PageRotation rotation)
{
switch (rotation)
@@ -87,10 +108,68 @@ QString preflightStateToString(pdfinteraction::PreflightController::State state)
return QStringLiteral("stale");
case pdfinteraction::PreflightController::State::Incomplete:
return QStringLiteral("incomplete");
+ case pdfinteraction::PreflightController::State::Error:
+ return QStringLiteral("error");
}
return QStringLiteral("not-checked");
}
+QString shellMenuGroupForAction(const QString& id, const QString& target)
+{
+ static const QStringList fileActions = {
+ QStringLiteral("actionOpen"),
+ QStringLiteral("actionClose"),
+ QStringLiteral("actionSave"),
+ QStringLiteral("actionSave_As"),
+ QStringLiteral("actionQuit"),
+ QStringLiteral("actionPrint"),
+ QStringLiteral("actionSendByEmail"),
+ QStringLiteral("actionRenderToImages"),
+ QStringLiteral("actionClearRecentFileHistory"),
+ QStringLiteral("actionAutomaticDocumentRefresh"),
+ };
+ if (fileActions.contains(id))
+ {
+ return QStringLiteral("File");
+ }
+
+ if (id.startsWith(QStringLiteral("actionCopy")) || id.startsWith(QStringLiteral("actionCut")) ||
+ id.startsWith(QStringLiteral("actionPaste")) || id == QStringLiteral("actionUndo") ||
+ id == QStringLiteral("actionRedo"))
+ {
+ return QStringLiteral("Edit");
+ }
+
+ if (id.startsWith(QStringLiteral("actionZoom")) || id.startsWith(QStringLiteral("actionFit")) ||
+ id.startsWith(QStringLiteral("actionRotate")) || id.startsWith(QStringLiteral("actionPageLayout")) ||
+ id.startsWith(QStringLiteral("actionGoTo")) || id.startsWith(QStringLiteral("actionFind")) ||
+ id == QStringLiteral("actionFullscreenMode"))
+ {
+ return QStringLiteral("View");
+ }
+
+ if (id == QStringLiteral("actionAbout") || id == QStringLiteral("actionBecomeASponsor") ||
+ id == QStringLiteral("actionGet_Source"))
+ {
+ return QStringLiteral("Help");
+ }
+
+ if (target == QStringLiteral("Preflight"))
+ {
+ return QStringLiteral("Preflight");
+ }
+ if (target == QStringLiteral("Production") || target == QStringLiteral("Pages") || target == QStringLiteral("Fix"))
+ {
+ return QStringLiteral("Production");
+ }
+ if (target == QStringLiteral("Document") || target == QStringLiteral("Inspect"))
+ {
+ return QStringLiteral("Document");
+ }
+
+ return QStringLiteral("Document");
+}
+
QVariantMap descriptorToVariant(const pdfinteraction::CommandDescriptor& descriptor, bool enabled)
{
QVariantMap entry;
@@ -98,6 +177,9 @@ QVariantMap descriptorToVariant(const pdfinteraction::CommandDescriptor& descrip
entry.insert(QStringLiteral("labelKey"), descriptor.labelKey);
entry.insert(QStringLiteral("implemented"), descriptor.isImplemented());
entry.insert(QStringLiteral("enabled"), enabled);
+ entry.insert(QStringLiteral("target"), descriptor.target);
+ entry.insert(QStringLiteral("disposition"), descriptor.disposition);
+ entry.insert(QStringLiteral("menuGroup"), shellMenuGroupForAction(descriptor.id, descriptor.target));
QVariantMap shortcut;
shortcut.insert(QStringLiteral("standardKey"), descriptor.shortcut.standardKey);
@@ -110,11 +192,29 @@ QVariantMap descriptorToVariant(const pdfinteraction::CommandDescriptor& descrip
} // namespace
+struct EditorHost::PreflightWorkerOutcome
+{
+ pdf::PreflightResult result;
+};
+
EditorHost::EditorHost(QObject* parent) :
QObject(parent),
m_session(std::make_unique(this)),
m_preflight(&m_session->scheduler(), this)
{
+ // Registers this constructing thread -- the one QML dispatches pointer
+ // and frame callbacks on -- as the thread blocking service adapters
+ // (PreflightEngine::run, and future OCR/AI/file-I/O adapters) must
+ // refuse to run on (issue #144).
+ pdf::PDFBlockingThreadGuard::registerInteractiveThread();
+
+ m_preflightProfileWatcher = new QFileSystemWatcher(this);
+ connect(m_preflightProfileWatcher, &QFileSystemWatcher::directoryChanged, this, [this](const QString&)
+ { reloadPreflightProfiles(); });
+ connect(m_preflightProfileWatcher, &QFileSystemWatcher::fileChanged, this, [this](const QString&)
+ { reloadPreflightProfiles(); });
+ reloadPreflightProfiles();
+
connectFacade();
connectViewport();
connectCatalog();
@@ -128,20 +228,46 @@ EditorHost::EditorHost(QObject* parent) :
m_preflightOverlayBridge.setInteractionController(m_session->interaction());
connect(&m_preflight, &pdfinteraction::PreflightController::stateChanged, this, &EditorHost::bumpPresentation);
+ connect(&m_preflight, &pdfinteraction::PreflightController::progressChanged, this, &EditorHost::bumpPresentation);
connect(m_preflight.findingsModel(), &pdfinteraction::PreflightFindingsModel::findingsReplaced, this, &EditorHost::refreshHitTestSources);
connect(&m_preflight, &pdfinteraction::PreflightController::navigationRequested, this, &EditorHost::onPreflightNavigation);
connect(&m_inspector, &pdfinteraction::InspectorModel::selectionChanged, this, &EditorHost::bumpPresentation);
connect(&m_preview, &pdfinteraction::PreviewStateModel::stateChanged, this, &EditorHost::bumpPresentation);
+ connect(&m_production, &pdfinteraction::ProductionModel::stateChanged, this, &EditorHost::bumpPresentation);
connect(&m_documentModel, &QuickDocumentModel::searchChanged, this, [this]
{
refreshFeatureAvailability();
bumpPresentation();
bumpCommandEpoch(); });
+
+ connect(&m_session->scheduler(), &pdf::PDFJobScheduler::jobQueued, this, [this](const pdf::PDFJobSnapshot& snapshot)
+ {
+ m_activeAsyncJobs.insert(snapshot.jobId, snapshot.kind);
+ refreshCanvasTrace(); });
+ connect(&m_session->scheduler(), &pdf::PDFJobScheduler::jobProgress, this, [this](const pdf::PDFJobSnapshot& snapshot)
+ { m_preflight.updateProgress(snapshot.jobId, snapshot.documentRevision, snapshot.progress); });
+ connect(&m_session->scheduler(), &pdf::PDFJobScheduler::jobFinished, this, [this](const pdf::PDFJobSnapshot& snapshot)
+ {
+ m_activeAsyncJobs.remove(snapshot.jobId);
+ finishPreflightJob(snapshot);
+ refreshCanvasTrace(); });
}
EditorHost::~EditorHost()
{
+ m_acceptPreflightResults = false;
+ cancelPreflight();
+ QObject::disconnect(&m_session->scheduler(), nullptr, this, nullptr);
unbindCanvas();
+
+ // The guard registration is global process state owned by the thread that
+ // built this host, so pair it with the host's lifetime: a host destroyed and
+ // recreated in one process must not leave a registration behind that keeps
+ // refusing synchronous blocking work on that thread.
+ if (pdf::PDFBlockingThreadGuard::isCurrentThreadInteractive())
+ {
+ pdf::PDFBlockingThreadGuard::clearInteractiveThread();
+ }
}
QString EditorHost::documentState() const
@@ -232,6 +358,20 @@ void EditorHost::goToOutlinePage(int pageIndex)
goToPage(pageIndex);
}
+void EditorHost::setWorkspace(LoopWorkspace workspace)
+{
+ if (workspace == m_workspace)
+ {
+ return;
+ }
+
+ const LoopWorkspace previous = m_workspace;
+ m_workspace = workspace;
+ m_workspaceRequest = -1;
+ Q_EMIT workspaceChanged(previous, workspace);
+ bumpPresentation();
+}
+
void EditorHost::acknowledgeWorkspaceRequest()
{
if (m_workspaceRequest < 0)
@@ -239,8 +379,44 @@ void EditorHost::acknowledgeWorkspaceRequest()
return;
}
+ const LoopWorkspace requested = static_cast(m_workspaceRequest);
m_workspaceRequest = -1;
- Q_EMIT presentationChanged();
+ setWorkspace(requested);
+}
+
+QString EditorHost::documentShellStatus() const
+{
+ return QString::fromLatin1(
+ pdfinteraction::getShellDocumentStatusName(m_session->facade().shellDocumentStatus()));
+}
+
+QString EditorHost::productionStateName() const
+{
+ if (!hasDocument())
+ {
+ return QStringLiteral("NOT_READY");
+ }
+
+ switch (m_session->facade().outputState())
+ {
+ case pdfinteraction::DocumentOutputState::Pending:
+ return QStringLiteral("OPERATION_PENDING");
+ case pdfinteraction::DocumentOutputState::Saved:
+ return QStringLiteral("OUTPUT_WRITTEN");
+ case pdfinteraction::DocumentOutputState::None:
+ break;
+ }
+
+ return pdfinteraction::ProductionModel::stateName(m_production.state());
+}
+
+bool EditorHost::allowDeveloperDiagnostics() const
+{
+#ifdef LOOP_LOOP_DISTRIBUTION_BUILD
+ return false;
+#else
+ return true;
+#endif
}
void EditorHost::acknowledgeSearchPanel()
@@ -259,6 +435,88 @@ QString EditorHost::preflightStateName() const
return preflightStateToString(m_preflight.state());
}
+QVariantMap EditorHost::preflightStateVisual() const
+{
+ const pdfquick::tokens::LoopStateVisual visual = pdfquick::tokens::resolvePreflightStateVisual(preflightStateName());
+
+ QVariantMap result;
+ result.insert(QStringLiteral("kind"), pdfquick::tokens::stateKindName(visual.kind));
+ result.insert(QStringLiteral("colorRole"), pdfquick::tokens::colorRoleName(visual.colorRole));
+ result.insert(QStringLiteral("icon"), pdfquick::tokens::stateIconName(visual.icon));
+ result.insert(QStringLiteral("accessibleName"), visual.accessibleName);
+ return result;
+}
+
+QColor EditorHost::preflightStateColor() const
+{
+ const pdfquick::tokens::LoopStateVisual visual = pdfquick::tokens::resolvePreflightStateVisual(preflightStateName());
+ const pdfquick::tokens::LoopTheme theme =
+ highContrast() ? pdfquick::tokens::LoopTheme::HighContrast : pdfquick::tokens::LoopTheme::Dark;
+ return pdfquick::tokens::color(visual.colorRole, theme);
+}
+
+QString EditorHost::preflightOperatorSummary() const
+{
+ return m_preflight.operatorSummary();
+}
+
+QVariantList EditorHost::preflightProfiles() const
+{
+ QVariantList profiles;
+ profiles.reserve(m_preflightProfiles.size());
+ for (const PreflightProfileChoice& profile : m_preflightProfiles)
+ {
+ QVariantMap item;
+ item.insert(QStringLiteral("id"), profile.id);
+ item.insert(QStringLiteral("name"), profile.name);
+ item.insert(QStringLiteral("version"), profile.version);
+ item.insert(QStringLiteral("source"), profile.source.startsWith(QLatin1Char(':'))
+ ? tr("Bundled")
+ : tr("Local"));
+ item.insert(QStringLiteral("valid"), profile.valid);
+ item.insert(QStringLiteral("diagnostic"), profile.diagnostic);
+ profiles.append(item);
+ }
+ return profiles;
+}
+
+QVariantList EditorHost::preflightVariables() const
+{
+ const auto it = std::find_if(m_preflightProfiles.cbegin(), m_preflightProfiles.cend(),
+ [this](const PreflightProfileChoice& profile)
+ { return profile.id == m_selectedPreflightProfileId; });
+ if (it == m_preflightProfiles.cend())
+ {
+ return {};
+ }
+
+ QVariantList variables;
+ const QStringList names = it->variables.keys();
+ for (const QString& name : names)
+ {
+ const QJsonObject declaration = it->variables.value(name).toObject();
+ QVariantMap item;
+ item.insert(QStringLiteral("name"), name);
+ item.insert(QStringLiteral("type"), declaration.value(QStringLiteral("type")).toString());
+ item.insert(QStringLiteral("required"), declaration.value(QStringLiteral("required")).toBool());
+ item.insert(QStringLiteral("description"), declaration.value(QStringLiteral("description")).toString());
+ item.insert(QStringLiteral("value"), m_preflightBindings.contains(name)
+ ? m_preflightBindings.value(name).toVariant()
+ : declaration.value(QStringLiteral("default")).toVariant());
+ if (declaration.contains(QStringLiteral("min")))
+ item.insert(QStringLiteral("min"), declaration.value(QStringLiteral("min")).toVariant());
+ if (declaration.contains(QStringLiteral("max")))
+ item.insert(QStringLiteral("max"), declaration.value(QStringLiteral("max")).toVariant());
+ variables.append(item);
+ }
+ return variables;
+}
+
+QString EditorHost::selectedPreflightProfileId() const
+{
+ return m_selectedPreflightProfileId;
+}
+
QString EditorHost::previewSummary() const
{
return m_preview.summary();
@@ -378,6 +636,293 @@ void EditorHost::announceDocumentState(const QString& message)
QAccessible::updateAccessibility(&event);
}
+bool EditorHost::runPreflight()
+{
+ if (!hasDocument() || m_preflight.state() == pdfinteraction::PreflightController::State::Running ||
+ !m_session->revisionSource())
+ {
+ return false;
+ }
+
+ const auto profileIt = std::find_if(m_preflightProfiles.cbegin(), m_preflightProfiles.cend(),
+ [this](const PreflightProfileChoice& profile)
+ { return profile.id == m_selectedPreflightProfileId; });
+ if (profileIt == m_preflightProfiles.cend() || !profileIt->valid)
+ {
+ return false;
+ }
+
+ const pdf::PDFDocumentPointer document = m_session->context().getDocumentPointer();
+ if (!document)
+ {
+ return false;
+ }
+
+ const QString documentKey = m_session->revisionSource()->documentKey();
+ const QString documentRevision = m_session->facade().currentRevision().toString();
+ const QString jobId = QUuid::createUuid().toString(QUuid::WithoutBraces);
+
+ pdf::PDFJobSpec spec;
+ spec.jobId = jobId;
+ spec.kind = pdf::PDFJobKind::Preflight;
+ spec.priority = pdf::PDFJobPriority::Operator;
+ spec.documentKey = documentKey;
+ spec.documentRevision = documentRevision;
+ spec.operationId = QStringLiteral("preflight.%1").arg(profileIt->id);
+ spec.checkId = profileIt->name;
+ spec.progressModel = QStringLiteral("preflight-progress-v1");
+ spec.staleResultPolicy = pdf::PDFJobStaleResultPolicy::Discard;
+
+ m_preflight.beginRun(documentKey,
+ documentRevision,
+ profileIt->digest,
+ jobId);
+ auto outcome = std::make_shared();
+ m_preflightOutcomes.insert(jobId, outcome);
+ const PreflightProfileChoice selectedProfile = *profileIt;
+ const QJsonObject bindings = m_preflightBindings;
+ const QByteArray sourceHash = m_session->context().getDocumentIdentity().sourceDataHash;
+
+ const QString submittedId = m_session->scheduler().submit(
+ spec,
+ [document, outcome, selectedProfile, bindings, sourceHash](pdf::PDFJobContext& context)
+ {
+ if (context.isCancellationRequested())
+ {
+ return;
+ }
+
+ const pdf::PreflightProfileImportResult imported = pdf::importPreflightProfile(selectedProfile.profile,
+ selectedProfile.source);
+ if (!imported.ok)
+ {
+ throw std::runtime_error(imported.errorMessage.toStdString());
+ }
+ const pdf::PreflightVariableBindResult bound =
+ pdf::bindPreflightProfileVariables(imported.profile, bindings);
+ if (!bound.ok)
+ {
+ throw std::runtime_error(bound.errorMessage.toStdString());
+ }
+ pdf::PreflightProfileResolver resolver;
+ const pdf::PreflightResolvedProfile resolved = resolver.resolveExplicitProfile(
+ bound.profile, selectedProfile.name,
+ imported.identity.version.isEmpty() ? QStringLiteral("explicit") : imported.identity.version);
+ if (!resolved.ok)
+ {
+ throw std::runtime_error(resolved.errorMessage.toStdString());
+ }
+
+ pdf::PreflightProfileData profile;
+ QString profileError;
+ if (!pdf::PreflightEngine::parseProfile(bound.profile, profile, profileError))
+ {
+ throw std::runtime_error(profileError.toStdString());
+ }
+ profile.variableBindings = bound.bindings;
+ profile.fileDigest = imported.identity.digest;
+ profile.effectiveDigest = pdf::computeProfileDigest(bound.profile);
+ profile.profileIdentity = imported.identity.toJson();
+ profile.profileIdentity.insert(QStringLiteral("effective_digest"), profile.effectiveDigest);
+ context.reportProgress(5);
+
+ std::unique_ptr session(
+ pdf::PDFDocumentSession::createForInspection(document.data()), &pdf::PDFDocumentSession::destroy);
+ pdf::PreflightEngine engine(session.get());
+ engine.setOperationControl(context.operationControl());
+ context.reportProgress(15);
+ outcome->result = engine.run(profile);
+ pdf::finalizePreflightResult(outcome->result, sourceHash, resolved);
+ if (context.isCancellationRequested())
+ {
+ return;
+ }
+ context.reportProgress(95);
+ context.setResultSummary(QStringLiteral("Preflight completed."));
+ });
+ if (submittedId != jobId)
+ {
+ m_preflightOutcomes.remove(jobId);
+ m_preflight.failRun(jobId, documentRevision, tr("Unable to submit preflight work."));
+ return false;
+ }
+
+ bumpPresentation();
+ return true;
+}
+
+bool EditorHost::cancelPreflight()
+{
+ return m_preflight.cancelRun(m_preflight.jobId());
+}
+
+bool EditorHost::selectPreflightProfile(const QString& id)
+{
+ const auto it = std::find_if(m_preflightProfiles.cbegin(), m_preflightProfiles.cend(),
+ [&id](const PreflightProfileChoice& profile)
+ { return profile.id == id; });
+ if (it == m_preflightProfiles.cend() || !it->valid || id == m_selectedPreflightProfileId)
+ {
+ return false;
+ }
+ m_selectedPreflightProfileId = id;
+ m_preflightBindings = QJsonObject();
+ m_preflight.markProfileStale();
+ Q_EMIT preflightProfilesChanged();
+ bumpPresentation();
+ return true;
+}
+
+bool EditorHost::setPreflightVariable(const QString& name, const QVariant& value)
+{
+ const auto it = std::find_if(m_preflightProfiles.cbegin(), m_preflightProfiles.cend(),
+ [this](const PreflightProfileChoice& profile)
+ { return profile.id == m_selectedPreflightProfileId; });
+ if (it == m_preflightProfiles.cend() || !it->variables.contains(name))
+ {
+ return false;
+ }
+ m_preflightBindings.insert(name, QJsonValue::fromVariant(value));
+ m_preflight.markProfileStale();
+ Q_EMIT preflightProfilesChanged();
+ bumpPresentation();
+ return true;
+}
+
+void EditorHost::requestPreflightReportExport()
+{
+ if (m_preflight.hasResult())
+ {
+ Q_EMIT preflightReportExportRequested();
+ }
+}
+
+bool EditorHost::exportPreflightReportFileUrl(const QUrl& url)
+{
+ if (!url.isValid() || !url.isLocalFile() || !m_preflight.hasResult())
+ {
+ return false;
+ }
+ const QByteArray report = m_preflight.serializedReport(m_session->facade().source().path);
+ const pdf::PDFOperationResult result = pdf::PDFSafeFileWriter::writeData(
+ url.toLocalFile(), report, pdf::PDFSafeFileWriter::OverwritePolicy::Overwrite);
+ if (!result)
+ {
+ announceDocumentState(tr("Could not export the preflight report: %1").arg(result.getErrorMessage()));
+ return false;
+ }
+ announceDocumentState(tr("Preflight report exported."));
+ return true;
+}
+
+void EditorHost::reloadPreflightProfiles()
+{
+ const QString priorId = m_selectedPreflightProfileId;
+ QString priorDigest;
+ for (const PreflightProfileChoice& profile : std::as_const(m_preflightProfiles))
+ {
+ if (profile.id == priorId)
+ {
+ priorDigest = profile.digest;
+ break;
+ }
+ }
+
+ QList profiles;
+ const auto addProfile = [&profiles](const QString& source, const QByteArray& data)
+ {
+ PreflightProfileChoice choice;
+ choice.id = source;
+ choice.source = source;
+ QJsonParseError parseError;
+ const QJsonDocument parsed = QJsonDocument::fromJson(data, &parseError);
+ if (parseError.error != QJsonParseError::NoError || !parsed.isObject())
+ {
+ choice.name = QFileInfo(source).completeBaseName();
+ choice.diagnostic = QStringLiteral("Profile JSON is invalid.");
+ profiles.append(choice);
+ return;
+ }
+ const pdf::PreflightProfileImportResult imported = pdf::importPreflightProfile(parsed.object(), source);
+ choice.name = imported.profile.value(QStringLiteral("name")).toString(QFileInfo(source).completeBaseName());
+ choice.version = imported.identity.version;
+ choice.digest = imported.identity.digest;
+ choice.profile = imported.profile;
+ choice.variables = imported.profile.value(QStringLiteral("variables")).toObject();
+ choice.valid = imported.ok;
+ choice.diagnostic = imported.ok ? QString() : imported.errorMessage;
+ profiles.append(choice);
+ };
+
+ const QDir bundled(QStringLiteral(":/profiles"));
+ for (const QFileInfo& file : bundled.entryInfoList({ QStringLiteral("*.json") }, QDir::Files, QDir::Name))
+ {
+ QFile input(file.filePath());
+ if (input.open(QIODevice::ReadOnly))
+ {
+ addProfile(file.filePath(), input.readAll());
+ }
+ }
+
+ const QString localDirectory = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation))
+ .filePath(QStringLiteral("profiles"));
+ const QDir local(localDirectory);
+ for (const QFileInfo& file : local.entryInfoList({ QStringLiteral("*.json") }, QDir::Files, QDir::Name))
+ {
+ QFile input(file.absoluteFilePath());
+ if (input.open(QIODevice::ReadOnly))
+ {
+ addProfile(file.absoluteFilePath(), input.readAll());
+ }
+ }
+
+ m_preflightProfiles = std::move(profiles);
+ if (m_selectedPreflightProfileId.isEmpty() ||
+ std::none_of(m_preflightProfiles.cbegin(), m_preflightProfiles.cend(),
+ [this](const PreflightProfileChoice& profile)
+ { return profile.id == m_selectedPreflightProfileId && profile.valid; }))
+ {
+ const auto valid = std::find_if(m_preflightProfiles.cbegin(), m_preflightProfiles.cend(),
+ [](const PreflightProfileChoice& profile)
+ { return profile.valid; });
+ m_selectedPreflightProfileId = valid == m_preflightProfiles.cend() ? QString() : valid->id;
+ m_preflightBindings = QJsonObject();
+ }
+ const auto current = std::find_if(m_preflightProfiles.cbegin(), m_preflightProfiles.cend(),
+ [this](const PreflightProfileChoice& profile)
+ { return profile.id == m_selectedPreflightProfileId; });
+ if (!priorId.isEmpty() && (priorId != m_selectedPreflightProfileId || current == m_preflightProfiles.cend() || current->digest != priorDigest))
+ {
+ m_preflight.markProfileStale();
+ }
+ updatePreflightProfileWatch();
+ Q_EMIT preflightProfilesChanged();
+ bumpPresentation();
+}
+
+void EditorHost::updatePreflightProfileWatch()
+{
+ if (!m_preflightProfileWatcher)
+ {
+ return;
+ }
+ m_preflightProfileWatcher->removePaths(m_preflightProfileWatcher->directories());
+ m_preflightProfileWatcher->removePaths(m_preflightProfileWatcher->files());
+ const QString localDirectory = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation))
+ .filePath(QStringLiteral("profiles"));
+ if (QFileInfo::exists(localDirectory))
+ {
+ m_preflightProfileWatcher->addPath(localDirectory);
+ }
+ for (const PreflightProfileChoice& profile : std::as_const(m_preflightProfiles))
+ {
+ if (!profile.source.startsWith(QLatin1Char(':')) && QFileInfo::exists(profile.source))
+ {
+ m_preflightProfileWatcher->addPath(profile.source);
+ }
+ }
+}
+
QVariantList EditorHost::commandDescriptors() const
{
QVariantList descriptors;
@@ -452,6 +997,13 @@ void EditorHost::cancelPendingOperation()
void EditorHost::attachCanvas(QObject* canvasObject)
{
m_canvas = qobject_cast(canvasObject);
+ if (m_canvas)
+ {
+ m_canvas->ensureTraceRecorder();
+ m_canvas->setAsyncWorkKindsProvider([this]
+ { return activeAsyncWorkKinds(); });
+ refreshCanvasTrace();
+ }
if (m_documentBound)
{
bindCanvas();
@@ -461,6 +1013,10 @@ void EditorHost::attachCanvas(QObject* canvasObject)
void EditorHost::detachCanvas()
{
unbindCanvas();
+ if (m_canvas)
+ {
+ m_canvas->setAsyncWorkKindsProvider({});
+ }
m_canvas.clear();
}
@@ -547,6 +1103,8 @@ void EditorHost::connectFacade()
this,
[this](const pdf::PDFRevisionIdentity&, const pdf::PDFRevisionIdentity&)
{
+ cancelPreflight();
+ syncRevisionModels();
if (m_documentBound)
{
m_documentModel.setDocument(&m_session->context());
@@ -570,6 +1128,7 @@ void EditorHost::connectFacade()
connect(&m_session->facade(), &pdfinteraction::DocumentFacade::facetsChanged, this, [this](pdfinteraction::DocumentFacets)
{
syncDocumentLifecycle();
+ syncProductionState();
bumpPresentation(); });
connect(&m_session->facade(), &pdfinteraction::DocumentFacade::documentReplaced, this, [this](quint64)
@@ -596,6 +1155,10 @@ void EditorHost::connectViewport()
void EditorHost::connectInteraction()
{
+ connect(m_session->interaction(),
+ &pdfinteraction::InteractionController::selectionChanged,
+ this,
+ &EditorHost::onInteractionSelectionChanged);
connect(m_session->interaction(),
&pdfinteraction::InteractionController::dragCompleted,
this,
@@ -654,13 +1217,13 @@ void EditorHost::registerFeatureHandlers()
bind(QStringLiteral("actionFind"), [this]
{
m_searchPanelVisible = true;
- m_workspaceRequest = 0; });
+ setWorkspace(LoopWorkspace::Document); });
bind(QStringLiteral("actionFindNext"), [this]
{ moveSearch(1); });
bind(QStringLiteral("actionFindPrevious"), [this]
{ moveSearch(-1); });
bind(QStringLiteral("actionProperties"), [this]
- { m_workspaceRequest = 2; });
+ { setWorkspace(LoopWorkspace::Inspect); });
refreshFeatureAvailability();
}
@@ -739,6 +1302,7 @@ void EditorHost::onDocumentReady()
m_documentBound = true;
bindCanvas();
updateCanvasAccessibilitySummary();
+ applyEmptyCanvasInspectorSelection();
announceDocumentState(tr("Document ready."));
}
@@ -767,13 +1331,15 @@ void EditorHost::syncDocumentLifecycle()
void EditorHost::onDocumentGone()
{
+ cancelPreflight();
unbindCanvas();
m_session->clearDocumentView();
- m_preflight.findingsModel()->clear();
+ m_preflight.clear();
m_inspector.clearSelection();
m_documentModel.clear();
m_searchRow = -1;
m_preview.clear();
+ m_production.clear();
m_session->hitTest()->clearSources();
m_documentBound = false;
updateCanvasAccessibilitySummary();
@@ -804,6 +1370,73 @@ void EditorHost::unbindCanvas()
m_canvas->bind(nullptr, nullptr, nullptr);
}
+QStringList EditorHost::activeAsyncWorkKinds() const
+{
+ QStringList kinds;
+ kinds.reserve(m_activeAsyncJobs.size());
+ for (auto it = m_activeAsyncJobs.cbegin(); it != m_activeAsyncJobs.cend(); ++it)
+ {
+ kinds.append(QString::fromLatin1(pdf::getPDFJobKindName(it.value())));
+ }
+ kinds.removeDuplicates();
+ std::sort(kinds.begin(), kinds.end());
+ return kinds;
+}
+
+void EditorHost::acceptPreflightResult(const QString& jobId,
+ const QString& documentRevision,
+ const pdf::PreflightResult& result)
+{
+ if (m_acceptPreflightResults && m_preflight.acceptResult(jobId, documentRevision, result))
+ {
+ refreshCanvasTrace();
+ bumpPresentation();
+ }
+}
+
+void EditorHost::finishPreflightJob(const pdf::PDFJobSnapshot& snapshot)
+{
+ const std::shared_ptr outcome = m_preflightOutcomes.take(snapshot.jobId);
+ if (snapshot.jobId != m_preflight.jobId() || m_preflight.state() != pdfinteraction::PreflightController::State::Running)
+ {
+ return;
+ }
+
+ switch (snapshot.status)
+ {
+ case pdf::PDFJobStatus::Succeeded:
+ if (outcome)
+ {
+ acceptPreflightResult(snapshot.jobId, snapshot.documentRevision, outcome->result);
+ }
+ else
+ {
+ m_preflight.failRun(snapshot.jobId, snapshot.documentRevision, tr("Preflight result was unavailable."));
+ }
+ break;
+ case pdf::PDFJobStatus::Failed:
+ m_preflight.failRun(snapshot.jobId, snapshot.documentRevision, snapshot.errorMessage);
+ break;
+ case pdf::PDFJobStatus::Cancelled:
+ m_preflight.cancelRun(snapshot.jobId);
+ break;
+ case pdf::PDFJobStatus::Stale:
+ syncRevisionModels();
+ break;
+ case pdf::PDFJobStatus::Queued:
+ case pdf::PDFJobStatus::Running:
+ break;
+ }
+}
+
+void EditorHost::refreshCanvasTrace()
+{
+ if (m_canvas)
+ {
+ m_canvas->update();
+ }
+}
+
void EditorHost::syncRevisionModels()
{
if (!m_session->revisionSource())
@@ -816,6 +1449,7 @@ void EditorHost::syncRevisionModels()
m_preflight.setCurrentRevision(documentKey, documentRevision);
m_inspector.setCurrentRevision(documentKey, documentRevision);
m_preview.setCurrentRevision(documentKey, documentRevision);
+ m_production.setCurrentRevision(documentKey, documentRevision);
if (hasDocument())
{
@@ -825,7 +1459,34 @@ void EditorHost::syncRevisionModels()
tr("Production preview is approximate until proof mode is active."),
tr("The current view uses the standard render path."),
QString());
+ syncProductionState();
+ }
+}
+
+void EditorHost::syncProductionState()
+{
+ if (!m_session->revisionSource() || !hasDocument())
+ {
+ return;
+ }
+
+ const QString documentKey = m_session->revisionSource()->documentKey();
+ const QString documentRevision = m_session->facade().currentRevision().toString();
+ pdfinteraction::ProductionModel::State state = pdfinteraction::ProductionModel::State::Ready;
+ if (m_session->facade().outputState() == pdfinteraction::DocumentOutputState::Pending)
+ {
+ state = pdfinteraction::ProductionModel::State::OperationPending;
}
+ else if (m_session->facade().outputState() == pdfinteraction::DocumentOutputState::Saved)
+ {
+ state = pdfinteraction::ProductionModel::State::OutputWritten;
+ }
+ else if (m_preview.status() == pdfinteraction::PreviewStateModel::Status::Unavailable)
+ {
+ state = pdfinteraction::ProductionModel::State::NotReady;
+ }
+
+ m_production.setState(documentKey, documentRevision, state);
}
void EditorHost::updateCanvasAccessibilitySummary()
@@ -874,3 +1535,127 @@ void EditorHost::onDragCompleted(pdfinteraction::DragSession session)
m_session->interaction()->refreshOverlay();
}
}
+
+void EditorHost::onInteractionSelectionChanged(pdfinteraction::InteractionTarget target)
+{
+ applyInspectorSelection(target);
+ bumpPresentation();
+}
+
+void EditorHost::applyEmptyCanvasInspectorSelection()
+{
+ if (!m_session->revisionSource() || !hasDocument())
+ {
+ m_inspector.clearSelection();
+ return;
+ }
+
+ pdfinteraction::InspectorModel::Selection selection;
+ selection.documentKey = m_session->revisionSource()->documentKey();
+ selection.documentRevision = m_session->facade().currentRevision().toString();
+ selection.selectionId = QStringLiteral("canvas");
+ selection.title = tr("Document canvas");
+ selection.kind = pdfinteraction::InspectorModel::SelectionKind::EmptyCanvas;
+ selection.properties = {
+ { QStringLiteral("document"), QStringLiteral("Document"), displayTitle() },
+ { QStringLiteral("pages"), QStringLiteral("Pages"), QString::number(pageCount()) },
+ { QStringLiteral("document-status"), QStringLiteral("Document status"), documentShellStatus() },
+ { QStringLiteral("preflight"), QStringLiteral("Preflight"), preflightStateName() },
+ { QStringLiteral("production"), QStringLiteral("Production"), productionStateName() },
+ };
+ m_inspector.setSelection(selection);
+}
+
+void EditorHost::applyInspectorSelection(const pdfinteraction::InteractionTarget& target)
+{
+ if (!m_session->revisionSource() || !hasDocument())
+ {
+ applyEmptyCanvasInspectorSelection();
+ return;
+ }
+
+ if (!target.isValid())
+ {
+ applyEmptyCanvasInspectorSelection();
+ return;
+ }
+
+ const QString documentKey = m_session->revisionSource()->documentKey();
+ const QString documentRevision = m_session->facade().currentRevision().toString();
+
+ if (target.kind == pdfinteraction::InteractionTargetKind::Finding)
+ {
+ m_inspector.setFindingSelection(*m_preflight.findingsModel(), target.id, documentRevision);
+ return;
+ }
+
+ if (target.id.startsWith(QStringLiteral("image:")))
+ {
+ pdfinteraction::InspectorModel::Selection selection;
+ selection.documentKey = documentKey;
+ selection.documentRevision = documentRevision;
+ selection.selectionId = target.id;
+ selection.title = tr("Image");
+ selection.kind = pdfinteraction::InspectorModel::SelectionKind::Image;
+ selection.properties = {
+ { QStringLiteral("id"), QStringLiteral("Image"), target.id.mid(6) },
+ { QStringLiteral("page"), QStringLiteral("Page"), QString::number(target.pageIndex + 1) },
+ { QStringLiteral("bounds"), QStringLiteral("Bounds"),
+ QStringLiteral("%1,%2 %3x%4")
+ .arg(QString::number(target.pageBounds.x()),
+ QString::number(target.pageBounds.y()),
+ QString::number(target.pageBounds.width()),
+ QString::number(target.pageBounds.height())) },
+ { QStringLiteral("dpi"), QStringLiteral("Effective DPI"), tr("pending") },
+ { QStringLiteral("colour-space"), QStringLiteral("Colour space"), tr("pending") },
+ { QStringLiteral("compression"), QStringLiteral("Compression"), tr("pending") },
+ { QStringLiteral("mask"), QStringLiteral("Mask"), tr("pending") },
+ };
+ m_inspector.setSelection(selection);
+ return;
+ }
+
+ if (target.id.startsWith(QStringLiteral("separation:")))
+ {
+ pdfinteraction::InspectorModel::Selection selection;
+ selection.documentKey = documentKey;
+ selection.documentRevision = documentRevision;
+ selection.selectionId = target.id;
+ selection.title = tr("Separation");
+ selection.kind = pdfinteraction::InspectorModel::SelectionKind::Separation;
+ selection.properties = {
+ { QStringLiteral("name"), QStringLiteral("Ink"), target.id.mid(11) },
+ { QStringLiteral("page"), QStringLiteral("Page"), QString::number(target.pageIndex + 1) },
+ { QStringLiteral("coverage"), QStringLiteral("Ink coverage"), tr("pending") },
+ { QStringLiteral("kind"), QStringLiteral("Process / spot"), tr("pending") },
+ };
+ m_inspector.setSelection(selection);
+ return;
+ }
+
+ if (target.kind == pdfinteraction::InteractionTargetKind::Page ||
+ target.kind == pdfinteraction::InteractionTargetKind::PageBox)
+ {
+ pdfinteraction::InspectorModel::Selection selection;
+ selection.documentKey = documentKey;
+ selection.documentRevision = documentRevision;
+ selection.selectionId = target.id.isEmpty() ? QStringLiteral("page") : target.id;
+ selection.title = target.kind == pdfinteraction::InteractionTargetKind::PageBox
+ ? tr("Page box: %1").arg(target.id)
+ : tr("Page %1").arg(target.pageIndex + 1);
+ selection.kind = pdfinteraction::InspectorModel::SelectionKind::Page;
+ selection.properties = {
+ { QStringLiteral("page"), QStringLiteral("Page"), QString::number(target.pageIndex + 1) },
+ { QStringLiteral("box"), QStringLiteral("Box"), target.id },
+ { QStringLiteral("size"), QStringLiteral("Size"),
+ QStringLiteral("%1 x %2")
+ .arg(QString::number(target.pageBounds.width()), QString::number(target.pageBounds.height())) },
+ { QStringLiteral("rotation"), QStringLiteral("Rotation"), QStringLiteral("%1°").arg(rotationDegrees()) },
+ { QStringLiteral("ocg"), QStringLiteral("Optional content"), m_documentModel.hasOptionalContent() ? tr("present") : tr("none") },
+ };
+ m_inspector.setSelection(selection);
+ return;
+ }
+
+ applyEmptyCanvasInspectorSelection();
+}
diff --git a/LoopEditor/editorhost.h b/LoopEditor/editorhost.h
index 794e3747d..7b00f1b7c 100644
--- a/LoopEditor/editorhost.h
+++ b/LoopEditor/editorhost.h
@@ -35,6 +35,7 @@
#include "preflightcontroller.h"
#include "preflightoverlaybridge.h"
#include "previewstatemodel.h"
+#include "productionmodel.h"
#include "viewportcommandbridge.h"
#include "viewportcontroller.h"
@@ -45,7 +46,10 @@
#include "pdfdocumentcontext.h"
#include "pdfjobscheduler.h"
+#include
#include
+#include
+#include
#include
#include
#include
@@ -91,6 +95,13 @@ class EditorHost final : public QObject
Q_PROPERTY(QObject* documentModel READ documentModel CONSTANT)
Q_PROPERTY(QObject* focusRestoration READ focusRestoration CONSTANT)
Q_PROPERTY(QString preflightStateName READ preflightStateName NOTIFY presentationChanged)
+ Q_PROPERTY(QVariantMap preflightStateVisual READ preflightStateVisual NOTIFY presentationChanged)
+ Q_PROPERTY(QColor preflightStateColor READ preflightStateColor NOTIFY presentationChanged)
+ Q_PROPERTY(QString preflightOperatorSummary READ preflightOperatorSummary NOTIFY presentationChanged)
+ Q_PROPERTY(QVariantList preflightProfiles READ preflightProfiles NOTIFY preflightProfilesChanged)
+ Q_PROPERTY(QVariantList preflightVariables READ preflightVariables NOTIFY preflightProfilesChanged)
+ Q_PROPERTY(QString selectedPreflightProfileId READ selectedPreflightProfileId NOTIFY preflightProfilesChanged)
+ Q_PROPERTY(bool hasPreflightReport READ hasPreflightReport NOTIFY presentationChanged)
Q_PROPERTY(QString previewSummary READ previewSummary NOTIFY presentationChanged)
Q_PROPERTY(QString inspectorTitle READ inspectorTitle NOTIFY presentationChanged)
Q_PROPERTY(bool preferReducedMotion READ preferReducedMotion NOTIFY presentationChanged)
@@ -101,8 +112,24 @@ class EditorHost final : public QObject
Q_PROPERTY(bool searchPanelVisible READ searchPanelVisible NOTIFY presentationChanged)
Q_PROPERTY(bool fullscreenRequested READ fullscreenRequested NOTIFY presentationChanged)
Q_PROPERTY(int workspaceRequest READ workspaceRequest NOTIFY presentationChanged)
+ Q_PROPERTY(LoopWorkspace workspace READ workspace WRITE setWorkspace NOTIFY workspaceChanged)
+ Q_PROPERTY(QString documentShellStatus READ documentShellStatus NOTIFY presentationChanged)
+ Q_PROPERTY(QString productionStateName READ productionStateName NOTIFY presentationChanged)
+ Q_PROPERTY(bool allowDeveloperDiagnostics READ allowDeveloperDiagnostics CONSTANT)
public:
+ enum LoopWorkspace
+ {
+ Document = 0,
+ Preflight = 1,
+ ProductionPreview = 2,
+ Pages = 3,
+ Inspect = 4,
+ Fix = 5,
+ Compare = 6
+ };
+ Q_ENUM(LoopWorkspace)
+
explicit EditorHost(QObject* parent = nullptr);
~EditorHost() override;
@@ -129,6 +156,21 @@ class EditorHost final : public QObject
FocusRestoration* focusRestoration() { return &m_focusRestoration; }
QString preflightStateName() const;
+
+ /// Canonical #194 treatment for the current document-level preflight state: the keys
+ /// `kind`, `colorRole`, `icon` and `accessibleName`, all computed by LoopLibQuick from Core's
+ /// own state name. QML renders it; it derives nothing and picks no roles.
+ QVariantMap preflightStateVisual() const;
+
+ /// The `colorRole` above, resolved to a colour for the current theme. QML must never map a role
+ /// name to a colour itself.
+ QColor preflightStateColor() const;
+
+ QString preflightOperatorSummary() const;
+ QVariantList preflightProfiles() const;
+ QVariantList preflightVariables() const;
+ QString selectedPreflightProfileId() const;
+ bool hasPreflightReport() const noexcept { return m_preflight.hasResult(); }
QString previewSummary() const;
QString inspectorTitle() const;
bool preferReducedMotion() const;
@@ -136,6 +178,10 @@ class EditorHost final : public QObject
bool searchPanelVisible() const noexcept { return m_searchPanelVisible; }
bool fullscreenRequested() const noexcept { return m_fullscreenRequested; }
int workspaceRequest() const noexcept { return m_workspaceRequest; }
+ LoopWorkspace workspace() const noexcept { return m_workspace; }
+ QString documentShellStatus() const;
+ QString productionStateName() const;
+ bool allowDeveloperDiagnostics() const;
/// Overprint render fidelity for the currently displayed page (issue #49).
/// True (and pageFidelityReason empty) when the page has no overprint
@@ -153,6 +199,12 @@ class EditorHost final : public QObject
Q_INVOKABLE void selectFinding(const QString& findingId);
Q_INVOKABLE void announceDocumentState(const QString& message);
+ Q_INVOKABLE bool runPreflight();
+ Q_INVOKABLE bool cancelPreflight();
+ Q_INVOKABLE bool selectPreflightProfile(const QString& id);
+ Q_INVOKABLE bool setPreflightVariable(const QString& name, const QVariant& value);
+ Q_INVOKABLE void requestPreflightReportExport();
+ Q_INVOKABLE bool exportPreflightReportFileUrl(const QUrl& url);
/// Toggles the current page between the fast approximate render and the
/// authoritative overprint-accurate one. Re-renders only that page;
@@ -160,6 +212,7 @@ class EditorHost final : public QObject
Q_INVOKABLE void toggleCurrentPageFidelity();
Q_INVOKABLE void goToPage(int pageIndex);
Q_INVOKABLE void goToOutlinePage(int pageIndex);
+ Q_INVOKABLE void setWorkspace(LoopWorkspace workspace);
Q_INVOKABLE void acknowledgeWorkspaceRequest();
Q_INVOKABLE void acknowledgeSearchPanel();
@@ -198,6 +251,9 @@ class EditorHost final : public QObject
signals:
void presentationChanged();
void commandEpochChanged();
+ void workspaceChanged(LoopWorkspace from, LoopWorkspace to);
+ void preflightProfilesChanged();
+ void preflightReportExportRequested();
private:
void connectFacade();
@@ -218,26 +274,60 @@ class EditorHost final : public QObject
void syncDocumentLifecycle();
void bindCanvas();
void unbindCanvas();
+ QStringList activeAsyncWorkKinds() const;
+ void acceptPreflightResult(const QString& jobId,
+ const QString& documentRevision,
+ const pdf::PreflightResult& result);
+ void finishPreflightJob(const pdf::PDFJobSnapshot& snapshot);
+ void refreshCanvasTrace();
+ void reloadPreflightProfiles();
+ void updatePreflightProfileWatch();
void syncRevisionModels();
void updateCanvasAccessibilitySummary();
void onPreflightNavigation(pdfinteraction::PreflightController::EvidenceNavigationRequest request);
void onDragCompleted(pdfinteraction::DragSession session);
+ void onInteractionSelectionChanged(pdfinteraction::InteractionTarget target);
+ void syncProductionState();
+ void applyInspectorSelection(const pdfinteraction::InteractionTarget& target);
+ void applyEmptyCanvasInspectorSelection();
std::unique_ptr m_session;
pdfinteraction::PreflightController m_preflight;
pdfinteraction::PreflightOverlayBridge m_preflightOverlayBridge;
pdfinteraction::InspectorModel m_inspector;
pdfinteraction::PreviewStateModel m_preview;
+ pdfinteraction::ProductionModel m_production;
QuickDocumentModel m_documentModel;
FocusRestoration m_focusRestoration;
pdfinteraction::FindingListHitTestSource m_findingsHitTest;
QPointer m_canvas;
+ QHash m_activeAsyncJobs;
+ struct PreflightWorkerOutcome;
+ QHash> m_preflightOutcomes;
+ struct PreflightProfileChoice
+ {
+ QString id;
+ QString name;
+ QString version;
+ QString source;
+ QString diagnostic;
+ QString digest;
+ QJsonObject profile;
+ QJsonObject variables;
+ bool valid = false;
+ };
+ QList m_preflightProfiles;
+ QJsonObject m_preflightBindings;
+ QString m_selectedPreflightProfileId;
+ class QFileSystemWatcher* m_preflightProfileWatcher = nullptr;
+ bool m_acceptPreflightResults = true;
int m_commandEpoch = 0;
bool m_documentBound = false;
bool m_searchPanelVisible = false;
bool m_fullscreenRequested = false;
int m_workspaceRequest = -1;
+ LoopWorkspace m_workspace = LoopWorkspace::Document;
int m_searchRow = -1;
};
diff --git a/LoopEditor/main.cpp b/LoopEditor/main.cpp
index f487659fb..cb8a92cae 100644
--- a/LoopEditor/main.cpp
+++ b/LoopEditor/main.cpp
@@ -36,6 +36,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -254,6 +255,11 @@ int runQuickSmoke(QGuiApplication& application, EditorHost& host, const QString&
engine.addImportPath(importPath);
}
engine.rootContext()->setContextProperty(QStringLiteral("editorHost"), &host);
+ qmlRegisterUncreatableType("Loop.Quick",
+ 1,
+ 0,
+ "EditorHost",
+ QStringLiteral("EditorHost is provided by the shell context"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &application,
[&application](QObject* object, const QUrl& url)
{
@@ -378,6 +384,11 @@ int main(int argc, char* argv[])
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("editorHost"), &host);
+ qmlRegisterUncreatableType("Loop.Quick",
+ 1,
+ 0,
+ "EditorHost",
+ QStringLiteral("EditorHost is provided by the shell context"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &application,
[&application](QObject* object, const QUrl& url)
diff --git a/LoopEditor/qml/DocumentPane.qml b/LoopEditor/qml/DocumentPane.qml
index f03119c9a..fd6193215 100644
--- a/LoopEditor/qml/DocumentPane.qml
+++ b/LoopEditor/qml/DocumentPane.qml
@@ -159,6 +159,14 @@ Item {
host: root.host
Accessible.name: qsTr("Document canvas pane")
}
+
+ InspectorPane {
+ id: inspectorPane
+ Layout.preferredWidth: 280
+ Layout.fillHeight: true
+ host: root.host
+ Accessible.name: qsTr("Contextual inspector dock")
+ }
}
Connections {
diff --git a/LoopEditor/qml/Main.qml b/LoopEditor/qml/Main.qml
index f73397e69..1cd5bdedc 100644
--- a/LoopEditor/qml/Main.qml
+++ b/LoopEditor/qml/Main.qml
@@ -17,49 +17,8 @@ ApplicationWindow {
height: 768
title: host && host.displayTitle.length > 0 ? host.displayTitle : qsTr("Loop")
- property var commandMap: ({})
- property int commandEpoch: host ? host.commandEpoch : 0
-
- function rebuildCommands() {
- const next = {}
- if (!host) {
- commandMap = next
- return
- }
-
- const descriptors = host.commandDescriptors()
- for (let index = 0; index < descriptors.length; ++index) {
- const entry = descriptors[index]
- next[entry.id] = entry
- }
- commandMap = next
- }
-
- function commandEnabled(commandId) {
- const entry = commandMap[commandId]
- return !!entry && entry.enabled === true
- }
-
- function invoke(commandId) {
- if (!host) {
- return
- }
- host.invokeCommand(commandId)
- }
-
- function shortcutSequence(entry) {
- if (!entry || !entry.id) {
- return ""
- }
- return entry.shortcutText || ""
- }
-
Connections {
target: host
- function onCommandEpochChanged() {
- window.commandEpoch = host.commandEpoch
- window.rebuildCommands()
- }
function onPresentationChanged() {
if (host && host.displayTitle.length > 0) {
window.title = host.displayTitle
@@ -76,171 +35,9 @@ ApplicationWindow {
}
}
- Component.onCompleted: rebuildCommands()
-
- menuBar: MenuBar {
- Menu {
- title: qsTr("&File")
- Action {
- text: qsTr("&Open…")
- enabled: commandEnabled("actionOpen")
- shortcut: shortcutSequence(commandMap["actionOpen"])
- onTriggered: {
- if (host && host.focusRestoration) {
- host.focusRestoration.remember(window.activeFocusItem)
- }
- openDialog.open()
- }
- }
- Action {
- text: qsTr("&Close")
- enabled: commandEnabled("actionClose")
- shortcut: shortcutSequence(commandMap["actionClose"])
- onTriggered: invoke("actionClose")
- }
- Action {
- text: qsTr("&Save")
- enabled: commandEnabled("actionSave")
- shortcut: shortcutSequence(commandMap["actionSave"])
- onTriggered: invoke("actionSave")
- }
- Action {
- text: qsTr("Save &As…")
- enabled: commandEnabled("actionSave_As")
- shortcut: shortcutSequence(commandMap["actionSave_As"])
- onTriggered: {
- if (host && host.focusRestoration) {
- host.focusRestoration.remember(window.activeFocusItem)
- }
- saveAsDialog.open()
- }
- }
- MenuSeparator {}
- Action {
- text: qsTr("E&xit")
- enabled: commandEnabled("actionQuit")
- shortcut: shortcutSequence(commandMap["actionQuit"])
- onTriggered: invoke("actionQuit")
- }
- }
-
- Menu {
- title: qsTr("&Navigate")
- Action {
- text: qsTr("Previous &Page")
- enabled: commandEnabled("actionGoToPreviousPage")
- shortcut: shortcutSequence(commandMap["actionGoToPreviousPage"])
- onTriggered: invoke("actionGoToPreviousPage")
- }
- Action {
- text: qsTr("Next &Page")
- enabled: commandEnabled("actionGoToNextPage")
- shortcut: shortcutSequence(commandMap["actionGoToNextPage"])
- onTriggered: invoke("actionGoToNextPage")
- }
- Action {
- text: qsTr("&First Page")
- enabled: commandEnabled("actionGoToDocumentStart")
- shortcut: shortcutSequence(commandMap["actionGoToDocumentStart"])
- onTriggered: invoke("actionGoToDocumentStart")
- }
- Action {
- text: qsTr("&Last Page")
- enabled: commandEnabled("actionGoToDocumentEnd")
- shortcut: shortcutSequence(commandMap["actionGoToDocumentEnd"])
- onTriggered: invoke("actionGoToDocumentEnd")
- }
- }
-
- Menu {
- title: qsTr("&View")
- Action {
- text: qsTr("&Find…")
- enabled: commandEnabled("actionFind")
- shortcut: shortcutSequence(commandMap["actionFind"])
- onTriggered: invoke("actionFind")
- }
- MenuSeparator {}
- Action {
- text: qsTr("Zoom &In")
- enabled: commandEnabled("actionZoom_In")
- shortcut: shortcutSequence(commandMap["actionZoom_In"])
- onTriggered: invoke("actionZoom_In")
- }
- Action {
- text: qsTr("Zoom &Out")
- enabled: commandEnabled("actionZoom_Out")
- shortcut: shortcutSequence(commandMap["actionZoom_Out"])
- onTriggered: invoke("actionZoom_Out")
- }
- Action {
- text: qsTr("&Fit Page")
- enabled: commandEnabled("actionFitPage")
- shortcut: shortcutSequence(commandMap["actionFitPage"])
- onTriggered: invoke("actionFitPage")
- }
- Action {
- text: qsTr("Fit &Width")
- enabled: commandEnabled("actionFitWidth")
- shortcut: shortcutSequence(commandMap["actionFitWidth"])
- onTriggered: invoke("actionFitWidth")
- }
- Action {
- text: qsTr("Fit &Height")
- enabled: commandEnabled("actionFitHeight")
- shortcut: shortcutSequence(commandMap["actionFitHeight"])
- onTriggered: invoke("actionFitHeight")
- }
- MenuSeparator {}
- Action {
- text: qsTr("Rotate &Left")
- enabled: commandEnabled("actionRotateLeft")
- shortcut: shortcutSequence(commandMap["actionRotateLeft"])
- onTriggered: invoke("actionRotateLeft")
- }
- Action {
- text: qsTr("Rotate &Right")
- enabled: commandEnabled("actionRotateRight")
- shortcut: shortcutSequence(commandMap["actionRotateRight"])
- onTriggered: invoke("actionRotateRight")
- }
- MenuSeparator {}
- Action {
- text: qsTr("Continuous Layout")
- enabled: commandEnabled("actionPageLayoutContinuous")
- onTriggered: invoke("actionPageLayoutContinuous")
- }
- Action {
- text: qsTr("Single Page Layout")
- enabled: commandEnabled("actionPageLayoutSinglePage")
- onTriggered: invoke("actionPageLayoutSinglePage")
- }
- Action {
- text: qsTr("Two-Column Layout")
- enabled: commandEnabled("actionPageLayoutTwoColumns")
- onTriggered: invoke("actionPageLayoutTwoColumns")
- }
- Action {
- text: qsTr("Two-Page Layout")
- enabled: commandEnabled("actionPageLayoutTwoPages")
- onTriggered: invoke("actionPageLayoutTwoPages")
- }
- Action {
- text: qsTr("Fullscreen")
- enabled: commandEnabled("actionFullscreenMode")
- shortcut: shortcutSequence(commandMap["actionFullscreenMode"])
- onTriggered: invoke("actionFullscreenMode")
- }
- }
-
- Menu {
- title: qsTr("&Document")
- Action {
- text: qsTr("&Properties")
- enabled: commandEnabled("actionProperties")
- onTriggered: invoke("actionProperties")
- }
- }
+ menuBar: ShellMenuBar {
+ host: window.host
+ window: window
}
FileDialog {
@@ -270,6 +67,27 @@ ApplicationWindow {
onRejected: if (host && host.focusRestoration) host.focusRestoration.restore()
}
+ FileDialog {
+ id: preflightReportDialog
+ title: qsTr("Export Preflight Report")
+ fileMode: FileDialog.SaveFile
+ nameFilters: [qsTr("JSON files (*.json)")]
+ onAccepted: {
+ if (host) {
+ host.exportPreflightReportFileUrl(selectedFile)
+ }
+ if (host && host.focusRestoration) host.focusRestoration.restore()
+ }
+ onRejected: if (host && host.focusRestoration) host.focusRestoration.restore()
+ }
+
+ Connections {
+ target: host
+ function onPreflightReportExportRequested() {
+ preflightReportDialog.open()
+ }
+ }
+
ColumnLayout {
anchors.fill: parent
spacing: 0
@@ -331,16 +149,38 @@ ApplicationWindow {
padding: 6
Accessible.role: Accessible.StatusBar
- Accessible.name: qsTr("Page status")
+ Accessible.name: qsTr("Shell status")
RowLayout {
anchors.fill: parent
- spacing: 12
+ spacing: 16
Label {
- text: host && host.hasDocument
- ? qsTr("Page %1 / %2").arg(host.currentPage + 1).arg(host.pageCount)
- : qsTr("No document")
+ text: host
+ ? qsTr("Document: %1").arg(host.documentShellStatus)
+ : qsTr("Document: NO_DOCUMENT")
+ Accessible.name: qsTr("Document status")
+ }
+
+ Label {
+ text: host
+ ? qsTr("Production: %1").arg(host.productionStateName)
+ : qsTr("Production: NOT_READY")
+ Accessible.name: qsTr("Production status")
+ }
+
+ Label {
+ text: host
+ ? qsTr("Preflight: %1").arg(host.preflightStateName)
+ : qsTr("Preflight: not-checked")
+ Accessible.name: qsTr("Preflight status")
+ }
+
+ Item { Layout.fillWidth: true }
+
+ Label {
+ visible: host && host.hasDocument
+ text: qsTr("Page %1 / %2").arg(host.currentPage + 1).arg(host.pageCount)
Accessible.name: qsTr("Current page")
}
@@ -356,8 +196,6 @@ ApplicationWindow {
Accessible.name: qsTr("Current rotation")
}
- Item { Layout.fillWidth: true }
-
Label {
visible: host && host.documentState === "opening"
text: qsTr("Opening…")
diff --git a/LoopEditor/qml/MenuModel.qml b/LoopEditor/qml/MenuModel.qml
new file mode 100644
index 000000000..89a14a6fa
--- /dev/null
+++ b/LoopEditor/qml/MenuModel.qml
@@ -0,0 +1,81 @@
+import QtQuick
+
+QtObject {
+ id: root
+
+ property var host: null
+ property var window: null
+
+ readonly property var menuGroups: [
+ "File", "Edit", "View", "Document", "Production", "Preflight", "Help", "Advanced"
+ ]
+
+ function descriptorsForGroup(groupName) {
+ if (!host) {
+ return []
+ }
+
+ const descriptors = host.commandDescriptors()
+ const entries = []
+ for (let index = 0; index < descriptors.length; ++index) {
+ const entry = descriptors[index]
+ const disposition = entry.disposition || ""
+ if (disposition === "HIDE" || disposition === "STOP-SHIPPING") {
+ continue
+ }
+ if (groupName === "Advanced") {
+ if (disposition !== "ADVANCED") {
+ continue
+ }
+ if (!host.allowDeveloperDiagnostics) {
+ continue
+ }
+ } else if (entry.menuGroup !== groupName) {
+ continue
+ } else if (disposition === "ADVANCED" && !host.allowDeveloperDiagnostics) {
+ continue
+ }
+ entries.push(entry)
+ }
+ return entries
+ }
+
+ function labelForEntry(entry) {
+ if (!entry || !entry.labelKey) {
+ return entry && entry.id ? entry.id : ""
+ }
+ const key = entry.labelKey
+ const suffix = key.startsWith("command.") ? key.slice("command.".length) : key
+ const base = suffix.endsWith(".label") ? suffix.slice(0, -".label".length) : suffix
+ const words = base.replace(/^action/, "").replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2")
+ if (words.length === 0) {
+ return entry.id
+ }
+ return words.charAt(0).toUpperCase() + words.slice(1)
+ }
+
+ function commandEnabled(commandId) {
+ return host ? host.isCommandEnabled(commandId) : false
+ }
+
+ function invoke(commandId) {
+ if (!host) {
+ return
+ }
+ if (commandId === "actionOpen" && window && window.openDialog) {
+ if (host.focusRestoration) {
+ host.focusRestoration.remember(window.activeFocusItem)
+ }
+ window.openDialog.open()
+ return
+ }
+ if (commandId === "actionSave_As" && window && window.saveAsDialog) {
+ if (host.focusRestoration) {
+ host.focusRestoration.remember(window.activeFocusItem)
+ }
+ window.saveAsDialog.open()
+ return
+ }
+ host.invokeCommand(commandId)
+ }
+}
diff --git a/LoopEditor/qml/PreflightPane.qml b/LoopEditor/qml/PreflightPane.qml
index 19547bf1f..68167ae7f 100644
--- a/LoopEditor/qml/PreflightPane.qml
+++ b/LoopEditor/qml/PreflightPane.qml
@@ -21,10 +21,104 @@ Pane {
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
- text: host ? qsTr("Preflight status: %1").arg(host.preflightStateName) : ""
+ text: {
+ if (!host)
+ return ""
+ if (host.preflightOperatorSummary)
+ return host.preflightOperatorSummary
+ return qsTr("Preflight status: %1").arg(host.preflightStateName)
+ }
Accessible.name: qsTr("Preflight status")
}
+ ComboBox {
+ id: profileSelector
+ Layout.fillWidth: true
+ model: root.host ? root.host.preflightProfiles : []
+ textRole: "name"
+ valueRole: "id"
+ enabled: root.host && root.host.preflightStateName !== "running"
+ currentIndex: {
+ if (!root.host)
+ return -1
+ for (var i = 0; i < model.length; ++i) {
+ if (model[i].id === root.host.selectedPreflightProfileId)
+ return i
+ }
+ return -1
+ }
+ Accessible.name: qsTr("Preflight profile")
+ Accessible.description: qsTr("Select a bundled or local validated preflight profile.")
+ onActivated: function(index) {
+ if (root.host && model[index])
+ root.host.selectPreflightProfile(model[index].id)
+ }
+ }
+
+ Repeater {
+ model: root.host ? root.host.preflightVariables : []
+ delegate: RowLayout {
+ Layout.fillWidth: true
+ required property var modelData
+
+ Label {
+ text: modelData.required ? qsTr("%1 (required)").arg(modelData.name) : modelData.name
+ Accessible.name: modelData.description.length > 0 ? modelData.description : text
+ }
+ CheckBox {
+ visible: modelData.type === "boolean"
+ checked: Boolean(modelData.value)
+ text: modelData.description
+ onToggled: if (root.host) root.host.setPreflightVariable(modelData.name, checked)
+ }
+ TextField {
+ Layout.fillWidth: true
+ visible: modelData.type !== "boolean"
+ text: modelData.value === undefined || modelData.value === null ? "" : String(modelData.value)
+ inputMethodHints: modelData.type === "string" ? Qt.ImhNone : Qt.ImhFormattedNumbersOnly
+ Accessible.name: qsTr("Preflight variable %1").arg(modelData.name)
+ onEditingFinished: if (root.host) root.host.setPreflightVariable(modelData.name, text)
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Button {
+ text: qsTr("Run Preflight")
+ enabled: root.host && root.host.hasDocument && root.host.preflightStateName !== "running"
+ Accessible.name: qsTr("Run preflight")
+ Accessible.description: qsTr("Runs the selected validated preflight profile.")
+ onClicked: root.host.runPreflight()
+ }
+
+ Button {
+ text: qsTr("Cancel")
+ enabled: root.host && root.host.preflightStateName === "running"
+ Accessible.name: qsTr("Cancel preflight")
+ Accessible.description: qsTr("Cancels the running preflight job.")
+ onClicked: root.host.cancelPreflight()
+ }
+
+ Button {
+ text: qsTr("Export Report")
+ enabled: root.host && root.host.hasPreflightReport
+ Accessible.name: qsTr("Export preflight report")
+ Accessible.description: qsTr("Exports the retained normalized preflight report as JSON.")
+ onClicked: root.host.requestPreflightReportExport()
+ }
+
+ ProgressBar {
+ Layout.fillWidth: true
+ from: 0
+ to: 100
+ value: root.host ? root.host.preflight.progress : 0
+ enabled: root.host && root.host.preflightStateName === "running"
+ Accessible.name: qsTr("Preflight progress")
+ }
+ }
+
ListView {
id: findingsView
Layout.fillWidth: true
@@ -40,10 +134,11 @@ Pane {
delegate: ItemDelegate {
width: findingsView.width
- text: model.message
+ text: "%1 — %2".arg(model.severity).arg(model.message)
highlighted: model.selected
Accessible.name: model.message
- Accessible.description: qsTr("Severity %1 on page %2").arg(model.severity).arg(model.page)
+ Accessible.description: qsTr("Severity %1, scope %2, page %3, object %4, check %5, evidence %6")
+ .arg(model.severity).arg(model.scope).arg(model.page).arg(model.objectId).arg(model.checkId).arg(model.evidenceIds.join(", "))
onClicked: {
findingsView.currentIndex = index
if (host) {
diff --git a/LoopEditor/qml/ShellMenuBar.qml b/LoopEditor/qml/ShellMenuBar.qml
new file mode 100644
index 000000000..fdb20fd90
--- /dev/null
+++ b/LoopEditor/qml/ShellMenuBar.qml
@@ -0,0 +1,54 @@
+import QtQuick
+import QtQuick.Controls
+
+MenuBar {
+ id: root
+
+ property var host: editorHost
+ property var window: null
+
+ readonly property var menuModel: MenuModel {
+ host: root.host
+ window: root.window
+ }
+
+ Repeater {
+ model: root.menuModel.menuGroups
+
+ delegate: Menu {
+ required property string modelData
+ title: {
+ switch (modelData) {
+ case "File": return qsTr("&File")
+ case "Edit": return qsTr("&Edit")
+ case "View": return qsTr("&View")
+ case "Document": return qsTr("&Document")
+ case "Production": return qsTr("&Production")
+ case "Preflight": return qsTr("&Preflight")
+ case "Help": return qsTr("&Help")
+ case "Advanced": return qsTr("&Advanced")
+ default: return modelData
+ }
+ }
+ visible: modelData !== "Advanced" || (root.host && root.host.allowDeveloperDiagnostics)
+
+ Instantiator {
+ model: root.menuModel.descriptorsForGroup(modelData)
+
+ delegate: MenuItem {
+ required property var modelData
+ text: root.menuModel.labelForEntry(modelData)
+ enabled: root.menuModel.commandEnabled(modelData.id)
+ onTriggered: root.menuModel.invoke(modelData.id)
+ }
+
+ onObjectAdded: function(index, object) {
+ parent.addItem(object)
+ }
+ onObjectRemoved: function(index, object) {
+ parent.removeItem(object)
+ }
+ }
+ }
+ }
+}
diff --git a/LoopEditor/qml/ShellToolBar.qml b/LoopEditor/qml/ShellToolBar.qml
new file mode 100644
index 000000000..cac820df3
--- /dev/null
+++ b/LoopEditor/qml/ShellToolBar.qml
@@ -0,0 +1,126 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+ToolBar {
+ id: root
+
+ property var host: editorHost
+ property var commandMap: ({})
+
+ function rebuildCommands() {
+ const next = {}
+ if (!host) {
+ commandMap = next
+ return
+ }
+ const descriptors = host.commandDescriptors()
+ for (let index = 0; index < descriptors.length; ++index) {
+ const entry = descriptors[index]
+ next[entry.id] = entry
+ }
+ commandMap = next
+ }
+
+ function commandEnabled(commandId) {
+ const entry = commandMap[commandId]
+ return !!entry && entry.enabled === true
+ }
+
+ function invoke(commandId) {
+ if (host) {
+ host.invokeCommand(commandId)
+ }
+ }
+
+ function shortcutText(commandId) {
+ const entry = commandMap[commandId]
+ return entry ? (entry.shortcutText || "") : ""
+ }
+
+ Component.onCompleted: rebuildCommands()
+
+ Connections {
+ target: host
+ function onCommandEpochChanged() {
+ root.rebuildCommands()
+ }
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 6
+
+ ToolButton {
+ text: qsTr("Open")
+ enabled: root.commandEnabled("actionOpen")
+ onClicked: root.invoke("actionOpen")
+ Accessible.name: qsTr("Open document")
+ }
+ ToolButton {
+ text: qsTr("Save")
+ enabled: root.commandEnabled("actionSave")
+ onClicked: root.invoke("actionSave")
+ Accessible.name: qsTr("Save document")
+ }
+ ToolButton {
+ text: qsTr("Export")
+ enabled: root.commandEnabled("actionSave_As")
+ onClicked: root.invoke("actionSave_As")
+ Accessible.name: qsTr("Export document")
+ }
+ ToolSeparator {}
+ ToolButton {
+ text: qsTr("Undo")
+ enabled: root.commandEnabled("actionUndo")
+ onClicked: root.invoke("actionUndo")
+ Accessible.name: qsTr("Undo")
+ }
+ ToolButton {
+ text: qsTr("Redo")
+ enabled: root.commandEnabled("actionRedo")
+ onClicked: root.invoke("actionRedo")
+ Accessible.name: qsTr("Redo")
+ }
+ ToolSeparator {}
+ ToolButton {
+ text: qsTr("Select")
+ checkable: true
+ checked: true
+ Accessible.name: qsTr("Select tool")
+ }
+ ToolButton {
+ text: qsTr("Hand")
+ checkable: true
+ Accessible.name: qsTr("Hand tool")
+ }
+ ToolSeparator {}
+ ToolButton {
+ text: qsTr("Zoom In")
+ enabled: root.commandEnabled("actionZoom_In")
+ onClicked: root.invoke("actionZoom_In")
+ Accessible.name: qsTr("Zoom in")
+ }
+ ToolButton {
+ text: qsTr("Zoom Out")
+ enabled: root.commandEnabled("actionZoom_Out")
+ onClicked: root.invoke("actionZoom_Out")
+ Accessible.name: qsTr("Zoom out")
+ }
+ ToolSeparator {}
+ ToolButton {
+ text: qsTr("Preflight")
+ enabled: root.host !== null
+ onClicked: if (root.host) root.host.setWorkspace(EditorHost.Preflight)
+ Accessible.name: qsTr("Open preflight workspace")
+ }
+ ToolButton {
+ text: qsTr("Preview")
+ enabled: root.host !== null
+ onClicked: if (root.host) root.host.setWorkspace(EditorHost.ProductionPreview)
+ Accessible.name: qsTr("Open production preview workspace")
+ }
+
+ Item { Layout.fillWidth: true }
+ }
+}
diff --git a/LoopEditor/qml/Workspace.qml b/LoopEditor/qml/Workspace.qml
index 9e300a655..c3017700b 100644
--- a/LoopEditor/qml/Workspace.qml
+++ b/LoopEditor/qml/Workspace.qml
@@ -10,78 +10,138 @@ Item {
property var host: editorHost
readonly property bool preferReducedMotion: host ? host.preferReducedMotion : false
- RowLayout {
+ function workspaceIndex(workspaceValue) {
+ switch (workspaceValue) {
+ case EditorHost.Document: return 0
+ case EditorHost.Preflight: return 1
+ case EditorHost.ProductionPreview: return 2
+ case EditorHost.Pages: return 3
+ case EditorHost.Inspect: return 4
+ case EditorHost.Fix: return 5
+ case EditorHost.Compare: return 6
+ default: return 0
+ }
+ }
+
+ function setWorkspaceFromRail(workspaceValue) {
+ if (!host || workspaceValue === EditorHost.Compare) {
+ return
+ }
+ host.setWorkspace(workspaceValue)
+ }
+
+ ColumnLayout {
anchors.fill: parent
spacing: 0
- Pane {
- id: workspaceRail
- Layout.preferredWidth: 120
+ ShellToolBar {
+ Layout.fillWidth: true
+ host: root.host
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
Layout.fillHeight: true
- padding: 8
-
- focus: true
- Accessible.role: Accessible.Grouping
- Accessible.name: qsTr("Workspace rail")
-
- ColumnLayout {
- anchors.fill: parent
- spacing: 8
-
- ToolButton {
- id: documentButton
- Layout.fillWidth: true
- text: qsTr("Document")
- checkable: true
- checked: workspaceStack.currentIndex === 0
- onClicked: workspaceStack.currentIndex = 0
- Accessible.name: qsTr("Document workspace")
- Accessible.role: Accessible.Button
+ spacing: 0
+
+ Pane {
+ id: workspaceRail
+ Layout.preferredWidth: 132
+ Layout.fillHeight: true
+ padding: 8
+
+ focus: true
+ Accessible.role: Accessible.Grouping
+ Accessible.name: qsTr("Workspace rail")
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 8
+
+ Repeater {
+ model: [
+ { label: qsTr("Document"), workspace: EditorHost.Document, enabled: true },
+ { label: qsTr("Preflight"), workspace: EditorHost.Preflight, enabled: true },
+ { label: qsTr("Production Preview"), workspace: EditorHost.ProductionPreview, enabled: true },
+ { label: qsTr("Pages / Production"), workspace: EditorHost.Pages, enabled: true },
+ { label: qsTr("Inspect"), workspace: EditorHost.Inspect, enabled: true },
+ { label: qsTr("Fix"), workspace: EditorHost.Fix, enabled: true },
+ { label: qsTr("Compare"), workspace: EditorHost.Compare, enabled: false }
+ ]
+
+ delegate: ToolButton {
+ required property string label
+ required property int workspace
+ required property bool enabled
+
+ Layout.fillWidth: true
+ text: label
+ checkable: true
+ enabled: enabled
+ checked: host && host.workspace === workspace
+ onClicked: root.setWorkspaceFromRail(workspace)
+ Accessible.name: workspace === EditorHost.Compare
+ ? qsTr("Compare workspace (product decision pending)")
+ : qsTr("%1 workspace").arg(label)
+ Accessible.description: workspace === EditorHost.Compare
+ ? qsTr("Compare is disabled until the product decision is approved.")
+ : ""
+ }
+ }
+
+ Item { Layout.fillHeight: true }
}
+ }
+
+ StackLayout {
+ id: workspaceStack
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ currentIndex: host ? root.workspaceIndex(host.workspace) : 0
- ToolButton {
- id: preflightButton
- Layout.fillWidth: true
- text: qsTr("Preflight")
- checkable: true
- checked: workspaceStack.currentIndex === 1
- onClicked: workspaceStack.currentIndex = 1
- Accessible.name: qsTr("Preflight workspace")
- Accessible.role: Accessible.Button
+ DocumentPane {
+ id: documentPane
+ host: root.host
}
- ToolButton {
- id: inspectorButton
- Layout.fillWidth: true
- text: qsTr("Inspector")
- checkable: true
- checked: workspaceStack.currentIndex === 2
- onClicked: workspaceStack.currentIndex = 2
- Accessible.name: qsTr("Inspector workspace")
- Accessible.role: Accessible.Button
+ PreflightPane {
+ host: root.host
}
- Item { Layout.fillHeight: true }
- }
- }
+ WorkspacePlaceholderPane {
+ host: root.host
+ titleText: qsTr("Production Preview")
+ descriptionText: qsTr("Soft proofing and output preview will appear here.")
+ Accessible.name: qsTr("Production Preview workspace")
+ }
- StackLayout {
- id: workspaceStack
- Layout.fillWidth: true
- Layout.fillHeight: true
- currentIndex: 0
+ WorkspacePlaceholderPane {
+ host: root.host
+ titleText: qsTr("Pages / Production")
+ descriptionText: qsTr("Page assembly and production geometry will appear here.")
+ Accessible.name: qsTr("Pages and Production workspace")
+ }
- DocumentPane {
- id: documentPane
- host: root.host
- }
+ WorkspacePlaceholderPane {
+ host: root.host
+ titleText: qsTr("Inspect")
+ descriptionText: qsTr("Dedicated inspection tools will appear here. Use the document inspector dock for contextual selection.")
+ Accessible.name: qsTr("Inspect workspace")
+ }
- PreflightPane {
- host: root.host
- }
+ WorkspacePlaceholderPane {
+ host: root.host
+ titleText: qsTr("Fix")
+ descriptionText: qsTr("Bounded corrective operations will appear here.")
+ Accessible.name: qsTr("Fix workspace")
+ }
- InspectorPane {
- host: root.host
+ WorkspacePlaceholderPane {
+ host: root.host
+ titleText: qsTr("Compare")
+ descriptionText: qsTr("Compare remains deferred pending the product decision.")
+ Accessible.name: qsTr("Compare workspace placeholder")
+ }
}
}
}
@@ -90,9 +150,13 @@ Item {
Connections {
target: root.host
+ function onWorkspaceChanged() {
+ if (root.host) {
+ workspaceStack.currentIndex = root.workspaceIndex(root.host.workspace)
+ }
+ }
function onPresentationChanged() {
if (root.host && root.host.workspaceRequest >= 0) {
- workspaceStack.currentIndex = root.host.workspaceRequest
root.host.acknowledgeWorkspaceRequest()
}
}
diff --git a/LoopEditor/qml/WorkspacePlaceholderPane.qml b/LoopEditor/qml/WorkspacePlaceholderPane.qml
new file mode 100644
index 000000000..d2c9680c3
--- /dev/null
+++ b/LoopEditor/qml/WorkspacePlaceholderPane.qml
@@ -0,0 +1,45 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Pane {
+ id: root
+
+ property var host: editorHost
+ property string titleText: ""
+ property string descriptionText: ""
+
+ padding: 24
+
+ Accessible.role: Accessible.Grouping
+ Accessible.name: titleText
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 12
+
+ Label {
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ font.bold: true
+ text: root.titleText
+ Accessible.name: root.titleText
+ }
+
+ Label {
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ text: root.descriptionText
+ Accessible.name: root.descriptionText
+ }
+
+ Label {
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ visible: root.host && root.host.hasDocument
+ text: qsTr("Document: %1").arg(root.host.displayTitle)
+ }
+
+ Item { Layout.fillHeight: true }
+ }
+}
diff --git a/LoopLibCore/CMakeLists.txt b/LoopLibCore/CMakeLists.txt
index f459deca4..0025debcc 100644
--- a/LoopLibCore/CMakeLists.txt
+++ b/LoopLibCore/CMakeLists.txt
@@ -96,6 +96,8 @@ add_library(LoopLibCore SHARED
sources/pdfprocessingbudget.h
sources/pdfjobscheduler.cpp
sources/pdfjobscheduler.h
+ sources/pdfblockingthreadguard.cpp
+ sources/pdfblockingthreadguard.h
sources/pdfschemaversion.cpp
sources/pdfschemaversion.h
sources/pdfevidencegraph.cpp
diff --git a/LoopLibCore/sources/pdfactionlist.cpp b/LoopLibCore/sources/pdfactionlist.cpp
index d21633f6e..17101a16f 100644
--- a/LoopLibCore/sources/pdfactionlist.cpp
+++ b/LoopLibCore/sources/pdfactionlist.cpp
@@ -22,6 +22,8 @@
#include "pdfactionlist.h"
+#include "pdfpreflightverdict.h"
+
#include
#include
#include
@@ -41,9 +43,12 @@ QString failurePolicyName(PDFActionListFailurePolicy policy)
{
switch (policy)
{
- case PDFActionListFailurePolicy::Inherit: return QStringLiteral("inherit");
- case PDFActionListFailurePolicy::Stop: return QStringLiteral("stop");
- case PDFActionListFailurePolicy::Continue: return QStringLiteral("continue");
+ case PDFActionListFailurePolicy::Inherit:
+ return QStringLiteral("inherit");
+ case PDFActionListFailurePolicy::Stop:
+ return QStringLiteral("stop");
+ case PDFActionListFailurePolicy::Continue:
+ return QStringLiteral("continue");
}
return QStringLiteral("inherit");
}
@@ -80,11 +85,16 @@ bool isJsonNumber(const QJsonValue& value)
bool matchesType(const QJsonValue& value, const QString& type)
{
- if (type == QStringLiteral("object")) return value.isObject();
- if (type == QStringLiteral("array")) return value.isArray();
- if (type == QStringLiteral("string")) return value.isString();
- if (type == QStringLiteral("boolean")) return value.isBool();
- if (type == QStringLiteral("number")) return isJsonNumber(value);
+ if (type == QStringLiteral("object"))
+ return value.isObject();
+ if (type == QStringLiteral("array"))
+ return value.isArray();
+ if (type == QStringLiteral("string"))
+ return value.isString();
+ if (type == QStringLiteral("boolean"))
+ return value.isBool();
+ if (type == QStringLiteral("number"))
+ return isJsonNumber(value);
if (type == QStringLiteral("integer"))
{
return isJsonNumber(value) && std::floor(value.toDouble()) == value.toDouble();
@@ -95,7 +105,8 @@ bool matchesType(const QJsonValue& value, const QString& type)
bool valuesEqual(const QJsonValue& left, const QJsonValue& right)
{
return QJsonDocument(left.toObject()).toJson(QJsonDocument::Compact) ==
- QJsonDocument(right.toObject()).toJson(QJsonDocument::Compact) || left == right;
+ QJsonDocument(right.toObject()).toJson(QJsonDocument::Compact) ||
+ left == right;
}
void appendError(QStringList* errors, const QString& error)
@@ -107,9 +118,9 @@ void appendError(QStringList* errors, const QString& error)
}
bool validateValue(const QJsonValue& value,
- const QJsonObject& schema,
- const QString& path,
- QStringList* errors)
+ const QJsonObject& schema,
+ const QString& path,
+ QStringList* errors)
{
const QString type = schema.value(QStringLiteral("type")).toString();
if (!type.isEmpty() && !matchesType(value, type))
@@ -301,8 +312,9 @@ PDFActionListFailurePolicy effectivePolicy(const PDFActionList& actionList, cons
QString recipeHash(const PDFActionList& actionList)
{
return QString::fromLatin1(QCryptographicHash::hash(
- QJsonDocument(actionList.toJson()).toJson(QJsonDocument::Compact),
- QCryptographicHash::Sha256).toHex());
+ QJsonDocument(actionList.toJson()).toJson(QJsonDocument::Compact),
+ QCryptographicHash::Sha256)
+ .toHex());
}
void addDiagnostic(PDFActionListStepResult* step, const QString& code, const QString& message)
@@ -310,8 +322,7 @@ void addDiagnostic(PDFActionListStepResult* step, const QString& code, const QSt
step->diagnostics.append(QJsonObject{
{ QStringLiteral("code"), code },
{ QStringLiteral("severity"), QStringLiteral("error") },
- { QStringLiteral("message"), message }
- });
+ { QStringLiteral("message"), message } });
}
void markRemaining(QVector* steps, int start, PDFActionListStepStatus status, const QString& code, const QString& message)
@@ -326,18 +337,46 @@ void markRemaining(QVector* steps, int start, PDFAction
}
}
-} // namespace
+} // namespace
+
+void applyCanonicalPreflightVerdict(PDFActionListStepResult* step, const PreflightVerdict& verdict)
+{
+ if (!step)
+ {
+ return;
+ }
+ step->verdict = verdict.toJson();
+ if (!verdict.isPass() && step->status == PDFActionListStepStatus::Succeeded)
+ {
+ step->status = PDFActionListStepStatus::Failed;
+ step->diagnostics.append(QJsonObject{
+ { QStringLiteral("code"), QStringLiteral("action-list.postflight-verdict") },
+ { QStringLiteral("severity"), QStringLiteral("error") },
+ { QStringLiteral("message"), preflightVerdictOperatorSummary(verdict) } });
+ }
+}
+
+void applyCanonicalPreflightVerdict(PDFActionListStepResult* step, const PreflightResult& result)
+{
+ applyCanonicalPreflightVerdict(step, reducePreflightVerdict(result));
+}
QString pdfActionListStepStatusName(PDFActionListStepStatus status)
{
switch (status)
{
- case PDFActionListStepStatus::Pending: return QStringLiteral("pending");
- case PDFActionListStepStatus::Running: return QStringLiteral("running");
- case PDFActionListStepStatus::Succeeded: return QStringLiteral("succeeded");
- case PDFActionListStepStatus::Skipped: return QStringLiteral("skipped");
- case PDFActionListStepStatus::Failed: return QStringLiteral("failed");
- case PDFActionListStepStatus::Cancelled: return QStringLiteral("cancelled");
+ case PDFActionListStepStatus::Pending:
+ return QStringLiteral("pending");
+ case PDFActionListStepStatus::Running:
+ return QStringLiteral("running");
+ case PDFActionListStepStatus::Succeeded:
+ return QStringLiteral("succeeded");
+ case PDFActionListStepStatus::Skipped:
+ return QStringLiteral("skipped");
+ case PDFActionListStepStatus::Failed:
+ return QStringLiteral("failed");
+ case PDFActionListStepStatus::Cancelled:
+ return QStringLiteral("cancelled");
}
return QStringLiteral("failed");
}
@@ -354,7 +393,8 @@ QJsonObject PDFActionListStep::toJson() const
{ QStringLiteral("operation"), operationId },
{ QStringLiteral("params"), parameters }
};
- if (!condition.isEmpty()) result.insert(QStringLiteral("when"), condition);
+ if (!condition.isEmpty())
+ result.insert(QStringLiteral("when"), condition);
if (failurePolicy != PDFActionListFailurePolicy::Inherit)
{
result.insert(QStringLiteral("onFailure"), failurePolicyName(failurePolicy));
@@ -427,6 +467,7 @@ QJsonObject PDFActionListStepResult::toJson() const
{ QStringLiteral("resolved_params"), resolvedParameters },
{ QStringLiteral("plan"), plan },
{ QStringLiteral("repair_result"), repairResult },
+ { QStringLiteral("verdict"), verdict },
{ QStringLiteral("diagnostics"), diagnostics },
{ QStringLiteral("affected_scope"), affectedScope }
};
@@ -465,7 +506,8 @@ PDFOperationResult PDFActionListExecutor::validate(const PDFActionList& actionLi
const PDFActionListExecutionOptions& options,
QStringList* errors) const
{
- if (errors) errors->clear();
+ if (errors)
+ errors->clear();
bool valid = true;
if (actionList.schema != schemaVersion())
{
@@ -562,8 +604,7 @@ PDFOperationResult PDFActionListExecutor::plan(const PDFActionList& actionList,
result->diagnostics.append(QJsonObject{
{ QStringLiteral("code"), QStringLiteral("action-list.validation-failed") },
{ QStringLiteral("severity"), QStringLiteral("error") },
- { QStringLiteral("message"), error }
- });
+ { QStringLiteral("message"), error } });
}
result->durationMs = totalTimer.elapsed();
return validation;
@@ -690,6 +731,14 @@ PDFOperationResult PDFActionListExecutor::execute(const PDFActionList& actionLis
}
stepResult.plan = currentPlan.toJson();
stepResult.repairResult = repairResult.toJson();
+ if (!repairResult.verdict.isEmpty())
+ {
+ applyCanonicalPreflightVerdict(&stepResult, preflightVerdictFromJson(repairResult.verdict));
+ if (stepResult.status == PDFActionListStepStatus::Failed)
+ {
+ hadFailure = true;
+ }
+ }
}
stepResult.durationMs = stepTimer.elapsed();
statuses.insert(step.id, pdfActionListStepStatusName(stepResult.status));
@@ -713,4 +762,4 @@ PDFOperationResult PDFActionListExecutor::execute(const PDFActionList& actionLis
return PDFOperationResult(true);
}
-} // namespace pdf
+} // namespace pdf
diff --git a/LoopLibCore/sources/pdfactionlist.h b/LoopLibCore/sources/pdfactionlist.h
index 878d7c374..0225147a4 100644
--- a/LoopLibCore/sources/pdfactionlist.h
+++ b/LoopLibCore/sources/pdfactionlist.h
@@ -34,6 +34,9 @@
namespace pdf
{
+struct PreflightVerdict;
+struct PreflightResult;
+
enum class PDFActionListStepStatus
{
Pending,
@@ -86,12 +89,16 @@ struct LOOPLIBCORESHARED_EXPORT PDFActionListStepResult
QJsonObject resolvedParameters;
QJsonObject plan;
QJsonObject repairResult;
+ QJsonObject verdict;
QJsonArray diagnostics;
QJsonArray affectedScope;
QJsonObject toJson() const;
};
+LOOPLIBCORESHARED_EXPORT void applyCanonicalPreflightVerdict(PDFActionListStepResult* step, const PreflightVerdict& verdict);
+LOOPLIBCORESHARED_EXPORT void applyCanonicalPreflightVerdict(PDFActionListStepResult* step, const PreflightResult& result);
+
struct LOOPLIBCORESHARED_EXPORT PDFActionListExecutionResult
{
QString schema = QStringLiteral("loop-action-list-result/1");
@@ -145,6 +152,6 @@ class LOOPLIBCORESHARED_EXPORT PDFActionListExecutor
const PDFRepairRegistry* m_registry = nullptr;
};
-} // namespace pdf
+} // namespace pdf
-#endif // PDFACTIONLIST_H
+#endif // PDFACTIONLIST_H
diff --git a/LoopLibCore/sources/pdfblockingthreadguard.cpp b/LoopLibCore/sources/pdfblockingthreadguard.cpp
new file mode 100644
index 000000000..b91be3811
--- /dev/null
+++ b/LoopLibCore/sources/pdfblockingthreadguard.cpp
@@ -0,0 +1,73 @@
+// MIT License
+//
+// Copyright (c) 2018-2025 Jakub Melka and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#include "pdfblockingthreadguard.h"
+
+#include
+#include
+
+#include
+
+namespace pdf
+{
+
+namespace
+{
+
+std::atomic s_interactiveThread{ nullptr };
+
+} // namespace
+
+void PDFBlockingThreadGuard::registerInteractiveThread()
+{
+ s_interactiveThread.store(QThread::currentThread(), std::memory_order_release);
+}
+
+void PDFBlockingThreadGuard::clearInteractiveThread()
+{
+ s_interactiveThread.store(nullptr, std::memory_order_release);
+}
+
+bool PDFBlockingThreadGuard::isInteractiveThreadRegistered()
+{
+ return s_interactiveThread.load(std::memory_order_acquire) != nullptr;
+}
+
+bool PDFBlockingThreadGuard::isCurrentThreadInteractive()
+{
+ QThread* registered = s_interactiveThread.load(std::memory_order_acquire);
+ return registered != nullptr && registered == QThread::currentThread();
+}
+
+bool PDFBlockingThreadGuard::assertOffInteractiveThread(const char* serviceName)
+{
+ if (!isCurrentThreadInteractive())
+ {
+ return true;
+ }
+
+ qWarning("%s must not run on the interactive thread: it would block pointer handling and frame callbacks.",
+ serviceName ? serviceName : "A blocking service");
+ return false;
+}
+
+} // namespace pdf
diff --git a/LoopLibCore/sources/pdfblockingthreadguard.h b/LoopLibCore/sources/pdfblockingthreadguard.h
new file mode 100644
index 000000000..72c4069b3
--- /dev/null
+++ b/LoopLibCore/sources/pdfblockingthreadguard.h
@@ -0,0 +1,67 @@
+// MIT License
+//
+// Copyright (c) 2018-2025 Jakub Melka and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#ifndef PDFBLOCKINGTHREADGUARD_H
+#define PDFBLOCKINGTHREADGUARD_H
+
+#include "pdfglobal.h"
+
+namespace pdf
+{
+
+/// Marks the thread that owns pointer handlers and frame callbacks so a
+/// blocking service adapter (preflight, OCR, font enumeration, parsing,
+/// filesystem/network I/O) can refuse to run there (issue #144).
+///
+/// Registration is opt-in and global rather than per-object: a host with an
+/// interactive canvas (Editor) registers once, early, from that thread. A
+/// tool with no such thread (PdfTool, Fuzz, CLI tests) never registers, and
+/// the guard is then a no-op that never blocks a caller: there is nothing to
+/// protect.
+class LOOPLIBCORESHARED_EXPORT PDFBlockingThreadGuard
+{
+public:
+ /// Registers the calling thread as the one interactive input and frame
+ /// callbacks run on.
+ static void registerInteractiveThread();
+
+ /// Drops the registration. A guard with no registered thread never
+ /// reports a violation.
+ static void clearInteractiveThread();
+
+ static bool isInteractiveThreadRegistered();
+
+ /// True when called from the registered interactive thread. Always false
+ /// when no thread is registered.
+ static bool isCurrentThreadInteractive();
+
+ /// A blocking service adapter calls this once, before doing any work.
+ /// Returns true when it is safe to proceed. Returns false, and logs a
+ /// warning naming \p serviceName, when called from the registered
+ /// interactive thread -- the caller must not run the blocking work and
+ /// should fold this into its own typed error result instead.
+ static bool assertOffInteractiveThread(const char* serviceName);
+};
+
+} // namespace pdf
+
+#endif // PDFBLOCKINGTHREADGUARD_H
diff --git a/LoopLibCore/sources/pdfcms.cpp b/LoopLibCore/sources/pdfcms.cpp
index 8374e4a7b..8573b6469 100644
--- a/LoopLibCore/sources/pdfcms.cpp
+++ b/LoopLibCore/sources/pdfcms.cpp
@@ -23,6 +23,7 @@
#include "pdfcms.h"
#include "pdfdocument.h"
#include "pdfexecutionpolicy.h"
+#include "pdfprocessingbudget.h"
#include
#include
@@ -41,7 +42,7 @@
#ifdef LOOP_COMPILER_MSVC
#pragma warning(push)
-#pragma warning(disable:5033)
+#pragma warning(disable : 5033)
#endif
#ifndef CMS_NO_REGISTER_KEYWORD
#define CMS_NO_REGISTER_KEYWORD
@@ -98,7 +99,7 @@ static QByteArray getWindowsColorProfileData(const QString& profileName)
{
std::wstring profileNameW = profileName.toStdWString();
- PROFILE profile = { };
+ PROFILE profile = {};
profile.dwType = PROFILE_FILENAME;
profile.pProfileData = profileNameW.data();
profile.cbDataSize = DWORD((profileNameW.size() + 1) * sizeof(wchar_t));
@@ -106,7 +107,7 @@ static QByteArray getWindowsColorProfileData(const QString& profileName)
HPROFILE profileHandle = OpenColorProfileW(&profile, PROFILE_READ, FILE_SHARE_READ, OPEN_EXISTING);
if (!profileHandle)
{
- return { };
+ return {};
}
DWORD profileSize = 0;
@@ -114,7 +115,7 @@ static QByteArray getWindowsColorProfileData(const QString& profileName)
if (!GetColorProfileFromHandle(profileHandle, nullptr, &profileSize) && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
{
CloseColorProfile(profileHandle);
- return { };
+ return {};
}
if (profileSize > 0)
@@ -138,7 +139,7 @@ static HPROFILE openWindowsColorProfile(const QString& profileName)
{
std::wstring profileNameW = profileName.toStdWString();
- PROFILE profile = { };
+ PROFILE profile = {};
profile.dwType = PROFILE_FILENAME;
profile.pProfileData = profileNameW.data();
profile.cbDataSize = DWORD((profileNameW.size() + 1) * sizeof(wchar_t));
@@ -152,7 +153,7 @@ static PDFColorProfileIdentifier::Type getWindowsColorProfileType(HPROFILE profi
constexpr DWORD spaceRgb = 0x52474220u;
constexpr DWORD spaceCmyk = 0x434d594bu;
- PROFILEHEADER header = { };
+ PROFILEHEADER header = {};
if (!GetColorProfileHeader(profileHandle, &header))
{
return PDFColorProfileIdentifier::Type::Invalid;
@@ -180,7 +181,7 @@ static PDFColorProfileIdentifiers getInstalledWindowsColorProfiles()
{
PDFColorProfileIdentifiers result;
- ENUMTYPEW enumRecord = { };
+ ENUMTYPEW enumRecord = {};
enumRecord.dwSize = sizeof(ENUMTYPEW);
enumRecord.dwVersion = ENUM_TYPE_VERSION;
@@ -253,7 +254,7 @@ class PDFLittleCMS : public PDFCMS
static int installCmsPlugins();
static cmsBool optimizePipeline(cmsPipeline** Lut,
- cmsUInt32Number Intent,
+ cmsUInt32Number Intent,
cmsUInt32Number* InputFormat,
cmsUInt32Number* OutputFormat,
cmsUInt32Number* dwFlags);
@@ -556,7 +557,6 @@ bool PDFLittleCMS::transformColorSpace(const PDFCMS::ColorSpaceTransformParams&
target(target),
pixelCount(pixelCount)
{
-
}
const float* source = nullptr;
@@ -695,7 +695,7 @@ QColor PDFLittleCMS::getColorFromDeviceGray(const PDFColor& color, RenderingInte
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_FLT);
const float grayColor = color[0];
- std::array rgbOutputColor = { };
+ std::array rgbOutputColor = {};
cmsDoTransform(transform, &grayColor, rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
@@ -722,7 +722,7 @@ QColor PDFLittleCMS::getColorFromDeviceRGB(const PDFColor& color, RenderingInten
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_FLT);
std::array rgbInputColor = { color[0], color[1], color[2] };
- std::array rgbOutputColor = { };
+ std::array rgbOutputColor = {};
cmsDoTransform(transform, rgbInputColor.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
@@ -749,7 +749,7 @@ QColor PDFLittleCMS::getColorFromDeviceCMYK(const PDFColor& color, RenderingInte
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_FLT);
std::array cmykInputColor = { color[0] * 100.0f, color[1] * 100.0f, color[2] * 100.0f, color[3] * 100.0f };
- std::array rgbOutputColor = { };
+ std::array rgbOutputColor = {};
cmsDoTransform(transform, cmykInputColor.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
@@ -777,7 +777,7 @@ QColor PDFLittleCMS::getColorFromXYZ(const PDFColor3& whitePoint, const PDFColor
const PDFColorComponentMatrix_3x3 adaptationMatrix = PDFChromaticAdaptationXYZ::createWhitepointChromaticAdaptation(getDefaultXYZWhitepoint(), whitePoint, m_settings.colorAdaptationXYZ);
const PDFColor3 xyzInputColor = adaptationMatrix * color;
- std::array rgbOutputColor = { };
+ std::array rgbOutputColor = {};
cmsDoTransform(transform, xyzInputColor.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
@@ -856,7 +856,7 @@ QColor PDFLittleCMS::getColorFromICC(const PDFColor& color, RenderingIntent rend
return QColor();
}
- std::array inputBuffer = { };
+ std::array inputBuffer = {};
const cmsUInt32Number format = cmsGetTransformInputFormat(transform);
const cmsUInt32Number channels = T_CHANNELS(format);
const cmsUInt32Number colorSpace = T_COLORSPACE(format);
@@ -868,7 +868,7 @@ QColor PDFLittleCMS::getColorFromICC(const PDFColor& color, RenderingIntent rend
inputBuffer[i] = isCMYK ? color[i] * 100.0f : color[i];
}
- std::array rgbOutputColor = { };
+ std::array rgbOutputColor = {};
cmsDoTransform(transform, inputBuffer.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
@@ -916,7 +916,7 @@ void PDFLittleCMS::init()
int PDFLittleCMS::installCmsPlugins()
{
- static cmsPluginOptimization optimizationPlugin = { };
+ static cmsPluginOptimization optimizationPlugin = {};
optimizationPlugin.base.Magic = cmsPluginMagicNumber;
optimizationPlugin.base.Type = cmsPluginOptimizationSig;
optimizationPlugin.base.Next = nullptr;
@@ -1027,14 +1027,16 @@ bool PDFLittleCMS::isSoftProofing() const
cmsHPROFILE PDFLittleCMS::createProfile(const QString& id, const PDFColorProfileIdentifiers& profileDescriptors, bool preferOutputProfile) const
{
- auto it = std::find_if(profileDescriptors.cbegin(), profileDescriptors.cend(), [&id](const PDFColorProfileIdentifier& identifier) { return identifier.id == id; });
+ auto it = std::find_if(profileDescriptors.cbegin(), profileDescriptors.cend(), [&id](const PDFColorProfileIdentifier& identifier)
+ { return identifier.id == id; });
if (preferOutputProfile && it != profileDescriptors.end())
{
const PDFColorProfileIdentifier& identifier = *it;
if (!identifier.isOutputIntentProfile)
{
// Find first output intent color profile
- auto itOutputIntentColorProfile = std::find_if(profileDescriptors.cbegin(), profileDescriptors.cend(), [](const PDFColorProfileIdentifier& identifier) { return identifier.isOutputIntentProfile; });
+ auto itOutputIntentColorProfile = std::find_if(profileDescriptors.cbegin(), profileDescriptors.cend(), [](const PDFColorProfileIdentifier& identifier)
+ { return identifier.isOutputIntentProfile; });
if (itOutputIntentColorProfile != profileDescriptors.end())
{
it = itOutputIntentColorProfile;
@@ -1049,7 +1051,7 @@ cmsHPROFILE PDFLittleCMS::createProfile(const QString& id, const PDFColorProfile
{
case PDFColorProfileIdentifier::Type::Gray:
{
- cmsCIExyY whitePoint{ };
+ cmsCIExyY whitePoint{};
if (cmsWhitePointFromTemp(&whitePoint, identifier.temperature))
{
cmsToneCurve* gammaCurve = cmsBuildGamma(cmsContext(), identifier.gamma);
@@ -1069,7 +1071,7 @@ cmsHPROFILE PDFLittleCMS::createProfile(const QString& id, const PDFColorProfile
case PDFColorProfileIdentifier::Type::RGB:
{
- cmsCIExyY whitePoint{ };
+ cmsCIExyY whitePoint{};
if (cmsWhitePointFromTemp(&whitePoint, identifier.temperature))
{
cmsCIExyYTRIPLE primaries;
@@ -1422,8 +1424,8 @@ QString getInfoFromProfile(cmsHPROFILE profile, cmsInfoType infoType)
QString country = QLocale::territoryToString(locale.territory());
QString language = QLocale::languageToString(locale.language());
- char countryCode[3] = { };
- char languageCode[3] = { };
+ char countryCode[3] = {};
+ char languageCode[3] = {};
if (country.size() == 2)
{
countryCode[0] = country[0].toLatin1();
@@ -1459,7 +1461,6 @@ QString getInfoFromProfile(cmsHPROFILE profile, cmsInfoType infoType)
PDFCMSGeneric::PDFCMSGeneric(const PDFColorConvertor& colorConvertor) :
m_colorConvertor(colorConvertor)
{
-
}
bool PDFCMSGeneric::isCompatible(const PDFCMSSettings& settings) const
@@ -1619,7 +1620,6 @@ PDFCMSManager::PDFCMSManager(QObject* parent) :
m_document(nullptr),
m_mutex()
{
-
}
void PDFCMSManager::finalize()
@@ -1715,6 +1715,11 @@ PDFCMSSettings PDFCMSManager::getDefaultSettings() const
}
void PDFCMSManager::setDocument(const PDFDocument* document)
+{
+ setDocument(document, nullptr);
+}
+
+void PDFCMSManager::setDocument(const PDFDocument* document, PDFProcessingBudget* processingBudget)
{
std::optional> lock;
lock.emplace(&m_mutex);
@@ -1741,9 +1746,17 @@ void PDFCMSManager::setDocument(const PDFDocument* document)
PDFObject outputProfileObject = m_document->getObject(outputIntent.getOutputProfile());
if (outputProfileObject.isStream())
{
- content = m_document->getDecodedStream(outputProfileObject.getStream());
+ content = m_document->getDecodedStream(outputProfileObject.getStream(), processingBudget);
}
}
+ catch (const PDFBudgetExceededException&)
+ {
+ // A budget failure is an incomplete operation, not a profile that
+ // failed to parse: it must reach the caller (see
+ // docs/RESOURCE_BUDGETS.md - "a budget failure is incomplete,
+ // never PASS").
+ throw;
+ }
catch (const PDFException&)
{
continue;
@@ -1869,8 +1882,7 @@ PDFColorProfileIdentifiers PDFCMSManager::getGrayProfilesImpl() const
{
// Jakub Melka: We create gray profiles for temperature 5000K, 6500K and 9300K.
// We also use linear gamma and gamma value 2.2.
- PDFColorProfileIdentifiers result =
- {
+ PDFColorProfileIdentifiers result = {
PDFColorProfileIdentifier::createGray(tr("Gray D65, γ = 2.2"), "@GENERIC_Gray_D65_g22", 6500.0, 2.2),
PDFColorProfileIdentifier::createGray(tr("Gray D50, γ = 2.2"), "@GENERIC_Gray_D50_g22", 5000.0, 2.2),
PDFColorProfileIdentifier::createGray(tr("Gray D93, γ = 2.2"), "@GENERIC_Gray_D93_g22", 9300.0, 2.2),
@@ -1890,8 +1902,7 @@ PDFColorProfileIdentifiers PDFCMSManager::getRGBProfilesImpl() const
{
// Jakub Melka: We create RGB profiles for common standards and also for
// default standard sRGB. See https://en.wikipedia.org/wiki/Color_spaces_with_RGB_primaries.
- PDFColorProfileIdentifiers result =
- {
+ PDFColorProfileIdentifiers result = {
PDFColorProfileIdentifier::createSRGB(),
PDFColorProfileIdentifier::createRGB(tr("HDTV (ITU-R BT.709)"), "@GENERIC_RGB_HDTV", 6500, QPointF(0.64, 0.33), QPointF(0.30, 0.60), QPointF(0.15, 0.06), 20.0 / 9.0),
PDFColorProfileIdentifier::createRGB(tr("Adobe RGB 1998"), "@GENERIC_RGB_Adobe1998", 6500, QPointF(0.64, 0.33), QPointF(0.30, 0.60), QPointF(0.15, 0.06), 563.0 / 256.0),
@@ -2046,7 +2057,8 @@ PDFColorProfileIdentifiers PDFCMSManager::getFilteredExternalProfiles(PDFColorPr
PDFColorProfileIdentifiers PDFCMSManager::getFilteredOutputIntentProfiles(PDFColorProfileIdentifier::Type type) const
{
PDFColorProfileIdentifiers result;
- std::copy_if(m_outputIntentProfiles.cbegin(), m_outputIntentProfiles.cend(), std::back_inserter(result), [type](const PDFColorProfileIdentifier& identifier) { return identifier.type == type; });
+ std::copy_if(m_outputIntentProfiles.cbegin(), m_outputIntentProfiles.cend(), std::back_inserter(result), [type](const PDFColorProfileIdentifier& identifier)
+ { return identifier.type == type; });
return result;
}
@@ -2134,24 +2146,23 @@ PDFColorComponentMatrix_3x3 PDFChromaticAdaptationXYZ::createWhitepointChromatic
case pdf::PDFCMSSettings::ColorAdaptationXYZ::XYZScaling:
matrix.makeDiagonal(std::array{ targetWhitePoint[0] / sourceWhitePoint[0],
- targetWhitePoint[1] / sourceWhitePoint[1],
- targetWhitePoint[2] / sourceWhitePoint[2]
- });
+ targetWhitePoint[1] / sourceWhitePoint[1],
+ targetWhitePoint[2] / sourceWhitePoint[2] });
break;
case pdf::PDFCMSSettings::ColorAdaptationXYZ::CAT97:
{
// CAT97 matrix, as defined in https://en.wikipedia.org/wiki/LMS_color_space
constexpr PDFColorComponentMatrix_3x3 cat97Matrix(
- 0.8562f, 0.3372f, -0.1934f,
- -0.8360f, 1.8327f, 0.0033f,
- 0.0357f, -0.0469f, 1.0112f);
+ 0.8562f, 0.3372f, -0.1934f,
+ -0.8360f, 1.8327f, 0.0033f,
+ 0.0357f, -0.0469f, 1.0112f);
// Inverse of CAT97 matrix (using wxMaxima to compute it)
constexpr PDFColorComponentMatrix_3x3 inverseCat97Matrix(
- 0.9873999149199271f, -0.1768250198556842f, 0.1894251049357571f,
- 0.4504351090445315f, 0.464932897752711f, 0.08463199320275755f,
- -0.01396832510725165f, 0.027806572501434f, 0.9861617526058175f);
+ 0.9873999149199271f, -0.1768250198556842f, 0.1894251049357571f,
+ 0.4504351090445315f, 0.464932897752711f, 0.08463199320275755f,
+ -0.01396832510725165f, 0.027806572501434f, 0.9861617526058175f);
PDFColor3 adaptedTargetWhitePoint = cat97Matrix * targetWhitePoint;
PDFColor3 adaptedSourceWhitePoint = cat97Matrix * sourceWhitePoint;
@@ -2172,15 +2183,15 @@ PDFColorComponentMatrix_3x3 PDFChromaticAdaptationXYZ::createWhitepointChromatic
{
// CAT02 matrix, as defined in https://en.wikipedia.org/wiki/LMS_color_space
constexpr PDFColorComponentMatrix_3x3 cat02Matrix(
- 0.7328f, 0.4296f, -0.1624f,
- -0.7036f, 1.6975f, 0.0061f,
- 0.0030f, 0.0136f, 0.9834f);
+ 0.7328f, 0.4296f, -0.1624f,
+ -0.7036f, 1.6975f, 0.0061f,
+ 0.0030f, 0.0136f, 0.9834f);
// Inverse of CAT02 matrix (using wxMaxima to compute it)
constexpr PDFColorComponentMatrix_3x3 inverseCat02Matrix(
- 1.096123820835514f, -0.2788690002182872f, 0.182745179382773f,
- 0.4543690419753592f, 0.4735331543074117f, 0.0720978037172291f,
- -0.009627608738429352f, -0.005698031216113419f, 1.015325639954543f);
+ 1.096123820835514f, -0.2788690002182872f, 0.182745179382773f,
+ 0.4543690419753592f, 0.4735331543074117f, 0.0720978037172291f,
+ -0.009627608738429352f, -0.005698031216113419f, 1.015325639954543f);
PDFColor3 adaptedTargetWhitePoint = cat02Matrix * targetWhitePoint;
PDFColor3 adaptedSourceWhitePoint = cat02Matrix * sourceWhitePoint;
@@ -2201,15 +2212,15 @@ PDFColorComponentMatrix_3x3 PDFChromaticAdaptationXYZ::createWhitepointChromatic
{
// Bradford matrix, as defined in https://en.wikipedia.org/wiki/LMS_color_space
constexpr PDFColorComponentMatrix_3x3 bradfordMatrix(
- 0.8951f, 0.2264f, -0.1614f,
- -0.7502f, 1.7135f, 0.0367f,
- 0.0389f, -0.0685f, 1.0296f);
+ 0.8951f, 0.2264f, -0.1614f,
+ -0.7502f, 1.7135f, 0.0367f,
+ 0.0389f, -0.0685f, 1.0296f);
// Inverse of bradford matrix (using wxMaxima to compute it)
constexpr PDFColorComponentMatrix_3x3 inverseBradfordMatrix(
- 1.004360519274085f, -0.1262294327613208f, 0.1619428982062721f,
- 0.4399123264001572f, 0.527481594455384f, 0.05015858096782513f,
- -0.008678739162151443f, 0.03986287311053728f, 0.9684695843590444f);
+ 1.004360519274085f, -0.1262294327613208f, 0.1619428982062721f,
+ 0.4399123264001572f, 0.527481594455384f, 0.05015858096782513f,
+ -0.008678739162151443f, 0.03986287311053728f, 0.9684695843590444f);
PDFColor3 adaptedTargetWhitePoint = bradfordMatrix * targetWhitePoint;
PDFColor3 adaptedSourceWhitePoint = bradfordMatrix * sourceWhitePoint;
diff --git a/LoopLibCore/sources/pdfcms.h b/LoopLibCore/sources/pdfcms.h
index 62ebbc1d0..43ed0934e 100644
--- a/LoopLibCore/sources/pdfcms.h
+++ b/LoopLibCore/sources/pdfcms.h
@@ -37,6 +37,8 @@
namespace pdf
{
+class PDFProcessingBudget;
+
/// This simple structure stores settings for color management system, and what
/// color management system should be used. At default, two color management
/// system are available - generic (which uses default imprecise color management),
@@ -82,13 +84,13 @@ struct PDFCMSSettings
bool isGamutChecking = false;
bool isSoftProofing = false;
bool isConsiderOutputIntent = true;
- QColor outOfGamutColor = Qt::red; ///< Color, which marks out-of-gamut when soft-proofing is proceeded
- QString outputCS; ///< Output (rendering) color space
- QString deviceGray; ///< Identifiers for color space (device gray)
- QString deviceRGB; ///< Identifiers for color space (device RGB)
- QString deviceCMYK; ///< Identifiers for color space (device CMYK)
- QString softProofingProfile; ///< Identifiers for soft proofing profile
- QString profileDirectory; ///< Directory containing color profiles
+ QColor outOfGamutColor = Qt::red; ///< Color, which marks out-of-gamut when soft-proofing is proceeded
+ QString outputCS; ///< Output (rendering) color space
+ QString deviceGray; ///< Identifiers for color space (device gray)
+ QString deviceRGB; ///< Identifiers for color space (device RGB)
+ QString deviceCMYK; ///< Identifiers for color space (device CMYK)
+ QString softProofingProfile; ///< Identifiers for soft proofing profile
+ QString profileDirectory; ///< Directory containing color profiles
// Postprocessing
QColor foregroundColor = Qt::green;
@@ -281,7 +283,7 @@ class LOOPLIBCORESHARED_EXPORT PDFCMSGeneric : public PDFCMS
virtual QColor getColorFromDeviceRGB(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const override;
virtual QColor getColorFromDeviceCMYK(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const override;
virtual QColor getColorFromXYZ(const PDFColor3& whitePoint, const PDFColor3& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const override;
- virtual QColor getColorFromICC(const PDFColor& color, RenderingIntent renderingIntent, const QByteArray& iccID, const QByteArray& iccData, PDFRenderErrorReporter* reporter) const override;
+ virtual QColor getColorFromICC(const PDFColor& color, RenderingIntent renderingIntent, const QByteArray& iccID, const QByteArray& iccData, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromDeviceGray(const std::vector& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromDeviceRGB(const std::vector& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromDeviceCMYK(const std::vector& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override;
@@ -409,6 +411,13 @@ class LOOPLIBCORESHARED_EXPORT PDFCMSManager : public QObject
/// \param document Document
void setDocument(const PDFDocument* document);
+ /// Same as setDocument(), but decodes output-intent profiles under the given
+ /// operation budget so a hostile /DestOutputProfile cannot bypass the
+ /// cumulative and elapsed decoded-stream accounting. The budget is a
+ /// parameter rather than a member: PDFCMSManager is an exported class and
+ /// growing it would break the Core ABI the Editor plugins link against.
+ void setDocument(const PDFDocument* document, PDFProcessingBudget* processingBudget);
+
/// Get translated name for color management system
/// \param system System
static QString getSystemName(PDFCMSSettings::System system);
@@ -475,4 +484,4 @@ class PDFChromaticAdaptationXYZ
} // namespace pdf
-#endif // PDFCMS_H
+#endif // PDFCMS_H
diff --git a/LoopLibCore/sources/pdfdiagnostics.cpp b/LoopLibCore/sources/pdfdiagnostics.cpp
index f57069718..ac878f55e 100644
--- a/LoopLibCore/sources/pdfdiagnostics.cpp
+++ b/LoopLibCore/sources/pdfdiagnostics.cpp
@@ -138,18 +138,35 @@ QJsonObject buildSystemInfo(const QString& applicationId)
return root;
}
+/// Longest plugin display string copied into a bundle. Plugin metadata is
+/// author-supplied JSON that Loop does not size-validate at load time, so an
+/// installed plugin with a multi-megabyte Description would otherwise bloat
+/// every future support bundle. The cap is generous for a real display string
+/// and the truncation is visible rather than silent.
+constexpr int PLUGIN_FIELD_LENGTH_LIMIT = 2048;
+
+QString truncatePluginField(const QString& value)
+{
+ if (value.size() <= PLUGIN_FIELD_LENGTH_LIMIT)
+ {
+ return value;
+ }
+
+ return value.left(PLUGIN_FIELD_LENGTH_LIMIT) + QStringLiteral("... ");
+}
+
QJsonObject buildPlugins(const PDFPluginInfos& plugins)
{
QJsonArray array;
for (const PDFPluginInfo& plugin : plugins)
{
QJsonObject entry;
- entry[QStringLiteral("name")] = plugin.name;
- entry[QStringLiteral("pluginId")] = plugin.pluginId;
+ entry[QStringLiteral("name")] = truncatePluginField(plugin.name);
+ entry[QStringLiteral("pluginId")] = truncatePluginField(plugin.pluginId);
entry[QStringLiteral("abiVersion")] = static_cast(plugin.abiVersion);
- entry[QStringLiteral("author")] = plugin.author;
- entry[QStringLiteral("version")] = plugin.version;
- entry[QStringLiteral("license")] = plugin.license;
+ entry[QStringLiteral("author")] = truncatePluginField(plugin.author);
+ entry[QStringLiteral("version")] = truncatePluginField(plugin.version);
+ entry[QStringLiteral("license")] = truncatePluginField(plugin.license);
array.append(entry);
}
diff --git a/LoopLibCore/sources/pdfdocumentreader.cpp b/LoopLibCore/sources/pdfdocumentreader.cpp
index 9f7f4e03d..1ef3dad2c 100644
--- a/LoopLibCore/sources/pdfdocumentreader.cpp
+++ b/LoopLibCore/sources/pdfdocumentreader.cpp
@@ -40,6 +40,19 @@
namespace pdf
{
+namespace
+{
+
+// Bounds for the dense object table built by damaged-document recovery. The
+// table is indexed by object number, so its size is driven by the highest
+// number a malformed document happens to declare rather than by how much was
+// actually recovered. A document numbered more sparsely than this is not a
+// recoverable document, it is a document asking for a large allocation.
+constexpr PDFInteger DAMAGED_DOCUMENT_MAX_OBJECT_NUMBER_DENSITY = 64;
+constexpr PDFInteger DAMAGED_DOCUMENT_MINIMUM_OBJECT_SLOTS = 4096;
+
+} // namespace
+
PDFDocumentReader::PDFDocumentReader(PDFProgress* progress,
const std::function& getPasswordCallback,
bool permissive,
@@ -313,7 +326,8 @@ PDFObject PDFDocumentReader::getObjectFromXrefTable(PDFXRefTable* xrefTable, PDF
PDFObject PDFDocumentReader::readDamagedTrailerDictionary() const
{
PDFObject object = PDFObject::createDictionary(std::make_shared(PDFDictionary()));
- PDFParsingContext context([](PDFParsingContext*, PDFObjectReference){ return PDFObject(); });
+ PDFParsingContext context([](PDFParsingContext*, PDFObjectReference)
+ { return PDFObject(); });
int offset = 0;
while (offset < m_source.size())
@@ -350,7 +364,8 @@ PDFObject PDFDocumentReader::readDamagedTrailerDictionary() const
PDFDocumentReader::Result PDFDocumentReader::processReferenceTableEntries(PDFXRefTable* xrefTable, const std::vector& occupiedEntries, PDFObjectStorage::PDFObjects& objects)
{
- auto objectFetcher = [this, xrefTable](PDFParsingContext* context, PDFObjectReference reference) { return getObjectFromXrefTable(xrefTable, context, reference); };
+ auto objectFetcher = [this, xrefTable](PDFParsingContext* context, PDFObjectReference reference)
+ { return getObjectFromXrefTable(xrefTable, context, reference); };
auto processEntry = [this, &objectFetcher, &objects](const PDFXRefTable::Entry& entry)
{
Q_ASSERT(entry.type == PDFXRefTable::EntryType::Occupied);
@@ -503,8 +518,9 @@ void PDFDocumentReader::processObjectStreams(PDFXRefTable* xrefTable, PDFObjectS
objectStreams.insert(entry.objectStream);
}
- auto objectFetcher = [this, xrefTable](PDFParsingContext* context, PDFObjectReference reference) { return getObjectFromXrefTable(xrefTable, context, reference); };
- auto processObjectStream = [this, &objectFetcher, &objects, &objectStreamEntries] (const PDFObjectReference& objectStreamReference)
+ auto objectFetcher = [this, xrefTable](PDFParsingContext* context, PDFObjectReference reference)
+ { return getObjectFromXrefTable(xrefTable, context, reference); };
+ auto processObjectStream = [this, &objectFetcher, &objects, &objectStreamEntries](const PDFObjectReference& objectStreamReference)
{
if (m_result != Result::OK)
{
@@ -603,7 +619,8 @@ void PDFDocumentReader::processObjectStreams(PDFXRefTable* xrefTable, PDFObjectS
parser.seek(offset);
PDFObject currentObject = parser.getObject();
- auto predicate = [objectNumber, objectStreamReference](const PDFXRefTable::Entry& entry) -> bool { return entry.reference.objectNumber == objectNumber && entry.objectStream == objectStreamReference; };
+ auto predicate = [objectNumber, objectStreamReference](const PDFXRefTable::Entry& entry) -> bool
+ { return entry.reference.objectNumber == objectNumber && entry.objectStream == objectStreamReference; };
if (std::find_if(objectStreamEntries.cbegin(), objectStreamEntries.cend(), predicate) != objectStreamEntries.cend())
{
QMutexLocker lock(&m_mutex);
@@ -671,6 +688,34 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
throw PDFException(tr("Empty xref table."));
}
+ // The reader allocates a dense object table for every declared slot -
+ // including free and never-referenced ones - so the declared cardinality,
+ // not the number of occupied entries, is what must fit the document-model
+ // object budget. A sparse table otherwise turns a small file into a large
+ // allocation before a single object has been visited.
+ //
+ // The refusal is raised through the budget vocabulary the rest of the
+ // reader already uses (PDFBudgetExceededException, kind ObjectsVisited)
+ // rather than a bespoke message: it is the same budget a document whose
+ // object table grows past the limit trips while being read, so the failure
+ // stays attributable ("attempted N, limit L") and a budget failure still
+ // fails the read closed instead of being retried permissively. The detail
+ // is built directly because there is no bulk charge in the public budget
+ // API, and charging one object at a time up to the limit would make the
+ // refusal cost attacker-amplifiable work.
+ const std::uint64_t declaredObjectCount = xrefTable.getSize();
+ const std::uint64_t maximalObjectTableSize = m_processingBudget.limits().maxObjectsVisited;
+ if (declaredObjectCount > maximalObjectTableSize)
+ {
+ PDFBudgetExceeded detail;
+ detail.kind = PDFBudgetKind::ObjectsVisited;
+ detail.pool = budgetPoolFor(detail.kind);
+ detail.limit = maximalObjectTableSize;
+ detail.attempted = declaredObjectCount;
+ detail.context = tr("PDF object table");
+ throw PDFBudgetExceededException(std::move(detail));
+ }
+
PDFObjectStorage::PDFObjects objects;
objects.resize(xrefTable.getSize());
@@ -708,7 +753,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
m_warnings << m_errorMessage;
return PDFDocument();
}
- catch (const PDFException &parserException)
+ catch (const PDFException& parserException)
{
m_result = Result::Failed;
m_errorMessage = parserException.getMessage();
@@ -884,7 +929,27 @@ PDFDocument PDFDocumentReader::readDamagedDocumentFromBuffer(const QByteArray& b
if (!restoredObjects.empty())
{
- objects.resize(restoredObjects.rbegin()->first.objectNumber + 1);
+ // The object table is dense: it is indexed by object number, so a
+ // single recovered object numbered 9999999 would allocate ten million
+ // entries. The per-reference check in restoreObjects() bounds an
+ // object number by the file size, which on a 50 MiB file still allows
+ // a table of fifty million entries. Bound the table by how many
+ // objects were actually recovered instead: real damaged documents are
+ // numbered densely, and a highest-number-to-recovered-count ratio far
+ // past that is a malformed document rather than a recoverable one.
+ const PDFInteger highestObjectNumber = restoredObjects.rbegin()->first.objectNumber;
+ const PDFInteger maximumObjectNumber = std::max(
+ DAMAGED_DOCUMENT_MINIMUM_OBJECT_SLOTS,
+ static_cast(restoredObjects.size()) * DAMAGED_DOCUMENT_MAX_OBJECT_NUMBER_DENSITY);
+
+ if (highestObjectNumber > maximumObjectNumber)
+ {
+ throw PDFException(PDFTranslationContext::tr("Damaged document declares object number %1, but only %2 objects could be recovered; refusing to build an object table of that size.")
+ .arg(highestObjectNumber)
+ .arg(restoredObjects.size()));
+ }
+
+ objects.resize(highestObjectNumber + 1);
for (auto& objectItem : restoredObjects)
{
@@ -901,9 +966,16 @@ PDFDocument PDFDocumentReader::readDamagedDocumentFromBuffer(const QByteArray& b
}
PDFObjectStorage storage(std::move(objects), PDFObject(trailerDictionaryObject), qMove(m_securityHandler));
- return PDFDocument(std::move(storage), m_version, QByteArray());
+
+ // The recovered document corresponds to exactly these bytes, so it gets a
+ // real source digest like any other successful read. Returning an empty
+ // digest here silently disabled the "the file changed underneath us" guard
+ // in PDFDocumentWriter::writeIncremental for every permissively recovered
+ // document - the one class of document where appending to the wrong bytes
+ // is most likely.
+ return PDFDocument(std::move(storage), m_version, hash(buffer));
}
- catch (const PDFException &parserException)
+ catch (const PDFException& parserException)
{
m_result = Result::Failed;
m_warnings << parserException.getMessage();
diff --git a/LoopLibCore/sources/pdfdocumentsanitizer.cpp b/LoopLibCore/sources/pdfdocumentsanitizer.cpp
index ea560e734..5e39fbc65 100644
--- a/LoopLibCore/sources/pdfdocumentsanitizer.cpp
+++ b/LoopLibCore/sources/pdfdocumentsanitizer.cpp
@@ -173,10 +173,10 @@ QByteArray PDFInvisibleTextSanitizerHelper::sanitizeInvisibleTextInContent(const
if (operatorIDPosition != -1 && operatorEIPosition != -1)
{
PDFLexicalAnalyzer inlineImageLexicalAnalyzer(content.constBegin() + operatorBIPosition, content.constBegin() + operatorIDPosition);
- PDFParser inlineImageParser([&inlineImageLexicalAnalyzer] { return inlineImageLexicalAnalyzer.fetch(); });
+ PDFParser inlineImageParser([&inlineImageLexicalAnalyzer]
+ { return inlineImageLexicalAnalyzer.fetch(); });
- constexpr std::pair replacements[] =
- {
+ constexpr std::pair replacements[] = {
{ "BPC", "BitsPerComponent" },
{ "CS", "ColorSpace" },
{ "D", "Decode" },
@@ -252,7 +252,9 @@ QByteArray PDFInvisibleTextSanitizerHelper::sanitizeInvisibleTextInContent(const
{
continue;
}
- stride = (stride + 7) / 8;
+ // The +7 above is the only rounding (see the inline-image
+ // length computation in pdfpagecontentprocessor.cpp).
+ stride = stride / 8;
if (!pdfTryMultiply(stride, height, dataLengthProduct))
{
continue;
@@ -562,7 +564,6 @@ class PDFRemoveMetadataVisitor : public PDFUpdateObjectVisitor
PDFUpdateObjectVisitor(storage),
m_counter(counter)
{
-
}
virtual void visitDictionary(const PDFDictionary* dictionary) override;
@@ -600,7 +601,6 @@ PDFDocumentSanitizer::PDFDocumentSanitizer(SanitizationFlag flags, QObject* pare
QObject(parent),
m_flags(flags)
{
-
}
void PDFDocumentSanitizer::sanitize()
@@ -691,7 +691,7 @@ void PDFDocumentSanitizer::performSanitizeMetadata()
{
std::atomic counter = 0;
- PDFObjectStorage::PDFObjects objects = m_storage.getObjects();
+ PDFObjectStorage::PDFObjects objects = m_storage.getObjects();
auto processEntry = [this, &counter](PDFObjectStorage::Entry& entry)
{
PDFRemoveMetadataVisitor visitor(&m_storage, &counter);
@@ -919,7 +919,7 @@ void PDFDocumentSanitizer::performSanitizeInvisibleText()
}
}
-void PDFDocumentSanitizer::removeAnnotations(const std::function& filter,
+void PDFDocumentSanitizer::removeAnnotations(const std::function& filter,
QString message)
{
PDFDocumentBuilder builder(m_storage, PDFVersion(2, 0));
diff --git a/LoopLibCore/sources/pdfdocumentsession.cpp b/LoopLibCore/sources/pdfdocumentsession.cpp
index aafdca460..14ec58cbe 100644
--- a/LoopLibCore/sources/pdfdocumentsession.cpp
+++ b/LoopLibCore/sources/pdfdocumentsession.cpp
@@ -735,7 +735,7 @@ void PDFDocumentSession::initializeRendering()
m_optionalContentActivity = std::make_unique(m_document, OCUsage::Export, nullptr);
m_cmsManager = std::make_unique(nullptr);
- m_cmsManager->setDocument(m_document);
+ m_cmsManager->setDocument(m_document, m_processingBudget.get());
m_cms = m_cmsManager->getCurrentCMS();
m_fontCache = std::make_unique(DEFAULT_FONT_CACHE_LIMIT, DEFAULT_REALIZED_FONT_CACHE_LIMIT);
diff --git a/LoopLibCore/sources/pdfdocumentwriter.cpp b/LoopLibCore/sources/pdfdocumentwriter.cpp
index 78e3d0564..aa7f63e6e 100644
--- a/LoopLibCore/sources/pdfdocumentwriter.cpp
+++ b/LoopLibCore/sources/pdfdocumentwriter.cpp
@@ -252,7 +252,7 @@ PDFOperationResult PDFDocumentWriter::write(QIODevice* device, const PDFDocument
PDFDictionary trailerDictionary = *document->getTrailerDictionary();
PDFDictionary newTrailerDictionary;
- for (const char* entry : { "Size", "Root", "Encrypt", "Info", "ID"})
+ for (const char* entry : { "Size", "Root", "Encrypt", "Info", "ID" })
{
PDFObject object = trailerDictionary.get(entry);
if (!object.isNull())
@@ -284,9 +284,18 @@ PDFOperationResult PDFDocumentWriter::write(QIODevice* device, const PDFDocument
}
PDFOperationResult PDFDocumentWriter::writeIncremental(const QString& fileName,
- const PDFDocument* originalDocument,
- const PDFDocument* document,
- bool safeWrite)
+ const PDFDocument* originalDocument,
+ const PDFDocument* document,
+ bool safeWrite)
+{
+ return writeIncremental(fileName, originalDocument, document, safeWrite, nullptr);
+}
+
+PDFOperationResult PDFDocumentWriter::writeIncremental(const QString& fileName,
+ const PDFDocument* originalDocument,
+ const PDFDocument* document,
+ bool safeWrite,
+ IncrementalWriteOutcome* outcome)
{
if (!originalDocument || !document)
{
@@ -311,14 +320,23 @@ PDFOperationResult PDFDocumentWriter::writeIncremental(const QString& fileName,
return tr("File '%1' can't be opened for incremental save. %2").arg(fileName, targetFile.errorString());
}
- const PDFOperationResult result = writeIncremental(&targetFile, originalData, originalDocument, document);
- if (result && !targetFile.commit())
+ // The nested write reports what it did on success, but a successful
+ // write is not a successful save: commit() can still fail. Hold the
+ // outcome locally and publish it only once the rename has landed.
+ IncrementalWriteOutcome nestedOutcome = IncrementalWriteOutcome::CopiedUnchanged;
+ const PDFOperationResult result = writeIncremental(&targetFile, originalData, originalDocument, document, &nestedOutcome);
+ if (!result)
+ {
+ targetFile.cancelWriting();
+ return result;
+ }
+ if (!targetFile.commit())
{
return tr("File '%1' can't be committed after incremental save. %2").arg(fileName, targetFile.errorString());
}
- if (!result)
+ if (outcome)
{
- targetFile.cancelWriting();
+ *outcome = nestedOutcome;
}
return result;
}
@@ -329,15 +347,29 @@ PDFOperationResult PDFDocumentWriter::writeIncremental(const QString& fileName,
return tr("File '%1' can't be opened for incremental save. %2").arg(fileName, targetFile.errorString());
}
- const PDFOperationResult result = writeIncremental(&targetFile, originalData, originalDocument, document);
+ IncrementalWriteOutcome nestedOutcome = IncrementalWriteOutcome::CopiedUnchanged;
+ const PDFOperationResult result = writeIncremental(&targetFile, originalData, originalDocument, document, &nestedOutcome);
targetFile.close();
+ if (result && outcome)
+ {
+ *outcome = nestedOutcome;
+ }
return result;
}
PDFOperationResult PDFDocumentWriter::writeIncremental(QIODevice* device,
- const QByteArray& originalData,
- const PDFDocument* originalDocument,
- const PDFDocument* document)
+ const QByteArray& originalData,
+ const PDFDocument* originalDocument,
+ const PDFDocument* document)
+{
+ return writeIncremental(device, originalData, originalDocument, document, nullptr);
+}
+
+PDFOperationResult PDFDocumentWriter::writeIncremental(QIODevice* device,
+ const QByteArray& originalData,
+ const PDFDocument* originalDocument,
+ const PDFDocument* document,
+ IncrementalWriteOutcome* outcome)
{
if (!device || !device->isWritable() || !originalDocument || !document)
{
@@ -411,9 +443,20 @@ PDFOperationResult PDFDocumentWriter::writeIncremental(QIODevice* device,
if (changedObjects.empty())
{
- return device->write(originalData) == originalData.size()
- ? PDFOperationResult(true)
- : PDFOperationResult(tr("Failed to copy the original PDF bytes."));
+ // Nothing changed, so there is nothing to append. The bytes are copied
+ // verbatim - which is the right output - but it is not an append, and a
+ // caller that asked for one is told so through \p outcome.
+ if (device->write(originalData) != originalData.size())
+ {
+ return PDFOperationResult(tr("Failed to copy the original PDF bytes."));
+ }
+
+ if (outcome)
+ {
+ *outcome = IncrementalWriteOutcome::CopiedUnchanged;
+ }
+
+ return PDFOperationResult(true);
}
if (device->write(originalData) != originalData.size())
@@ -516,12 +559,17 @@ PDFOperationResult PDFDocumentWriter::writeIncremental(QIODevice* device,
writeCRLF(device);
device->write("%%EOF");
+ if (outcome)
+ {
+ *outcome = IncrementalWriteOutcome::Appended;
+ }
+
return true;
}
PDFDocumentWriter::WriteMode PDFDocumentWriter::getRecommendedWriteMode(const PDFDocument* sourceDocument,
- bool requiresFullRewrite,
- bool saveAsNewOutput)
+ bool requiresFullRewrite,
+ bool saveAsNewOutput)
{
return getRecommendedWriteMode(sourceDocument,
requiresFullRewrite
@@ -531,8 +579,8 @@ PDFDocumentWriter::WriteMode PDFDocumentWriter::getRecommendedWriteMode(const PD
}
PDFDocumentWriter::WriteMode PDFDocumentWriter::getRecommendedWriteMode(const PDFDocument* sourceDocument,
- const PDFOperationSavePolicy& policy,
- bool saveAsNewOutput)
+ const PDFOperationSavePolicy& policy,
+ bool saveAsNewOutput)
{
if (policy.mode != PDFSaveMode::IncrementalAppend || saveAsNewOutput || !sourceDocument)
{
@@ -626,7 +674,7 @@ qint64 getPreviousXrefOffset(const QByteArray& data)
return ok ? result : -1;
}
-} // namespace
+} // namespace
class PDFSizeCounterIODevice : public QIODevice
{
@@ -634,7 +682,6 @@ class PDFSizeCounterIODevice : public QIODevice
explicit PDFSizeCounterIODevice(QObject* parent) :
QIODevice(parent)
{
-
}
virtual bool isSequential() const override;
diff --git a/LoopLibCore/sources/pdfdocumentwriter.h b/LoopLibCore/sources/pdfdocumentwriter.h
index 911aebeae..fcff360fe 100644
--- a/LoopLibCore/sources/pdfdocumentwriter.h
+++ b/LoopLibCore/sources/pdfdocumentwriter.h
@@ -50,6 +50,18 @@ class LOOPLIBCORESHARED_EXPORT PDFDocumentWriter
Incremental
};
+ /// What an incremental save actually did. Every refusal to append is already
+ /// reported as a failed PDFOperationResult naming the reason, but one success
+ /// path is not an append at all: a document with no changed objects is
+ /// byte-copied. A caller that asked for an incremental save specifically to
+ /// preserve `Prev`/signature coverage needs to be able to tell those apart,
+ /// so writeIncremental reports the outcome on request.
+ enum class IncrementalWriteOutcome
+ {
+ Appended, ///< Changed objects plus a new xref section were appended
+ CopiedUnchanged ///< Nothing changed; the original bytes were copied verbatim
+ };
+
explicit inline PDFDocumentWriter(PDFProgress* progress,
const PDFOperationControl* operationControl = nullptr) :
m_operationControl(operationControl)
@@ -83,18 +95,38 @@ class LOOPLIBCORESHARED_EXPORT PDFDocumentWriter
/// Appends an incremental update to an existing PDF. The original bytes
/// are copied unchanged and only changed objects plus a new xref/trailer
/// section are appended.
+ ///
+ /// This is the original four-argument entry point. It is kept as a real
+ /// exported overload (not a defaulted parameter on the reporting one) so
+ /// existing binaries keep resolving the same mangled symbol, and so a
+ /// four-argument call is not ambiguous.
PDFOperationResult writeIncremental(const QString& fileName,
- const PDFDocument* originalDocument,
- const PDFDocument* document,
- bool safeWrite);
+ const PDFDocument* originalDocument,
+ const PDFDocument* document,
+ bool safeWrite);
+
+ /// As above, and reports through \p outcome what the save actually did.
+ /// \p outcome is set only on success.
+ PDFOperationResult writeIncremental(const QString& fileName,
+ const PDFDocument* originalDocument,
+ const PDFDocument* document,
+ bool safeWrite,
+ IncrementalWriteOutcome* outcome);
+
+ /// Writes an incremental update using the supplied original bytes. Kept for
+ /// the same binary-compatibility reason as the four-argument file overload.
+ PDFOperationResult writeIncremental(QIODevice* device,
+ const QByteArray& originalData,
+ const PDFDocument* originalDocument,
+ const PDFDocument* document);
- /// Writes an incremental update using the supplied original bytes. This
- /// overload is useful for callers that already hold the source buffer and
- /// for byte-preservation tests.
+ /// As above, and reports through \p outcome what the save actually did.
+ /// \p outcome is set only on success.
PDFOperationResult writeIncremental(QIODevice* device,
- const QByteArray& originalData,
- const PDFDocument* originalDocument,
- const PDFDocument* document);
+ const QByteArray& originalData,
+ const PDFDocument* originalDocument,
+ const PDFDocument* document,
+ IncrementalWriteOutcome* outcome);
/// Chooses the default save mode for an existing document. Save As and
/// destructive operations must pass the corresponding opt-out flags.
@@ -139,4 +171,4 @@ class LOOPLIBCORESHARED_EXPORT PDFDocumentWriter
} // namespace pdf
-#endif // PDFDOCUMENTWRITER_H
+#endif // PDFDOCUMENTWRITER_H
diff --git a/LoopLibCore/sources/pdffilenamesanitizer.cpp b/LoopLibCore/sources/pdffilenamesanitizer.cpp
index fe93a8324..d3578c47e 100644
--- a/LoopLibCore/sources/pdffilenamesanitizer.cpp
+++ b/LoopLibCore/sources/pdffilenamesanitizer.cpp
@@ -89,17 +89,32 @@ QString PDFFilenameSanitizer::sanitize(const QString& rawFilename, const QString
bool PDFFilenameSanitizer::isPathContained(const QString& resolvedPath, const QString& targetDirectory)
{
- const QString canonicalTarget = QDir(targetDirectory).canonicalPath();
+ // A target that does not exist yet cannot be canonicalized, but callers
+ // legitimately validate a planned output before creating its directory. Fall
+ // back to the cleaned absolute path in that case: traversal is still caught,
+ // because cleanPath() resolves "..", and there is no symlink to resolve in a
+ // directory that does not exist. Note that the *file* side keeps its
+ // stricter rule below - a name whose parent is a symlink is not treated as
+ // contained even when it resolves inside the target.
+ QString canonicalTarget = QDir(targetDirectory).canonicalPath();
if (canonicalTarget.isEmpty())
{
- // Target directory does not exist — cannot verify containment
- return false;
+ if (targetDirectory.isEmpty())
+ {
+ return false;
+ }
+
+ canonicalTarget = QDir::cleanPath(QDir(targetDirectory).absolutePath());
+ if (canonicalTarget.isEmpty())
+ {
+ return false;
+ }
}
const QString canonicalFilePath = QFileInfo(resolvedPath).canonicalFilePath();
const QString canonicalFile = canonicalFilePath.isEmpty()
- ? QDir::cleanPath(QFileInfo(resolvedPath).absoluteFilePath())
- : canonicalFilePath;
+ ? QDir::cleanPath(QFileInfo(resolvedPath).absoluteFilePath())
+ : canonicalFilePath;
// The file path must start with the target directory path followed by a separator
if (canonicalFile == canonicalTarget)
@@ -107,8 +122,7 @@ bool PDFFilenameSanitizer::isPathContained(const QString& resolvedPath, const QS
return false;
}
- return canonicalFile.startsWith(canonicalTarget + QLatin1Char('/'))
- || canonicalFile.startsWith(canonicalTarget + QLatin1Char('\\'));
+ return canonicalFile.startsWith(canonicalTarget + QLatin1Char('/')) || canonicalFile.startsWith(canonicalTarget + QLatin1Char('\\'));
}
} // namespace pdf
diff --git a/LoopLibCore/sources/pdffont.cpp b/LoopLibCore/sources/pdffont.cpp
index dcd967a18..e1aff6460 100644
--- a/LoopLibCore/sources/pdffont.cpp
+++ b/LoopLibCore/sources/pdffont.cpp
@@ -77,8 +77,7 @@ struct PDF_Default_CJK_Font
const char* name = nullptr;
};
-static constexpr std::array S_DEFAULT_CJK_FONTS =
-{
+static constexpr std::array S_DEFAULT_CJK_FONTS = {
PDF_Default_CJK_Font{ ECjkDefaultFontType::AdobeGB, true, "KaiTi_GB2312" },
PDF_Default_CJK_Font{ ECjkDefaultFontType::AdobeGB, true, "Song" },
PDF_Default_CJK_Font{ ECjkDefaultFontType::AdobeGB, false, "Heiti" },
@@ -111,15 +110,14 @@ struct PDF_Font_Replacement
const char* replaceFont;
};
-static constexpr std::array S_FONT_REPLACEMENTS
-{
- PDF_Font_Replacement{"Futura", "Calibri"},
- PDF_Font_Replacement{"Utopia-Bold", "Georgia"},
- PDF_Font_Replacement{"Utopia-BoldItalic", "Georgia"},
- PDF_Font_Replacement{"Utopia-Italic", "Georgia"},
- PDF_Font_Replacement{"Utopia-Semibold", "Georgia"},
- PDF_Font_Replacement{"Utopia-SemiboldItalic", "Georgia"},
- PDF_Font_Replacement{"Utopia", "Georgia"}
+static constexpr std::array S_FONT_REPLACEMENTS{
+ PDF_Font_Replacement{ "Futura", "Calibri" },
+ PDF_Font_Replacement{ "Utopia-Bold", "Georgia" },
+ PDF_Font_Replacement{ "Utopia-BoldItalic", "Georgia" },
+ PDF_Font_Replacement{ "Utopia-Italic", "Georgia" },
+ PDF_Font_Replacement{ "Utopia-Semibold", "Georgia" },
+ PDF_Font_Replacement{ "Utopia-SemiboldItalic", "Georgia" },
+ PDF_Font_Replacement{ "Utopia", "Georgia" }
};
static bool isMicrosoftSymbolCharmap(FT_CharMap charMap)
@@ -137,7 +135,7 @@ static bool isUnicodeCharmap(FT_CharMap charMap)
return charMap && charMap->encoding == FT_ENCODING_UNICODE;
}
-template
+template
static bool hasCharmap(FT_Face face, Predicate predicate)
{
for (FT_Int i = 0; i < face->num_charmaps; ++i)
@@ -151,7 +149,7 @@ static bool hasCharmap(FT_Face face, Predicate predicate)
return false;
}
-template
+template
static FT_CharMap selectCharmap(FT_Face face, Predicate predicate)
{
for (FT_Int i = 0; i < face->num_charmaps; ++i)
@@ -184,7 +182,8 @@ static FT_CharMap selectSymbolicTrueTypeCMap(FT_Face face)
return charMap;
}
- return selectCharmap(face, [](FT_CharMap charMap) { return !isUnicodeCharmap(charMap); });
+ return selectCharmap(face, [](FT_CharMap charMap)
+ { return !isUnicodeCharmap(charMap); });
}
static FT_UInt getSymbolicTrueTypeGlyphIndex(FT_Face face, FT_CharMap charMap, FT_ULong characterCode)
@@ -213,7 +212,7 @@ struct SystemFontData
};
#if defined(Q_OS_WIN)
-template
+template
static void releaseComObject(T*& object)
{
if (object)
@@ -268,7 +267,6 @@ static bool matchesDirectWriteFontName(const QString& fontName, const QString& c
class PDFSystemFontInfoStorage
{
public:
-
/// Returns instance of storage
static const PDFSystemFontInfoStorage* getInstance();
@@ -649,33 +647,37 @@ SystemFontData PDFSystemFontInfoStorage::loadFontImpl(const FontDescriptor* desc
}
constexpr const std::array, 9> weights{
- std::pair{100, FC_WEIGHT_EXTRALIGHT},
- std::pair{200, FC_WEIGHT_LIGHT},
- std::pair{300, FC_WEIGHT_BOOK},
- std::pair{400, FC_WEIGHT_NORMAL},
- std::pair{500, FC_WEIGHT_MEDIUM},
- std::pair{600, FC_WEIGHT_DEMIBOLD},
- std::pair{700, FC_WEIGHT_BOLD},
- std::pair{800, FC_WEIGHT_EXTRABOLD},
- std::pair{900, FC_WEIGHT_EXTRABOLD}};
- auto wit = std::lower_bound(weights.cbegin(), weights.cend(), descriptor->fontWeight, [](const std::pair& data, PDFReal key) { return data.first < key; });
+ std::pair{ 100, FC_WEIGHT_EXTRALIGHT },
+ std::pair{ 200, FC_WEIGHT_LIGHT },
+ std::pair{ 300, FC_WEIGHT_BOOK },
+ std::pair{ 400, FC_WEIGHT_NORMAL },
+ std::pair{ 500, FC_WEIGHT_MEDIUM },
+ std::pair{ 600, FC_WEIGHT_DEMIBOLD },
+ std::pair{ 700, FC_WEIGHT_BOLD },
+ std::pair{ 800, FC_WEIGHT_EXTRABOLD },
+ std::pair{ 900, FC_WEIGHT_EXTRABOLD }
+ };
+ auto wit = std::lower_bound(weights.cbegin(), weights.cend(), descriptor->fontWeight, [](const std::pair& data, PDFReal key)
+ { return data.first < key; });
if (wit != weights.cend())
{
checkFontConfigError(FcPatternAddInteger(p, FC_WEIGHT, wit->second));
}
constexpr const std::array, 9> stretches{
- std::pair{QFont::UltraCondensed, FC_WIDTH_ULTRACONDENSED},
- std::pair{QFont::ExtraCondensed, FC_WIDTH_EXTRACONDENSED},
- std::pair{QFont::Condensed, FC_WIDTH_CONDENSED},
- std::pair{QFont::SemiCondensed, FC_WIDTH_SEMICONDENSED},
- std::pair{QFont::Unstretched, FC_WIDTH_NORMAL},
- std::pair{QFont::SemiExpanded, FC_WIDTH_SEMIEXPANDED},
- std::pair{QFont::Expanded, FC_WIDTH_EXPANDED},
- std::pair{QFont::ExtraExpanded, FC_WIDTH_EXTRAEXPANDED},
- std::pair{QFont::UltraExpanded, FC_WIDTH_ULTRAEXPANDED}};
-
- auto sit = std::find_if(stretches.cbegin(), stretches.cend(), [&](const std::pair& item) { return item.first == descriptor->fontStretch; });
+ std::pair{ QFont::UltraCondensed, FC_WIDTH_ULTRACONDENSED },
+ std::pair{ QFont::ExtraCondensed, FC_WIDTH_EXTRACONDENSED },
+ std::pair{ QFont::Condensed, FC_WIDTH_CONDENSED },
+ std::pair{ QFont::SemiCondensed, FC_WIDTH_SEMICONDENSED },
+ std::pair{ QFont::Unstretched, FC_WIDTH_NORMAL },
+ std::pair{ QFont::SemiExpanded, FC_WIDTH_SEMIEXPANDED },
+ std::pair{ QFont::Expanded, FC_WIDTH_EXPANDED },
+ std::pair{ QFont::ExtraExpanded, FC_WIDTH_EXTRAEXPANDED },
+ std::pair{ QFont::UltraExpanded, FC_WIDTH_ULTRAEXPANDED }
+ };
+
+ auto sit = std::find_if(stretches.cbegin(), stretches.cend(), [&](const std::pair& item)
+ { return item.first == descriptor->fontStretch; });
if (sit != stretches.cend())
{
checkFontConfigError(FcPatternAddInteger(p, FC_WIDTH, sit->second));
@@ -691,7 +693,7 @@ SystemFontData PDFSystemFontInfoStorage::loadFontImpl(const FontDescriptor* desc
if (FcPatternGetString(match, FC_FILE, 0, &s) == FcResultMatch)
{
QFile f(QString::fromUtf8(reinterpret_cast(s)));
- if ( f.open(QIODevice::ReadOnly) )
+ if (f.open(QIODevice::ReadOnly))
{
result.data = f.readAll();
f.close();
@@ -1023,7 +1025,6 @@ PDFFont::PDFFont(CIDSystemInfo CIDSystemInfo, QByteArray fontId, FontDescriptor
m_fontDescriptor(qMove(fontDescriptor)),
m_fontId(qMove(fontId))
{
-
}
class IRealizedFontImpl
@@ -1055,7 +1056,11 @@ class IRealizedFontImpl
class PDFRealizedType3FontImpl : public IRealizedFontImpl
{
public:
- explicit PDFRealizedType3FontImpl(PDFFontPointer parentFont, PDFReal pixelSize) : m_pixelSize(pixelSize), m_parentFont(parentFont) { }
+ explicit PDFRealizedType3FontImpl(PDFFontPointer parentFont, PDFReal pixelSize) :
+ m_pixelSize(pixelSize),
+ m_parentFont(parentFont)
+ {
+ }
virtual ~PDFRealizedType3FontImpl() override = default;
PDFReal getPixelSize() const { return m_pixelSize; }
@@ -1161,7 +1166,6 @@ PDFRealizedFontImpl::PDFRealizedFontImpl() :
m_isEmbedded(false),
m_isVertical(false)
{
-
}
PDFRealizedFontImpl::~PDFRealizedFontImpl()
@@ -1423,13 +1427,13 @@ void PDFRealizedFontImpl::dumpFontToTreeItem(ITreeFactory* treeFactory) const
QString yesString = PDFTranslationContext::tr("Yes");
QString noString = PDFTranslationContext::tr("No");
- treeFactory->addItem( { PDFTranslationContext::tr("Glyph count"), QString::number(m_face->num_glyphs) });
- treeFactory->addItem( { PDFTranslationContext::tr("Is CID keyed"), (m_face->face_flags & FT_FACE_FLAG_CID_KEYED) ? yesString : noString });
- treeFactory->addItem( { PDFTranslationContext::tr("Is bold"), (m_face->style_flags & FT_STYLE_FLAG_BOLD) ? yesString : noString });
- treeFactory->addItem( { PDFTranslationContext::tr("Is italics"), (m_face->style_flags & FT_STYLE_FLAG_ITALIC) ? yesString : noString });
- treeFactory->addItem( { PDFTranslationContext::tr("Has vertical writing system"), (m_face->face_flags & FT_FACE_FLAG_VERTICAL) ? yesString : noString });
- treeFactory->addItem( { PDFTranslationContext::tr("Has SFNT storage scheme"), (m_face->face_flags & FT_FACE_FLAG_SFNT) ? yesString : noString });
- treeFactory->addItem( { PDFTranslationContext::tr("Has glyph names"), (m_face->face_flags & FT_FACE_FLAG_GLYPH_NAMES) ? yesString : noString });
+ treeFactory->addItem({ PDFTranslationContext::tr("Glyph count"), QString::number(m_face->num_glyphs) });
+ treeFactory->addItem({ PDFTranslationContext::tr("Is CID keyed"), (m_face->face_flags & FT_FACE_FLAG_CID_KEYED) ? yesString : noString });
+ treeFactory->addItem({ PDFTranslationContext::tr("Is bold"), (m_face->style_flags & FT_STYLE_FLAG_BOLD) ? yesString : noString });
+ treeFactory->addItem({ PDFTranslationContext::tr("Is italics"), (m_face->style_flags & FT_STYLE_FLAG_ITALIC) ? yesString : noString });
+ treeFactory->addItem({ PDFTranslationContext::tr("Has vertical writing system"), (m_face->face_flags & FT_FACE_FLAG_VERTICAL) ? yesString : noString });
+ treeFactory->addItem({ PDFTranslationContext::tr("Has SFNT storage scheme"), (m_face->face_flags & FT_FACE_FLAG_SFNT) ? yesString : noString });
+ treeFactory->addItem({ PDFTranslationContext::tr("Has glyph names"), (m_face->face_flags & FT_FACE_FLAG_GLYPH_NAMES) ? yesString : noString });
if (m_face->num_charmaps > 0)
{
@@ -1591,7 +1595,7 @@ bool PDFRealizedFontImpl::canRenderGlyphIndex(GID glyphIndex, QChar character) c
if (m_face && FT_Has_PS_Glyph_Names(m_face))
{
- char glyphName[128] = { };
+ char glyphName[128] = {};
if (!FT_Get_Glyph_Name(m_face, glyphIndex, glyphName, static_cast(std::size(glyphName))))
{
return qstrcmp(glyphName, ".notdef") != 0;
@@ -1679,7 +1683,7 @@ PDFRealizedFontPointer PDFRealizedFont::createRealizedFont(PDFFontPointer font,
Q_ASSERT(!impl->m_embeddedFontData.isEmpty());
PDFRealizedFontImpl::checkFreeTypeError(FT_New_Memory_Face(impl->m_library, reinterpret_cast(impl->m_embeddedFontData.constData()), impl->m_embeddedFontData.size(), 0, &impl->m_face));
- FT_Select_Charmap(impl->m_face, FT_ENCODING_UNICODE); // We try to select unicode encoding, but if it fails, we don't do anything (use glyph indices instead)
+ FT_Select_Charmap(impl->m_face, FT_ENCODING_UNICODE); // We try to select unicode encoding, but if it fails, we don't do anything (use glyph indices instead)
PDFRealizedFontImpl::checkFreeTypeError(FT_Set_Pixel_Sizes(impl->m_face, 0, qRound(pixelSize * PDFRealizedFontImpl::PIXEL_SIZE_MULTIPLIER)));
impl->m_isVertical = cmap ? cmap->isVertical() : false;
impl->m_isEmbedded = true;
@@ -1709,7 +1713,7 @@ PDFRealizedFontPointer PDFRealizedFont::createRealizedFont(PDFFontPointer font,
throw PDFException(PDFTranslationContext::tr("System font '%1' is too large to be loaded by FreeType.").arg(QString::fromLatin1(descriptor->fontName)));
}
PDFRealizedFontImpl::checkFreeTypeError(FT_New_Memory_Face(impl->m_library, reinterpret_cast(impl->m_systemFontData.constData()), static_cast(impl->m_systemFontData.size()), impl->m_systemFontFaceIndex, &impl->m_face));
- FT_Select_Charmap(impl->m_face, FT_ENCODING_UNICODE); // We try to select unicode encoding, but if it fails, we don't do anything (use glyph indices instead)
+ FT_Select_Charmap(impl->m_face, FT_ENCODING_UNICODE); // We try to select unicode encoding, but if it fails, we don't do anything (use glyph indices instead)
PDFRealizedFontImpl::checkFreeTypeError(FT_Set_Pixel_Sizes(impl->m_face, 0, qRound(pixelSize * PDFRealizedFontImpl::PIXEL_SIZE_MULTIPLIER)));
impl->m_isVertical = cmap ? cmap->isVertical() : false;
impl->m_isEmbedded = false;
@@ -1851,7 +1855,7 @@ PDFFontPointer PDFFont::createFont(const PDFObject& object, QByteArray fontId, c
std::pair{ "Type0", FontType::Type0 },
std::pair{ "Type1", FontType::Type1 },
std::pair{ "TrueType", FontType::TrueType },
- std::pair{ "Type3", FontType::Type3},
+ std::pair{ "Type3", FontType::Type3 },
std::pair{ "MMType1", FontType::MMType1 }
};
@@ -1921,11 +1925,11 @@ PDFFontPointer PDFFont::createFont(const PDFObject& object, QByteArray fontId, c
// After the encoding is obtained, try to extract glyph indices for embedded font.
PDFEncoding::Encoding encoding = PDFEncoding::Encoding::Invalid;
- encoding::EncodingTable simpleFontEncodingTable = { };
- encoding::EncodingTable simpleFontToUnicodeTable = { };
+ encoding::EncodingTable simpleFontEncodingTable = {};
+ encoding::EncodingTable simpleFontToUnicodeTable = {};
bool hasToUnicode = false;
- GlyphIndices glyphIndexArray = { };
- GlyphNames glyphNameArray = { };
+ GlyphIndices glyphIndexArray = {};
+ GlyphNames glyphNameArray = {};
switch (fontType)
{
case FontType::Type1:
@@ -1933,8 +1937,8 @@ PDFFontPointer PDFFont::createFont(const PDFObject& object, QByteArray fontId, c
case FontType::TrueType:
{
bool hasDifferences = false;
- encoding::EncodingTable differences = { };
- GlyphNames differenceGlyphNames = { };
+ encoding::EncodingTable differences = {};
+ GlyphNames differenceGlyphNames = {};
bool useEmbeddedBuiltInEncoding = false;
if (fontDictionary->hasKey("Encoding"))
@@ -2108,8 +2112,8 @@ PDFFontPointer PDFFont::createFont(const PDFObject& object, QByteArray fontId, c
for (size_t i = 0; i < standardEncoding.size(); ++i)
{
if (differenceGlyphNames[i].isEmpty() &&
- (simpleFontEncodingTable[i].isNull() || simpleFontEncodingTable[i] == QChar(QChar::SpecialCharacter::ReplacementCharacter)) &&
- (!standardEncoding[i].isNull() && standardEncoding[i] != QChar(QChar::SpecialCharacter::ReplacementCharacter)))
+ (simpleFontEncodingTable[i].isNull() || simpleFontEncodingTable[i] == QChar(QChar::SpecialCharacter::ReplacementCharacter)) &&
+ (!standardEncoding[i].isNull() && standardEncoding[i] != QChar(QChar::SpecialCharacter::ReplacementCharacter)))
{
simpleFontEncodingTable[i] = standardEncoding[i];
}
@@ -2163,7 +2167,7 @@ PDFFontPointer PDFFont::createFont(const PDFObject& object, QByteArray fontId, c
glyphIndexArray[iTable] = glyphIndex;
// Set mapping to unicode
- char buffer[128] = { };
+ char buffer[128] = {};
if (!FT_Get_Glyph_Name(face, glyphIndex, buffer, static_cast(std::size(buffer))))
{
QByteArray byteArrayBuffer(buffer);
@@ -2362,7 +2366,7 @@ PDFFontPointer PDFFont::createFont(const PDFObject& object, QByteArray fontId, c
// Read default advance
PDFReal dw = fontLoader.readNumberFromDictionary(descendantFontDictionary, "DW", 1000.0);
- std::array dw2 = { };
+ std::array dw2 = {};
fontLoader.readNumberArrayFromDictionary(descendantFontDictionary, "DW2", dw2.begin(), dw2.end());
PDFReal defaultWidth = descendantFontDictionary->hasKey("DW") ? dw : dw2.back();
@@ -2370,51 +2374,51 @@ PDFFontPointer PDFFont::createFont(const PDFObject& object, QByteArray fontId, c
std::unordered_map advances;
if (descendantFontDictionary->hasKey("W"))
{
- const PDFObject& wArrayObject = document->getObject(descendantFontDictionary->get("W"));
- if (wArrayObject.isArray())
- {
- const PDFArray* wArray = wArrayObject.getArray();
- const size_t size = wArray->getCount();
-
- static constexpr CID MAX_W_ARRAY_RANGE = 1'000'000;
- for (size_t i = 0; i < size;)
- {
- CID startCID = fontLoader.readInteger(wArray->getItem(i++), 0);
- if (i >= size)
- {
- break;
- }
- const PDFObject& arrayOrCID = document->getObject(wArray->getItem(i++));
+ const PDFObject& wArrayObject = document->getObject(descendantFontDictionary->get("W"));
+ if (wArrayObject.isArray())
+ {
+ const PDFArray* wArray = wArrayObject.getArray();
+ const size_t size = wArray->getCount();
- if (arrayOrCID.isInt())
- {
- if (i >= size)
- {
- break;
- }
- CID endCID = arrayOrCID.getInteger();
- if (endCID < startCID || static_cast(endCID - startCID) > MAX_W_ARRAY_RANGE)
- {
- continue;
- }
- PDFReal width = fontLoader.readInteger(wArray->getItem(i++), 0);
- for (CID currentCID = startCID; currentCID <= endCID; ++currentCID)
- {
- advances[currentCID] = width;
- }
- }
- else if (arrayOrCID.isArray())
- {
- const PDFArray* widthArray = arrayOrCID.getArray();
- const size_t widthArraySize = widthArray->getCount();
- for (size_t widthArrayIndex = 0; widthArrayIndex < widthArraySize; ++widthArrayIndex)
- {
- PDFReal width = fontLoader.readNumber(widthArray->getItem(widthArrayIndex), 0);
- advances[startCID + static_cast(widthArrayIndex)] = width;
- }
- }
- }
- }
+ static constexpr CID MAX_W_ARRAY_RANGE = 1'000'000;
+ for (size_t i = 0; i < size;)
+ {
+ CID startCID = fontLoader.readInteger(wArray->getItem(i++), 0);
+ if (i >= size)
+ {
+ break;
+ }
+ const PDFObject& arrayOrCID = document->getObject(wArray->getItem(i++));
+
+ if (arrayOrCID.isInt())
+ {
+ if (i >= size)
+ {
+ break;
+ }
+ CID endCID = arrayOrCID.getInteger();
+ if (endCID < startCID || static_cast(endCID - startCID) > MAX_W_ARRAY_RANGE)
+ {
+ continue;
+ }
+ PDFReal width = fontLoader.readInteger(wArray->getItem(i++), 0);
+ for (CID currentCID = startCID; currentCID <= endCID; ++currentCID)
+ {
+ advances[currentCID] = width;
+ }
+ }
+ else if (arrayOrCID.isArray())
+ {
+ const PDFArray* widthArray = arrayOrCID.getArray();
+ const size_t widthArraySize = widthArray->getCount();
+ for (size_t widthArrayIndex = 0; widthArrayIndex < widthArraySize; ++widthArrayIndex)
+ {
+ PDFReal width = fontLoader.readNumber(widthArray->getItem(widthArrayIndex), 0);
+ advances[startCID + static_cast(widthArrayIndex)] = width;
+ }
+ }
+ }
+ }
}
PDFFontCMap toUnicodeCMap;
@@ -2583,7 +2587,6 @@ PDFSimpleFont::PDFSimpleFont(CIDSystemInfo cidSystemInfo,
m_glyphNames(qMove(glyphNames)),
m_standardFontType(standardFontType)
{
-
}
QChar PDFSimpleFont::getUnicode(CID cid) const
@@ -2658,39 +2661,39 @@ void PDFSimpleFont::dumpFontToTreeItem(ITreeFactory* treeFactory) const
QString encodingTypeString;
switch (m_encodingType)
{
- case PDFEncoding::Encoding::Standard:
+ case PDFEncoding::Encoding::Standard:
encodingTypeString = PDFTranslationContext::tr("Standard");
break;
- case PDFEncoding::Encoding::MacRoman:
+ case PDFEncoding::Encoding::MacRoman:
encodingTypeString = PDFTranslationContext::tr("Mac Roman");
break;
- case PDFEncoding::Encoding::WinAnsi:
+ case PDFEncoding::Encoding::WinAnsi:
encodingTypeString = PDFTranslationContext::tr("Win Ansi");
break;
- case PDFEncoding::Encoding::PDFDoc:
+ case PDFEncoding::Encoding::PDFDoc:
encodingTypeString = PDFTranslationContext::tr("PDF Doc");
break;
- case PDFEncoding::Encoding::MacExpert:
+ case PDFEncoding::Encoding::MacExpert:
encodingTypeString = PDFTranslationContext::tr("Mac Expert");
break;
- case PDFEncoding::Encoding::Symbol:
+ case PDFEncoding::Encoding::Symbol:
encodingTypeString = PDFTranslationContext::tr("Symbol");
break;
- case PDFEncoding::Encoding::ZapfDingbats:
+ case PDFEncoding::Encoding::ZapfDingbats:
encodingTypeString = PDFTranslationContext::tr("Zapf Dingbats");
break;
- case PDFEncoding::Encoding::MacOsRoman:
+ case PDFEncoding::Encoding::MacOsRoman:
encodingTypeString = PDFTranslationContext::tr("Mac OS Roman");
break;
- case PDFEncoding::Encoding::Custom:
+ case PDFEncoding::Encoding::Custom:
encodingTypeString = PDFTranslationContext::tr("Custom");
break;
@@ -2723,7 +2726,6 @@ PDFType1Font::PDFType1Font(FontType fontType,
PDFSimpleFont(qMove(cidSystemInfo), qMove(fontId), qMove(fontDescriptor), qMove(name), qMove(baseFont), firstChar, lastChar, qMove(widths), encodingType, encoding, toUnicode, hasToUnicode, standardFontType, glyphIndices, qMove(glyphNames)),
m_fontType(fontType)
{
-
}
FontType PDFType1Font::getFontType() const
@@ -2863,15 +2865,15 @@ const PDFFontCache::TextDrawingFontInfo* PDFFontCache::getFontForTextDrawing(con
substituteFont.setWeight(QFont::Weight(qBound(1, int(descriptor->fontWeight), 1000)));
substituteFont.setStretch(descriptor->fontStretch);
substituteFont.setItalic(descriptor->isItalic());
- substituteFont.setStyleHint(descriptor->isFixedPitch() ? QFont::Monospace :
- descriptor->isSerif() ? QFont::Serif : QFont::SansSerif);
+ substituteFont.setStyleHint(descriptor->isFixedPitch() ? QFont::Monospace : descriptor->isSerif() ? QFont::Serif
+ : QFont::SansSerif);
info.font = substituteFont;
info.isUsable = true;
if (reporter)
{
reporter->reportRenderErrorOnce(RenderErrorType::Warning,
- PDFTranslationContext::tr("Font '%1' is not embedded, using substitute font for real text drawing.").arg(QString::fromLatin1(descriptor->fontName)));
+ PDFTranslationContext::tr("Font '%1' is not embedded, using substitute font for real text drawing.").arg(QString::fromLatin1(descriptor->fontName)));
}
}
}
@@ -3018,7 +3020,7 @@ PDFFontCMap PDFFontCMap::createFromName(const QByteArray& name)
PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
{
Entries entries;
- entries.reserve(1024); // Arbitrary number, we have enough memory, better than perform reallocation each time
+ entries.reserve(1024); // Arbitrary number, we have enough memory, better than perform reallocation each time
std::vector additionalMappings;
PDFLexicalAnalyzer parser(data.constBegin(), data.constEnd());
@@ -3036,7 +3038,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
continue;
}
- auto fetchCode = [] (const PDFLexicalAnalyzer::Token& currentToken) -> std::pair
+ auto fetchCode = [](const PDFLexicalAnalyzer::Token& currentToken) -> std::pair
{
if (currentToken.type == PDFLexicalAnalyzer::TokenType::String)
{
@@ -3054,7 +3056,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
throw PDFException(PDFTranslationContext::tr("Can't fetch code from CMap definition."));
};
- auto fetchCID = [] (const PDFLexicalAnalyzer::Token& currentToken) -> CID
+ auto fetchCID = [](const PDFLexicalAnalyzer::Token& currentToken) -> CID
{
if (currentToken.type == PDFLexicalAnalyzer::TokenType::Integer)
{
@@ -3084,6 +3086,21 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
return 0;
};
+ // A truncated CMap is malformed input, not an empty one. PDFLexicalAnalyzer
+ // returns EndOfFile for every fetch past the end of the buffer, so a range
+ // operator whose terminator is missing would spin here forever and grow
+ // `entries` until the process is killed (hostile /ToUnicode stream). Every
+ // fetch inside a range operator therefore fails closed on EndOfFile.
+ auto fetchRequired = [&parser](const char* operatorName)
+ {
+ PDFLexicalAnalyzer::Token fetchedToken = parser.fetch();
+ if (fetchedToken.type == PDFLexicalAnalyzer::TokenType::EndOfFile)
+ {
+ throw PDFException(PDFTranslationContext::tr("CMap operator '%1' is not terminated.").arg(QString::fromLatin1(operatorName)));
+ }
+ return fetchedToken;
+ };
+
if (token.type == PDFLexicalAnalyzer::TokenType::Command)
{
QByteArray command = token.data.toByteArray();
@@ -3102,16 +3119,16 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
{
while (true)
{
- PDFLexicalAnalyzer::Token token1 = parser.fetch();
+ PDFLexicalAnalyzer::Token token1 = fetchRequired("beginbfrange");
if (token1.type == PDFLexicalAnalyzer::TokenType::Command &&
- token1.data.toByteArray() == "endbfrange")
+ token1.data.toByteArray() == "endbfrange")
{
break;
}
- PDFLexicalAnalyzer::Token token2 = parser.fetch();
- PDFLexicalAnalyzer::Token token3 = parser.fetch();
+ PDFLexicalAnalyzer::Token token2 = fetchRequired("beginbfrange");
+ PDFLexicalAnalyzer::Token token3 = fetchRequired("beginbfrange");
std::pair from = fetchCode(token1);
std::pair to = fetchCode(token2);
@@ -3122,7 +3139,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
while (true)
{
- PDFLexicalAnalyzer::Token arrayToken = parser.fetch();
+ PDFLexicalAnalyzer::Token arrayToken = fetchRequired("beginbfrange");
// Do we have end of array?
if (arrayToken.type == PDFLexicalAnalyzer::TokenType::ArrayEnd)
@@ -3146,7 +3163,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
{
while (true)
{
- PDFLexicalAnalyzer::Token token1 = parser.fetch();
+ PDFLexicalAnalyzer::Token token1 = fetchRequired("begincidrange");
if (token1.type == PDFLexicalAnalyzer::TokenType::Command &&
token1.data.toByteArray() == "endcidrange")
@@ -3154,8 +3171,8 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
break;
}
- PDFLexicalAnalyzer::Token token2 = parser.fetch();
- PDFLexicalAnalyzer::Token token3 = parser.fetch();
+ PDFLexicalAnalyzer::Token token2 = fetchRequired("begincidrange");
+ PDFLexicalAnalyzer::Token token3 = fetchRequired("begincidrange");
std::pair from = fetchCode(token1);
std::pair to = fetchCode(token2);
@@ -3168,7 +3185,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
{
while (true)
{
- PDFLexicalAnalyzer::Token token1 = parser.fetch();
+ PDFLexicalAnalyzer::Token token1 = fetchRequired("begincidchar");
if (token1.type == PDFLexicalAnalyzer::TokenType::Command &&
token1.data.toByteArray() == "endcidchar")
@@ -3176,7 +3193,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
break;
}
- PDFLexicalAnalyzer::Token token2 = parser.fetch();
+ PDFLexicalAnalyzer::Token token2 = fetchRequired("begincidchar");
std::pair code = fetchCode(token1);
CID cid = fetchCID(token2);
@@ -3188,7 +3205,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
{
while (true)
{
- PDFLexicalAnalyzer::Token token1 = parser.fetch();
+ PDFLexicalAnalyzer::Token token1 = fetchRequired("beginbfchar");
if (token1.type == PDFLexicalAnalyzer::TokenType::Command &&
token1.data.toByteArray() == "endbfchar")
@@ -3196,7 +3213,7 @@ PDFFontCMap PDFFontCMap::createFromData(const QByteArray& data)
break;
}
- PDFLexicalAnalyzer::Token token2 = parser.fetch();
+ PDFLexicalAnalyzer::Token token2 = fetchRequired("beginbfchar");
std::pair code = fetchCode(token1);
CID cid = fetchUnicode(token2);
@@ -3343,7 +3360,8 @@ std::vector PDFFontCMap::interpretWithCode(const QByteA
++scannedBytes;
// Find suitable mapping
- auto it = std::find_if(m_entries.cbegin(), m_entries.cend(), [value, scannedBytes](const Entry& entry) { return entry.from <= value && entry.to >= value && entry.byteCount == scannedBytes; });
+ auto it = std::find_if(m_entries.cbegin(), m_entries.cend(), [value, scannedBytes](const Entry& entry)
+ { return entry.from <= value && entry.to >= value && entry.byteCount == scannedBytes; });
if (it != m_entries.cend())
{
const Entry& entry = *it;
@@ -3404,10 +3422,10 @@ QChar PDFFontCMap::getToUnicode(CID cid, unsigned int byteCount) const
{
if (isValid())
{
- auto it = std::find_if(m_entries.cbegin(), m_entries.cend(), [cid, byteCount](const Entry& entry) {
+ auto it = std::find_if(m_entries.cbegin(), m_entries.cend(), [cid, byteCount](const Entry& entry)
+ {
const bool byteCountMatches = byteCount == 0 || entry.byteCount == byteCount;
- return byteCountMatches && entry.from <= cid && entry.to >= cid;
- });
+ return byteCountMatches && entry.from <= cid && entry.to >= cid; });
if (it != m_entries.cend())
{
const Entry& entry = *it;
@@ -3469,7 +3487,8 @@ void PDFFontCMap::enumerate(const std::function= code && entry.byteCount == byteCount; });
+ return std::any_of(m_entries.cbegin(), m_entries.cend(), [code, byteCount](const Entry& entry)
+ { return entry.from <= code && entry.to >= code && entry.byteCount == byteCount; });
}
PDFFontCMap::PDFFontCMap(Entries&& entries, bool vertical, bool unicodeEncoded) :
@@ -3478,7 +3497,8 @@ PDFFontCMap::PDFFontCMap(Entries&& entries, bool vertical, bool unicodeEncoded)
m_vertical(vertical),
m_unicodeEncoded(unicodeEncoded)
{
- m_maxKeyLength = std::accumulate(m_entries.cbegin(), m_entries.cend(), 0, [](unsigned int a, const Entry& b) { return qMax(a, b.byteCount); });
+ m_maxKeyLength = std::accumulate(m_entries.cbegin(), m_entries.cend(), 0, [](unsigned int a, const Entry& b)
+ { return qMax(a, b.byteCount); });
}
PDFFontCMap::Entries PDFFontCMap::optimize(const PDFFontCMap::Entries& entries)
@@ -3565,7 +3585,6 @@ bool PDFFontCMapRepository::loadFromFile(const QString& fileName)
PDFFontCMapRepository::PDFFontCMapRepository()
{
-
}
PDFReal PDFType0Font::getGlyphAdvance(CID cid) const
@@ -3611,7 +3630,7 @@ void PDFType0Font::buildEncodeMap() const
// the ToUnicode character is still produced by the forward pass.
const unsigned int maxKeyLength = m_cmap.getMaxKeyLength();
m_toUnicode.enumerate([&, this](unsigned int code, unsigned int byteCount, CID unicodeValue)
- {
+ {
if (unicodeValue == 0)
{
return;
@@ -3635,8 +3654,7 @@ void PDFType0Font::buildEncodeMap() const
return;
}
- m_encodeMap.emplace(codePoint, serializeCode(code, byteCount));
- });
+ m_encodeMap.emplace(codePoint, serializeCode(code, byteCount)); });
// Pass 2: for unicode encoded predefined CMaps of non-embedded fonts, the forward
// pass falls back to interpreting the character code directly as unicode, but only
@@ -3644,7 +3662,7 @@ void PDFType0Font::buildEncodeMap() const
if (!m_fontDescriptor.isEmbedded() && m_cmap.isUnicodeEncoded())
{
m_cmap.enumerate([&, this](unsigned int code, unsigned int byteCount, CID)
- {
+ {
if (code == 0 || code > 0xFFFF)
{
return;
@@ -3661,8 +3679,7 @@ void PDFType0Font::buildEncodeMap() const
return;
}
- m_encodeMap.emplace(codePoint, serializeCode(code, byteCount));
- });
+ m_encodeMap.emplace(codePoint, serializeCode(code, byteCount)); });
}
}
@@ -3684,7 +3701,6 @@ PDFType3Font::PDFType3Font(FontDescriptor fontDescriptor,
m_resources(resources),
m_toUnicode(qMove(toUnicode))
{
-
}
FontType PDFType3Font::getFontType() const
diff --git a/LoopLibCore/sources/pdffunction.cpp b/LoopLibCore/sources/pdffunction.cpp
index 0317c24a0..030414fa8 100644
--- a/LoopLibCore/sources/pdffunction.cpp
+++ b/LoopLibCore/sources/pdffunction.cpp
@@ -39,13 +39,19 @@
namespace pdf
{
+// PDFSampledFunction materialises a 2^m hypercube offset table in its constructor
+// and a 2^m sample buffer on every apply() call. The specification permits up to 32
+// input variables, but 2^32 four-byte offsets is a denial of service, and a real
+// sampled function uses a handful of dimensions at most. This ceiling also makes the
+// dimension count safe to shift and to index m_size with.
+constexpr uint32_t SAMPLED_FUNCTION_MAXIMUM_DIMENSIONS = 20;
+
PDFFunction::PDFFunction(uint32_t m, uint32_t n, std::vector&& domain, std::vector&& range) :
m_m(m),
m_n(n),
m_domain(std::move(domain)),
m_range(std::move(range))
{
-
}
PDFFunctionPtr PDFFunction::createFunction(const PDFDocument* document, const PDFObject& object)
@@ -108,7 +114,8 @@ PDFFunctionPtr PDFFunction::createFunctionImpl(const PDFDocument* document, cons
std::vector encode = loader.readNumberArrayFromDictionary(dictionary, "Encode");
std::vector decode = loader.readNumberArrayFromDictionary(dictionary, "Decode");
- if (size.empty() || !std::all_of(size.cbegin(), size.cend(), [](PDFInteger size) { return size >= 1; }))
+ if (size.empty() || !std::all_of(size.cbegin(), size.cend(), [](PDFInteger size)
+ { return size >= 1; }))
{
throw PDFException(PDFParsingContext::tr("Sampled function has invalid sample size."));
}
@@ -118,6 +125,11 @@ PDFFunctionPtr PDFFunction::createFunctionImpl(const PDFDocument* document, cons
throw PDFException(PDFParsingContext::tr("Sampled function has invalid count of bits per sample."));
}
+ if (size.size() > SAMPLED_FUNCTION_MAXIMUM_DIMENSIONS)
+ {
+ throw PDFException(PDFParsingContext::tr("Sampled function has invalid sample size."));
+ }
+
if (encode.empty())
{
// Construct default array according to the PDF 1.7 specification
@@ -142,7 +154,12 @@ PDFFunctionPtr PDFFunction::createFunctionImpl(const PDFDocument* document, cons
throw PDFException(PDFParsingContext::tr("Sampled function hasn't any output."));
}
- if (domain.size() != encode.size())
+ // The PDF 1.7 specification defines Encode as 2 x m numbers and Decode
+ // as 2 x n numbers, and PDFSampledFunction indexes m_domain/m_encoder as
+ // 2 x m and m_range/m_decoder as 2 x n. Comparing Domain.size() with
+ // Encode.size() alone accepted arrays shorter than 2 x m, which the
+ // constructor only rejects with Q_ASSERT - i.e. not in a release build.
+ if (domain.size() != 2 * m || encode.size() != 2 * m)
{
throw PDFException(PDFParsingContext::tr("Sampled function has invalid encode array."));
}
@@ -168,6 +185,20 @@ PDFFunctionPtr PDFFunction::createFunctionImpl(const PDFDocument* document, cons
{
throw PDFException(PDFParsingContext::tr("Sampled function has invalid sample size."));
}
+
+ // The stream is the only source of samples, and each one costs
+ // `bitsPerSample` bits of it, so a declared count larger than the
+ // stream can carry must fail here - before resize() reserves memory
+ // proportional to a hostile number. This accepts exactly the documents
+ // the read loop below would have accepted (it throws "Not enough
+ // samples" at the same bit count); it only fails earlier.
+ const uint64_t availableSampleBits = static_cast(streamData.size()) * 8;
+ const uint64_t requiredSampleBits = static_cast(sampleCount) * static_cast(bitsPerSample);
+ if (requiredSampleBits > availableSampleBits)
+ {
+ throw PDFException(PDFParsingContext::tr("Sampled function declares more samples than its stream contains (%1 samples need %2 bits, the stream has %3).").arg(sampleCount).arg(requiredSampleBits).arg(availableSampleBits));
+ }
+
std::vector samples;
samples.resize(sampleCount, 0.0);
@@ -201,12 +232,8 @@ PDFFunctionPtr PDFFunction::createFunctionImpl(const PDFDocument* document, cons
}
std::vector sizeAsUint;
- std::transform(size.cbegin(), size.cend(), std::back_inserter(sizeAsUint), [](PDFInteger integer) { return static_cast(integer); });
-
- if (m > 30)
- {
- throw PDFException(PDFParsingContext::tr("Sampled function has invalid sample size."));
- }
+ std::transform(size.cbegin(), size.cend(), std::back_inserter(sizeAsUint), [](PDFInteger integer)
+ { return static_cast(integer); });
return std::make_shared(static_cast(m), static_cast(n), std::move(domain), std::move(range), std::move(sizeAsUint), std::move(samples), std::move(encode), std::move(decode), sampleMaxValue, loader.readIntegerFromDictionary(dictionary, "Order", 1));
}
@@ -599,7 +626,6 @@ PDFStitchingFunction::PDFStitchingFunction(uint32_t m, uint32_t n,
PDFStitchingFunction::~PDFStitchingFunction()
{
-
}
PDFFunction::FunctionResult PDFStitchingFunction::apply(const_iterator x_1,
@@ -624,7 +650,8 @@ PDFFunction::FunctionResult PDFStitchingFunction::apply(const_iterator x_1,
// First search for partial function, which defines our range. Use algorithm
// similar to the std::lower_bound.
- auto it = std::lower_bound(m_partialFunctions.cbegin(), m_partialFunctions.cend(), x, [](const auto& partialFunction, PDFReal value) { return partialFunction.bound1 < value; });
+ auto it = std::lower_bound(m_partialFunctions.cbegin(), m_partialFunctions.cend(), x, [](const auto& partialFunction, PDFReal value)
+ { return partialFunction.bound1 < value; });
if (it == m_partialFunctions.cend())
{
--it;
@@ -650,7 +677,6 @@ PDFFunction::FunctionResult PDFStitchingFunction::apply(const_iterator x_1,
PDFIdentityFunction::PDFIdentityFunction() :
PDFFunction(0, 0, std::vector(), std::vector())
{
-
}
PDFFunction::FunctionResult PDFIdentityFunction::apply(const_iterator x_1,
@@ -678,10 +704,26 @@ class PDFPostScriptFunctionStack
using OperandObject = PDFPostScriptFunction::OperandObject;
using InstructionPointer = PDFPostScriptFunction::InstructionPointer;
- inline void pushReal(PDFReal value) { m_stack.push_back(OperandObject::createReal(value)); checkOverflow(); }
- inline void pushInteger(PDFInteger value) { m_stack.push_back(OperandObject::createInteger(value)); checkOverflow(); }
- inline void pushBoolean(bool value) { m_stack.push_back(OperandObject::createBoolean(value)); checkOverflow(); }
- inline void pushInstructionPointer(InstructionPointer value) { m_stack.push_back(OperandObject::createInstructionPointer(value)); checkOverflow(); }
+ inline void pushReal(PDFReal value)
+ {
+ m_stack.push_back(OperandObject::createReal(value));
+ checkOverflow();
+ }
+ inline void pushInteger(PDFInteger value)
+ {
+ m_stack.push_back(OperandObject::createInteger(value));
+ checkOverflow();
+ }
+ inline void pushBoolean(bool value)
+ {
+ m_stack.push_back(OperandObject::createBoolean(value));
+ checkOverflow();
+ }
+ inline void pushInstructionPointer(InstructionPointer value)
+ {
+ m_stack.push_back(OperandObject::createInstructionPointer(value));
+ checkOverflow();
+ }
/// Returns true, if integer operation should be performed instead of operation with real values.
/// (two top elements are integer).
@@ -712,13 +754,25 @@ class PDFPostScriptFunctionStack
PDFReal popNumber();
/// Returns true, if current value is real
- bool isReal() const { checkUnderflow(); return m_stack.back().type == PDFPostScriptFunction::OperandType::Real; }
+ bool isReal() const
+ {
+ checkUnderflow();
+ return m_stack.back().type == PDFPostScriptFunction::OperandType::Real;
+ }
/// Returns true, if current value is integer
- bool isInteger() const { checkUnderflow(); return m_stack.back().type == PDFPostScriptFunction::OperandType::Integer; }
+ bool isInteger() const
+ {
+ checkUnderflow();
+ return m_stack.back().type == PDFPostScriptFunction::OperandType::Integer;
+ }
/// Pops the current value
- inline void pop() { checkUnderflow(); m_stack.pop_back(); }
+ inline void pop()
+ {
+ checkUnderflow();
+ m_stack.pop_back();
+ }
/// Exchange the two top elements
void exch();
@@ -740,7 +794,11 @@ class PDFPostScriptFunctionStack
void roll(PDFInteger n, PDFInteger j);
/// Pushes the operand onto the stack
- void push(const OperandObject& operand) { m_stack.push_back(operand); checkOverflow(); }
+ void push(const OperandObject& operand)
+ {
+ m_stack.push_back(operand);
+ checkOverflow();
+ }
/// Returns true, if stack is empty
bool empty() const { return m_stack.empty(); }
@@ -774,14 +832,13 @@ class PDFPostScriptFunctionExecutor
m_program(program),
m_stack(stack)
{
-
}
/// Executes the postscript program
void execute();
private:
- template typename Comparator>
+ template typename Comparator>
void executeRelationOperator()
{
if (m_stack.isBinaryOperationInteger())
@@ -808,7 +865,7 @@ void PDFPostScriptFunctionExecutor::execute()
std::stack callStack;
- InstructionPointer ip = 0; // First instruction is at zero
+ InstructionPointer ip = 0; // First instruction is at zero
while (ip != PDFPostScriptFunction::INVALID_INSTRUCTION_POINTER)
{
if (ip >= m_program.size())
@@ -1401,7 +1458,7 @@ bool PDFPostScriptFunctionStack::isBinaryOperationInteger() const
const size_t size = m_stack.size();
return m_stack[size - 1].type == PDFPostScriptFunction::OperandType::Integer &&
- m_stack[size - 2].type == PDFPostScriptFunction::OperandType::Integer;
+ m_stack[size - 2].type == PDFPostScriptFunction::OperandType::Integer;
}
bool PDFPostScriptFunctionStack::isBinaryOperationBoolean() const
@@ -1410,7 +1467,7 @@ bool PDFPostScriptFunctionStack::isBinaryOperationBoolean() const
const size_t size = m_stack.size();
return m_stack[size - 1].type == PDFPostScriptFunction::OperandType::Boolean &&
- m_stack[size - 2].type == PDFPostScriptFunction::OperandType::Boolean;
+ m_stack[size - 2].type == PDFPostScriptFunction::OperandType::Boolean;
}
PDFReal PDFPostScriptFunctionStack::popReal()
@@ -1603,60 +1660,59 @@ void PDFPostScriptFunctionStack::checkUnderflow(size_t n) const
PDFPostScriptFunction::Code PDFPostScriptFunction::getCode(const QByteArray& byteArray)
{
- static constexpr const std::pair codes[] =
- {
+ static constexpr const std::pair codes[] = {
// B.1 Arithmetic operators
- std::pair{ Code::Add, "add" },
- std::pair{ Code::Sub, "sub" },
- std::pair{ Code::Mul, "mul" },
- std::pair{ Code::Div, "div" },
- std::pair{ Code::Idiv, "idiv" },
- std::pair{ Code::Mod, "mod" },
- std::pair{ Code::Neg, "neg" },
- std::pair{ Code::Abs, "abs" },
- std::pair{ Code::Ceiling, "ceiling" },
- std::pair{ Code::Floor, "floor" },
- std::pair{ Code::Round, "round" },
- std::pair{ Code::Truncate, "truncate" },
- std::pair{ Code::Sqrt, "sqrt" },
- std::pair{ Code::Sin, "sin" },
- std::pair{ Code::Cos, "cos" },
- std::pair{ Code::Atan, "atan" },
- std::pair{ Code::Exp, "exp" },
- std::pair{ Code::Ln, "ln" },
- std::pair{ Code::Log, "log" },
- std::pair{ Code::Cvi, "cvi" },
- std::pair{ Code::Cvr, "cvr" },
+ std::pair{ Code::Add, "add" },
+ std::pair{ Code::Sub, "sub" },
+ std::pair{ Code::Mul, "mul" },
+ std::pair{ Code::Div, "div" },
+ std::pair{ Code::Idiv, "idiv" },
+ std::pair{ Code::Mod, "mod" },
+ std::pair{ Code::Neg, "neg" },
+ std::pair{ Code::Abs, "abs" },
+ std::pair{ Code::Ceiling, "ceiling" },
+ std::pair{ Code::Floor, "floor" },
+ std::pair{ Code::Round, "round" },
+ std::pair{ Code::Truncate, "truncate" },
+ std::pair{ Code::Sqrt, "sqrt" },
+ std::pair{ Code::Sin, "sin" },
+ std::pair{ Code::Cos, "cos" },
+ std::pair{ Code::Atan, "atan" },
+ std::pair{ Code::Exp, "exp" },
+ std::pair{ Code::Ln, "ln" },
+ std::pair{ Code::Log, "log" },
+ std::pair{ Code::Cvi, "cvi" },
+ std::pair{ Code::Cvr, "cvr" },
// B.2 Relational, Boolean and Bitwise operators
- std::pair{ Code::Eq, "eq" },
- std::pair{ Code::Ne, "ne" },
- std::pair{ Code::Gt, "gt" },
- std::pair{ Code::Ge, "ge" },
- std::pair{ Code::Lt, "lt" },
- std::pair{ Code::Le, "le" },
- std::pair{ Code::And, "and" },
- std::pair{ Code::Or, "or" },
- std::pair{ Code::Xor, "xor" },
- std::pair{ Code::Not, "not" },
- std::pair{ Code::Bitshift, "bitshift" },
- std::pair{ Code::True, "true" },
- std::pair{ Code::False, "false" },
+ std::pair{ Code::Eq, "eq" },
+ std::pair{ Code::Ne, "ne" },
+ std::pair{ Code::Gt, "gt" },
+ std::pair{ Code::Ge, "ge" },
+ std::pair{ Code::Lt, "lt" },
+ std::pair{ Code::Le, "le" },
+ std::pair{ Code::And, "and" },
+ std::pair{ Code::Or, "or" },
+ std::pair