Files
pdfcraft/crates/print
d749fd8062 Print colour management: the app converts printed pictures into a printer profile (#6269) (#6853)
* Print: app-managed colour converts printed pictures into a printer profile (#6269)

doc_print takes color_management (printer, the default, or app). With app,
color_profile (an ICC file or an installed profile's name), rendering_intent
and black_point_compensation convert each rendered page picture from sRGB
into the printer's RGB or grey profile before it is sent. app prints the
pages as pictures; CMYK and non-destination profiles are refused.
print_color_profiles lists the installed RGB and grey profiles.

The conversion is craft-cms at craft-print's rev, in pdfcraft_print::colour,
hooked into print_raster::rasterize. Raster carries the colour and is Clone
rather than Copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Print colour: look for installed profiles inside the read root when confined

With automation roots, print_color_profiles and doc_print's color_profile
name lookup read the system's colour folders, outside the roots. They now
look in the read root instead (as text_font_files does), check each file
against it before reading it, and are refused without read authority.
Under --root they look in the root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* automation: keep colour profile discovery inside --root, links followed

Under --root alone, print_color_profiles and the color_profile name
lookup listed the root's .icc files without checking each against the
root, so a link inside it could read a profile outside. Each discovered
file now goes through the same --root resolution as any other read
(links followed) before it is opened.

colour_profile_links_out_of_the_root_are_not_read makes such a link and
checks it is neither listed nor found by name; it fails without the
check (the link is listed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* print: as_chunks for the grey fill; area print keeps managed colour

Clippy 1.99 (`chunks_exact_to_as_chunks`) flags the constant-width
`chunks_exact_mut(4)` in the grey conversion; use `as_chunks_mut::<4>()`.

main's print-area path built `Raster` without the new `colour` field;
build it through `PrintDraft::raster()` so a printed area is colour
managed like a printed page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Lock craft-cms at the craft-libs commit craft-print now uses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(print): read only regular files as printer profiles, and no more than the cap

A FIFO or device named `.icc`/`.icm` in a profile folder was listed and
read, and opening a FIFO for reading waits for a writer. The listing now
keeps regular files only (links followed), `PrinterProfile::read` refuses
anything else before opening it, and the read goes through a limit one
byte past `MAX_PROFILE_BYTES`, so a file that grew after it was measured
is refused rather than read whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 06:28:18 +01:00
..
…

pdfcraft-print

Layer L4: printing (execution plan M10.5).

let pages = select_pages(count, Some("1-3, ii, 9-"), &labels, Subset::All, false)?;
let settings = Settings { pages, paper: PAPERS[0].1, layout: Layout::multiple(4), ..Default::default() };
let sheets = layout(&display_sizes, &settings)?;   // geometry only (previews)
let pdf = impose(&doc, &settings)?;                // the print-ready PDF
spool::submit(&pdf, &spool::Job { printer: None, copies: 2, ..Default::default() })?;
  • Size: fit (to the sheet minus an 18 pt margin), actual size, shrink oversized, custom %; centred; auto orientation turns the sheet for landscape pages.
  • Multiple: 2/4/6/9/16 (or any n) pages per sheet, horizontal/vertical (reversed) order, page borders, auto-rotation of pages that don't match the cell. With auto orientation a predefined count turns the sheet to suit its grid (2 per sheet prints side by side on landscape). A custom grid (Layout::grid, doc_print cols/rows, 1-99 each) is laid out across and down the sheet as given and doesn't turn it: auto prints portrait.
  • Cut and stack (Multiple's page order): consecutive pages in each cell's pile, with aligned cut marks in the gutters. For example, 10 pages at 4 per sheet give [1, 4, 7, 10], [2, 5, 8, blank], [3, 6, 9, blank]. Print single-sided, keep sheets in output order, cut at the marks, then stack the cell piles from left to right, top to bottom. Blank cells can be discarded. Page ranges and Reverse pages are applied before imposition. Duplex imposition is not supported. Headless: doc_print with layout: "multiple", order: "cut-stack" and per_sheet: 4 (or any supported grid size), plus path or printer.
  • Booklet: saddle-stitch imposition padded to a multiple of 4; both sides, front or back only; left or right binding; creep compensation, which moves each sheet's pages toward the fold by its place from the outside times the creep (points per sheet, up to MAX_CREEP), clipped at the fold. Headless: doc_print with layout: "booklet" and creep.
  • Poster: tile scale, overlap shared by neighbouring tiles, cut marks.
  • Comments & forms: document, + markups, + stamps, or form fields only; annotations print only with their Print flag, drawn from their appearance streams (Algorithm 8.1). Existing Link appearances are document content in the first three modes; Link appearances are not synthesized when absent, and form fields only excludes them.
  • Properties… on CUPS reads the queue's PPD and its current defaults (lpoptions -l) off the GUI thread, and gives lpoptions ten seconds: a CUPS that has stopped answering leaves the PPD's own defaults in force instead of holding the dialog (#1818; spool::output_within kills and reaps the command at the deadline).
  • Remembered choices (#2001): the dialog keeps two-sided (including leaving it to the printer), grayscale, collate, page sizing (with its custom percent), Comments & forms and a booklet's binding side across restarts, in the settings file under print with the names doc_print takes (names::duplex, names::content, names::size_mode and their parse_* counterparts are the one vocabulary). The paper is saved by its own setting (#2156).
  • Each job starts afresh: copies, pages (the choice, the range, odd/even and reverse), the layout with its own options (pages per sheet, order, borders, booklet subset, poster scale, overlap and cut marks) and orientation reset to the defaults each time Print opens, in the same session too; ISO 32000-2 Table 147 initialises them per document when the print dialog opens. Picked thumbnails still open it on Selected pages. The printer picked last carries over within a session (#3537) but is not saved.

Each source page becomes a Form XObject (its content wrapped in q/Q, plus the printable annotations); sheets place them with a clip. The result is a fresh, unencrypted, garbage-collected file (callers check the print permission). Annotation appearances receive unused XObject names, preserving existing page resources even when they use the same naming prefix. Vector printing also carries each annotation's optional-content membership onto its own appearance invocation. Hidden groups, membership policies, visibility expressions and existing layer overrides therefore remain effective in the generated PDF, including when appearances are shared. Malformed top-level membership or required resource containers produce a print error instead of an unguarded appearance. Nested policy data is preserved without a new evaluator; readers remain responsible for optional-content semantics. The separate on-screen and Print as image annotation-visibility gap is tracked in #3572 / #3624. Appearance placement keeps the full PDF number precision, including small coefficients needed by large appearance bounding boxes or matrices. Existing printable NoRotate appearances keep their orientation and upper-left rectangle anchor when the source page is rotated. Text annotations use this upright placement even with the NoRotate flag clear (ISO 32000-1 section 12.5.6.4). Crop origins, UserUnit, sheet scaling, and later imposition rotations still apply; NoZoom does not cancel printing scale. Missing or unusable appearance streams still cannot be printed.

The GUI Print dialog remembers its last selected paper size across application sessions, including when Print is cancelled. The preference stores a canonical name such as A4; unrecognized saved values retain the current selection (US Letter on a fresh app). This change does not alter persistence of other print preferences. Headless doc_print defaults are unchanged.

spool delegates discovery, driver options, preferences and submission to craft-print. PdfCraft retains page selection, imposition, annotations and print-permission checks. CUPS jobs stream through stdin with fit-to-page=false, and with orientation-requested=4 when every sheet is landscape (cups_orientation), since CUPS would otherwise shrink a landscape sheet into portrait paper. On macOS they also carry the job name and owner (com.apple.print.JobInfo.PMJobName, PMJobOwner) as the system print dialog sends them, which some vendors' filters need (#5743). Windows renders sheets through Windows.Data.Pdf and spools them at their physical PDF size with per-page paper and orientation, zero added margins and hardware-edge clipping. Long-edge and short-edge duplex use the corresponding Windows binding edges. Random, exclusively created Windows job files are removed on success and failure. Other targets offer the print-ready PDF instead.

On Windows, Printer default reads the selected driver's current DEVMODEW duplex setting immediately before submission and carries that binding explicitly into the job (#5130). This also applies to doc_print with duplex: "printer"; its omitted-argument default remains off. Explicit Off/Long Edge/Short Edge choices, booklet binding and single-sided cut-and-stack bypass this lookup. Drivers without DM_DUPLEX retain the unspecified setting. Two-sided defaults also stay unspecified when the driver does not confirm automatic two-sided support, preserving manual duplex preferences (#5772). A failed lookup, settings read or invalid declared duplex value is logged and falls back to leaving duplex unspecified, so the driver can still choose its default. If the queue was resolved before the settings failure, the job remains pinned to that queue. CUPS still leaves the choice to the queue. The Windows backend selects JobDuplexAllDocumentsContiguously, falling back to DocumentDuplex when the driver exposes only document-level duplex, and validates the retained binding before submission (craft-libs #411).

Before a job reaches the Windows spooler, every embedded TrueType or OpenType program is renamed PdfCraftPrint<n> in its name table and in the PDF (/BaseFont, /FontName; a subset keeps its tag). Windows otherwise draws a subset's renumbered glyphs with the installed font of the same name, so ReportLab's Arial subsets printed as shifted letters (#3132). Glyphs, encodings and /ToUnicode are unchanged, and CUPS and Save as PDF get the PDF as it was. A program PdfCraft can't read and rewrite keeps its old name in the PDF too, so a viewer that can't load it either still picks its substitute by that name.

The shared dependency requires Rust 1.95. Transport and native Windows regression tests live in craft-libs; local tests retain composition, facade, engine, automation and UI coverage. The desktop UI discovers printers and submits jobs on workers, so neither operation blocks the dialog; the Windows backend starts without a console window. Physical printer output still needs hardware validation.

In a browser there is no spooler: the app (pdfcraft-ui-egui's web_print) draws the print-ready PDF's sheets with PdfCraft's renderer and prints them through the browser's print dialog, which sets copies, two-sided printing and colour. This crate stays free of web APIs.

Not yet: poster labels, PostScript output, colour conversion for grayscale.

Architectural sheets and scale

The Paper menu and doc_print share Letter, Legal, Tabloid, A3/A4/A5/A6 plus ARCH A/B/C/D/E1/E, ANSI C/D/E and ISO A0/A1/A2. ARCH D is 24 × 36 inches; ANSI D is 22 × 34 inches. Dimensions follow the HP DesignJet paper-size table. Automation accepts short names such as "ARCH D" (case-insensitive) or the full menu label. For example, doc_print with paper: "ARCH D", layout: "actual", orientation: "auto" and path: "plan.pdf" writes a print-ready PDF at 100%.

Match page at 100% uses the currently viewed document page to choose a standard sheet (either orientation, within 0.5 pt for rounded metric dimensions), Actual size and Auto orientation. It leaves the selected page range intact. It is disabled for a custom page size with no matching preset. One preset applies to the whole job; this is not per-page paper-source selection.

PDF scale describes the source-to-sheet transform in the previewed print-ready PDF, not measured physical output. Fit reserves an 18 pt margin and can reduce even a matching page. On Windows craft-print sends the sheets as vectors with a PrintTicket for the sheet's exact paper and no scaling (#599's approach), validated by the driver: a job is refused, not shrunk, when the driver lacks the paper or would change it. Verify the driver's paper settings and physical calibration before relying on drawing scale.

Vector printing resolves direct and indirect annotation rectangle coordinates before placing existing appearances. Rectangles must contain exactly four finite numbers; malformed rectangles are skipped. For an appearance-state dictionary, direct or indirect AS names select the existing artwork. Rectangle arrays, individual coordinates and AS names follow up to 64 indirect references with cycle detection. Missing, invalid, unresolved, cyclic, overly deep, or unknown state names leave that annotation unprinted; a single normal appearance stream does not require AS. The print content mode still controls whether a field or comment prints, and printing preserves the source objects.