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

11 KiB
Raw Permalink Blame History

10 — Print system (shared types & UI)

Line numbers refer to ICCery v0.8.5 (/tmp/ICCery at analysis time) and the Gronod ArgyllCMS 3.5.0 fork.

Architecture map

Stage 2 UI (src/js/printtarg.js + src/index.html #rawPrintPanel)
        │  invoke("get_printers")
        │  invoke("get_printer_capabilities", { printerName })
        │  invoke("show_printer_properties", { printerName })
        │  invoke("print_target_native", { printerName, tiffPath, options })
        ▼
src-tauri/src/commands.rs  (cfg-gated dispatch)
        │
        ├── windows  → print/windows.rs   GDI + DEVMODE + SetICMMode(ICM_OFF)
        ├── macos    → print/macos.rs     NSPrintPanel + private SPI + lp
        └── unix     → print/unix.rs      lpstat/lpoptions + lp -o raw

Shared types live in src-tauri/src/print/mod.rs. PrinterDevModeStore is a Tauri managed state (lib.rs:53). macOS re-exports Unix enumeration (macos.rs:10).

ICCery-CPU (TargetPrint.app) is a separate AppKit process that ICCery is specified to spawn fire-and-forget with --job <json>. It is not yet wired into the Tauri commands; the current ICCery print path is in-process / lp.


Shared print types (src-tauri/src/print/mod.rs)

All structs derive Debug, Clone, Serialize, Deserialize. Frontend JS uses snake_case field names when building the PrintOptions payload (see printtarg.js:268-275), matching serde's default.

Printer (mod.rs:613)

Field Type Meaning
name String OS queue / destination name. Windows: PRINTER_INFO_4W.pPrinterName. Unix/macOS: CUPS destination from lpstat -e / -p. This is the value sent back as printerName to all subsequent commands.
status String Windows always "Ready". Unix: "Idle" / "Printing" / "Stopped" / "Unknown" from lpstat -p.
is_default bool Windows: always false (default printer is not queried). Unix: true when name matches lpstat -d (system default destination:).
display_name Option<String> Human-readable CUPS printer-info from lpoptions -p <name>. Windows always None. Used on macOS as NSPrinter::printerWithName fallback when PMPrinterCreateFromPrinterID fails (#188). Serde #[serde(default)].

AGENTS.md warns: adding fields requires updating every platform constructor (windows.rs, unix.rs) or the other OS builds break.

PrinterTray (mod.rs:1619)

Field Type Meaning
id u16 Windows: Win32 bin ID from DC_BINS. Unix: 1-based index of the InputSlot / MediaSource value (not a PPD code).
name String Windows: 24-WCHAR DC_BINNAMES string, or "Tray {id}" if empty. Unix: PPD choice with leading * stripped (e.g. Auto, Upper).

PrinterPaperSize (mod.rs:2225)

Field Type Meaning
id u16 Windows: DC_PAPERS DMPAPER_* id. Unix: 1-based index of PageSize / MediaSize choice.
name String Windows: 64-WCHAR DC_PAPERNAMES, or "Paper Size {id}". Unix: PPD token (A4, Letter, …).

Gap: the frontend never populates a paper-size <select> from caps.paper_sizes. Stage 2's #pageSizeSelect is the printtarg layout page size (mm), not the printer's PPD PageSize. PrintOptions.paper_size is set from that same #pageSizeSelect (printtarg.js:271) and forwarded to lp -o PageSize= on Unix/macOS. Windows apply_print_options_to_devmode ignores paper_size.

PrinterMediaType (mod.rs:2831)

Field Type Meaning
id String Windows: DC_MEDIATYPES DWORD as decimal string (parsed back to u32 for dmMediaType). Unix: PPD choice token (92, 13, Plain, …). When a PPD file is readable, id is the machine token and name is the human label after /.
name String Display label. Unix lpoptions -l path uses the same token for both. Unix PPD path (parse_ppd_media_types) splits *CNIJMediaType 42/Photo Paper Plus Semi-gloss: into id=42, name=Photo Paper Plus Semi-gloss.

PrinterCapabilities (mod.rs:3440)

Field Type Meaning
trays Vec<PrinterTray> Paper sources.
paper_sizes Vec<PrinterPaperSize> Queried but unused by the Stage 2 UI.
media_types Vec<PrinterMediaType> #[serde(default)]. Frontend fills #printerMediaTypeSelect.
supports_orientation bool Always true on both Windows and Unix.

PrintOptions (mod.rs:4255) — Default + PartialEq + Eq

Field Type Windows Linux macOS
paper_source Option<u16> dmDefaultSource (tray id) ignored (unix build_lp_args never emits InputSlot) ignored unless present in captured cups_options
orientation Option<String> "landscape"DMORIENT_LANDSCAPE (2), else portrait (1) -o orientation-requested=4 (landscape) or =3 (portrait) same, skipped if cups_options already has the key
paper_size Option<String> ignored -o PageSize=<value> same, skipped if pagesize already added
media_type Option<String> parsed as u32dmMediaType -o MediaType=<value> (generic key only) uses detect_media_type_key (CNIJMediaType / EPIJ_Medi / StpMediaType / MediaType); skipped if any of those keys already in cups_options
ppd_uncorrected_passthrough Option<bool> ignored if true: -o ColorModel=Gray -o cm-calibration instead of -o raw ignored as a gate; macOS always ColorSync-bypasses and always injects driver color-bypass. Native panel sets this to Some(true) on OK.
cups_options Option<String> ignored ignored space-separated key=value captured from PMPrintSettingsToOptions, filtered, then expanded to -o key=value. #[serde(default)].

PrintPropertiesResult (mod.rs:5761)

Returned by show_printer_properties on macOS only.

Field Type Meaning
selected_printer Option<String> CUPS Printer ID from PMPrinterGetID after the panel, or the original printer_name if the session printer cannot be read but PMPrinterCreateFromPrinterID had succeeded. None if the panel was opened via the NSPrinter::printerWithName(display_name) fallback.
options PrintOptions Snapshot: media_type extracted from captured options, cups_options filtered string, ppd_uncorrected_passthrough: Some(true), everything else Default.

Windows show_printer_properties returns Ok(None) from the Tauri command after storing DEVMODE (the inner fn returns Result<(), String>). Linux returns Ok(None) with no dialog. Frontend treats null as user cancelled (printtarg.js:390-393) — so on Windows a successful Preferences OK also shows "Printer properties dialog cancelled." That is a known UX mismatch: Windows success and cancel both surface as null.

PrinterDevModeStore (mod.rs:6387)

pub struct PrinterDevModeStore {
    pub devmodes: Arc<Mutex<HashMap<String, Vec<u8>>>>,
}
  • Keyed by printer name.
  • Stores the full DocumentPropertiesW output buffer (public DEVMODEW + dmDriverExtra private OEM bytes). This is the #36 fix.
  • get clones; set inserts. Poisoned mutex → get returns None, set is a no-op.
  • #[allow(dead_code)] because the type is compiled on all platforms; only Windows uses it.
  • Constructed once in lib.rs:53: .manage(print::PrinterDevModeStore::new()).
  • Session-only. Not persisted to disk.

Module cfg gates (mod.rs:8999)

#[cfg(windows)]                          pub mod windows;
#[cfg(any(target_os = "macos", test))]   pub mod macos;
#[cfg(any(unix, test))]                  pub mod unix;
#[cfg(test)]                             mod tests;

macOS is unix, so both macos and unix compile on macOS. Tests always compile both macos and unix. macos.rs pub uses unix::{get_printer_capabilities, get_printers}.


Tauri command surface (src-tauri/src/commands.rs:13041415)

Registered in lib.rs:9295.

Command Args Windows macOS Linux
get_printers none windows::get_printers macos::get_printers (= unix) unix::get_printers
get_printer_capabilities printer_name: String DeviceCapabilitiesW unix lpoptions/PPD unix lpoptions/PPD
show_printer_properties printer_name: String + AppHandle + State<PrinterDevModeStore> DocumentPropertiesW modal; returns Ok(None) NSPrintPanel on main thread; returns Option<PrintPropertiesResult> no-op Ok(None)
print_target_native printer_name, tiff_path, options: Option<PrintOptions> GDI path, passes store macos::print_targetlp unix::print_targetlp

IPC argument casing is camelCase at the Tauri boundary (printerName, tiffPath) and snake_case inside the nested PrintOptions struct because the frontend builds that object itself.


Frontend (src/js/printtarg.js + src/index.html)

Stage 2 print UI (index.html:568638)

Hidden #rawPrintPanel revealed after printtarg succeeds and a JSON manifest is parsed.

  • #printerSelect + #btnRefreshPrinters (↻) + #btnPrinterProperties (⚙️ Preferences, class .btn-properties) + #printerStatusBadge
  • #cupsOptionsGroup / #chkPpdFallback — "PPD Uncorrected Passthrough (Fallback)". Hidden on Windows (#48, printtarg.js:167-170) via navigator.userAgent / userAgentData.platform.
  • #printerTraySelect, #printerMediaTypeSelect
  • Orientation toggle #btnOrientPortrait / #btnOrientLandscape (default portrait)
  • #btnPrintAll — "Print All Pages (Bypass CM)"
  • #btnAdvanceToStage3
  • Per-page "Print Page" buttons on gallery cards (printtarg.js:737-748)

There is no printer paper-size dropdown in the raw-print panel. #pageSizeSelect above is printtarg's layout size and is reused as PrintOptions.paper_size.

capturedCupsOptions (printtarg.js:1417, 266, 411-415)

Module-level {} keyed by printer name. Populated only when macOS show_printer_properties returns a result with options.cups_options. Cleared for that printer if the result has no cups_options. Fed into every subsequent print_target_native via getSelectedPrintOptions().

Cancellation: result === null → info toast, no map mutation.

If the user switched printer inside NSPrintPanel, the dropdown is updated when that CUPS id exists in the option list (printtarg.js:402-409). Captured media type is applied to #printerMediaTypeSelect when a matching option exists.

Print payload (printtarg.js:260-276)

{
  paper_source: trayVal ? parseInt(trayVal, 10) : null,  // NaN if unix tray name is non-numeric — Unix tray ids are 1-based indexes so this is OK
  orientation: selectedOrientation,                       // "portrait" | "landscape"
  paper_size: pageSizeSelect.value,                       // printtarg page size, not printer PageSize
  media_type: mediaType || null,
  ppd_uncorrected_passthrough: chkPpdFallback.checked,
  cups_options: capturedCupsOptions[activePrinter] || null,
}

Wizard state

wizardState.printerName is set at spool time (printtarg.js:579, 625) so Stage 5 verification history (#95) can record the device.