mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 06:52:23 +00:00
* print: driver_settings_dialog sets a Windows driver's own options for one job A Windows print job could take a driver's own options, such as media type and print quality, only from the printer's saved Printing Preferences (open_printer_preferences), so choosing them for one job changed the printer's defaults for every later job. driver_settings_dialog(&mut job, parent) shows the driver's dialog (DocumentProperties) for the job's printer. It opens on the job's earlier choices for that printer, without the ones the job decides, when the driver still reads them, else on the settings the driver gives a new job, with the job's copies and collation. OK stores the settings in the new Job::driver_settings. Cancel leaves the job as it was, and so do settings the dialog returns exactly as it opened on them, since some drivers return OK from Cancel with the settings they were given. craft-print does not save the settings as the printer's defaults. When the job prints, Windows converts the settings to a PrintTicket (PrintTicket::from_dev_mode). craft-print removes the features the job's own ticket decides: copies, collation, paper, orientation and scaling, pages per sheet, binding and posters, two-sided when the job sets it, and color when it asks for grayscale. It then merges the job's ticket after the rest. The driver's private settings are kept, so a driver that also keeps pages per sheet there can still apply it. The existing paper, orientation and option checks run on the ticket the driver validates. Settings are used only on the printer whose name they were chosen for, and command_preview marks them as not used when the job, or the default printer, is another one. Other platforms return an error, and CUPS jobs ignore the settings. winprint is pinned to dotCooCoo/winprint.rs 72951d7, which adds from_dev_mode on top of storytold/winprint.rs main, until that change is merged there. examples/driver_settings_dialog.rs opens the dialog and prints a test sheet. * print: the hand-run driver test accepts zeroed settings read as defaults the_driver_dialogs_settings_leave_the_jobs_own_options expected every driver to refuse a DEVMODE of zeros. Two drivers read it as the settings they give a new job instead. The test now accepts either outcome: settings the driver refuses are replaced by the dialog, settings it reads are kept, and either way the dialog opens as if the job had no settings. The README says such settings stay on the job after Cancel and print as the driver's defaults. * print: the README and hand-run test describe settings with no fields set The hand-run Windows test compares the seed's replaces_unreadable with whether the driver refused the settings, in place of a branch whose check always passed, and describes its input as a DEVMODE with only its size set. The README says such settings stay on the job after Cancel and after OK with nothing changed, links the DEVMODE dmFields description, and no longer says how the job prints with them. * print: refuse a job whose driver replaced the dialog's media type, resolution or quality A driver that validates the job's ticket can replace a media type, resolution or print quality chosen in its dialog when it conflicts with the job's paper, and Windows reports that as a success. The validated ticket is now compared with the dialog's settings, by option name and scored values, and the job is refused when one of them changed. The Windows preview lists two-sided printing and color that the job leaves open as "driver settings or queue default" and "driver settings or driver default": it matches the settings to the printer by name and does not ask the driver to read them. * print: the driver dialog opens without earlier settings the driver reads but refuses to merge Earlier settings that a driver still converts to a ticket but then refuses to merge, or to convert back to its DEVMODE, are left out, as unreadable ones already are, so the dialog opens and can replace them. --------- Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
229 lines
19 KiB
Markdown
229 lines
19 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, takes a Windows driver's own settings for one job, and
|
||
submits jobs with copies, collation, duplex and color.
|
||
`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.
|
||
|
||
`driver_settings_dialog(&mut job, parent)` opens the job printer's driver dialog
|
||
([`DocumentProperties`](https://learn.microsoft.com/en-us/windows/win32/printdocs/documentproperties)),
|
||
modal to the `parent` window handle, for that one job: options craft-print does not set, such
|
||
as media type, print quality and the driver's own options. The dialog opens on the job's
|
||
earlier choices in it for the same printer, without the ones the job decides (below), when the
|
||
driver still reads them, else on the settings the driver gives a new job; either way with the
|
||
job's copies and collation. OK stores the settings in `Job::driver_settings` for that printer.
|
||
Cancel leaves the job unchanged, and so do settings the dialog returns exactly as it opened on
|
||
them: some drivers return OK from Cancel with the settings they were given. Earlier settings
|
||
the driver can no longer read are replaced even then. Some drivers read settings that set no
|
||
fields (a DEVMODE whose [`dmFields`](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-devmodew) is 0
|
||
and that has no private section) without an error instead of refusing them; such settings stay
|
||
on the job after Cancel, and after OK with nothing changed. craft-print keeps the settings in
|
||
the job only and does not save them as the printer's defaults (`open_printer_preferences` does
|
||
that). When the job is printed, Windows converts the settings to a PrintTicket
|
||
([`PTConvertDevModeToPrintTicket`](https://learn.microsoft.com/en-us/windows/win32/api/prntvpt/nf-prntvpt-ptconvertdevmodetoprintticket)),
|
||
craft-print removes from it what the job's own ticket decides, and merges the job's ticket
|
||
after it. The dialog's copies, collation, paper, orientation and scaling are removed, and so
|
||
are its pages per sheet, binding (booklets included) and poster features. Its two-sided choice
|
||
is removed when `duplex` is `Some`, and its color when grayscale is on. The dialog does not
|
||
open on the job's two-sided or grayscale choice, so when the job later leaves that choice open,
|
||
the dialog's settings decide it. The driver's other options, vendor options and private
|
||
settings included, are kept, so a driver that also keeps pages per sheet in its private
|
||
settings can still apply it from there. The dialog's borderless choice is kept too, and on a
|
||
driver that enlarges the page for borderless printing it changes the printed size. The checks
|
||
above still run on the ticket the driver validates; they read the validated ticket, not the
|
||
driver's private settings. The job is refused when the driver replaces the dialog's media type,
|
||
resolution or print quality while it validates the job's ticket, which it can do when one
|
||
conflicts with the job's paper. Settings are used only on the printer whose name they were
|
||
chosen for: Windows may replace settings meant for another printer with defaults.
|
||
`command_preview` marks them as not used when the job, or the default printer for a job that
|
||
names none, is another printer. It does not ask the driver to read them, and it lists two-sided
|
||
printing and color that the job leaves open as the dialog's settings or the default. An error
|
||
names the step that failed: the printer, reading the driver's settings, the dialog (a driver
|
||
without one), the driver refusing the settings, or the driver replacing one of them.
|
||
`HAS_DRIVER_SETTINGS_DIALOG` is false and the call returns an error on other platforms, and CUPS
|
||
jobs ignore `driver_settings`. `examples/driver_settings_dialog.rs` opens the dialog for a
|
||
printer named on the command line and prints a one-page test sheet with the settings chosen.
|
||
|
||
`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, what is removed from the driver dialog's settings, the
|
||
dialog's Cancel, 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. Ignored tests check a real printer by hand: `an_installed_driver_validates_the_ticket`
|
||
and `the_driver_dialogs_settings_leave_the_jobs_own_options` (`CRAFT_PRINT_TEST_PRINTER`; send 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.
|