[Bug/Critical] Printer quality and media type settings ignored when printing target — lp path missing PMPrintSettings ticket #201

Closed
opened 2026-09-17 09:01:00 +01:00 by gronod · 1 comment
Owner

Summary

When printing profiling targets via the lp CLI (the current spooling path in PrintSessionViewModel.swift / CupsService.printTarget), printer drivers such as the Epson raster filter ignore the media type and print quality selections. The printed target falls back to plain paper and normal quality, which corrupts the profiling data.

Root Cause Analysis

Following investigation of the macOS printing pipeline (PrintPanelService, CupsParsers, LpArgs), the defect originates at the driver boundary:

  1. Missing Apple Print Ticket — ICCery captures NSPrintPanel properties into standard CUPS string options (e.g., EPIJ_Qual=3) and passes them to lp -o. However, lp on macOS only creates an IPP job with string attributes; it does not generate the native com.apple.print.PrintSettings ticket (a serialized CoreFoundation plist containing vendor PDE state).
  2. Vendor PDEs depend on the Print Ticket — vendor raster filters (like Epson's) read the PMPrintSettings dictionary to configure hardware parameters (ink density, media type, quality, ColorSync bypass). Because lp omits this ticket, the driver ignores the CUPS string options and applies defaults (plain paper, standard quality, standard color management).
  3. UI State Desync (secondary) — in LpArgs.build (lines 59–105), captured CUPS options silently override explicit Stage 2 selections. If a user sets "Glossy" in the Preferences panel then changes the Stage 2 inline dropdown to "Matte", LpArgs prioritizes the captured EPIJ_Medi key and ignores the inline change.

What was checked and dismissed

  • Value space splitting — a hypothesis that -o values with spaces were incorrectly split by shell tokenization was dismissed. ProcessManager.runCaptured passes argv directly to Process without shell evaluation, so space-containing arguments are safely preserved.
  • ColorSync override — AP_ColorMatchingMode=AP_ApplicationColorMatching is correctly passed, but without the print ticket its effectiveness relies entirely on the driver parsing basic IPP attributes.

Proposed Fix (Deterministic)

Replace the lp spooler with a headless NSPrintOperation in CupsService (bringing forward the Quartz rendering architecture from #16).

  1. Replace lp spooling with Quartz/NSPrintOperation
    • Introduce a native macOS print function in CupsService that avoids lp.
    • Read the TIFF using CGImageSource (disable interpolation and antialiasing, respecting 1:1 points-to-pixels based on DPI, per docs/14-iccery-cpu-targetprint.md).
    • Wrap drawing in an NSView subclass implementing knowsPageRange and rectForPage, ensuring isFlipped returns true for correct coordinate mapping.
  2. Rehydrate NSPrintInfo with captured settings
    • Store PMPrintSettings (or its serialized Data/NSDictionary) natively in memory when the user clicks "Use Settings" in NSPrintPanel, instead of flattening via PMPrintSettingsToOptions (which loses opaque PDE binary data).
    • When printing, create an NSPrintInfo, bind it to the queue, and restore the saved PMPrintSettings dictionary.
    • Apply inline Stage 2 overrides (media, quality, orientation) directly into NSPrintInfo.printSettings before spooling.
  3. Spool the job
    • Create an NSPrintOperation with the configured NSPrintInfo and target NSView.
    • Set showsPrintPanel = false and showsProgressPanel = true (or custom UI) to execute on the Main Actor.
  4. Deprecate lpoptions mapping
    • The fragile CupsParsers logic manually mapping driver bypass keys (CNIJIntent2, EPIJ_CMat) can be phased out, as the native PMPrintSettings dictionary retains these when captured from NSPrintPanel.

Note: #16 covers the full migration to ICCeryPrintKit, but fixing this defect for v2.0 requires replacing lp with NSPrintOperation in CupsService.

Acceptance Criteria

  • Printed targets honor the media type and print quality selected in NSPrintPanel / Stage 2 (verified against an Epson raster driver, e.g., EPIJ_Qual, EPIJ_Medi).
  • The native com.apple.print.PrintSettings ticket is present on the spooled job (vendor PDE state preserved), verified via executing sudo cupsctl --debug-logging followed by sudo grep 'com.apple.print.PrintSettings' /var/log/cups/error_log.
  • Stage 2 inline overrides take precedence over captured CUPS options (no silent desync).
  • No lp invocation remains on the target-print path; TIFF rendering is 1:1 points-to-pixels with interpolation/antialiasing disabled.
  • Device color space mapping verified physically via chart reads for 8-bit RGB, 16-bit RGB, 8-bit DeviceGray, and 16-bit CMYK.

Dependencies

  • Related: #16 (full ICCeryPrintKit Quartz migration)

References

  • Analysis doc: docs/issue-printer-quality-ignored.md
  • Packages/ICCeryCore/Sources/ICCeryCore/Print/CupsService.swift
  • Packages/ICCeryCore/Sources/ICCeryCore/Print/LpArgs.swift
  • Packages/ICCeryCore/Sources/ICCeryCore/Print/CupsParsers.swift
  • Packages/ICCeryCore/Sources/ICCeryCore/Print/PrintPanelService.swift
  • Spec: docs/14-iccery-cpu-targetprint.md
## Summary When printing profiling targets via the `lp` CLI (the current spooling path in `PrintSessionViewModel.swift` / `CupsService.printTarget`), printer drivers such as the Epson raster filter ignore the media type and print quality selections. The printed target falls back to plain paper and normal quality, which corrupts the profiling data. ## Root Cause Analysis Following investigation of the macOS printing pipeline (`PrintPanelService`, `CupsParsers`, `LpArgs`), the defect originates at the driver boundary: 1. **Missing Apple Print Ticket** — ICCery captures `NSPrintPanel` properties into standard CUPS string options (e.g., `EPIJ_Qual=3`) and passes them to `lp -o`. However, `lp` on macOS only creates an IPP job with string attributes; it does **not** generate the native `com.apple.print.PrintSettings` ticket (a serialized CoreFoundation plist containing vendor PDE state). 2. **Vendor PDEs depend on the Print Ticket** — vendor raster filters (like Epson's) read the `PMPrintSettings` dictionary to configure hardware parameters (ink density, media type, quality, ColorSync bypass). Because `lp` omits this ticket, the driver ignores the CUPS string options and applies defaults (plain paper, standard quality, standard color management). 3. **UI State Desync (secondary)** — in `LpArgs.build` (lines 59–105), captured CUPS options silently override explicit Stage 2 selections. If a user sets "Glossy" in the Preferences panel then changes the Stage 2 inline dropdown to "Matte", `LpArgs` prioritizes the captured `EPIJ_Medi` key and ignores the inline change. ### What was checked and dismissed - **Value space splitting** — a hypothesis that `-o` values with spaces were incorrectly split by shell tokenization was dismissed. `ProcessManager.runCaptured` passes `argv` directly to `Process` without shell evaluation, so space-containing arguments are safely preserved. - **ColorSync override** — `AP_ColorMatchingMode=AP_ApplicationColorMatching` is correctly passed, but without the print ticket its effectiveness relies entirely on the driver parsing basic IPP attributes. ## Proposed Fix (Deterministic) Replace the `lp` spooler with a headless `NSPrintOperation` in `CupsService` (bringing forward the Quartz rendering architecture from #16). 1. **Replace `lp` spooling with Quartz/`NSPrintOperation`** - Introduce a native macOS print function in `CupsService` that avoids `lp`. - Read the TIFF using `CGImageSource` (disable interpolation and antialiasing, respecting 1:1 points-to-pixels based on DPI, per `docs/14-iccery-cpu-targetprint.md`). - Wrap drawing in an `NSView` subclass implementing `knowsPageRange` and `rectForPage`, ensuring `isFlipped` returns `true` for correct coordinate mapping. 2. **Rehydrate `NSPrintInfo` with captured settings** - Store `PMPrintSettings` (or its serialized `Data`/`NSDictionary`) natively in memory when the user clicks "Use Settings" in `NSPrintPanel`, instead of flattening via `PMPrintSettingsToOptions` (which loses opaque PDE binary data). - When printing, create an `NSPrintInfo`, bind it to the queue, and restore the saved `PMPrintSettings` dictionary. - Apply inline Stage 2 overrides (media, quality, orientation) directly into `NSPrintInfo.printSettings` before spooling. 3. **Spool the job** - Create an `NSPrintOperation` with the configured `NSPrintInfo` and target `NSView`. - Set `showsPrintPanel = false` and `showsProgressPanel = true` (or custom UI) to execute on the Main Actor. 4. **Deprecate `lpoptions` mapping** - The fragile `CupsParsers` logic manually mapping driver bypass keys (`CNIJIntent2`, `EPIJ_CMat`) can be phased out, as the native `PMPrintSettings` dictionary retains these when captured from `NSPrintPanel`. *Note: #16 covers the full migration to `ICCeryPrintKit`, but fixing this defect for v2.0 requires replacing `lp` with `NSPrintOperation` in `CupsService`.* ## Acceptance Criteria - [ ] Printed targets honor the media type and print quality selected in `NSPrintPanel` / Stage 2 (verified against an Epson raster driver, e.g., `EPIJ_Qual`, `EPIJ_Medi`). - [ ] The native `com.apple.print.PrintSettings` ticket is present on the spooled job (vendor PDE state preserved), verified via executing `sudo cupsctl --debug-logging` followed by `sudo grep 'com.apple.print.PrintSettings' /var/log/cups/error_log`. - [ ] Stage 2 inline overrides take precedence over captured CUPS options (no silent desync). - [ ] No `lp` invocation remains on the target-print path; TIFF rendering is 1:1 points-to-pixels with interpolation/antialiasing disabled. - [ ] Device color space mapping verified physically via chart reads for 8-bit RGB, 16-bit RGB, 8-bit DeviceGray, and 16-bit CMYK. ## Dependencies - **Related:** #16 (full `ICCeryPrintKit` Quartz migration) ## References - Analysis doc: `docs/issue-printer-quality-ignored.md` - `Packages/ICCeryCore/Sources/ICCeryCore/Print/CupsService.swift` - `Packages/ICCeryCore/Sources/ICCeryCore/Print/LpArgs.swift` - `Packages/ICCeryCore/Sources/ICCeryCore/Print/CupsParsers.swift` - `Packages/ICCeryCore/Sources/ICCeryCore/Print/PrintPanelService.swift` - Spec: `docs/14-iccery-cpu-targetprint.md`
gronod added this to the M11 — Printer settings completeness & dialog binding milestone 2026-09-17 09:01:00 +01:00
gronod added the Kind/Bug
Priority
Critical
1
Project/ICCery-v2Bug/Backend
labels 2026-09-17 09:01:00 +01:00
gronod modified the milestone from M11 — Printer settings completeness & dialog binding to M12 — Native print spool 2026-09-17 11:18:18 +01:00
Author
Owner

Architectural Decisions & Errata (M12 Megaplan)

Following architectural review against the macOS 12 Monterey baseline (plan-be303e6f3f5e89da.md), the following locked decisions supersede earlier notes:

  1. App-Target Placement (D1): The native NSPrintOperation spooler will reside in the application target (Sources/ICCery/Print/), not in CupsService. ICCeryCore remains strictly AppKit-free by contract. CupsService.printTarget will be deprecated and removed.
  2. Retaining Parsers (D4): CupsParsers and CupsOptionsFilter are retained; they remain required for printer capabilities, media/quality detection, and Stage 2 preference mirroring. Only LpArgs is deprecated and removed.
  3. Dual ColorSync Vocabulary (D2): The native spool path will write both the AP_* keys and the Quartz ColorSync keys (PMColorMatchingMode=APCustomColorMatching, PMCustomColorMatchingProfile="", and legacy dictionary keys) simultaneously.
  4. Scope Clarifications:
    • Adds user-selectable job granularity (single multi-page spool job vs. one job per page, defaulting to one job per page) (D5).
    • Mandates device-colour-space verification for CMYK 8-bit and RGB 16-bit target rasters (D10).
    • AirPrint detection and warning banner are decoupled into a dedicated companion issue blocked by #201 (D11).
### Architectural Decisions & Errata (M12 Megaplan) Following architectural review against the macOS 12 Monterey baseline (`plan-be303e6f3f5e89da.md`), the following locked decisions supersede earlier notes: 1. **App-Target Placement (D1):** The native `NSPrintOperation` spooler will reside in the application target (`Sources/ICCery/Print/`), not in `CupsService`. `ICCeryCore` remains strictly AppKit-free by contract. `CupsService.printTarget` will be deprecated and removed. 2. **Retaining Parsers (D4):** `CupsParsers` and `CupsOptionsFilter` are retained; they remain required for printer capabilities, media/quality detection, and Stage 2 preference mirroring. Only `LpArgs` is deprecated and removed. 3. **Dual ColorSync Vocabulary (D2):** The native spool path will write both the AP_* keys and the Quartz ColorSync keys (`PMColorMatchingMode=APCustomColorMatching`, `PMCustomColorMatchingProfile=""`, and legacy dictionary keys) simultaneously. 4. **Scope Clarifications:** - Adds user-selectable job granularity (single multi-page spool job vs. one job per page, defaulting to one job per page) (D5). - Mandates device-colour-space verification for CMYK 8-bit and RGB 16-bit target rasters (D10). - AirPrint detection and warning banner are decoupled into a dedicated companion issue blocked by #201 (D11).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/iccery-v2-mac#201