Files
iccery-v2-mac/docs/11-print-macos.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

370 lines
16 KiB
Markdown

# 11 — macOS printing and ColorSync suppression
> Line numbers refer to ICCery v0.8.5 (`/tmp/ICCery` at analysis time) and the Gronod ArgyllCMS 3.5.0 fork.
This is the most implementation-sensitive chapter. A rewrite that opens System Settings or the CUPS web UI instead of `NSPrintPanel` will regress #188.
## macOS (`src-tauri/src/print/macos.rs`) — MOST DETAIL
864 lines. This is the ColorSync-suppression engine built for #92 and rewritten for #188.
### What Preferences opens — NSPrintPanel vs CUPS web UI vs System Settings
**Issue #188 (user report):** clicking Preferences opened **System Settings → Printers & Scanners** and did not bind the selected Epson/Canon queue. No driver PDE, so no "Off (No Color Adjustment)".
**Current code (AGENTS.md:105-118, macos.rs:245-516, 679-710):** Preferences opens a **native AppKit `NSPrintPanel`**, pre-bound to the selected CUPS destination via Core Printing. It is **not**:
- CUPS web UI (`http://localhost:631/printers/…`)
- System Settings → Printers & Scanners
- `NSWorkspace` open of the printer
- `lpoptions` GUI
Linux/Windows do not share this panel. Default button title is **"Use Settings"** (macos.rs:439) — this is a settings-capture dialog, not a print-now dialog. Actual spooling is a later `lp` invocation.
Must run on the Cocoa main thread. `show_printer_properties` (macos.rs:689-710):
1. Resolve `display_name` **off** the main thread via `unix::get_printer_display_name` (`lpoptions -p`).
2. `app.run_on_main_thread``run_native_print_panel`.
3. `tokio::sync::oneshot` awaits the result.
### objc2 crates (`Cargo.toml:42-47`, AGENTS.md:120-126)
```
objc2 0.6 MainThreadMarker
objc2-app-kit 0.3.2 features = ["NSPrintInfo", "NSPrintPanel", "NSPrinter"]
objc2-foundation 0.3.2 NSString
objc2-core-foundation 0.3.2 CFString, CFType
objc2-application-services 0.3.2 features = ["PMCore", "PMDefinitions", "PrintCore"]
PMPrintSession, PMPrinter, PMPrintSettings, PMPageFormat, …
```
Local `extern "C" { fn free; fn dlsym; }`. No `libloading` crate.
### Binding the selected printer (macos.rs:276-347)
CUPS destination IDs ≠ NSPrinter display names. Core Printing "Printer ID" **is** the CUPS queue name.
```
printer_id_cf = CFString(printer_name)
pm_printer = PMPrinterCreateFromPrinterID(&printer_id_cf)
if pm_printer is null:
fallback NSPrinter::printerWithName(display_name)
print_info.setPrinter(ns_printer)
print_info.setUpPrintOperationDefaultValues()
if that also fails → Err("No printer found for '…'")
pm_session = print_info.PMPrintSession()
pm_settings = print_info.PMPrintSettings()
pm_page_format = print_info.PMPageFormat()
if pm_printer was created from ID:
PMSessionSetCurrentPMPrinter(pm_session, pm_printer) // non-zero → Err
PMSessionDefaultPrintSettings(pm_session, pm_settings) // warn on fail
PMSessionDefaultPageFormat(pm_session, pm_page_format) // warn on fail
```
`Printer.display_name` is the CUPS `printer-info` cached at enumeration. That is what System Settings shows ("Epson XP-55 Series") while `printer_name` is the queue id (`EPSON_XP_55_Series`). #188 failed because the old path used the wrong name with the wrong API.
`PMPrinter` from `PMPrinterCreateFromPrinterID` is released with `PMRelease` on all exit paths (cancel, error, success). The NSPrinter fallback path leaves `pm_printer` null so no release.
### ColorSync suppression strategy — UI click to `lp`
End-to-end, **six independent layers**. All of them exist because no single Apple API is sufficient across Epson PDE / Canon PDE / `cgpdftoraster` / CUPS.
```
[Preferences click]
show_printer_properties
run_on_main_thread
run_native_print_panel
① PMSessionSetCurrentPMPrinter bind queue
② set_session_color_matching_mode SPI gray out PDE Color Matching
③ PMPrintSettingsSetValue AP_ColorMatchingMode + dotted
④ detect_driver_color_bypass → SetValue pre-select Canon/Epson/Gutenprint "off"
⑤ NSPrintInfo.printSettings dictionary same keys for AppKit PDEs
NSPrintPanel.runModalWithPrintInfo
user picks media / quality (color locked)
⑥ PMPrintSettingsToOptions → filter → PrintPropertiesResult
[frontend]
capturedCupsOptions[printer] = cups_options
[Print Target]
print_target_native → macos::print_target → build_lp_args
ALWAYS -o AP_ColorMatchingMode=AP_ApplicationColorMatching
ALWAYS -o AP.ColorMatchingMode=AP_ApplicationColorMatching
THEN captured cups_options as -o k=v
THEN media_type if not already present (detected key)
THEN detect_driver_color_bypass if no color-bypass key yet
THEN orientation / PageSize if not already present
lp -d <queue> -t "ICCery Target - …" … <tiff>
```
Linux uses `-o raw` instead of AP_* flags. macOS **does not** use `-o raw`: a raw queue would skip the raster filter that actually understands `AP_ColorMatchingMode`. The macOS strategy is "tell the filter the application already matched color", not "skip the filter".
### Layer ② — private SPI via `dlsym` (`set_session_color_matching_mode`, macos.rs:32-130)
Used by Photoshop, Lightroom, X-Rite i1Profiler to gray-out Color Matching in driver PDEs. Undocumented; resolved at runtime so the binary does not hard-link a private symbol.
```
RTLD_DEFAULT = (-2isize) as *mut c_void
dlsym(RTLD_DEFAULT, "PMSessionSetColorMatchingModeLock")
dlsym(RTLD_DEFAULT, "PMSessionSetColorMatchingMode")
dlsym(RTLD_DEFAULT, "PMSessionSetColorMatchingModeNoLock")
```
**Signature (all three):**
```c
OSStatus PMSessionSetColorMatchingModeLock (PMPrintSession, CFStringRef mode);
OSStatus PMSessionSetColorMatchingMode (PMPrintSession, CFStringRef mode);
OSStatus PMSessionSetColorMatchingModeNoLock(PMPrintSession, CFStringRef mode);
```
Rust:
```rust
type ModeFn = unsafe extern "C" fn(PMPrintSession, *const CFString) -> i32;
```
`status == 0` is success (`noErr`).
**Priority:**
1. `PMSessionSetColorMatchingModeLock` — sets **and locks** the UI in one call (controls grayed).
2. `PMSessionSetColorMatchingMode` — sets mode; lock behaviour unspecified.
3. `PMSessionSetColorMatchingModeNoLock` — sets without locking.
For each symbol, try mode strings in order:
```
"AP_ApplicationColorMatching" // kPMApplicationColorMatching
"ApplicationColorMatching" // unprefixed alias
```
First `(symbol, mode)` that returns 0 wins; then break both loops.
**Preconditions:** `pm_session` non-null; `PMSessionGetCurrentPrinter` succeeds with a non-null printer. Otherwise skip SPI (session has no printer to attach a PDE to).
**Stub on non-macOS:** always `false` so unit tests compile.
If all fail: log `"All PMSessionSetColorMatchingMode calls failed; color controls may not be grayed"` and fall through to public `PMPrintSettingsSetValue`.
### Why `AP_ColorSyncMatching` and `AP_VendorColorMatching` are avoided
AGENTS.md:118, macos.rs:82-88:
> Only application-managed color-matching modes are appropriate for profiling targets. ColorSync or vendor modes would apply color management and corrupt the target patches.
| Mode string | Effect on a profiling target |
|-------------|------------------------------|
| `AP_ApplicationColorMatching` / `ApplicationColorMatching` | **Used.** "Application already did color." Filters/PDEs must not transform pixels. UI Color Matching grayed. |
| `AP_ColorSyncMatching` | **Avoided.** ColorSync applies the printer/display profile. Patches become color-managed. |
| `AP_VendorColorMatching` | **Avoided.** Epson/Canon driver color engine (ICM inside the PDE). Same corruption. |
ICCery-CPU uses a **different** vocabulary (`APCustomColorMatching` / `APColorSync` / `APPrinterExtension` on `PMColorMatchingMode`). See the CPU section. Do not mix the two dictionaries.
### Layer ③ — `PMPrintSettingsSetValue` (macos.rs:357-375)
```
key = "AP_ColorMatchingMode" and "AP.ColorMatchingMode"
val = "AP_ApplicationColorMatching"
lock = true
PMPrintSettingsSetValue(pm_settings, key, val, true)
```
The dotted `AP.ColorMatchingMode` is a **legacy form used by some raster drivers and PDEs** (macos.rs:540-541). Both are always written.
### Layer ④ — pre-select driver "no color adjustment" in the panel (macos.rs:377-397, 422-432)
`lpoptions -p <printer> -l``unix::detect_driver_color_bypass`:
| Driver | Key | Value |
|--------|-----|-------|
| Canon | `CNIJIntent2` | `4` |
| Canon (old) | `CNIJIntent` | `4` |
| Epson | `EPIJ_CCor` | `0` (if that key exists, preferred) |
| Epson | `EPIJ_CMat` | `3` |
| Gutenprint | `StpColorCorrection` | `Uncorrected` |
| generic | `ColorCorrection` | `Uncorrected` |
| Epson alt | `EpsonColorMode` | `Off` |
Written twice: `PMPrintSettingsSetValue(..., lock=false)` **and** `print_info.printSettings().insert`. Lock is false so the user can still change media/quality; only color matching is intended to be locked by the SPI.
### Layer ⑤ — `NSPrintInfo.printSettings` dictionary (macos.rs:399-432)
AppKit PDEs and some raster drivers read this `NSMutableDictionary`, not the PM object.
```
print_info.updateFromPMPageFormat()
print_info.updateFromPMPrintSettings()
print_settings.insert("AP_ColorMatchingMode", "AP_ApplicationColorMatching")
print_settings.insert("AP.ColorMatchingMode", "AP_ApplicationColorMatching")
print_settings.insert(<bypass_key>, <bypass_val>)
```
`NSString` is transmuted to `&AnyObject` for the dictionary (`macos.rs:414-416`).
### Panel options (macos.rs:434-449)
```
NSPrintPanel::printPanel(mtm)
opts = NSPrintPanelOptions::all()
opts.insert(ShowsPageSetupAccessory)
panel.setOptions(opts)
panel.setDefaultButtonTitle(Some("Use Settings"))
response = panel.runModalWithPrintInfo(&print_info)
```
`response != 1` (`NSModalResponseOK` / `NSOKButton`) → **`Ok(None)`** (cancellation is not an error). `PMPrinter` released.
### Layer ⑥ — `PMPrintSettingsToOptions` capture (macos.rs:452-515)
After OK:
1. `PMSessionGetCurrentPrinter``PMPrinterGetID``selected_printer` string.
2. `panel.printInfo().PMPrintSettings()`.
3. `PMPrintSettingsToOptions(updated_settings, &mut opts_ptr)` — CUPS malloc'd C string of `key=value key=value …`.
4. Copy via `CStr`, `free(opts_ptr)`.
5. `filter_cups_options_string``cups_options`.
6. `extract_media_type_from_options``media_type`.
7. Return `PrintPropertiesResult { selected_printer, options: PrintOptions { media_type, cups_options, ppd_uncorrected_passthrough: Some(true), ..Default } }`.
Failure of `PMPrintSettingsToOptions` is a hard `Err`.
### `RELEVANT_CUPS_OPTION_KEYS` (macos.rs:142-174)
Forwarded from the panel to `lp`:
```
Media: MediaType, CNIJMediaType, EPIJ_Medi, StpMediaType
Tray: InputSlot, AP_D_InputSlot
Size: PageSize
Color: CNIJIntent2, CNIJIntent, EPIJ_CMat, EPIJ_CCor, EPIJ_OSColMat,
ColorCorrection, StpColorCorrection, EpsonColorMode, ColorModel
Quality: Resolution, cupsPrintQuality, Quality, EPIJ_Quality, CNIJQuality,
StpQuality, OutputMode
Duplex: Duplex, sides
```
`is_relevant_cups_option` (macos.rs:178-204) additionally:
- Drops empty keys
- Drops `com.apple.*` ticket keys
- Drops `AP_ColorMatchingMode` and `AP.ColorMatchingMode` (we always set those ourselves)
- Drops empty values (`AP_D_InputSlot=`)
- Drops `collate`, `copies`, `pserrorhandler-requested`, `job-sheets`
- **Keeps unknown non-`com.*` keys** (permissive: unknown driver keys survive)
### `build_lp_args` (macos.rs:518-643)
Always, even with `options=None`:
```
lp -d <printer> -t "ICCery Target - <filename>"
-o AP_ColorMatchingMode=AP_ApplicationColorMatching
-o AP.ColorMatchingMode=AP_ApplicationColorMatching
… captured / detected options …
<tiff_path>
```
Order after the two AP_* flags:
1. Parse `opts.cups_options` into `-o k=v`, record lowercased keys in `added_keys`.
2. If `media_type` set and none of `mediatype` / `cnijmediatype` / `epij_medi` / `stpmediatype` already added: `detect_media_type_key(lpoptions)` and add it.
3. If no color-bypass key yet (`cnijintent2`, `cnijintent`, `epij_cmat`, `epij_ccor`, `epij_oscolmat`, `colorcorrection`, `stpcolorcorrection`, `epsoncolormode`): `detect_driver_color_bypass` and add. **Not gated on `ppd_uncorrected_passthrough`.**
4. Orientation → `orientation-requested=4|3` unless already present.
5. `PageSize=` unless `pagesize` already present.
`ppd_uncorrected_passthrough` is stored from the panel but **does not change macOS lp flags**. There is no `-o raw` on macOS.
### `print_target` (macos.rs:646-677)
Exists-check, `build_lp_args`, `Command::new("lp").args(&args).output()`. Error wrapping same pattern as Unix (`"macOS CUPS print job failed: …"`).
### Cancellation as `None`
`run_native_print_panel``Ok(None)` on cancel. `show_printer_properties` propagates that. Frontend: `if (result === null) { showNotification("info", "Printer properties dialog cancelled."); return; }`.
### Preferences crash / #188
#188 was not a segfault; it was a **wrong UI**: System Settings instead of the driver PDE, so color management could not be disabled on Epson XP-55 / Canon Pro 9500 II.
The rewrite:
- Bind by CUPS Printer ID (`PMPrinterCreateFromPrinterID` + `PMSessionSetCurrentPMPrinter`), not by opening System Settings.
- `display_name` fallback for `NSPrinter::printerWithName`.
- Private SPI to lock Color Matching.
- Dual AP_* keys (underscore + dotted).
- Driver-specific PPD bypass pre-selected and re-applied on `lp`.
- Capture via `PMPrintSettingsToOptions` into `capturedCupsOptions`.
`PMPrinter` lifetime is explicit `PMRelease` on every path. SPI is `dlsym`'d so missing symbols on old OS X do not prevent launch. Panel **must** be main-thread (`MainThreadMarker::new().ok_or("Print panel must be invoked on the main thread")`).
### macOS tests (macos.rs:712-863 + tests.rs:278-318)
- Filter drops `com.apple.*`, `collate`, `copies`, `AP_ColorMatchingMode`, empty `AP_D_InputSlot`; keeps `MediaType`, `EPIJ_CMat`, `PageSize`, `CNIJIntent2`, `ColorCorrection`.
- `extract_media_type_from_options` prefers `MediaType` then `EPIJ_Medi`.
- `build_lp_args` always contains both AP_* flags; captured options win over explicit `media_type` / orientation / auto color-bypass; last arg is the TIFF path.
- `detect_driver_color_bypass` Canon `4`, Epson `3`, Gutenprint `Uncorrected`.
- Missing TIFF errors.
---
## ColorSync suppression — complete key/SPI/flag roster
### Private SPI symbols (dlsym, macOS only)
```
PMSessionSetColorMatchingModeLock (PMPrintSession, CFStringRef) -> i32
PMSessionSetColorMatchingMode (PMPrintSession, CFStringRef) -> i32
PMSessionSetColorMatchingModeNoLock (PMPrintSession, CFStringRef) -> i32
```
Mode strings attempted: `AP_ApplicationColorMatching`, `ApplicationColorMatching`.
Public Core Printing used around the SPI:
```
PMPrinterCreateFromPrinterID
PMSessionSetCurrentPMPrinter
PMSessionGetCurrentPrinter
PMSessionDefaultPrintSettings
PMSessionDefaultPageFormat
PMPrintSettingsSetValue
PMPrintSettingsToOptions
PMPrinterGetID
PMRelease
```
AppKit: `NSPrintInfo`, `NSPrintPanel`, `NSPrinter::printerWithName`, `NSPrintPanelOptions::all` + `ShowsPageSetupAccessory`.
### CUPS / lp flags
| Flag | Platform | When |
|------|----------|------|
| `-o AP_ColorMatchingMode=AP_ApplicationColorMatching` | macOS | always |
| `-o AP.ColorMatchingMode=AP_ApplicationColorMatching` | macOS | always |
| `-o CNIJIntent2=4` | macOS (auto) / if captured | Canon |
| `-o CNIJIntent=4` | macOS auto if no Intent2 | Canon old |
| `-o EPIJ_CCor=0` | macOS auto if present in lpoptions | Epson |
| `-o EPIJ_CMat=3` | macOS auto | Epson Off |
| `-o StpColorCorrection=Uncorrected` | macOS auto | Gutenprint |
| `-o ColorCorrection=Uncorrected` | macOS auto | generic |
| `-o EpsonColorMode=Off` | macOS auto | Epson alt |
| `-o CNIJMediaType=` / `EPIJ_Medi=` / `StpMediaType=` / `MediaType=` | macOS (detected key); Linux always `MediaType=` | media |
| `-o PageSize=` | macOS/Linux | if set |
| `-o orientation-requested=3\|4` | macOS/Linux | portrait/landscape |
| `-o raw` | **Linux only** | default |
| `-o ColorModel=Gray -o cm-calibration` | **Linux only** | PPD fallback checkbox |
| captured `InputSlot`, `AP_D_InputSlot`, `Resolution`, `cupsPrintQuality`, `Quality`, `EPIJ_Quality`, `CNIJQuality`, `StpQuality`, `OutputMode`, `Duplex`, `sides`, `EPIJ_OSColMat`, `ColorModel` | macOS | if panel produced them |
### PPD keys (detection)
```
Media: CNIJMediaType, EPIJ_Medi, StpMediaType, MediaType, MediaSource, InputSlot, PageSize, MediaSize
Color: CNIJIntent2, CNIJIntent, EPIJ_CCor, EPIJ_CMat, StpColorCorrection, ColorCorrection, EpsonColorMode
```
---