Skip to content

[0.2.1] RC | Preflight & GUI Foundation - #554

Draft
mberrys wants to merge 48 commits into
unstablefrom
dev
Draft

[0.2.1] RC | Preflight & GUI Foundation #554
mberrys wants to merge 48 commits into
unstablefrom
dev

Conversation

@mberrys

@mberrys mberrys commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

No description provided.

claude and others added 30 commits September 5, 2026 20:41
Adds pdfquick::tokens (LoopLibQuick/sources/looptokens.h/.cpp): semantic
spacing and colour-role tokens with dark/light/high-contrast values, each
foreground/background pair WCAG contrast-checked. Adds resolveStateVisual()
(loopstatevisual.h/.cpp), the canonical finding/check presentation mapping
issue #194 asks for, with a table-driven test (UnitTestsLoopStateVisual)
asserting an incomplete check and an actively-waived finding never resolve
to the passed treatment. Documents both in docs/LOOP_DESIGN_SYSTEM.md,
including why this uses the repo's current Loop/Quick naming rather than
the issue's stale pre-rebrand Pdf4QtLibGui/Loupe paths, and what remains
open (component implementations land with their consuming surfaces #193,

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
scripts/ci/check_loop_identity.py fails CI on any tracked text file
containing the pre-rebrand product name outside its historical-evidence
allowlist. The "Naming note" section quoted that name three times while
explaining why this doc uses current naming instead of issue #194's stale
paths; reword it to make the same point without the literal token.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
Adding UnitTestsLoopStateVisual to UnitTests/CMakeLists.txt bumped the
project-wide CMake target count the Phase 5 Widgets inventory tracks (its
generator enumerates every target to prove widgets-linkage status, not just
widgets-related ones). Regenerate docs/generated/phase5-widgets-inventory.json
via scripts/generate_phase5_widgets_evidence.py --write (new target's
widgets_linkage is "none", as expected) and update the test's hardcoded
target count (70 -> 71) to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
agent-fast's clang-tidy step runs on every changed C++ file regardless of
module classification, but the cmake --build step that generates each
test's AUTOMOC .moc file only runs for targets selected by
classify()/selected_values() against agent-policy.json's module_boundaries
paths. UnitTestsBudgetExhaustion was already listed in core's `tests`, but
its source file, UnitTests/tst_budgetexhaustiontest.cpp, was missing from
core's `paths` glob list, so a change touching only that file never
classified as "core" and the target was never built before clang-tidy ran
on it standalone -- producing "tst_budgetexhaustiontest.moc file not
found". Surfaced by 0aa0d4c's one-line fix to that file. Add the missing
path entry so the target builds first, as it does for every other core
test file already listed there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
PDF/X-1a:2001 and PDF/X-3:2002 forbid live transparency, but
standards-convert never called the existing PDFTransparencyFlattener
operation (#164) -- live transparency was an unconditional, unfixable
blocker even though Core already has a working flatten path that
PageMaster's export pipeline uses as a separate stage.

Wire PDFTransparencyFlattener::apply()/hasLiveTransparency() into
PDFStandardConversion::preview()/apply(), mirroring the existing
RGB-to-CMYK integration exactly:

- New PDFStandardConversionSettings::flattenTransparency (default-on
  for X-1a/X-3, matching normalizeColor's existing default pattern;
  opt-in for X-4/PDF-A, which permit live transparency).
- pdfx.transparency.allowed becomes a fixable preflight blocker only
  when flattening is requested, so unrelated fixtures without live
  transparency are unaffected (transparencyObjects stays 0, the rule
  already reports Passed).
- The flatten runs before the output-intent/page-box rewrite and its
  report is surfaced verbatim under a new transparency_flatten report
  field -- a real, reported content change, never a silent
  approximation.
- New flatten_transparency parameter on the standards-convert
  operation, available identically from PdfTool's repair command and
  PageMaster's export job (the one shared Core implementation).

Also correct docs/STANDARD_CONVERSION.md's stale claim that an Editor
adapter can land "after the 0.1.1 GUI gate": that gate is already
complete per docs/LOOP_SHELL_CONTRACT.md, which gates product GUI work
behind the still-closed S21/S22 admission contracts instead. Note that
docs/REPO_MAP.md's LoopEditorPlugins/ module does not exist in the
current Qt-Quick-based tree, so a future Editor adapter belongs under
LoopLibInteraction/ + LoopEditor/qml/.

No Qt/CMake toolchain is available in this environment, so the build
and UnitTestsStandardOracle/UnitTestsConversionOracle/UnitTestsRepairOperation
targets could not be run locally; clang-format, source-integrity, and
architecture-catalog checks all pass. CI will provide the first real
build/test signal for this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgoNmy614kqRiTBjiSjRJU
Works the net-new findings from the September 2026 read-only review that were
reproducible against the current tree, plus three adjacent defects the review
did not name.

Security / privacy
- PDFLogScrubber scrubs credential material (URL userinfo such as a Sentry DSN,
  HTTP authorization values, secret-named key/value pairs) ahead of the existing
  path and email passes. The bare auth-scheme pass deliberately excludes "Token"
  so parser diagnostics are not redacted as secrets.
- loop-ocr reads a staged raster once by descriptor instead of re-resolving the
  path for isfile(), PIL, and easyocr in turn, closing the TOCTOU window;
  language codes are shape-validated before they reach easyocr's model file
  names; PdfTool stages the raster 0600.
- Diagnostics bundles truncate plugin display fields.

Fail-closed behaviour
- PdfTool extraction commands record output.empty-result and accept a shared
  --fail-if-empty (exit 1, findings) so an empty output directory cannot pass a
  pipeline that gates on produced files.
- writeIncremental reports whether it appended or only byte-copied; damaged
  documents now carry a source digest, so its "file changed underneath us"
  guard is no longer silently disabled for permissively recovered documents.

Bounds on attacker-controlled shapes
- Damaged-document recovery bounds its dense object table by objects recovered,
  not by the highest declared object number.
- PDFNameTreeLoader terminates cyclic Kids chains and caps depth, entry count,
  and key length (net-new: the cycle was unbounded recursion).
- Structure-tree parsing bounds recursion depth on long acyclic chains.
- PDFJBIG2Bitmap::paint validates grown dimensions on its expandY path, the one
  path that escaped the constructor's dimension check (net-new).

Ergonomics
- isPathContained no longer rejects a planned output whose target directory does
  not exist yet, keeping the stricter symlinked-parent rule for the file side.
- makeUniqueFileName probes 128 sequential names, then random ones.
- OCR option defaults are defined once and shared by capability discovery and
  the command-line parser.

Not built or run here: this environment has no Qt, so the C++ changes are
unverified by compilation. The Python sidecar tests pass and the new scrubber
patterns were validated against a reference implementation of the same passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDJQDFR5ctoKDJbp7LG4GS
Whole-file clang-format across LoopLibCore/PdfTool/UnitTests sources to clear format drift flagged by agent-fast. Add moc-generated autogen include dirs to UnitTests target so Qt meta-object headers resolve. Map agent-policy tidy gates to the three active test suites.
Add canonical milestone text for 0.2.1 (patch on the 0.2.0 line), register
GitHub milestone 17 in the sync manifest, and update living-sequence
references across milestone docs and the 0.5.0–0.10.0 roadmap extension.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Replace legacy product tokens in milestone issue summaries, rename the
budget-exhaustion corpus schema kind to loop, and fix workflow
working-directory typos left from the Loupe-to-Loop rebrand.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Manual .moc includes are generated at build time, so clang-tidy cannot
analyze those sources during agent-fast. Exclude them from tidy while
keeping format checks.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
…#144)

Add pdf::PDFBlockingThreadGuard, a runtime thread-affinity check a blocking
service adapter opens with so it refuses to run on the registered
interactive (canvas) thread instead of silently stalling pointer and frame
handling. Wire it into PreflightEngine::run(), the one blocking
implementation already reached through PDFJobScheduler, and register
EditorHost's owning thread as the interactive thread at construction.

Add PDFJobKind to PDFJobTraceEvent so job traces identify async work by
type, and document the interactive-thread boundary (what pointer/frame
callbacks may do directly vs. what must go through a submitted job) in
docs/JOB_SCHEDULER.md.

Wiring an actual UI trigger for interactive preflight runs, and correlating
PDFJobTraceEvent timing against InteractionTraceRecorder's slow-frame
attribution, remain open follow-up work for #144.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xnr9TZYfGr1nMgBeh16CSb
Add pdfinteraction::evaluateTraceContracts() and its TraceContract/TracePhase
enums (LoopLibInteraction/sources/interactiontracecontract.h/.cpp): the
fixed-order pass/fail evaluator issue #146 AC7 asks for. Given an ordered
QList<TraceContractCheck>, it returns the first unsatisfied contract and the
phase responsible, matching scripts/ci/check_interaction_traces.py's
CONTRACTS/PHASES tuples and docs/schemas/interaction-trace-report.schema.json
exactly, so a future harness that supplies the nine checks in order gets AC7
for free. phaseForStage() is docs/INTERACTION_CONTRACT.md's TraceStage-to-phase
attribution table given a type.

The evaluator takes an already-assembled checklist rather than an
InteractionTraceRecorder or a live replay, so it is fully testable with
synthetic checks -- no InteractionController, no scheduler, no hit-test
dispatch -- and is covered by the new UnitTestsInteractionTraceContract target.

The prior gh-146 work (scenario/report schemas, the nine-scenario corpus, and
check_interaction_traces.py) validates the corpus as data but has no test
binary to produce a run; this evaluator is the piece that decides a run's
verdict once one exists. Replaying a scenario through InteractionController,
applying its cost model, and assembling a full report run remain open, as
UnitTestsInteractionTraces/UnitTestsInteractionTracesPresent in
docs/INTERACTION_CONTRACT.md's two-lanes table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xnr9TZYfGr1nMgBeh16CSb
…readGuard

docs/generated/phase5-widgets-inventory.json went stale when
UnitTestsBlockingThreadGuard landed (issue #144 commit): it's a target
directly in UnitTests/CMakeLists.txt, which
scripts/generate_phase5_widgets_evidence.py scans, but the catalog was never
regenerated for it. CI's policy job caught the drift --
test_verify_phase5_widgets_contract.py's crlf-currency check and its
hardcoded target count (70) both went red against the actual count (71).

Regenerate via scripts/generate_phase5_widgets_evidence.py --write and bump
the test's hardcoded count to match.

(scripts/generate-architecture-catalogs.py's separate architecture-catalog.json
was already regenerated for this target in the #144 commit; this is the
sibling widgets-surface catalog that step didn't cover.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xnr9TZYfGr1nMgBeh16CSb
Rename leftover working-directory: loupe to loop in reusable Linux and
Windows workflows so agent-fast/build can run generate_corpus.py --check.

Rename schema_kind from loupe-processing-budget-exhaustion-corpus to
loop-processing-budget-exhaustion-corpus in the corpus generator, test,
and manifest so check_loop_identity passes.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
The agent-fast build proof never built the blocking-thread-guard test
target because it was absent from the core module's test list, so its
AUTOMOC .moc file was never generated and clang-tidy failed on
tst_blockingthreadguardtest.cpp. Registering the target lets
check-change.py build it (and generate the moc) before static analysis.
…ues (#194)

Continue PR #525 on current origin/dev. Shared pdfquick::tokens cover
spacing, typography, focus geometry, and colour roles; resolveStateVisual()
is the canonical finding/check mapping. Incomplete and waived states never
resolve as passed, and each state carries a unique icon plus accessible name.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Qt::red against white is only ~4.0:1. Use the light-theme fill (#B91C1C) so
a destructive button label still meets WCAG text contrast in high contrast.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Drop restated enum notes, unused includes, and CMake/doc padding. Mapping
invariants stay in tests and LOOP_DESIGN_SYSTEM.md.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Keep both UnitTests/tst_jbig2decodertest.cpp (PR 519) and
UnitTests/tst_lifecycletest.cpp (dev) in the core module boundary paths.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Keep 0.2.1 in the living release train while adopting dev's consolidated
0.5.0-0.8.0 planned-train wording and retired-title note.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Resolve agent-policy.json conflict by keeping both
tst_budgetexhaustiontest.cpp and tst_diagnosticstest.cpp
in the core module paths.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
@mberrys
mberrys marked this pull request as draft September 10, 2026 18:40
@mberrys mberrys changed the title [0.2.1] Preflight & GUI Foundation [0.2.1] RC | Preflight & GUI Foundation Sep 10, 2026
Editor, PdfTool, PageMaster, Action List postflight, and the certificate
gate now consume reducePreflightVerdict so budget exhaustion with zero
findings cannot report PASS. Closes the remaining #234 surface gaps.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
@mberrys

mberrys commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a89ed56646

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +522 to +523
const bool flattenTransparency = settings.flattenTransparency || flattensTransparencyByDefault(settings.target);
if (flattenTransparency)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor the explicit transparency opt-out

For PDF/X-1a and PDF/X-3 conversions, an explicit flatten_transparency: false is stored as settings.flattenTransparency == false, but this || flattensTransparencyByDefault(...) immediately turns it back on. Consequently the preview claims transparency will be flattened and apply() rasterizes page content despite the documented opt-out; the setting needs an explicit auto/true/false distinction or the parsed value must be used directly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2eb00de. The parsed flatten_transparency value is now a three-state policy (Automatic / Always / Never) resolved once by flattensTransparency(), so an explicit false is no longer re-enabled by the target default: preview() stops advertising a transparency.flatten change and apply() does not run the flattener. An explicit opt-out on a document with live transparency is fail-closed — pdfx.transparency.allowed stays a blocker. PDFStandardConversionSettings::flattenTransparency was added by this same PR and has never shipped in unstable/stable, so replacing it is not a break for a released consumer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth recording alongside this fix: while verifying it I found that the PDF/X rules were not running at all. pdfxProfile() returned a profile with no checks array, and PreflightEngine::parseProfile() (preflightengine.cpp:6110-6115) rejects an empty checks array before it reads pdfx. So preview() reported zero blockers for every PDF/X target — making collectPreflightBlockers() dead code and the pdfx.transparency.allowed fixability clause unreachable — and every PDF/X apply() failed at postflight. Fixed here in 08d42b9 by giving the profile the shared checks of loop-preflight/examples/profile-pdfx-x1a2001.json; this is what makes the transparency opt-out actually fail closed rather than silently succeeding. Recorded as #556.

Comment thread changes/0.2.2.md
@@ -0,0 +1,6 @@
# Async interactive-thread boundary (issue #144) and interaction-trace contract evaluator (issue #146)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Consolidate the change fragments

This commit adds eight separate changes/*.md fragments, whereas repository policy requires every PR to add exactly one. Consolidate these entries into the single fragment representing this change so changelog attribution and change validation remain deterministic.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable to this PR, and no fragment is removed.

The eight fragments this PR carries are the per-topic-PR fragments of the PRs already merged into dev; each of those PRs added exactly one fragment named after its own head branch. For this promotion PR the changelog check is explicitly not-applicable, because the head branch (dev) is an integration branch:

$ python scripts/agent/check-change.py --base origin/unstable --head origin/dev --head-branch dev --dry-run
  →  changelog: {"result": "not-applicable", "reason": "integration branch"}

scripts/agent/test_check_change.py::test_check_changelog_allows_stacked_topic_fragments already asserts that stacked topic fragments are legal, and deleting them here would falsify the changelog attribution of those merged PRs. changes/dev.md (this PR's own fragment) was updated in 7bd2363 to cover the review fixes.

Comment on lines +56 to +57
setState(State::Stale);
m_operatorSummary = QStringLiteral("Preflight is stale for the current revision.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assign the summary before emitting its notify signal

When a completed preflight becomes stale, setState() emits stateChanged before m_operatorSummary is updated, even though that signal is the operatorSummary property's notifier and is also what triggers EditorHost::presentationChanged. Observers handling the signal can therefore read and retain the previous run's summary; cancelRun() has the same ordering. Set the summary first or emit a dedicated notification after changing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22225d1. setCurrentRevision() and cancelRun() now assign m_operatorSummary before calling setState(), so an observer reading the summary from the stateChanged handler never sees the previous run's copy. The new slot operatorSummaryIsCurrentWhenTheStateSignalFires reads the summary from inside the signal handler and fails on the unmodified code ("Preflight is running." vs the expected stale/cancelled text).

Comment on lines 315 to 318
const PDFOperationResult result = writeIncremental(&targetFile, originalData, originalDocument, document, outcome);
if (result && !targetFile.commit())
{
return tr("File '%1' can't be committed after incremental save. %2").arg(fileName, targetFile.errorString());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish the outcome only after commit succeeds

With safeWrite == true, the nested device overload writes Appended or CopiedUnchanged directly into the caller's outcome before QSaveFile::commit() runs. If the final rename or flush fails, this overload returns an error while leaving an outcome that claims the save completed, contradicting the API's “set on success” contract. Hold the nested outcome locally and copy it to the caller only after a successful commit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c2c7373. The fileName overload now holds the nested IncrementalWriteOutcome locally and copies it to the caller only after QSaveFile::commit() succeeds (and, in the direct-write branch, only after close()).

One caveat, stated plainly: on this platform the failure path cannot be covered by a test. QSaveFile::open() refuses a read-only target ("Existing file ... is not writable") before the nested write runs, so no test can reach commit() and be made to fail for the intended reason. That is recorded in the commit message rather than papered over with a test that cannot fail. The new slot fileOverloadReportsWhatItDid covers the file overload's success reporting (append vs verbatim copy), which was previously untested; it passes identically before and after this change.

Comment thread LoopEditor/editorhost.cpp
// 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the interactive-thread registration on host teardown

EditorHost registers its construction thread globally, but its destructor never calls clearInteractiveThread(). When a host is destroyed while the process continues—such as an embedded or recreated editor—later synchronous preflight work on that thread is still rejected as an interactive-thread violation, and a host created on a temporary thread leaves a stale QThread* registration. Pair this registration with lifetime-aware cleanup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 057cba7. EditorHost::~EditorHost() now clears the registration when the destroying thread is the one that registered it, so a recreated or embedded host no longer leaves a stale registration that keeps refusing synchronous blocking work on that thread.

Comment on lines +525 to +528
PDFTransparencyFlattenSettings transparencySettings = settings.transparencyFlattenSettings;
transparencySettings.analyzeOnly = false;
PDFTransparencyFlattenReport transparencyReport;
const PDFOperationResult transparencyResult = PDFTransparencyFlattener::apply(&candidate, transparencySettings, &transparencyReport);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Flatten before the CMYK normalization pass

For a typical non-spot PDF/X-1a conversion, both color normalization and transparency flattening are enabled, but the new flattening call runs after PDFRgbToCmykFixup. The flattener's non-spot path embeds its QImage page raster as DeviceRGB, reintroducing exactly the color space the earlier normalization removed; the independent validator or internal pdfx.color.device-rgb postflight then rejects the candidate. Run flattening before RGB-to-CMYK or emit a CMYK raster.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred, not dismissed. Reordering alone cannot fix this.

PDFRgbToCmykFixup refuses to convert RGB image XObjects — LoopLibCore/sources/pdfrgbtocmykfixup.cpp:462-479 appends every image whose /ColorSpace resolves to an RGB name to report->unsupported with the reason "RGB image XObjects require image-sample conversion", and both preview() (blockers) and apply() (early return) treat that as a hard failure. The flattener's non-spot path embeds exactly such a DeviceRGB page raster (pdftransparencyflattener.cpp:384-416).

So with today's order (CMYK, then flatten) a working postflight rejects the DeviceRGB candidate; with the order you propose (flatten, then CMYK) the CMYK pass fails first with "RGB image XObjects require image-sample conversion". Neither order lets a PDF/X-1a conversion of a document with live transparency succeed.

Resolving it needs either image-sample conversion in the fixup (no API change, but it changes behaviour for every consumer) or a CMYK raster from the flattener (which changes the exported PDFTransparencyFlattenSettings, i.e. a protected interface). Filed as #555 and deliberately left out of this review-fix PR.

Comment on lines +175 to +178
case PreflightVerdictState::Pass:
return verdict.waivedFindingIds.isEmpty()
? QStringLiteral("No problems found.")
: QStringLiteral("No problems found. Active dispositions cover previously blocking findings.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Translate the operator-facing verdict summaries

preflightVerdictOperatorSummary() now supplies the text displayed directly by PreflightPane.qml, but its pass and fallback messages use QStringLiteral rather than a translation context. In every non-English build, a clean, incomplete, or otherwise reason-less run therefore replaces the previously translatable QML status with English-only copy. Route these operator-facing strings through QCoreApplication::translate or the Core translation context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ed4314f. preflightVerdictOperatorSummary() now routes every literal through QCoreApplication::translate("pdf::PreflightVerdict", ...) — the same context preflightGateFailureMessage() already used — so PreflightPane.qml is no longer English-only. The literals are byte-identical, which is why the pre-existing untranslated-path assertions still pass. The test binary needed QTEST_GUILESS_MAIN instead of QTEST_APPLESS_MAIN for a QTranslator to be installable at all; that macro change is in the same commit.

Comment on lines +100 to +105
const pdf::PreflightVerdict verdict = pdf::reducePreflightVerdict(result);
m_operatorSummary = pdf::preflightVerdictOperatorSummary(verdict);
switch (verdict.state)
{
setState(State::Pass);
case pdf::PreflightVerdictState::Pass:
setState(State::Pass);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Carry waiver state into the findings model

When all error findings have active dispositions, the reducer returns Pass here, but the controller has already copied the original error findings into PreflightFindingsModel without any decision or waived-state information. The Editor consequently announces a passing verdict while its findings list and canvas overlays continue to expose those entries as ordinary errors. Pass the active dispositions into the model so waived findings receive their distinct presentation instead of contradicting the overall state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 82e4f43. acceptResult() now passes verdict.waivedFindingIds into PreflightFindingsModel::replace(), which marks the affected views (waived, exposed as WaivedRole) and maps them to a non-blocking overlay severity, so the canvas stops painting a PASS run's findings as blocking errors. The dispositions now reach the same model the verdict was reduced from, so the list and the headline can no longer contradict each other.

Comment thread LoopLibCore/sources/pdfdocumentwriter.h Outdated
Comment on lines +99 to +103
PDFOperationResult writeIncremental(const QString& fileName,
const PDFDocument* originalDocument,
const PDFDocument* document,
bool safeWrite);
const PDFDocument* originalDocument,
const PDFDocument* document,
bool safeWrite,
IncrementalWriteOutcome* outcome = nullptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the exported incremental-writer overloads

Adding a defaulted parameter changes the mangled C++ symbol; it does not preserve the previous four-argument exported overload. Existing binaries built against LoopLibCore will therefore fail to resolve writeIncremental(...) after updating to this library even though the change is marked non-breaking. Keep the old signature as a forwarding overload and add the outcome-reporting overload separately.

AGENTS.md reference: AGENTS.md:L37-L37

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8ddc6e0. Both four-argument overloads are back as real forwarding functions and both default arguments are gone, so the original mangled symbols are preserved and a four-argument call stays unambiguous (a default argument plus an overload would have made every such call ambiguous).

Symbol check (dumpbin is not installed here; this is the DLL's PE export table): LoopLibCore.dll exports exactly four writeIncremental symbols — the two four-argument signatures plus the two five-argument ones. UnitTestsIncrementalSave now static_casts both signatures, so a future defaulted parameter cannot silently remove them again.

"languages": {
"type": "array",
"items": { "type": "string", "minLength": 1 }
"items": { "type": "string", "minLength": 1, "pattern": "^[a-z]{2,3}(_[a-z]{2,4})?$" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align the OCR language schema with runtime normalization

The service deliberately trims and lowercases language values before validating them, so a request containing "EN" is accepted and normalized to "en" (the added test explicitly expects this). The tightened protocol schema, however, rejects the same request because its pattern permits only lowercase input. Clients validating requests against the published schema will reject inputs the runtime contract accepts; either make the schema case-insensitive in shape or stop accepting uppercase at runtime.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b6832f3. The schema pattern is now case-tolerant in shape (^[A-Za-z]{2,3}(_[A-Za-z]{2,4})?$, plus a description saying the service normalizes case) instead of lower-case only, so a client validating the published contract no longer rejects a request the runtime accepts and normalizes. The new test checks the schema and the runtime agree in both directions: the codes the runtime normalizes must match the pattern, and the traversal-shaped junk it refuses must not.

…ds-convert (#554)

The target default was re-applied with '||' on top of the parsed value, so
'flatten_transparency: false' on PDF/X-1a/X-3 still flattened. The parsed
setting becomes a three-state policy (Automatic/Always/Never) resolved once by
flattensTransparency(), so an explicit opt-out is no longer overridden:
preview() stops advertising a transparency.flatten change and apply() does not
run the flattener.

PDFStandardConversionSettings::flattenTransparency was added by this same PR
and has never shipped in unstable or stable, so replacing it is not a break
for a released consumer.
PDFTransparencyFlattener::apply() rasterizes every selected page, while
preview() only advertises a transparency change when hasLiveTransparency() is
true. Guarding the apply path with the same condition stops opaque vector/text
documents from being silently replaced by full-page rasters.
…ecks (#554)

preview() and apply() pass pdfxProfile() to PreflightEngine, but
parseProfile() rejects a profile with an empty 'checks' array before it reads
'pdfx', so result.pdfx was never populated: preview() reported no blockers for
any PDF/X target and every PDF/X apply() failed at postflight. The profile now
carries the shared checks of loop-preflight/examples/profile-pdfx-x1a2001.json,
which is what makes an explicit flatten opt-out fail closed.
The file overload handed the caller's outcome pointer to the device overload,
which filled it in before QSaveFile::commit(). A failed rename returned an
error while the outcome claimed the save had completed.

The commit-failure path is verified by construction rather than by a unit test:
QSaveFile::open() refuses a read-only target ('Existing file ... is not
writable') before the nested write runs, so a test cannot reach commit() on
this platform and none can be made to fail for the intended reason. The new
slot covers the file overload's success reporting (append vs verbatim copy).
…ds (#554)

A defaulted parameter changes the mangled symbol, so the previous overloads
would have disappeared from LoopLibCore. They return as forwarding overloads
and the default argument is gone, which also keeps a four-argument call
unambiguous. UnitTestsIncrementalSave now static_casts both signatures so a
future defaulted parameter cannot silently remove them again.

Symbol check (dumpbin is not installed here; this is the DLL's PE export
table): LoopLibCore.dll exports exactly four writeIncremental symbols - the two
four-argument signatures plus the two five-argument ones.
The standalone authorization pass required a digit or punctuation character in
the credential, so an opaque value such as 'Bearer abcdefghijklmnop' reached
support bundles verbatim. A second alternative now matches scheme-delimited
all-letter tokens of 16+ characters; prose after a scheme name still survives.
PreflightPane.qml renders preflightVerdictOperatorSummary() directly, so its
pass and fallback copy must go through a translation context instead of
QStringLiteral - otherwise every non-English build shows English-only status.
…#554)

stateChanged is operatorSummary's notifier, but setCurrentRevision() and
cancelRun() emitted it before assigning the new text, so observers retained the
previous run's summary.
…554)

acceptResult() copied the raw error findings into the model, so a run whose
blocking findings were all covered by an active disposition announced PASS while
the findings list and canvas overlays still showed them as errors. The model now
takes the verdict's waived IDs, exposes them through WaivedRole, and maps them
to a non-blocking overlay severity.
…wn (#554)

EditorHost::registerInteractiveThread() had no matching cleanup, so a host recreated in the same process left a stale registration that kept refusing synchronous blocking work on that thread.
…554)

The schema pattern was lower-case only, so a client validating the published contract rejected 'EN' - a request the service accepts and normalizes to 'en'. The pattern is now case-tolerant in shape and says so in a description, and a test keeps the schema and the runtime normalization in step in both directions.
…gment (#554)

Covers the ten review findings fixed here, the PDF/X profile defect fixed in
this PR (issue #556), and the CMYK-ordering finding deferred to issue #555.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment