[Bug/High] Printer properties dialog reopens on driver defaults — captured ticket never restored, pre-selections unvalidated #217

Closed
opened 2026-09-22 16:34:53 +01:00 by gronod · 0 comments
Owner

Summary

The Stage 2 printer-properties dialog (gear button → bound NSPrintPanel) always opens on the queue's driver defaults. Stage 2 selections are not presented, and — more importantly — vendor PDE state captured in a previous dialog session is discarded. Printed output is unaffected: TicketWriteResolver applies Stage 2 overrides unconditionally at spool time (#201 D6). This is a dialog-fidelity and state-loss defect, not an output defect.

Observed on an Epson XP-55 (InkjetPrinter2 driver family), macOS:

  1. Settings chosen in Stage 2 are honoured by the actual print, but the driver dialog shows driver defaults rather than the chosen settings.
  2. Changes made inside the driver dialog do apply back to the Stage 2 mirror — the capture path works.
  3. Reopening the dialog shows defaults again; even vendor settings the driver itself just wrote are gone.
  4. The panel's "Last Used Settings" preset restores only the paper source (InputSlot); every other vendor setting reverts.

Follow-up to #186 (dialog binding/pre-select) in the light of #201's ticket machinery, which that issue predates.

Root Cause Analysis

Line numbers: develop @ f7f6a8b.

RC1 — the captured PrintTicket is never restored when the panel opens (primary).
PrintPanelService.runNativePanel (Sources/ICCery/Print/PrintPanelService.swift:90-118) builds a fresh NSPrintInfo, binds the queue (PMTicketBridge.bind, PMTicketBridge.swift:87-97), and seeds PMSessionDefaultPrintSettings + PMSessionDefaultPageFormat — queue defaults. It then pre-writes only four keys (PageSize, detected media key, detected quality key, orientation-requested; applyInitialSelections, PrintPanelService.swift:214-244) plus PMPageFormat paper (PMTicketBridge.applyPaper, :319-372).

PMTicketBridge.restore (:166-229) — the mechanism that rehydrates a captured ticket — is invoked only by NativeTargetSpooler S4 (NativeTargetSpooler.swift:162-166). PrintSessionViewModel.openPrinterPreferences stores the ticket into capturedTickets[queue] (:259-261) but showProperties has no ticket parameter (PrintPanelService.swift:56-80) — it is never passed back.

Every vendor-PDE setting outside the four mirrored fields lives solely in that discarded ticket → the next open reverts to defaults. This fully explains "changes made in the driver are reflected in ICCery but revert when the dialog is reopened".

RC2 — pre-selection writes are never validated into the ticket.
After applyInitialSelections, neither PMSessionValidatePrintSettings nor printInfo.updateFromPMPrintSettings() runs before runModal (PrintPanelService.swift:106-154; contrast restore at PMTicketBridge.swift:197-203 and the spooler at NativeTargetSpooler.swift:187). The vendor PDE opens on an unreconciled ticket; drivers that derive pane state from validated/companion keys can present defaults even for the four keys we do write.

RC3 — why InputSlot alone survives "Last Used Settings" (hypothesis — verify on hardware).
PMSessionDefaultPrintSettings sources persisted queue defaults (lpoptions / org.cups.PrintingPrefs), where InputSlot/MediaSource lands when the driver persists it; vendor PDE keys live only in the discarded PMPrintSettings ticket. Related evidence: MediaQualityConstraints evaluates Epson *EPIJUIConstraint LHS keys (EPIJ_PSrc, EPIJ_FdSo) against lpoptions defaults specifically to "reproduce the driver's PDE state when it opens" (MediaQualityConstraints.swift:11-14) — Epson's PDE open-state is at least partially lpoptions-derived, which may limit how much a restored ticket alone fixes. Verify: lpoptions -p <queue> before vs after a driver session; compare against a PMPrintSettingsToOptions dump of the pre-open ticket.

Checked and dismissed:

  • Stage 2 → spool delivery — works: makeRequest → TicketWriteResolver writes unconditionally (D6); prints honour Stage 2 picks.
  • Capture-back — works: layer ⑥ PMPrintSettingsToOptions + mirror (openPrinterPreferences :255-284) demonstrably returns driver changes to Stage 2.
  • Individual PMPrintSettingsSetValue rejection — logged warn-only; cannot explain wholesale defaults including state the driver itself set last session.

Proposed Fix

Restore-then-override on the panel path, mirroring the spooler's S3–S5 order (docs/11 §native spool):

  1. PrintSessionViewModel.openPrinterPreferences passes capturedTickets[queue] into showProperties (new optional ticket: parameter).
  2. runNativePanel order: bind (PMSessionSetCurrentPMPrinter + PMSessionDefault*) → PMTicketBridge.restore(ticket) when present (cross-queue refusal already built in, R3) → applyInitialSelections (Stage 2 wins over the ticket — same rule as D6) → PMSessionValidatePrintSettings + printInfo.updateFromPMPrintSettings() (warn-only; lets the driver reconcile companion keys) → applyPaper → suppression layers ②–⑤ → runModal.
  3. Extract the pre-modal configuration into a testable helper (same pattern as NativeTargetSpooler.makePrintInfo) so unit tests can assert the ticket is honoured without driving the modal.
  4. Diagnostics for hardware verification: DEBUG env flag (e.g. ICCERY_DEBUG_PANEL_DUMP=1) logging the PMPrintSettingsToOptions output and serialised ticket size immediately before runModal and after OK.
  5. Restore is within-session only — PrintTicket is deliberately session-only (queue- and driver-version-specific; PrintTicket.swift:8-9). Cross-launch persistence of driver state is out of scope.

Contingency (only if hardware verification shows the Epson PDE sources pane state from lpoptions defaults rather than the session ticket — see RC3): persist the Stage 2 mirror via lpoptions -p <queue> -o … before opening the panel. This mutates user-visible queue defaults — discuss before doing it.

Fallback (when no ticket exists for the queue but capturedCupsOptions[queue] does): optionally replay the captured k=v pairs as PMPrintSettingsSetValue writes beneath the explicit selections.

Acceptance Criteria

  • Reopening the properties dialog after a capture presents the captured driver state — vendor PDE settings persist across opens within the session
  • Stage 2 selections still win over the restored ticket (paper/media/quality/orientation pre-selected)
  • First-ever open (no ticket) keeps current behaviour: queue defaults + Stage 2 selections
  • Capture-back (driver → Stage 2 mirror + ticket re-serialise) unchanged
  • Unit tests: ticket restore invoked pre-modal (via the extracted seam), validate called after pre-writes, cross-queue ticket refused
  • Hardware (Epson XP-55): change a vendor-pane-only setting → OK → reopen → setting persists

Dependencies

None blocking. Related: #186 (original dialog binding/pre-select — this supersedes its residual symptom), #201 (introduced PrintTicket/restore, wired to the spooler only). Blocks #218 (paper-source feature) — both touch PrintPanelInitialSelections, runNativePanel, and TicketWriteResolver.

References

  • Sources/ICCery/Print/PrintPanelService.swift — runNativePanel (:84-208), applyInitialSelections (:214-244)
  • Sources/ICCery/Print/PMTicketBridge.swift — bind (:87-97), serialise (:121-156), restore (:166-229)
  • Sources/ICCery/Print/NativeTargetSpooler.swift — S3–S5 order (:147-188)
  • Sources/ICCery/Print/PrintSessionViewModel.swift — openPrinterPreferences (:223-294), capturedTickets (:45)
  • docs/11-print-macos.md §ColorSync suppression layers ①–⑦ / §native spool S1–S14
  • docs/issue-printer-quality-ignored.md — #201 analysis (D3, D6)
## Summary The Stage 2 printer-properties dialog (gear button → bound `NSPrintPanel`) always opens on the queue's driver defaults. Stage 2 selections are not presented, and — more importantly — vendor PDE state captured in a previous dialog session is discarded. Printed output is unaffected: `TicketWriteResolver` applies Stage 2 overrides unconditionally at spool time (#201 D6). This is a dialog-fidelity and state-loss defect, not an output defect. Observed on an Epson XP-55 (InkjetPrinter2 driver family), macOS: 1. Settings chosen in Stage 2 are honoured by the actual print, but the driver dialog shows driver defaults rather than the chosen settings. 2. Changes made inside the driver dialog do apply back to the Stage 2 mirror — the capture path works. 3. Reopening the dialog shows defaults again; even vendor settings the driver itself just wrote are gone. 4. The panel's "Last Used Settings" preset restores only the paper source (InputSlot); every other vendor setting reverts. Follow-up to #186 (dialog binding/pre-select) in the light of #201's ticket machinery, which that issue predates. ## Root Cause Analysis Line numbers: `develop` @ f7f6a8b. **RC1 — the captured `PrintTicket` is never restored when the panel opens (primary).** `PrintPanelService.runNativePanel` (`Sources/ICCery/Print/PrintPanelService.swift:90-118`) builds a fresh `NSPrintInfo`, binds the queue (`PMTicketBridge.bind`, `PMTicketBridge.swift:87-97`), and seeds `PMSessionDefaultPrintSettings` + `PMSessionDefaultPageFormat` — queue defaults. It then pre-writes only four keys (`PageSize`, detected media key, detected quality key, `orientation-requested`; `applyInitialSelections`, PrintPanelService.swift:214-244) plus `PMPageFormat` paper (`PMTicketBridge.applyPaper`, :319-372). `PMTicketBridge.restore` (:166-229) — the mechanism that rehydrates a captured ticket — is invoked only by `NativeTargetSpooler` S4 (`NativeTargetSpooler.swift:162-166`). `PrintSessionViewModel.openPrinterPreferences` stores the ticket into `capturedTickets[queue]` (:259-261) but `showProperties` has no ticket parameter (PrintPanelService.swift:56-80) — it is never passed back. Every vendor-PDE setting outside the four mirrored fields lives solely in that discarded ticket → the next open reverts to defaults. This fully explains "changes made in the driver are reflected in ICCery but revert when the dialog is reopened". **RC2 — pre-selection writes are never validated into the ticket.** After `applyInitialSelections`, neither `PMSessionValidatePrintSettings` nor `printInfo.updateFromPMPrintSettings()` runs before `runModal` (PrintPanelService.swift:106-154; contrast `restore` at PMTicketBridge.swift:197-203 and the spooler at NativeTargetSpooler.swift:187). The vendor PDE opens on an unreconciled ticket; drivers that derive pane state from validated/companion keys can present defaults even for the four keys we do write. **RC3 — why InputSlot alone survives "Last Used Settings" (hypothesis — verify on hardware).** `PMSessionDefaultPrintSettings` sources persisted queue defaults (lpoptions / org.cups.PrintingPrefs), where `InputSlot`/`MediaSource` lands when the driver persists it; vendor PDE keys live only in the discarded `PMPrintSettings` ticket. Related evidence: `MediaQualityConstraints` evaluates Epson `*EPIJUIConstraint` LHS keys (`EPIJ_PSrc`, `EPIJ_FdSo`) against lpoptions defaults specifically to "reproduce the driver's PDE state when it opens" (MediaQualityConstraints.swift:11-14) — Epson's PDE open-state is at least partially lpoptions-derived, which may limit how much a restored ticket alone fixes. Verify: `lpoptions -p <queue>` before vs after a driver session; compare against a `PMPrintSettingsToOptions` dump of the pre-open ticket. **Checked and dismissed:** - Stage 2 → spool delivery — works: `makeRequest` → `TicketWriteResolver` writes unconditionally (D6); prints honour Stage 2 picks. - Capture-back — works: layer ⑥ `PMPrintSettingsToOptions` + mirror (`openPrinterPreferences` :255-284) demonstrably returns driver changes to Stage 2. - Individual `PMPrintSettingsSetValue` rejection — logged warn-only; cannot explain wholesale defaults including state the driver itself set last session. ## Proposed Fix Restore-then-override on the panel path, mirroring the spooler's S3–S5 order (docs/11 §native spool): 1. `PrintSessionViewModel.openPrinterPreferences` passes `capturedTickets[queue]` into `showProperties` (new optional `ticket:` parameter). 2. `runNativePanel` order: bind (`PMSessionSetCurrentPMPrinter` + `PMSessionDefault*`) → **`PMTicketBridge.restore(ticket)` when present** (cross-queue refusal already built in, R3) → `applyInitialSelections` (Stage 2 wins over the ticket — same rule as D6) → `PMSessionValidatePrintSettings` + `printInfo.updateFromPMPrintSettings()` (warn-only; lets the driver reconcile companion keys) → `applyPaper` → suppression layers ②–⑤ → `runModal`. 3. Extract the pre-modal configuration into a testable helper (same pattern as `NativeTargetSpooler.makePrintInfo`) so unit tests can assert the ticket is honoured without driving the modal. 4. Diagnostics for hardware verification: DEBUG env flag (e.g. `ICCERY_DEBUG_PANEL_DUMP=1`) logging the `PMPrintSettingsToOptions` output and serialised ticket size immediately before `runModal` and after OK. 5. Restore is **within-session only** — `PrintTicket` is deliberately session-only (queue- and driver-version-specific; PrintTicket.swift:8-9). Cross-launch persistence of driver state is out of scope. Contingency (only if hardware verification shows the Epson PDE sources pane state from lpoptions defaults rather than the session ticket — see RC3): persist the Stage 2 mirror via `lpoptions -p <queue> -o …` before opening the panel. This mutates user-visible queue defaults — discuss before doing it. Fallback (when no ticket exists for the queue but `capturedCupsOptions[queue]` does): optionally replay the captured `k=v` pairs as `PMPrintSettingsSetValue` writes beneath the explicit selections. ## Acceptance Criteria - [ ] Reopening the properties dialog after a capture presents the captured driver state — vendor PDE settings persist across opens within the session - [ ] Stage 2 selections still win over the restored ticket (paper/media/quality/orientation pre-selected) - [ ] First-ever open (no ticket) keeps current behaviour: queue defaults + Stage 2 selections - [ ] Capture-back (driver → Stage 2 mirror + ticket re-serialise) unchanged - [ ] Unit tests: ticket restore invoked pre-modal (via the extracted seam), validate called after pre-writes, cross-queue ticket refused - [ ] Hardware (Epson XP-55): change a vendor-pane-only setting → OK → reopen → setting persists ## Dependencies None blocking. Related: #186 (original dialog binding/pre-select — this supersedes its residual symptom), #201 (introduced `PrintTicket`/`restore`, wired to the spooler only). Blocks #218 (paper-source feature) — both touch `PrintPanelInitialSelections`, `runNativePanel`, and `TicketWriteResolver`. ## References - `Sources/ICCery/Print/PrintPanelService.swift` — `runNativePanel` (:84-208), `applyInitialSelections` (:214-244) - `Sources/ICCery/Print/PMTicketBridge.swift` — `bind` (:87-97), `serialise` (:121-156), `restore` (:166-229) - `Sources/ICCery/Print/NativeTargetSpooler.swift` — S3–S5 order (:147-188) - `Sources/ICCery/Print/PrintSessionViewModel.swift` — `openPrinterPreferences` (:223-294), `capturedTickets` (:45) - `docs/11-print-macos.md` §ColorSync suppression layers ①–⑦ / §native spool S1–S14 - `docs/issue-printer-quality-ignored.md` — #201 analysis (D3, D6)
gronod added the Kind/Bug
Priority
High
2
Project/ICCery-v2Bug/Backend
labels 2026-09-22 16:34:53 +01:00
gronod added this to the M13 — Print ticket persistence & paper source milestone 2026-09-22 17:02:14 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/iccery-v2-mac#217