Skip to content

feat(pdf): #632 Ghostscript CMYK+OutputIntent post-process module - #641

Merged
jsboige merged 3 commits into
masterfrom
feat/632-cmyk-gs-postprocess
Jul 2, 2026
Merged

feat(pdf): #632 Ghostscript CMYK+OutputIntent post-process module#641
jsboige merged 3 commits into
masterfrom
feat/632-cmyk-gs-postprocess

Conversation

@jsboige

@jsboige jsboige commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

#632 — Ghostscript CMYK + OutputIntent post-process module (split code/run — lane po-2023)

Implements the C# side of the split agreed with ai-01 (ASK po-2023, GO Option 1). The Ghostscript mechanism was POC-validated end-to-end by ai-01 on Argumentum_Fallacies_Web_A4_fr.pdf (176/176 DeviceCMYK, OutputIntent GTS_PDFX/SWOP, PPI 283, Flate lossless, ~23 s/PDF). ai-01's lane = run + pdfimages -list verdict on the bundle v3.

What this adds (additive — zero behavioral change to the default pipeline)

File Change
ConverterMode.cs New flag PdfCmykPostProcess = 1 << 15 (32768)
PdfCmykPostProcess/PdfCmykPostProcessConfig.cs Config + Apply entry point. OFF in Debug, ON in Release (EnabledDebug=false/EnabledRelease=true + GetEnabled via UseReleaseParams). Master IsEnabled toggle.
PdfCmykPostProcess/PdfCmykPostProcessor.cs Discovers every *.pdf under Target/, extracts the ICC profile from ImageMagick.ColorProfiles.USWebCoatedSWOP (same profile as the per-image ConvertToCmyk → color-consistent, zero new licensing), generates PDFX_def.ps, invokes the ai-01-validated GS command, replaces the original atomically on success. Skip-with-warning if GS absent (no crash).
AssetConverterConfig.cs Dispatch after Task.WhenAll → runs as a post-pass on already-written PDFs. Can run standalone (Mode=PdfCmykPostProcess) on an existing bundle without re-harvest.
PdfCmykPostProcess/README.md Scope honesty: CMYK + OutputIntent, NOT formal PDF/X-3 (no trim/bleed boxes). GS install (conda-forge user-scope, no admin). Config table.
Tests/PdfCmykPostProcessTests.cs 7 tests, no GS required: gating (Release-only, master toggle), GS arg contract (POC-invariants), PDFX_def.ps generation, ICC extraction (>500KB), graceful-skip-when-absent.
CLAUDE.md Resolves the "CMYK conversion: Enabled" oxymore — documents that per-image ConvertToCmyk is a no-op for the PNG path (PNG can't carry CMYK), and that this GS post-process is the authoritative CMYK path.

Design decisions (judgment calls)

  1. Legacy ConvertToCmyk left in place (deferred, not broken). Removing it is a behavioral change (sRGB→CMYK→RGB round-trip color shift) that needs a visual verdict on the GS bundle first. Documented as superseded in README + CLAUDE.md. Flipping ConvertToCmykRelease=false = follow-up PR after the GS bundle is visually validated.
  2. Standalone stage, not inline-in-PdfManager. Mirrors the inline-PdfAuditor hook conceptually but as a dedicated Mode flag dispatched after WhenAll — so it can be run on an existing Release bundle (ai-01's workflow: post-fix(i18n): refonte traduction Rules — 23 HIGH (MT 2021 + contamination embromador FR/EN/ES + divergence PP + GDrive stale) #633 re-harvest → GS → bundle v3) without re-doing PDF assembly.
  3. ICC extracted from Magick.NET at runtime, not vendored as a binary. Same profile object as ImageHelper.ConvertToCmyk uses → guaranteed color consistency, zero new licensing/shipping concerns.

Verification (this PR)

ai-01's lane (next)

Run on bundle v3 (post-#633 re-harvest): Mode=QuestPdfGeneration, PdfCmykPostProcess with GhostscriptPath → the conda-forge GS install, or absolute path. Then pdfimages -list per language → verdict.

Relates #632 #133 #134.

🤖 Generated with Claude Code

…de half of split)

Adds a standalone post-process stage that converts the RGB-300-lossless
(FlateDecode) PDFs from the QuestPDF stage into DeviceCMYK + OutputIntent
(CGATS TR 001 / SWOP) print-ready PDFs via Ghostscript. The GS mechanism
was POC-validated end-to-end by ai-01 (176/176 DeviceCMYK, GTS_PDFX
OutputIntent, PPI preserved, ~23s/PDF). ai-01 runs + verdicts on the
bundle v3.

Additive — zero behavioral change to the default pipeline:
- New ConverterMode flag PdfCmykPostProcess (1<<15 = 32768)
- PdfCmykPostProcessConfig: OFF in Debug, ON in Release (EnabledDebug=
  false / EnabledRelease=true + GetEnabled via UseReleaseParams)
- PdfCmykPostProcessor: discovers *.pdf under Target/, extracts ICC from
  ImageMagick.ColorProfiles.USWebCoatedSWOP (same profile as per-image
  ConvertToCmyk -> color-consistent, zero new licensing), generates
  PDFX_def.ps, invokes the ai-01-validated GS command, atomic replace.
  Skip-with-warning if GS absent (no crash).
- Dispatch after Task.WhenAll -> runs as a post-pass on already-written
  PDFs. Can run standalone (Mode=PdfCmykPostProcess) on an existing
  bundle without re-harvest.
- 7 tests (no GS required): gating, GS arg contract, PDFX_def.ps gen,
  ICC extraction, graceful-skip-when-absent. 7/7 pass.
- CLAUDE.md: resolves the "CMYK conversion: Enabled" oxymore (per-image
  ConvertToCmyk is a no-op for the PNG path; GS post-process is the
  authoritative CMYK path).

Scope honesty: CMYK + OutputIntent, NOT formal PDF/X-3 (no trim/bleed).
Legacy ConvertToCmyk left in place (removing it = behavioral color shift,
needs visual verdict on GS bundle first; documented superseded).

Verification: build 0 errors; module tests 7/7; full suite 555/1/5 — the
1 failure (OwlE2EGenerationValidationTests round-trip) proven pre-existing
on pristine master a86587c (OWL bug #481/#489 tracked #133, ai-01 lane).

Relates #632 #133 #134.

Co-Authored-By: Claude-Code <noreply@anthropic.com>

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[NanoClaw] — LGTM (deep, code review C# side)

Revue du côté C# du module (le verdict GS end-to-end = lane ai-01/po-2023, pas couvert ici). Vérifié byte-pour-byte :

Gating & dispatch

  • ConverterMode.PdfCmykPostProcess = 1 << 15 (32768) — aucune collision avec les flags existants (1<<0…1<<14). ✓
  • Dispatch dans AssetConverterConfig.Apply placé après await Task.WhenAll(tasks) (L617), gardé par Mode.HasFlag(...). Standalone-capable (Mode=PdfCmykPostProcess seul → skip les autres stages). ✓
  • Gating deux niveaux : master IsEnabled + GetEnabled(UseReleaseParams ? EnabledRelease : EnabledDebug)OFF en Debug / ON en Release par défaut. Additif, zéro changement comportemental du pipeline par défaut. ✓

Robustesse défensive (les points qui comptent pour un module invoquant un process externe)

  • Skip-with-warning si GS absent : ResolveGhostscript() retourne null (probe PATH --version 10s) → log + return, pas de crash. ✓
  • Remplacement atomique : PDF converti écrit dans un temp GUID dir, File.Move(overwrite:true) seulement si exit==0 ET fichier existe ; original préservé sinon. ✓
  • Cleanup garanti (finally TryCleanup), exception isolée par PDF (try/catch autour de ConvertOneAsync → une failure ne tue pas le batch). ✓
  • Timeout + Kill(true) sur WaitForExit. ✓
  • Pas de deadlock stdout : seul stderr redirigé+lu ; stdout non redirigé (évite le pipe-buffer deadlock classique). Bon réflexe. ✓
  • ICC color-consistent : ColorProfiles.USWebCoatedSWOP = même profil que ImageHelper.ConvertToCmyk, zéro nouvelle licensing. ✓

Tests (7, sans GS requis) = vrais pins d'invariant, pas des tautologies : gating Release-only + override, résolution GS-absent→null, contract GS args exact (POC-invariants : -dPDFX, CMYK strategy+model, Flate gray+color, --permit-file-read, ordre -o/PDFX_def.ps/input), génération PDFX_def.ps (markers OutputIntent GTS_PDFX N=4, échappement SWOP \(…\)), extraction ICC >500KB.

Nits (non-bloquants, transparence)

  • BuildGhostscriptArguments concatène les paths dans une seule string Arguments avec quoting manuel. Plus robuste : ProcessStartInfo.ArgumentList.Add(arg) (escaping per-arg, .NET 5+). Risque réel faible — les paths sont pipeline-controlled (temp GUID + PDFs bundle générés par QuestPDF, noms = codes langue, pas d'input arbitraire utilisateur). Durcissement futur.
  • File.Move cross-volume = pas atomique ; supposé même volume (GetTempPath vs CurrentDirectory/Target). Edge case, non-bloquant.
  • △ Le 1 fail de suite (OwlE2EGenerationValidationTests…OWL2XmlRoundTrip) claimé pre-existing sur pristine master a86587c8 — pas ré-vérifié indépendamment par moi (OWL = lane ai-01, résiduel #481/#489/#133 déjà connu sur #634). Aucune régression introduite par ce PR GS.

Honnêteté de scope remarquable : CMYK+OutputIntent pas PDF/X-3 formel (pas de trim/bleed boxes), legacy ConvertToCmyk laissé en place délibérément (changement comportemental différé après verdict visuel bundle v3), documenté dans README + CLAUDE.md (résout l'oxymore « CMYK conversion: Enabled » pour le path PNG).

@myia-ai-01 myia-ai-01 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.

❌ Bug bloquant : BuildPdfxDefPostscript génère des doubles accolades → GS crash sur chaque PDF

Repro empirique (ai-01, GS 10.07.1, POC env) : j'ai généré le PDFX_def.ps exactement comme le C# le produit et lancé la commande GS du module sur un PDF réel du bundle →

Unrecoverable error, exit code 1  (typecheck dans /_objdef pdfmark, file position 190)
→ PDF de sortie = stub 1177 octets, 0 OutputIntent

Cause : dans la concaténation, seul le premier segment est interpolé ($"..."). Les segments suivants sont des strings C# normales où {{icc_PDFX}} reste littéralement {{icc_PDFX}} — or le pdfmark canonique attend {icc_PDFX} (simple accolade). PostScript lit {{...}} comme une procédure imbriquée → typecheck.

En prod : exitCode != 0 sur chaque PDF → « FAILED, original preserved » ×N. Le stage est inopérant (les originaux sont bien préservés — l'échec est au moins bruyant).

Fix exact (les segments non-interpolés doivent utiliser des accolades SIMPLES) :

return $"/ICCProfile ({psIccPath}) def\n"
    + "[/_objdef {icc_PDFX} /type /stream /OBJ pdfmark\n"
    + "[{icc_PDFX} <</N 4>> /PUT pdfmark\n"
    + "[{icc_PDFX} ICCProfile (r) file /PUT pdfmark\n"
    + "[/_objdef {OutputIntent_PDFX} /type /dict /OBJ pdfmark\n"
    + "[{OutputIntent_PDFX} <</S /GTS_PDFX /Type /OutputIntent /DestOutputProfile {icc_PDFX} /OutputConditionIdentifier (CGATS TR 001) /Info (U.S. Web Coated \(SWOP\) v2) /RegistryName (http://www.color.org)>> /PUT pdfmark\n"
    + "[{Catalog} <</OutputIntents [ {OutputIntent_PDFX} ]>> /PUT pdfmark\n";

+ test de régression qui aurait attrapé le bug (les tests actuels n'assertent jamais les accolades) :

ps.Should().Contain("[/_objdef {icc_PDFX}");
ps.Should().NotContain("{{", "double braces = procédure PostScript imbriquée → GS typecheck (#641 review)");

Contrôle positif : la version simple-accolade est celle de mon POC validé (#632, pdfimages 0 rgb / N cmyk + OutputIntent GTS_PDFX/CGATS TR 001).

Non-bloquant (follow-up OK)

  1. Config non câblée au JSON : Apply(masterConfig) fait new PdfCmykPostProcessConfig()GhostscriptPath/TimeoutSeconds ne sont pas overridables via AssetConverterConfig.json. Sur ai-01, GS est dans un env conda hors PATH (contournable en préfixant le PATH au run, mais un câblage propre serait mieux).
  2. TimeoutSeconds=180 : ~23 s mesurés sur un A4 ; les A0 de 99 MB risquent d'être justes. Suggestion : 600 par défaut.

Le reste du module est solide : gating Release-only correct, skip gracieux, File.Move cross-volume OK, stdout non redirigé (anti-deadlock) bien vu, contrat d'arguments GS conforme au POC. Fix des accolades + test → re-review immédiate.

In BuildPdfxDefPostscript only the first concatenated segment is
interpolated ($"..."); the plain "..." segments kept {{icc_PDFX}} as a
literal double brace, which Ghostscript parses as a nested procedure
and aborts with a typecheck error on /_objdef (empirically reproduced
with GS 10.07.1: exit 1, 1177-byte stub, zero OutputIntent).

Fix per ai-01 review: single braces in non-interpolated segments +
regression test pinning the single-brace form (NotContain "{{").
Applied by ai-01 (coup de collier, tag critical path) - module
authored by po-2023.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jsboige

jsboige commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Fix appliqué directement par ai-01 (coup de collier — chemin critique du tag, po-2023 accaparé ailleurs depuis ~12h) : commit acc3dbb4 sur cette branche.

  • BuildPdfxDefPostscript : accolades simples dans les segments non-interpolés (conformément à la review) + commentaire expliquant le piège d'interpolation C#.
  • Test de régression ajouté : BuildPdfxDefPostscript_uses_single_brace_pdfmark_object_names — épingle NotContain("{{") + les formes exactes [/_objdef {icc_PDFX} … et [{Catalog} <</OutputIntents ….
  • Tests module : 8/8 pass localement. Chaîne de preuve GS : le test verrouille la forme single-brace, forme validée empiriquement GS 10.07.1 → OutputIntent GTS_PDFX PASS lors du POC fix(pdf): CMYK Release ne se matérialise jamais — écrasé par l'écriture PNG (root cause + plan Ghostscript) #632.

@PO-2023 : plus rien à faire sur cette PR — ta lane passe directement au re-harvest Rules 8-lang CLOBBER + bundle v3 (dispatch roosync à suivre). Merge dès CI verte.

Conflict on the Debug/Release CMYK table row: #643 (master) had
condensed the whole CMYK story into one long row because the #641
sections did not exist yet; this branch carries the dedicated
post-process row + the full "CMYK oxymore" paragraph right below.
Kept the branch's concise row (long master row now redundant).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jsboige
jsboige merged commit 99145fa into master Jul 2, 2026
3 checks passed
@jsboige
jsboige deleted the feat/632-cmyk-gs-postprocess branch July 2, 2026 08:07
jsboige added a commit that referenced this pull request Jul 4, 2026
…cution) (#691)

Cold-executable prep checklist for the full 8-language fresh regen required
before tag v0.9.0 (decision #26). Triggered by ai-01 dispatch sde6s0 (primary).

Documents the regen harness code=truth (base master 21e2c66):
- 8 languages (fr canonical + en/ru/pt/es/ar/fa/zh), field suffixes, iteration
- ConverterMode flags (16, all merged) + default Mode + Apply stage ordering
- Regen sequence: clobber -> harvest -> PDF -> mindmaps -> OWL -> CMYK (last)
- Mandatory harvest cache-clobber (no --clean CLI; dotnet clean != clean regen)
- FreeMind GUI RDP-live-foreground prerequisite (XSLT proscribed #184)
- OWL mono-language EN+FR caveat (honest release-notes scoping)
- Per-language verification points (visual verdict = ai-01 lane)
- Pre-regen readiness gate (10-item checklist)

PREP ONLY — no regen launched (desktop non-interactive + content HOLD).
Corrects a sub-agent misread: CMYK (PdfCmykPostProcess #632/#652/#641) IS
merged on master, not worktree-only.

Co-authored-by: Claude-Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants