Files
Dylan M. Taylor 6fdd6bb1b7 packaging: Flatpak finds and prints to CUPS printers (#10196)
* packaging: Flatpak finds and prints to CUPS printers

The Print dialog listed no printers in the Flatpak: the runtime has libcups but not
lpstat/lp, the sandbox couldn't reach the CUPS socket, and driverless network printers
(lpstat -e) need Avahi. Bundle lpstat/lp/lpoptions from CUPS 2.4.20 (Apache-2.0,
statically linked, licence installed) and grant --socket=cups and
--system-talk-name=org.freedesktop.Avahi in both manifests.

* print: lp takes the job only when CUPS lists a printer; the only printer by default; ~/.cups read-only

Review (CodeRabbit) on bundling lp in the Flatpak:
- With lp present but CUPS down or empty, the job failed on lp instead of reaching the
  IPP route and the print portal. lp_route now gives up the route when lpstat lists no
  printer, before anything is submitted.
- With no printer named, no default and one queue, IPP picked it but lp failed. lp_route
  names the only printer.
- The sandbox ignored the user's default printer, saved options and client.conf server:
  grant ~/.cups read-only in both manifests.

* print: no printers for lp means the portal, not IPP; bounded listing; CUPS socket wrapper

Review:
- When lp is there but CUPS lists no printer, the job went to the IPP route, whose
  server detection ignores ~/.cups/client.conf and could print to a local default
  printer instead of the configured server. It now goes to the print portal, or fails
  with no portal.
- The lpstat listing before a job is bounded (10 s, then lp takes the job as before)
  and runs outside SPOOLER, which guards only Windows driver calls.
- Flatpak mounts the host's CUPS socket at /var/run/cups/cups.sock whatever path
  client.conf or CUPS_SERVER names; the CUPS tools now run through a wrapper that
  points a socket path missing in the sandbox there, leaving server names alone.

* packaging: redirect a custom CUPS socket only when it is the one Flatpak mounted

Review (CodeRabbit): Flatpak picks the socket it mounts at /var/run/cups/cups.sock
with stricter rules than CUPS (flatpak-run-cups.c: a socket CUPS_SERVER, else the
first line spelled exactly "ServerName /path" in ~/.cups/client.conf, else the
default), while CUPS reads the keyword in any case and takes the last ServerName. The
wrapper now redirects only when CUPS' socket is the one Flatpak chose, so a job can't
reach a different server; otherwise CUPS fails on the missing path.

* print: a printer listing past its deadline kills and reaps lpstat

Review: the 10 s limit stopped waiting but left a hung lpstat and its thread
running, so repeated print attempts could pile them up. photocraft now runs the two
lpstat listings itself (craft-print's lpstat_command and an equivalent lpstat -e,
parsed by printers_parsed) and kills and reaps each one at the shared deadline. The
output is read on a thread that ends with the process, so nothing outlives the job.

* packaging: never redirect a CUPS_SERVER the sandbox was given

Review (CodeRabbit): Flatpak picks the socket it mounts from the host's CUPS_SERVER,
so one set by flatpak run --env or an override may not be what it mounted. The
wrapper now leaves any CUPS_SERVER alone and redirects only a client.conf socket that
matches Flatpak's choice; otherwise CUPS fails on the missing path instead of
reaching a different server.

* packaging: point CUPS at the mounted socket only when it is the one Flatpak chose, else fail

Review (CodeRabbit): with a custom socket listed first and the default last in
~/.cups/client.conf, Flatpak mounts the custom one at /var/run/cups/cups.sock while
CUPS picks the last line, finds that path, and talks to the custom server. The
wrapper now compares CUPS' socket with Flatpak's choice even when the path exists:
equal, CUPS is pointed at the mount; different, the tool fails with an error instead
of reaching another server.

Also (CodeRabbit):
- A printer named in the job goes to lp even when CUPS lists none: listing can be
  forbidden while printing is not, and the portal can't honour a name.
- A failed lpstat -e (CUPS unreachable, listing forbidden) means the list can't be
  told, so lp takes the job, instead of an empty list sending it to the portal.
- A listing longer than 1 MiB counts as no answer, instead of growing without bound.
- The kill test checks the process with kill -0, which works on every Unix.
- docs/printing.md: the only-printer fallback is on the lp and IPP routes, and the
  portal is Linux only.
2026-10-29 18:51:10 -04:00

6.6 KiB
Raw Permalink Blame History

Printing

File › Print renders the flattened document onto one PDF page (paper size and orientation, position and scale, crop and registration marks, labels, colour handling) and hands that page to a printer. The rendering is print_cmds.rs in photocraft-engine, shared by the dialog, the batch commands and automation (file.print, file.printOneCopy); this page is about the hand-off.

Positioning the preview

Drag the visible image on the paper to change Top and Left. Starting a drag switches Center off using the currently resolved layout, including Scale to Fit Media, so the image stays at the grab point. Top/Left stay visible but disabled while Center is on, keeping the dialog and paper stationary when a drag enables them. Position fields accept negative values in inches, centimetres or millimetres; the stored command parameters are inches from the sheet's top-left. Manual positions are bounded to ±10000 inches. Height, width and paper dimensions retain their positive fields.

The preview converts egui point displacement to PDF points using its paper display scale. No extra Retina pixel factor is needed: painting and pointer input both use egui points. Emulsion Down reverses the horizontal position change to match the mirrored output. The image is clipped to the paper; any visible portion remains a drag target. A completely off-page image can be recovered with Top/Left or Center. These controls affect print parameters only, and the preview and generated PDF continue to share print_cmds::layout.

Routes to the printer

Route When What the user sees
craft-print (lp on CUPS, the Windows spooler) lp is on the path (macOS, Linux, the Flatpak, which bundles it) and CUPS lists a printer, or on Windows Sent to the printer (request id is Office-42)
CUPS over IPP (photocraft-ipp) No lp on the path and a print server answers: CUPS_SERVER, then /run/cups/cups.sock, /var/run/cups/cups.sock, /private/var/run/cupsd, then localhost:631; always for a printServer named in the request Sent to the printer (job 42 on Office)
Print portal (org.freedesktop.portal.Print, desktop app on Linux) Linux only (the Flatpak included): no print server reachable or no printer set up (including when lp is there but CUPS lists no printer and none was named), or always with PHOTOCRAFT_PRINT_PORTAL=1. Elsewhere a missing route is an error The system print dialog, seeded with the copies and paper; the outcome goes to the log
Print to PDF output given (the Output field of the dialog) The file; nothing is spooled unless send is true

A route that exists but fails (an unknown printer, a job the queue refuses) is an error; only a missing route (no lp, no print server, no printer) moves on to the next one. With no printer named and no default, the only printer takes the job on the lp and IPP routes; Windows needs a named or default printer.

The IPP client is in-process, pure Rust (RFC 8010/8011: CUPS-Get-Printers, CUPS-Get-Classes, CUPS-Get-Default, Print-Job with the PDF as the document), so no lp binary is needed. It only sees queues the server has, not driverless network printers, which is why the Flatpak bundles lp/lpstat (the manifest's cups-client module) and grants --socket=cups (the host's CUPS socket) and org.freedesktop.Avahi (lpstat -e finds driverless printers). On a system without CUPS, or in a sandbox without the socket, the app falls back to the print portal, whose dialog runs outside the sandbox.

Which printer

The dialog's Printer field, or "printer" in the command, names a queue (case-insensitively; a CUPS class counts, as with lp -d). Left empty, the server's default printer is used; without a default, the only printer; with several and no default, the error names them. With no printer at all, the portal takes over when it is available, otherwise the error says to add one in the system settings or print to PDF.

file.printers lists what the print server knows: {"printServer": "host[:port]|socket"} is optional and defaults to the system's CUPS; the result is {server, default, printers: [{name, info, state, acceptingJobs, default, class}]}, or an empty list with a note when no server answers. "printServer" on file.print sends one job to a named server; "dryRun": true renders the page and reports the equivalent lp command without spooling.

Orientation (Windows)

On Windows the paper and its orientation are the driver's: the Print dialog takes them from the printer's settings. Its Orientation buttons (Portrait, Landscape; Photoshop's Layout) turn those settings, as Print Settings… would, and the paper and preview turn with them. file.print.page {"orientation": "portrait|landscape"} does the same for scripts: it returns the turned settings as printerSettings, with the page they give. A print sent to a Windows printer whose layout is the driver's paper the other way round (a landscape layout, portrait settings) turns the settings to the layout before printing, rather than stopping with "the driver paper or orientation differs"; another paper is still refused.

Results

file.print returns sent, backend (lp, windows, ipp, portal), spooler (where the job went) and pdf (the page's path, only when output named one).

Environment

Variable Effect
CUPS_SERVER The print server, as CUPS itself reads it: host[:port] or a socket path
PHOTOCRAFT_PRINT_PORTAL=1 Always use the system print dialog (the portal) instead of lp or CUPS directly
PHOTOCRAFT_PRINT_PORTAL=0 Never use the portal; a missing print server is then an error

Verification

  • crates/ipp/src/tests.rs: the client against a fake CUPS over TCP and a Unix socket: printer listing with state and default, the default printer and its absence, Print-Job with the document and the job id, chunked and content-length replies, HTTP and IPP error statuses, broken replies.
  • crates/engine/src/print_cmds/tests.rs: printer resolution (requested, default, only one, several without a default, unknown, none); a complete file.print against a fake CUPS (printer URI, copies, job name, document format, the PDF itself) and file.printers; lp taking the job when it is on the path; the IPP route when it is not (a child process with an empty path); the app's spooler when preferred, as the fallback without a printer, and skipped when a server is named; print to PDF untouched.
  • apps/photocraft/src/print_portal.rs: the request path rule and the paper/copies seeding of the dialog (PWG names for the standard papers, millimetres for custom sizes).