Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
370 lines
16 KiB
Markdown
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
|
|
```
|
|
|
|
---
|
|
|