Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
8.7 KiB
12 — Windows GDI raw printing
Line numbers refer to ICCery v0.8.5 (
/tmp/ICCeryat analysis time) and the Gronod ArgyllCMS 3.5.0 fork.
Windows (src-tauri/src/print/windows.rs)
Direct Win32. No lp, no XPS, no winprint crate. Evaluated and rejected in #26.
Exact Win32 APIs used
From windows crate 0.61 (Cargo.toml:50-55: Win32_Foundation, Win32_Graphics_Gdi, Win32_Graphics_Printing, Win32_UI_WindowsAndMessaging) plus a local extern "system" block for symbols the crate feature set does not expose.
| API | Header / crate | Where | Purpose |
|---|---|---|---|
EnumPrintersW |
Win32::Graphics::Printing |
get_printers |
Level 4 (PRINTER_INFO_4W) first; fall back to Level 1 (PRINTER_INFO_1W) |
OpenPrinterW |
Printing | properties + print | PRINTER_HANDLE |
ClosePrinter |
Printing | both | |
DocumentPropertiesW |
Printing | properties + print | size query (fMode=0), DM_OUT_BUFFER init, DM_IN_PROMPT|DM_IN_BUFFER|DM_OUT_BUFFER dialog |
GetForegroundWindow |
Win32::UI::WindowsAndMessaging |
properties | parent HWND of the modal |
DeviceCapabilitiesW |
local extern | capabilities | DC_BINS(6), DC_BINNAMES(12), DC_PAPERS(2), DC_PAPERNAMES(16), DC_MEDIATYPES(35), DC_MEDIATYPENAMES(34) |
CreateDCW |
Win32::Graphics::Gdi |
printer DC with DEVMODE | |
DeleteDC |
Gdi | ||
GetDeviceCaps |
Gdi | LOGPIXELSX/Y, HORZRES, VERTRES |
|
SetICMMode |
local extern | ICM_OFF = 1 |
|
StartDocW / StartPage / EndPage / EndDoc |
local extern | GDI job | |
StretchDIBits |
Gdi | unmanaged 24-bit BGR DIB |
DOCINFOW is a local #[repr(C)] struct (windows.rs:24-31), not the crate type, because StartDocW is also local-extern.
Constants (windows.rs:48-66):
ICM_OFF = 1
DM_IN_PROMPT = 4
DM_IN_BUFFER = 8
DM_OUT_BUFFER = 2
DM_ORIENTATION = 0x00000001
DM_DEFAULTSOURCE = 0x00000200
DM_ICMMETHOD = 0x00800000
DMICMMETHOD_NONE = 1
DMORIENT_PORTRAIT = 1
DMORIENT_LANDSCAPE = 2
DM_MEDIATYPE = 0x02000000
DC_PAPERS=2, DC_BINS=6, DC_BINNAMES=12, DC_PAPERNAMES=16,
DC_MEDIATYPENAMES=34, DC_MEDIATYPES=35
IDOK = 1
Enumeration (get_printers, windows.rs:78-168)
EnumPrintersW(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, NULL, 4, …)to get byte count.- If
bytes_needed == 0, retry at Level 1 (older/limited spooler). - Level 4:
pPrinterName. Level 1:pName. is_defaultalways false,display_namealwaysNone,statusalways"Ready".- Empty list is success, not error.
Capabilities (get_printer_capabilities, windows.rs:171-326)
DeviceCapabilitiesW(printer, NULL port, cap, …). Bin names are 24 WCHARs each; paper and media names 64 WCHARs. Media type IDs are read as u32 via a *mut u16 cast of a Vec<u32> (windows.rs:288-289) — the Win32 API writes DWORDs for DC_MEDIATYPES.
supports_orientation: true unconditionally.
Driver preferences dialog (show_printer_properties, windows.rs:329-401) — #34 / #36
OpenPrinterW
DocumentPropertiesW(..., fMode=0) → required buffer size
load cached DEVMODE or zeros
if all-zero: DocumentPropertiesW(DM_OUT_BUFFER) to populate defaults
DocumentPropertiesW(DM_IN_PROMPT | DM_IN_BUFFER | DM_OUT_BUFFER)
ClosePrinter
if res == IDOK (1): store out_buf, Ok(())
if res == 2: user cancel, Ok(()) // IDCANCEL
else: Err(...)
Parent window is GetForegroundWindow(), not the Tauri webview HWND. The dialog is modal to whatever is foreground.
The entire out_buf (header + dmDriverExtra) is stored. That is the #36 fix: Epson "Print Preview" and other OEM private flags live past dmSize and were previously dropped.
ICM bypass + option overlay (apply_print_options_to_devmode, windows.rs:404-437)
Always:
dmFields |= DM_ICMMETHOD
dmICMMethod = DMICMMETHOD_NONE // 1
Then, if options present:
paper_source→dmFields |= DM_DEFAULTSOURCE;dmDefaultSource = tray_id as i16orientation == "landscape"(case-insensitive) →DMORIENT_LANDSCAPEelse portraitmedia_typeparsed asu32→dmFields |= DM_MEDIATYPE;dmMediaType = id
Not applied: paper_size, ppd_uncorrected_passthrough, cups_options. Private OEM bytes after the public header are not touched (verified by tests.rs:219-276).
GDI raw print path (print_target, windows.rs:440-643)
image::openTIFF →to_rgb8(). Crate features:png,tiff(Cargo.toml:40).- Pack 24-bit BGR DIB, 4-byte row stride:
row_stride = ((w*3+3)/4)*4. Top-down (biHeightnegative). OpenPrinterW→DocumentPropertiesWsize → reuse cached DEVMODE (resized up if needed) orDM_OUT_BUFFERdefaults →apply_print_options_to_devmode.CreateDCW(NULL, printer, NULL, pDevMode).SetICMMode(hdc, ICM_OFF)— "STRICT ICM BYPASS".StartDocWwith titleICCery Target - <filename>.lpszDatatypenull (driver default, not"RAW").StartPage.- Auto-fit scaler (#34):
dpi_x/y = GetDeviceCaps(LOGPIXELSX/Y) // used only for BITMAPINFO biX/YPelsPerMeter
page_w/h = GetDeviceCaps(HORZRES/VERTRES)
scale = min(page_w/img_w, page_h/img_h)
dest = floor(img * scale), centered
Physical offsets (PHYSICALOFFSETX/Y) from #34's wish-list are not queried. Fit is to the printable DC area (HORZRES/VERTRES), which already excludes hardware margins.
-
StretchDIBits(..., DIB_RGB_COLORS, SRCCOPY). This can resample if dest ≠ source pixels. Color is unmanaged (ICM off, BI_RGB) but geometric interpolation is GDI's. Contrast with ICCery-CPU, which forbids interpolation. -
EndPage/EndDoc/DeleteDC/ClosePrinter.
Zero-dimension images error out before GDI. Missing TIFF: "Target TIFF file not found: …".
CREATE_NO_WINDOW vs print
CREATE_NO_WINDOW (0x08000000) is not used on the print path. Windows printing is in-process GDI; no child process is spawned.
CREATE_NO_WINDOW is applied to ArgyllCMS console-subsystem children so a black cmd window does not flash (#46):
src-tauri/src/process_manager.rs:99-103— everyProcessManager::spawn(targen,printtarg,chartread,colprof,profcheck,instlist, …)src-tauri/src/calibration.rs:849-853—printcal/applycalcaptured runs
printtarg itself only generates TIFFs; it does not print. Native print is a separate IPC command.
NSIS USB drivers (#127) — not printer drivers
USB install is for spectrophotometer libusb-win32 drivers (i1Pro, ColorMunki, SpyderPrint), not printer OEM drivers.
- Fetch:
scripts/fetch-argyll.mjscopiesArgyll_V*/usb/→src-tauri/argyll/usb/from the Windows zip (ArgyllCMS_install_USB.exe,.inf,.cat,libusb0.sysfor x86/amd64/arm64). - Bundle:
tauri.conf.jsonresources: ["argyll/**/*"]+ NSISinstallMode: "both"+installerHooks: "windows/hooks.nsh". hooks.nshNSIS_HOOK_PREINSTALL: if elevated,MessageBoxYes/No "Install ArgyllCMS USB instrument drivers?".NSIS_HOOK_POSTINSTALL:ExecWait$INSTDIR\argyll\usb\ArgyllCMS_install_USB.exeor$INSTDIR\resources\argyll\usb\.... Not silent. Missing file → exclamation box.- Uninstall does not run
ArgyllCMS_uninstall_USB.exe(would break other Argyll apps). - Also maps missing HKCU shell-folder drive letters via
DefineDosDeviceto avoid "Invalid Drive" on domain profiles.
WiX/MSI has no equivalent prompt.
Windows idiosyncrasies / bugs fixed
| Issue | Symptom | Fix |
|---|---|---|
| #25/#26 | Need ACPU-equivalent raw print | GDI + SetICMMode(ICM_OFF) + DMICMMETHOD_NONE + 24-bit BGR DIB |
| #34 | No driver dialog / trays / orientation / scaler | DocumentPropertiesW modal, DeviceCapabilitiesW, auto-fit StretchDIBits dest |
| #36 | Preferences (Epson Print Preview, private OEM) ignored | Cache full DEVMODE buffer including dmDriverExtra; pass that pointer to CreateDCW |
| #48 | "PPD Uncorrected Passthrough" shown on Windows | Hide #cupsOptionsGroup |
| #46 | Argyll console windows cover UI | CREATE_NO_WINDOW on subprocess spawn only — not on print |
| #67 | Duplicate get_windows_printers / print_target_windows |
Collapsed to get_printers / print_target_native |
Remaining gaps:
- Default printer never flagged.
paper_sizenot applied toDEVMODE(DM_PAPERSIZEunused).PHYSICALOFFSET*not used; scaler usesHORZRES/VERTRESonly.StretchDIBitsmay interpolate when scaling.show_printer_propertiesTauri result is alwaysNone, so the frontend reports cancel even on OK.- No
PrinterProperties/AdvancedDocumentPropertiesalternative; onlyDocumentPropertiesW. GetForegroundWindowcan attach the modal to the wrong top-level window.