mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* craft-print: ask macOS for Auto Select when a job names no tray
CUPS gives a job that names no tray the PPD's default tray. On a queue
whose default tray is empty (a Xerox AltaLink defaulting to Tray 1), the
printer holds every job ("Tray 1 is empty") while CUPS reports it
completed, so nothing prints. Preview prints because the macOS Print
dialog's Paper Feed: Auto Select sends AP_D_InputSlot with an empty value,
which drops the PPD's default tray (storytold/pdfcraft#3752).
- lp_args sends `-o AP_D_InputSlot=` on macOS when the job's options name
no InputSlot. A tray the job names wins; checked with cupsfilter on a
synthetic PPD: with both, CUPS emits the InputSlot's code.
- apply_queue_defaults (now shared with apps that read options
themselves, such as PdfCraft) gives the driver's tray option an
AUTO_SELECT choice, labelled "Auto Select", first and as the default,
so a dialog shows what lp_args sends. It is left out when the user's
lpoptions defaults name another tray, since lp adds that tray to every
job and it wins.
Linux jobs are unchanged: AP_D_InputSlot is Apple's.
* craft-print: match queue defaults to the PPD without regard to case
CUPS treats PPD option and choice names as case-insensitive. A queue
default spelled otherwise than the PPD (inputslot=tray2 for InputSlot
Tray2) was ignored, so on macOS apply_queue_defaults showed Auto Select
although lp would send Tray2. Keys and choices now match case-
insensitively and keep the PPD's spelling. lpoptions -l prints the PPD's
spelling, so this matters for callers passing defaults from elsewhere.
Review: storytold/craft-libs#232 (CodeRabbit).
* README: keep both the macOS tray and the Flatpak notes (the merge left the conflict markers in)
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
187 lines
15 KiB
Markdown
187 lines
15 KiB
Markdown
# craft-print
|
||
|
||
Submit a print-ready PDF to the system spooler. Used by PdfCraft and DesignCraft;
|
||
there are no dependencies on either app or on a UI framework. Requires Rust 1.95.
|
||
|
||
```rust,no_run
|
||
let job = craft_print::Job { title: "Report".into(), ..Default::default() };
|
||
let receipt = craft_print::submit(&pdf_bytes, &job)?;
|
||
```
|
||
|
||
The caller generates the PDF, checks document permissions, and handles page selection,
|
||
imposition, marks and bleed. This crate discovers printers, exposes CUPS driver options,
|
||
opens Windows printer preferences and submits jobs with copies, collation, duplex and colour.
|
||
`command_preview` returns diagnostics without creating files or starting a process.
|
||
|
||
- **macOS/Linux:** CUPS `lpstat`, `lpoptions` and `lp`. Jobs stream through stdin. Exact-size
|
||
sheets use `fit-to-page=false`; shrink-to-fit uses IPP `print-scaling=auto-fit`.
|
||
- **macOS paper tray:** a job whose options name no `InputSlot` also sends `AP_D_InputSlot=`,
|
||
the system Print dialog's Paper Feed: Auto Select. Without it CUPS uses the PPD's default
|
||
tray, and a printer whose default tray is empty holds the job while CUPS reports it completed
|
||
(storytold/pdfcraft#3752). A tray the job names wins, as does one in the user's `lpoptions`
|
||
defaults, which `lp` adds to every job. `apply_queue_defaults` puts the same choice in the
|
||
driver's tray option: `AUTO_SELECT` (an empty keyword, which `lp_args` never sends), labelled
|
||
"Auto Select", first and as the default unless the user's defaults name another tray. Apps
|
||
may show their own translation of the label. Other CUPS builds don't know `AP_D_InputSlot`,
|
||
so Linux jobs are unchanged.
|
||
- **Flatpak:** the Freedesktop runtime has libcups but not `lpstat`/`lp`/`lpoptions`, so the app
|
||
bundles them (the apps' manifests build them from CUPS as a `cups-client` module). What each
|
||
piece is for:
|
||
- `lpstat` and `--socket=cups`: the printer list. Without either it is empty, and a missing
|
||
`lpstat` is logged.
|
||
- `--system-talk-name=org.freedesktop.Avahi`: driverless network printers (`lpstat -e`).
|
||
Without it the list keeps the spooler's queues and loses only those.
|
||
- `lp`: submitting jobs.
|
||
- Driver options (`printer_options`) come from the queue's PPD in `/etc/cups/ppd`; `lpoptions`
|
||
only marks which choices are current. Flatpak reserves `/etc`, so the host's PPDs, and with
|
||
them these options, are out of reach in a Flatpak whether or not `lpoptions` is there.
|
||
- `--filesystem=~/.cups:ro`: the user's default printer and saved options, and a server named
|
||
in their `client.conf`. A network server also needs `--share=network`. A server named only
|
||
in the host's `/etc/cups/client.conf` is invisible in the sandbox, as is all of `/etc`;
|
||
`flatpak override --user --env=CUPS_SERVER=host:port <app-id>` names it for that one app.
|
||
- A socket path in `client.conf`: Flatpak mounts the socket it chooses at
|
||
`/var/run/cups/cups.sock`, so the manifests' wrapper around the tools points CUPS there when
|
||
CUPS' socket is the one Flatpak chose, and fails when it isn't rather than reach another
|
||
server. Server names and `CUPS_SERVER` are left alone.
|
||
- **Windows:** in this process, with no PowerShell, script or other program started, so
|
||
printing works where App Control for Business or AppLocker enforce script rules. Printers
|
||
come from `EnumPrinters` (local queues and `\\server\queue` connections) and the default from
|
||
`GetDefaultPrinter`. `printers` also reads each driver's PrintCapabilities to report two-sided
|
||
support, which can wait on the device (a networked printer in power save); `printer_queues`
|
||
lists the same queues without asking any driver, so a Print dialog's list never waits on a
|
||
device, and `two_sided(name)` asks the driver of just the printer picked. A job asks the
|
||
driver too before it is sent; `submit_cancellable` with a
|
||
`Cancel` token lets the app stop a job still waiting on it, and a cancelled job is never
|
||
sent. A job is drawn as vectors by the in-box `Windows.Data.Pdf` with Direct2D
|
||
into the XPS print API (the `winprint` crate), and copies, collation, duplex, colour, paper,
|
||
orientation and "no scaling" travel in a PrintTicket that the driver validates first; a
|
||
driver that would change the paper, orientation or scaling is refused before anything is
|
||
sent. Paper is the driver's own option for the sheet's size (to 0.25 mm), preferring the one
|
||
named like the standard size ("ARCH D" over a custom 24 × 36 in entry). Windows' ticket merge
|
||
renames a driver's vendor namespace prefix (`brpsk` to `ns0000`) once the ticket uses one of
|
||
its names, and a Brother driver then fails every page with `0x80040003`; the validated ticket
|
||
is given the prefixes of the driver's own default ticket back (`ticket::respell`). This backend
|
||
comes from storytold/pdfcraft#599 and #1132.
|
||
A job has one ticket, so one paper and orientation. Sheets of the other orientation are
|
||
turned back with `/Rotate`, so portrait and landscape sheets still print as one job. Exact
|
||
size refuses sheets of different sizes and paper the driver lacks. Shrink-to-fit uses the
|
||
driver's matching paper, else its default paper, and puts every sheet on it: a sheet that
|
||
doesn't fit inside the paper's widest hardware margin is shrunk, a smaller one keeps its
|
||
size, and both are centred. Both are done by appending an incremental update to the PDF
|
||
(new versions of the pages that need it, the original bytes untouched); the crate reads
|
||
only cross-reference sections, object streams and the page tree for this, leniently and
|
||
with every size bounded. Encrypted PDFs can be turned but not reframed. Annotations on a
|
||
reframed page are dropped; print-ready PDFs have them flattened. Nondefault or invalid
|
||
`/UserUnit` is refused because the native renderer does not reliably honor physical units.
|
||
Parsing caps cumulative cross-reference/object-stream records at 500,000, decoded streams
|
||
at 16 MiB each and 32 MiB per job, and parsed nodes/tokens at 250,000/16 MiB per job and
|
||
32,000/1 MiB per object. Files exceeding these limits return a clear error.
|
||
Long-edge duplex is `TwoSidedLongEdge` and short-edge `TwoSidedShortEdge`.
|
||
Explicit bindings use `JobDuplexAllDocumentsContiguously` when it offers the requested standard option,
|
||
otherwise `DocumentDuplex` when that feature offers that exact binding. Discovery and selection
|
||
resolve option names against their in-scope namespaces, including the default namespace. Validation accepts either
|
||
scope and rejects missing, changed or conflicting bindings before submission.
|
||
`duplex: None` leaves duplex out of the ticket. Driver-specific defaults remain in Windows printer
|
||
preferences (`rundll32 printui.dll`, the one process this crate starts, and only when asked).
|
||
- **Browser/other targets:** discovery is empty and submission reports an unsupported
|
||
platform; callers can offer PDF export instead.
|
||
|
||
Windows jobs briefly use a file in a private temporary folder (random name, this user's only),
|
||
named after the job's title because the spooler shows it as the document name; the folder is
|
||
removed once the spooler has the job, on success and failure. Failed and canceled terminal
|
||
package statuses are errors, including failures received after every sheet was processed.
|
||
A missing completion notification times out after two minutes after the native print control
|
||
is closed and requests cancellation. Calls into a driver and interactive output-file dialogs
|
||
are not covered by this notification timeout. Windows answers when the job is in
|
||
the queue ("spooled 3 sheet(s)"): a paused queue or an offline printer keeps it there. CUPS jobs
|
||
never use a PDF temporary file. Printing is synchronous; UI callers should submit on a worker.
|
||
`submit_with_progress` reports `(processed, sheets)` before the first Windows sheet and after
|
||
each sheet is successfully added to the native print job. These are source PDF sheets, excluding
|
||
copies and duplex; the events describe sheet processing, and do not confirm physical output.
|
||
The callback runs on the thread calling submission, can capture non-`Send` state, and should
|
||
return promptly. It is never retained. Keep waiting for the final `Result` even at full progress:
|
||
the job can still fail. `submit_cancellable` can cancel a job only until it starts reaching
|
||
the spooler (on Windows after craft-print's driver queries and preflight DEVMODE conversion;
|
||
with CUPS before `lp` starts); a job already being sent completes. On Windows, winprint repeats
|
||
the DEVMODE conversion after that boundary. Cancellation is already unavailable during this
|
||
repeat, so a driver that stalls there can still block submission. CUPS reports no progress.
|
||
UI callers must forward events to their own progress state; adopting the API alone does not
|
||
change an application's UI.
|
||
Receipts are backend messages, not portable job IDs. `Job::duplex = None` preserves the queue's
|
||
duplex default; `Some(Duplex::Off)` forces simplex. `Job::scaling` defaults to `Scaling::Exact`;
|
||
callers exporting arbitrary document sizes can choose `Scaling::ShrinkToFit`. Exact mode reports
|
||
a driver paper-size mismatch. On Windows `command_preview` describes the in-process job
|
||
(`xps-print`, then the printer and options as `key=value`) instead of a command line. The crate
|
||
does not provide PostScript output, separations, trapping or colour-managed device rendering.
|
||
|
||
Windows shrink-to-fit refuses pages with annotations when their geometry needs fitting.
|
||
Callers must flatten annotation appearances into page content first. Exact-size printing and
|
||
rotation retain the annotations. This avoids losing printed comments, forms or other marks.
|
||
The Windows renderer uses viewer visibility. Native submission therefore accepts only plain
|
||
visible printable annotation flags (`F=4`); annotations without Print, with NoView, or with
|
||
other visibility/rotation flags require caller flattening. This prevents printing comments
|
||
that were marked nonprinting or omitting print-only marks. A visible Square appearance with
|
||
`F=4` was rendered in the Windows qualification fixture; other appearance types still need
|
||
their own fidelity checks.
|
||
|
||
Native Windows PDF submission checks the driver's validated copy count and explicit
|
||
simplex/duplex and grayscale requests before sending. Collation must be retained when both
|
||
the page count and copy count exceed one. A driver that offers one of these options and drops
|
||
or changes it is refused; select supported options or flatten grayscale into the print-ready
|
||
page content. A driver without the feature, per its PrintCapabilities, is accepted when it can
|
||
only do what was asked: one-sided on a queue with no two-sided feature, grayscale on one with no
|
||
colour option, uncollated copies on one that can't collate. Two-sided on such a queue is
|
||
refused. Windows doesn't document collating for a driver that can't collate, so collated copies
|
||
on one are a single job of one copy that holds the document's pages once per copy; on two-sided
|
||
paper an odd page count gets a blank page after each copy but the last, so every copy starts on
|
||
a sheet of its own. Where one job would print the copies wrongly or can't hold them (pages with
|
||
annotations, an encrypted PDF, a very large job, a queue set to several pages per sheet, binding,
|
||
stapling or a two-sided option outside the Print Schema's), each copy is a job of its own.
|
||
Without an explicit duplex request, the queue default applies. Without a grayscale request the
|
||
ticket names no colour, so the colour in the user's printing preferences (Properties…, or
|
||
Windows Settings) applies, black and white included. Single-page or single-copy jobs have no
|
||
collating order to check.
|
||
|
||
`cargo test -p craft-print` tests CUPS transport (including pipe-sized jobs and refusal),
|
||
printer parsing, PPD options and argument mapping, and on every platform the Windows pieces that
|
||
need no printer: the PrintTicket, the checks on the driver's validated ticket and capabilities,
|
||
paper matching, job file names, and the PDF sheet reader and its update (classic and compressed
|
||
cross-references, turning, reframing, refusal, and thousands of truncated and corrupted files
|
||
that must fail without a panic). On Windows it also submits a mixed-orientation exact-size job
|
||
and a shrink-to-fit job to a temporary paused queue (inbox "Generic / Text Only" driver on
|
||
`LPT1:`) and checks each arrives as one job with all its sheets. The queue portion explicitly
|
||
skips if the driver or administrative rights are absent. Windows CI installs the inbox test
|
||
driver and sets `CRAFT_PRINT_REQUIRE_QUEUE=1`, making queue preparation/submission mandatory on
|
||
its runner. Two ignored tests check a real printer by hand: `an_installed_driver_validates_the_ticket`
|
||
(`CRAFT_PRINT_TEST_PRINTER`; sends no job) and `an_installed_printer_prints_a_file`
|
||
(`CRAFT_PRINT_TEST_PRINTER`, `CRAFT_PRINT_TEST_FILE`, optional `CRAFT_PRINT_TEST_SCALING=shrink`
|
||
and `CRAFT_PRINT_TEST_DUPLEX=long`). Physical printer fidelity remains a hardware check.
|
||
|
||
The cross-platform GitHub Actions workflow is defined in `.github/workflows/print.yml`.
|
||
It runs formatting, Clippy and this crate's own tests on Windows, Linux and macOS when print
|
||
code changes, and supports manual runs. The Windows job executes the spooler regression above;
|
||
physical printer fidelity still requires hardware verification.
|
||
|
||
Windows discovery reports driver duplex support in `Printer::two_sided` (a two-sided option in
|
||
the driver's PrintCapabilities); CUPS leaves it unknown. Explicit simplex/duplex choices that the driver
|
||
drops are refused before submission.
|
||
|
||
Windows dependencies: the maintained [`winprint` fork](https://github.com/storytold/winprint.rs)
|
||
(BSD-3-Clause, pinned to an immutable Git revision, its PDFium-downloading `pdfium` feature off) and `winsafe` (MIT) hold every Win32 and WinRT call, so the crate keeps `unsafe` forbidden;
|
||
`roxmltree` reads PrintTickets and `miniz_oxide` inflates PDF streams. `winprint` 0.2.1 lists
|
||
reqwest (with rustls) and sigstore crates as non-optional build-dependencies, used only by its
|
||
`pdfium` feature: they compile into its build script on Windows but not into the library. Apps
|
||
running `cargo deny` need a licence exception for `webpki-root-certs` (CDLA-Permissive-2.0, CA
|
||
list data reqwest brings) and an advisory ignore for RUSTSEC-2024-0370 (`proc-macro-error`
|
||
unmaintained, a compile-time proc macro of winprint's `fmt-derive`).
|
||
|
||
`craft_print::rgb` exposes the safe `craft-winprint-gdi` API for native driver settings,
|
||
printable geometry, RGB pages and caller-composed vector strokes. Applications retain ICC
|
||
conversion, document layout, labels and print-mark composition. The shared transport validates
|
||
single-copy DEVMODE settings before emitting manual copies and checks GDI errors. Color policy
|
||
is explicit: application-managed/unmanaged RGB disables DC and old-style halftone correction
|
||
with `ICM_DONE_OUTSIDEDC`, reapplied after `StartPage`; driver-managed RGB uses `ICM_OFF`.
|
||
Vendor driver automatic color correction remains a driver setting and requires qualification.
|
||
Other platforms return `rgb::PrintError::Unsupported`. The Windows PDF backend and its existing
|
||
completion dependency pin are independent of this additional MIT OR Apache-2.0 API.
|