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

182 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `u32``dmMediaType` | `-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)
```rust
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 use`s `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_target``lp` | `unix::print_target``lp` |
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`)
```js
{
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.
---