Files
iccery-v2-mac/docs/14-iccery-cpu-targetprint.md
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 4c2863e52c Add bootstrap docs: AGENTS.md, BUILD-PLAN.md, LICENCE.md, vendored spec (docs/), brand assets
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 17:36:50 +01:00

257 lines
13 KiB
Markdown

# 14 — ICCery-CPU / TargetPrint (macOS companion)
> Line numbers refer to ICCery v0.8.5 (`/tmp/ICCery` at analysis time) and the Gronod ArgyllCMS 3.5.0 fork.
## ICCery-CPU / TargetPrint
Separate native macOS AppKit app. Spec: `/tmp/ICCery-CPU/SPEC.md`. Binary name `TargetPrint`. Zero third-party deps. Universal 2. Hardened Runtime on, App Sandbox **off** (needs `cupsGetPPD`).
### Why it exists
ICCery's Tauri path spools TIFF via `lp` and never goes through Quartz. That is correct for "don't let ColorSync touch the file", but:
- No 1:1 physical-size preview
- Windows-style `StretchDIBits` scaler (macOS `lp` may still scale inside the filter)
- No AirPrint warning
- NSPrintPanel is settings-only; the job is a second step
TargetPrint is the **Adobe Color Print Utility analogue**: decode TIFF with ImageIO, draw 1:1 in PostScript points with interpolation off, present `NSPrintPanel` as the actual print operation, inject ColorSync-off **and** vendor PPD keys into `NSPrintInfo`.
### Architecture (SPEC §2)
```
ICCery (Tauri) --job /tmp/iccery_job_<uuid>.json (fire-and-forget, do not wait)
TargetPrint.app (always NSApplication, never a CLI tool)
main.swift → AppDelegate → PreviewWindowController
TargetCanvasView (ImageIO, knowsPageRange/rectForPage)
PrintEngine (NSPrintOperation + NSPrintInfo + ColorMatching)
CUPSManager (libcups, AirPrint, vendor PPD)
```
### Invocation contract (SPEC §4, `main.swift`, `AppDelegate.consumeArguments`)
```
TargetPrint.app/Contents/MacOS/TargetPrint --job /tmp/iccery_job_<uuid>.json
TargetPrint --job=/path.json
TargetPrint --verbose --job … # ~/Library/Logs/TargetPrint/TargetPrint.log
# or open a .targetjob / .json via file association (UTI com.iccery.targetjob)
```
Exit codes (`AppError.swift`): `0` ok/cancel, `1` invalid job, `2` unreadable TIFF, `3` CUPS, `4` fatal. Job JSON is **never deleted** by TargetPrint.
Mode B: no args / drop TIFFs / File→Open — inspector + Print….
`--job` presents `NSPrintPanel` immediately (`AppDelegate:32-33`, SPEC §4.5). Tauri must **not** `wait()`.
### JSON `TargetJob` v1 (SPEC §5, `TargetJob.swift`, `Resources/sample.targetjob`)
```json
{
"version": 1,
"jobTitle": "Epson_XP55_IlfordLustre_Target_P1-2",
"files": ["/abs/page1.tif", "/abs/page2.tif"],
"printSettings": {
"printerName": "EPSON_XP_55_Series",
"mediaSize": "A4",
"mediaType": "PremiumGlossy",
"paperSource": "Auto",
"resolution": "5760x1440dpi",
"printQuality": null,
"scaling": 1.0,
"centered": true,
"forceUnmanagedColor": true
},
"uiPolicy": {
"lockColorManagement": true,
"allowBasicDriverChanges": true
}
}
```
Required: `version==1`, `jobTitle`, non-empty `files`, `printSettings.{printerName,mediaSize,scaling,centered,forceUnmanagedColor}`, `uiPolicy.{lockColorManagement,allowBasicDriverChanges}`. Unknown keys ignored. `printQuality` is implemented in Swift beyond the spec sample.
### Geometry & pixel integrity (SPEC §6, §15)
- 72 pt = 1 in. **Never** `backingScaleFactor` (`Geometry.swift`).
- `physicalInches = pixels / dpi`; `pointSize = physical * 72 * scaling`.
- Papers: A4/A3/A5/Letter/Legal/Tabloid/4x6/5x7. Unknown names fall back to A4, with substring match for `A4.Borderless`.
- Destination origin rounded to 0.001 pt to avoid fractional-device resampling while staying inside G1 ±0.1 mm.
- ImageIO load: `kCGImageSourceShouldCache=true`, `ShouldAllowFloat=false`. DPI from `kCGImagePropertyDPIWidth/Height`, default 72.
- `TargetPage.makeUnmanagedDeviceRGB`: redraw into `CGColorSpaceCreateDeviceRGB()` with interpolation **none**, antialias **off**, `shouldInterpolate: false`. SPEC §6.3: do **not** `CGImageCreateCopyWithColorSpace` into a calibrated space.
- Print draw (`TargetCanvasView.drawPrintedPage`): `interpolationQuality = .none`, `setShouldAntialias(false)`, `setAllowsAntialiasing(false)`, `setShouldSmoothFonts(false)`, fill paper white, `context.draw(cgImage, in: dest)`. GState saved/restored so page N does not inherit page N-1's transform.
- `PixelIntegrity.scanVerticalSeam`: any pixel on a red\|green seam that is not exactly left or right fails P1 (interpolation/antialias detected).
- Pagination: `knowsPageRange` 1…N, `rectForPage` = paper size, empty header/footer.
### ColorSync disable in TargetPrint (SPEC §7) — **different keys from ICCery**
`ColorMatching.swift`:
| User mode | `PMColorMatchingMode` value |
|-----------|-----------------------------|
| Unmanaged (profiling default) | `"APCustomColorMatching"` |
| ColorSync | `"APColorSync"` |
| Driver / Vendor | `"APPrinterExtension"` |
Injected into:
- `printInfo.dictionary()["PMColorMatchingMode"]`
- `printInfo.dictionary()["PMCustomColorMatchingProfile"]` = `""` when unmanaged else `"System"`
- `printInfo.dictionary()["com.apple.print.PrintSettings.PMColorMatchingMode"]` (legacy)
- nested `com.apple.print.printSettings` dictionary, same keys
**This is not `AP_ApplicationColorMatching`.** TargetPrint talks to Quartz/`NSPrintOperation`. ICCery talks to the CUPS `lp` ticket / `cgpdftoraster`. A rewrite that unifies them must keep both vocabularies or prove one is honored on both paths.
Panel policy (`ColorMatching.configurePanel`):
- Always: copies, page range, preview
- If `allowBasicDriverChanges`: paper size, orientation, scaling
- If `lockColorManagement`: `stripColorMatchingAccessories` — walk `panel.accessoryControllers`, remove any whose class/title/nib contains "color matching" / "colour matching" / "colormatch", requiring `NSPrintPanelAccessorizing`. Stripped twice (configure + immediately before `runModal`) because system accessories install lazily.
### CUPS / PPD (`CUPSManager.swift`, `Bridging-Header.h`)
Bridging header:
```objc
#import <cups/cups.h>
#import <cups/ppd.h>
static inline const char *TPCupsGetPPD(const char *name) {
return cupsGetPPD(name); // deprecated; Swift overlay marks cupsGetPPD unavailable
}
```
- `cupsGetDests` / `cupsFreeDests` / `cupsGetOption` for `printer-uri-supported`, `device-uri`, `printer-make-and-model`.
- `TPCupsGetPPD` → read ISO-Latin-1 → `unlink` the temp PPD.
- AirPrint (SPEC §10.2) if any of: URI `apple-airprint://`; PPD `*APAirPrint: True`; make Apple + model contains AirPrint; `ipps://` **and** PPD text contains `airprint`. Persistent warning badge; unmanaged color cannot be trusted. Tests in `AirPrintTests.swift`.
- Vendor bypass (SPEC §10.3) — **different keys from ICCery's lpoptions detector:**
| Vendor | TargetPrint keys | ICCery macOS `lp` keys |
|--------|------------------|------------------------|
| Epson | `ColorModel=RGB`, `EPSONColorControls=Off` | `EPIJ_CMat=3` / `EPIJ_CCor=0` / `EpsonColorMode=Off` |
| Canon | `CNColorMatching=None` | `CNIJIntent2=4` / `CNIJIntent=4` |
| HP | `ColorModel=RGB`, `HPColorControl=Off` | (none auto) |
| Generic | any Color/Colour OpenUI choice in `{none,off,no,nocoloradjustment}` | `ColorCorrection=Uncorrected` / `StpColorCorrection=Uncorrected` |
A rewrite should apply **both** dictionaries (ICCery's empirically captured PDE keys **and** TargetPrint's SPEC keys).
Media/tray/quality discovery walks `*Keyword code/Title:` and `*OpenUI` translations (`media type`, `paper source`, `print quality`, …). Keywords tried:
- Media: `MediaType`, `CNIJMediaType`
- Tray: `InputSlot`, `EPIJ_FdSo`, `CNIJMediaSupply`
- Quality: `CNIJPrintQuality`, `EPIJ_Qual`, `cupsPrintQuality`, `PrintQuality`, `CNIJPrintMode2`, `Quality`, `StpQuality`
Canon quality is a triple (`CNIJPrintQuality` + `CNIJPrintMode2` + `CNIJPQualitySlider`) mapped in `PrintEngine.applyOptionalPPDKeys`.
PPD hex escapes (`<2F>``/`) decoded by `decodePPDString`.
### PrintEngine (`PrintEngine.swift`)
`makePrintInfo()`:
- `jobDisposition = .spool`
- margins 0, centering **off** (geometry is in the view), pagination `.clip`, `scalingFactor = 1.0`
- `NSPrinter(name: printerName)`
- paper size from `Geometry.paper(named:)`
- `ColorMatching.apply`
- `CUPSManager.namedQueue``vendorColorBypass` + optional media/tray/quality/resolution extras
- `canSpawnSeparateThread = false` on the operation
- `runModal(for: window, delegate:didRun:)` — exit code 0 on success **or** cancel (SPEC §4)
### How ICCery would invoke it (SPEC §16, `VENDOR.md`)
Not implemented in current ICCery Rust. Specified as:
```rust
let json = serde_json::to_string_pretty(&job)?;
let mut path = env::temp_dir();
path.push(format!("iccery_job_{}.json", uuid::Uuid::new_v4()));
fs::write(&path, json)?;
Command::new("/Applications/TargetPrint.app/Contents/MacOS/TargetPrint")
// or vendored: src-tauri/targetprint/macos-{x86_64,aarch64,universal}/TargetPrint.app/Contents/MacOS/TargetPrint
.arg("--job")
.arg(&path)
.spawn()?; // fire-and-forget — do not wait
```
CI publishes `vendor-iccery.zip` with `macos-x86_64` / `macos-aarch64` / `macos-universal` app bundles to drop into `src-tauri/targetprint/`.
Suggested ICCery integration:
1. Keep current `lp` path as the headless/fast path (and the only path on Linux).
2. On macOS, Preferences / Print can spawn TargetPrint with a `TargetJob` built from `PrintOptions` + TIFF list + `forceUnmanagedColor: true` + `lockColorManagement: true`.
3. Do not `CREATE_NO_WINDOW` (macOS); do not `wait()`. Cleanup of the JSON is ICCery's job after process exit, or leave in `/tmp` as an audit trail (SPEC §13).
### TargetPrint vs ICCery macOS — decision table for the rewrite
| Concern | ICCery `macos.rs` | TargetPrint | Rewrite recommendation |
|---------|-------------------|-------------|------------------------|
| Spool | `lp` TIFF | Quartz `NSPrintOperation` | Keep `lp` for unattended; TargetPrint for preview+panel |
| ColorSync ticket | `AP_ApplicationColorMatching` (+ dotted) | `PMColorMatchingMode=APCustomColorMatching` | Set **both** if using NSPrintOperation; keep AP_* on `lp` |
| Lock PDE UI | private `PMSessionSetColorMatchingMode*` SPI | strip Color Matching accessories | Use SPI **and** strip; accessories API misses driver PDEs (the #188 failure mode) |
| Canon off | `CNIJIntent2=4` | `CNColorMatching=None` | Apply both |
| Epson off | `EPIJ_CMat=3` / `EPIJ_CCor=0` | `EPSONColorControls=Off` + `ColorModel=RGB` | Apply both; prefer captured panel values |
| Geometry | none (filter decides) | 1:1 pt from DPI | TargetPrint (or do not scale in GDI/`lp`) |
| Interpolation | n/a (file passthrough) | explicitly disabled | Required for patch edges |
| AirPrint | none | detected + warned | Port detector into ICCery printer list |
| Linux | `-o raw` | n/a (macOS only) | Keep raw + PPD fallback |
| Windows | GDI ICM_OFF | n/a | Keep GDI; do not route through TargetPrint |
---
## Issue cross-reference
| # | Title | Print relevance |
|---|-------|-----------------|
| 25 | Raw OS Printing Engine | Parent feature. ACPU-equivalent, no `window.print()`. |
| 26 | Windows GDI/ICM | `SetICMMode`, `DMICMMETHOD_NONE`, BGR DIB, `StretchDIBits`. |
| 27 | macOS/Linux CUPS | Originally `-o raw`. macOS later diverged to AP_* (#92/#188). |
| 28 | UI + IPC | `get_printers`, printer `<select>`, Print Target button, toasts. |
| 34 | Driver properties, trays, orientation, auto-fit | Windows dialog + scaler; Unix lpoptions; `PrintOptions`. |
| 36 | DEVMODE not applied | Full buffer + `dmDriverExtra` retained into `CreateDCW`. |
| 46 | Console windows on Windows | `CREATE_NO_WINDOW` on Argyll spawn, **not** on print. |
| 48 | Hide PPD checkbox on Windows | `#cupsOptionsGroup.hidden`. |
| 50 | Stage 2 badge cleanup | Removed ACPU / auto-fit badges. |
| 67 | Consolidate print commands | `get_printers` / `print_target_native` only. |
| 68 | printtarg JSON manifest | Gallery + per-page print consume `event: manifest`. |
| 92 | macOS CUPS raw spooler | `macos.rs` created; ColorSync research. Milestone 9. |
| 127 | Fetch Argyll + NSIS USB | Instrument USB, not printer drivers. |
| 188 | Cannot disable Epson/Canon CM on macOS | Preferences was System Settings. Now NSPrintPanel + SPI + AP_* + PPD bypass + capture. |
---
## Files (absolute)
```
/tmp/ICCery/src-tauri/src/print/mod.rs
/tmp/ICCery/src-tauri/src/print/macos.rs
/tmp/ICCery/src-tauri/src/print/windows.rs
/tmp/ICCery/src-tauri/src/print/unix.rs
/tmp/ICCery/src-tauri/src/print/tests.rs
/tmp/ICCery/src-tauri/src/commands.rs # get_printers, get_printer_capabilities,
# show_printer_properties, print_target_native
/tmp/ICCery/src-tauri/src/lib.rs # PrinterDevModeStore manage + command list
/tmp/ICCery/src-tauri/src/process_manager.rs # CREATE_NO_WINDOW for Argyll
/tmp/ICCery/src-tauri/src/calibration.rs # CREATE_NO_WINDOW for printcal
/tmp/ICCery/src-tauri/Cargo.toml # objc2* / windows features
/tmp/ICCery/src-tauri/windows/hooks.nsh # NSIS USB
/tmp/ICCery/src/js/printtarg.js
/tmp/ICCery/src/index.html # #rawPrintPanel
/tmp/ICCery/AGENTS.md # #188 contract
/tmp/ICCery-CPU/SPEC.md
/tmp/ICCery-CPU/VENDOR.md
/tmp/ICCery-CPU/Bridging-Header.h
/tmp/ICCery-CPU/Sources/main.swift
/tmp/ICCery-CPU/Sources/AppDelegate.swift
/tmp/ICCery-CPU/Sources/AppError.swift
/tmp/ICCery-CPU/Sources/Printing/ColorMatching.swift
/tmp/ICCery-CPU/Sources/Printing/PrintEngine.swift
/tmp/ICCery-CPU/Sources/Printing/CUPSManager.swift
/tmp/ICCery-CPU/Sources/Models/TargetJob.swift
/tmp/ICCery-CPU/Sources/Models/TargetPage.swift
/tmp/ICCery-CPU/Sources/Models/Geometry.swift
/tmp/ICCery-CPU/Sources/Models/PixelIntegrity.swift
/tmp/ICCery-CPU/Sources/Views/TargetCanvasView.swift
/tmp/ICCery-CPU/Sources/Controllers/PreviewWindowController.swift
```