* 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>
* Print: flatten transparent sheets for GDI (v3) printer drivers
Windows sends a job for a GDI (v3) driver through its XPS-to-GDI
conversion, which fails on soft masks (#6155), prints halos around them
(#5838) or prints transparency as black boxes (#5940). XPS (v4) drivers
print the same pages correctly.
spooler_copy takes a flatten_transparency flag. With it, after greying
and before tiling, each sheet that uses transparency is replaced by an
opaque picture of itself (spooler::flattened): rendered at 300 dpi, lower
for a sheet past 80 MP or 16000 px a side, in tiles of at most
2000 x 2000 px, each an RGB Flate image over white, so no tile reaches
Windows' 2^22 px image cap and tiling leaves them alone. The sheet keeps
its boxes and rotation; its group and annotations go. Sheets without
transparency stay vector.
pdfcraft-preflight gets page_uses_transparency, the PDF/A transparency
search without counting a transparency group on its own, which Word
puts on every page.
Both callers pass false until the GDI-driver check lands.
* Print: flatten transparent sheets only for a GDI (v3) printer driver
spool::xps_driver / spool::gdi_driver read whether the printer's driver renders XPS itself,
through craft-print's new xps_driver (storytold/craft-libs#804, winprint
storytold/winprint.rs#21). The Print dialog and doc_print pass gdi_driver to spooler_copy's
flatten_transparency, so XPS (v4) drivers, PDF printers among them, keep vector sheets.
craft-print pinned to the craft-libs#804 branch, winprint to #21's, until they merge.
* craft-print 60ede99985ef1e5df87a2aca66ed538eb23dec99: craft-libs main with #804 (xps_driver), winprint bc06a7c4 (#21)
* Cargo.lock: the two winprint entries in Cargo's order (by source, after the re-pin)
* Print: the printer's own paper sizes in the Paper list, named as the job's media
The work of #2672 on current main as one commit (its earlier four commits are on
MaxFairwinds/pdfcraft `feat/printer-paper-sizes-pre-squash`), moved to the split Print dialog
(workflows/print_ui/{draft,body}.rs) and the single integration-test binaries.
- spool::printer_papers: on CUPS the queue PPD's *PaperDimension entries (named by their labels,
the *DefaultPageSize one marked), read with the same queue checks and capped PPD read as
printer_options; paper_of_size finds one by size, as the driver feeds it.
- The Paper list offers the printer's sizes after the standard ones, read each time the dialog
opens and when the printer changes. It starts on the printer's default size (the standard
entry when that is a standard size as the driver feeds it), unless a paper was chosen, in
the dialog or remembered from the last session (#2156), and the printer lists it.
- The job names the chosen sheet as its media by the printer's own keyword, also for a standard
size the printer has, else by main's media_option.
- printer_papers tool; doc_print takes one of the printer's sizes as paper (paper_choice), and
with none named uses the printer's default, for "default" the default queue's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Print: name the driver's default keyword for a size it offers twice; fix a parity evidence path
A standard size chosen in the Paper list is sent as the printer's own keyword
for it. When the driver offers that size under two keywords (Letter and
Letter.Fullbleed), the job now names the driver's default one rather than the
first. The printer_papers_are_read_from_the_queue_ppd evidence entry named a
module path that xtask parity cannot resolve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Print: a chosen printer paper keeps its keyword when the sizes are read again
When the chosen paper was one of the printer's own entries and its size is a
standard one (Letter.Fullbleed), reading the printer's sizes again (the dialog
reopened) matched it by size alone: it became the standard Letter and the job
named the driver's default keyword for that size. The entry is now kept by its
keyword when the printer still lists it at that size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Print: test that a PPD keyword that would break a CUPS option is not offered as a paper
A paper's keyword becomes the job's `media` option. parse_ppd_papers already
leaves out keywords with a space, `=` or a quote; the PPD test now has lines
with each, and fails when that check is loosened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Print: on CUPS a job on a standard size names the PPD keyword of that size
PdfCraft's 4 x 6 in preset goes out as w288h432, which a driver with its
own keyword for that stock (a 102 x 152 mm label) didn't get mapped. A
cups_media name, a w<w>h<h> name or a PWG self-describing name now maps
to the nearest PPD paper within 5 pt a side, the epsilon CUPS's
_ppdCacheGetPageSize uses.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* macOS: a job that names no paper tray asks for Auto Select (craft-print 1ee01a4c, #3752)
craft-print 1ee01a4c (storytold/craft-libs#232) sends `AP_D_InputSlot=` on macOS for a job whose
options name no tray, as the system Print dialog's Paper Feed: Auto Select does. Without it CUPS
used the PPD's default tray, and a printer whose default tray is empty held every job while CUPS
reported it completed (the Xerox AltaLink in #3752).
- The Print dialog's driver options take the queue's defaults through craft-print's
apply_queue_defaults: option and choice names match as CUPS matches them, and on macOS the
paper tray starts on Auto Select, which is what the job sends.
- The two tests that pin the exact `lp` arguments expect the Auto Select option on macOS.
* Tests: on macOS a CUPS queue on its driver's default tray shows Auto Select (craft-libs#232)
instances_read_the_queue_ppd_and_their_own_defaults and
a_queue_that_never_answers_is_given_up_at_the_deadline expected the PPD's default tray; on macOS
apply_queue_defaults now offers Auto Select first and as the default there, which is what a job
naming no tray sends. Found by running the print tests on hosted macOS.
* M10: a booklet's both sides follow a driver that keeps its own two-sided setting (#5772)
Booklet ▸ Both sides always asked for two-sided printing on the short
edge (#2707). A driver that doesn't take two-sided settings from other
programs (no two-sided printing, manual two-sided only in its
Properties, Microsoft Print to PDF) then made craft-print refuse the
whole job. The reporter's Epson ET-2870 offers only OneSided, and its
manual two-sided setting is what prints their normal two-sided jobs.
print::booklet_job_duplex asks such a driver for nothing on both
sides, so its own setting applies, as for ordinary printing (#2422)
and as Acrobat enables two-sided for a booklet only "if available".
Unknown support still asks for the short edge; one side on its own
stays one-sided. The Print dialog takes the chosen printer's support
from the printer list (PrintDraft::printer_two_sided, also behind its
"its own setting applies" note), and doc_print reads it only for a
booklet's both sides.
The dialog's booklet test used the machine's printers, and on Windows
the default, Microsoft Print to PDF, now gives the other answer: it
sets its own printers and checks the note too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: doc_print's lookup of a booklet printer's two-sided support is tested (#5772)
The booklet test fed job_duplex the printer's answer directly, so a
lookup that missed the default printer or matched the wrong one would
have gone unnoticed. The search over the printer list is now its own
function, two_sided_in, tested with a list of named, default and
unknown printers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: a booklet printed to "default" goes to the printer whose driver was asked (#5772)
doc_print decides a booklet's both sides from the printer's two-sided
support. For "default", the job named no printer, so the spooler picked
whichever printer was the default when the job was submitted, after the
document was rendered. The printer found in the list is now the job's
printer, by name, and the lookup test covers a one-sided default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5903: the booklet test lists its printers through a stand-in source
It set the dialog's printer list by hand while the real list was still loading. On a machine with
no printers (CI's Linux runner) the list that arrived replaced them and reset the printer, so the
dialog the test clicked through was not the one it set up. The printers now come from the
dialog's own source, drivers unasked as #3554 lists them, with each driver's two-sided answer in
the map.
---------
Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* M10 #5130: resolve Windows printer-default duplex before spooling
* M10 #5130: document printer duplex resolution
* M10 #5130: adopt DocumentDuplex driver fix from craft-print
* M10 #5130: adopt two-sided capability selection fix
* M10 #5130: update documented craft-print revision
* M10 #5130: fall back when Windows duplex defaults cannot be read
* fix(print): satisfy duplex review formatting and refresh shared pin
* Repin craft-print to craft-libs main (#411 and #133 merged); ask only the picked printer's driver for two-sided support
craft-libs b6b8a55b has #411 (DocumentDuplex) and #133 (two_sided(name)). The
Printer default support check now reads that one driver instead of listing
every printer's capabilities at submit time (#2192).
---------
Co-authored-by: robertobulltech <341520377+robertobulltech@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Print: list printers without asking drivers; ask only about the one picked (#2192)
On Windows, craft-print's printers() reads every driver's capabilities to
report two-sided support. Windows takes about 48 s to give up on a printer
it can't reach, such as a WSD printer on another network, so the Print
dialog waited for each one before it listed anything (#2192).
The dialog now lists printers from craft-print's printer_queues(), which
asks no driver, and asks two_sided() about the printer the user picks, on
a background thread (PdfCraftApp::two_sided_source, set by the desktop app
like printer_source). Each answer is kept for the session, so a printer is
asked about at most once, and one nobody picks is never waited on. The
"doesn't take two-sided settings" note shows once the answer arrives.
The automation printers tool still uses printers(), so it keeps reporting
two_sided.
Needs the craft-libs change that adds craft_print::two_sided.
* Build: take craft-print from storytold/craft-libs#133
Pins craft-print to the head of storytold/craft-libs#133 (b6a3b87), which
adds craft_print::two_sided as one commit on craft-libs main at 5ee809f.
craft-print is otherwise unchanged from 7ee35a9, the rev main pins now; the
other craft-libs crates stay where they are. Fetched through the
storytold/craft-libs URL, so cargo-deny's allow-git is unchanged. To be
replaced by the merge commit once #133 lands.
* Print: run at most four two-sided reads at once
A driver Windows can't reach holds its two-sided read for about 48 s, so
picking printer after printer started a thread for each. At most four now
run at once; past that, the printer picked is asked once a read finishes.
Tests: hanging reads are capped and the picked printer is asked after one
finishes; a list that already says whether a driver takes two-sided
settings is used as it is, with no driver asked.
* Repin craft-print to craft-libs main, where #133 merged (b6b8a55b)
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* fix(print): M10 — Windows ya no cambia las letras de las fuentes TrueType incrustadas (#3132)
Un subconjunto TrueType incrustado numera sus glifos de nuevo (en el
subconjunto de Arial de ReportLab el espacio es el glifo 11; en la Arial
instalada, el 3). Al imprimir en Windows, Windows.Data.Pdf → Direct2D →
XPS conservaba esos números de glifo pero los dibujaba con la fuente
instalada del mismo nombre: «Revisión» salía como «Wj{nxn» y la «z» en
negrita como «Ä», tanto en las colas Brother como en la impresora Adobe PDF.
La copia que va a la cola de Windows renombra ahora cada programa
TrueType/OpenType incrustado como PdfCraftPrint<n>, en su tabla name
y en el PDF (/BaseFont, /FontName; el subconjunto conserva su etiqueta),
para que ninguna fuente instalada responda a ese nombre. Glifos,
codificaciones y /ToUnicode no cambian. CUPS y Guardar como PDF reciben
el PDF tal cual.
Tests: extremo a extremo con impose (la página se renderiza idéntica
antes y después, y el test falla sin el arreglo), fuentes que comparten
programa, Type 0, reescritura por plataforma de la tabla name y programas
dañados sin pánico. Falta comprobarlo en una impresora física con Windows.
Fixes#3132
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(print): M10 — solo se renombran en el PDF las fuentes cuyo programa se reescribe (#3132)
Si PdfCraft no puede leer o reescribir un programa incrustado (no es un
sfnt, está dañado o pasa de 64 MiB), ahora conserva también su /BaseFont
y su /FontName. Un visor que tampoco pueda cargarlo seguirá eligiendo el
sustituto por el nombre original («Arial») en vez de por PdfCraftPrint<n>.
Si no queda ningún programa reescrito, el PDF va a la cola tal cual.
Lo señaló CodeRabbit en el PR #4098. Test nuevo:
a_program_that_cannot_be_rewritten_keeps_its_name_in_the_pdf_too (falla
con el comportamiento anterior).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(print): M10 — el renombrado de fuentes para Windows decodifica contra un presupuesto total (#3132)
Cada programa ya tenía un tope de 64 MiB, pero un PDF con muchos
programas distintos podía sumar mucho trabajo de descompresión al
imprimir. Ahora todo el renombrado comparte un DecodeBudget de 512 MiB,
el mecanismo que main acaba de introducir en print. Agotado el
presupuesto, los programas que faltan conservan su nombre, igual que uno
ilegible.
Test nuevo: programs_past_the_shared_decode_budget_keep_their_names
(falla si se decodifica sin presupuesto).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(print): M10 — también se renombran los CIDFont escritos dentro de su fuente Type 0 (#3132)
Si /DescendantFonts lleva el CIDFont como diccionario en línea (en el
propio Type 0 o en un objeto array aparte), el recorrido no llegaba a
él. Cuando compartía programa con otra fuente, el programa se renombraba,
pero el Type 0, el CIDFont y su descriptor seguían diciendo «Arial», y
Windows podía volver a cruzarlos con la Arial instalada. Ahora el Type 0
se trata en el mismo recorrido, sin el segundo que hacía type0_names: un
CIDFont en línea apunta su programa, y al renombrar se cambian el Type 0,
el CIDFont y su descriptor en línea.
Lo señaló CodeRabbit en el PR #4098. Test nuevo:
a_cidfont_written_inside_its_type0_font_is_renamed_with_it (falla sin el
arreglo).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(print): M10 — renamed_table ya no copia cada cadena de la tabla name (#3132)
renamed_table copiaba el texto de cada registro antes de comprobar el
tamaño del resultado: una tabla name de menos de 1 MB con 65.535
registros apuntando a una misma cadena de 65.535 bytes reservaba unos
4 GiB, y un fallo de reserva cierra el proceso al imprimir en Windows.
Ahora la tabla se lee entera sin copiar ningún texto (cada registro
guarda el offset y la longitud de origen) y después cada cadena distinta
se guarda una sola vez: los registros que compartían cadena la siguen
compartiendo y el almacén nunca pasa de lo que alcanzan los offsets de
16 bits. Una tabla que declara más registros de los que caben antes de
sus cadenas se rechaza de entrada.
Lo encontró RomanLis91 en el PR #4098. Tests nuevos:
records_that_share_one_string_keep_sharing_it (falla con el código
anterior) y a_name_table_claiming_more_records_than_fit_before_its_strings_is_refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M10: grayscale with Print as image prints on a driver that keeps no grayscale (#5806)
With Print as image and grayscale, PdfCraft renders the pages as grey
pictures itself, yet the job still asked the driver for grayscale. A
driver that keeps no grayscale setting, Microsoft Print to PDF among
them, made craft-print refuse the job ("the printer's driver did not
retain the requested grayscale; nothing was sent"), so even the grey
pictures never printed. The job now asks the driver for grayscale only
when the pages aren't grey pictures already
(print::driver_needs_grayscale), in the dialog and in doc_print.
The print.grayscale parity note and the doc_print as_image description
say so, and the note names what is still missing: vector pages to such
a driver are refused rather than printed in colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: the grey-pictures test checks that the pictures are grey too (#5806)
The test checked that grey pictures don't ask the driver for grayscale,
but not that the pictures are grey. A change that dropped grayscale from
the dialog's raster settings, while the job still skipped the driver,
would have printed in colour with the test passing. It now asserts both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* fix(print): fall back to the lpoptions listing when the PPD is unreadable
cupsd keeps /etc/cups/ppd/<queue>.ppd at 0640 root:cups on Arch and
similar setups, so an ordinary desktop user cannot open it. The option
reader treated that permission error as "no options", and Print ▸
Properties… said "This printer offers no settings of its own" for a
queue whose driver options cupsd was serving all along through
lpoptions.
When the PPD is missing, unreadable, or parses to no options, the list is
now built from `lpoptions -p <queue> -l`. The options Craft handles
itself are skipped, as the PPD parser does, and the starred choice is the
fallback default.
Fixes#4699
* fix(print): skip CUPS custom-value templates in the lpoptions fallback
`lpoptions -l` prints `Custom`, `Custom.<TYPE>` (`Custom.PASSCODE`,
`Custom.STRING`, …) or `Custom.WIDTHxHEIGHT` in place of the choices of an
option whose value the user has to type. Nothing in the dialog collects that
value, so offering the template sent its literal text to the driver. Those
entries are now dropped, and a starred template never becomes the default.
The choice cap is also tested before the duplicate scan, so a listing past
`MAX_LPOPTIONS_CHOICES` no longer walks the stored choices on every token.
Refs #4699
* fix(print): refuse a destination that starts with a dash
`lpoptions` parses its argv with getopt, so `-p -E -l` reads `-E` as an
option rather than as the destination; `--` cannot help, because it ends
option parsing before the `-p` that names the queue. Reject a leading
`-` on the queue and on the instance component beside the existing name
checks, before the PPD is opened or the command is spawned.
* fix(print): read the listing by its last colon, and stop inventing a default
`list_group()` prints the option as `"%s/%s:"` from the PPD's keyword and
translated text and each choice as a bare `" %s"`, so a colon can sit in
the text but not in a choice keyword. Splitting at the first colon turned
the rest of the text ("Supporto: Tipo") into invented choices; split at
the last one instead.
When the choice CUPS marks is a custom-value template, the parser dropped
it and stood the first retained choice in as the default. The UI then
showed a value the job does not use, because only options changed away
from the default are submitted. Leave the default empty in that case:
there is nothing the dialog could collect, and an invented choice is
worse than no selection.
---------
Co-authored-by: Jarvis (Hermes) <jarvis@localhost>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Add Folio (8.5 x 13 in / 612 x 936 pt) and F4 (210 x 330 mm / 595.28 x 935.43 pt)
to the PAPERS preset table and map them to standard PWG 5101.1 media names
(na_foolscap_8.5x13in and om_folio_210x330mm) in cups_media.
Closes#4777.
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* M10: the Print dialog remembers two-sided, grayscale, collate, sizing and comments & forms (#2001)
Persist the choices that describe how this computer prints and restore
them on the next launch, under the names doc_print takes; copies, pages,
layout, orientation and the printer still start fresh for each job.
Those names now live in one place, pdfcraft_print::names, used by the
tool and the settings file. Settings are untrusted: an unknown name or a
custom scale outside the dialog's 1..=1000 % keeps the default.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* M10: the Print dialog also remembers a booklet's binding side (#2707)
Acrobat keeps the booklet binding (left or right) between prints;
PdfCraft started at Left every time. It is now kept with the other
remembered print choices, named as doc_print names it, and an unknown
saved value keeps the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Print starts each job's copies, pages, layout and orientation afresh (#2001)
CodeRabbit on #2103: the README promised that copies, pages, layout and orientation
start fresh, but open_print carried them over through `..keep` within a session.
ISO 32000-2 Table 147 initialises these per document when the print dialog opens
(NumCopies "typically 1", PrintPageRange), so each opening now resets them to the
defaults (Selected pages still win when thumbnails are picked); two-sided,
grayscale, collate, sizing, Comments & forms, binding, the paper and the printer
picked last carry over as before. Two-sided names now include "printer" (leave it to
the printer, main's default since #2422), so that choice is remembered too.
main's paper-persistence test expected grayscale not to persist; it is one of the
remembered preferences.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: a new print job also resets its layout's own options and leaves Label (#2001)
CodeRabbit on #2103: resetting the layout to Size kept pages per sheet, order, borders,
booklet subset and the poster options through `..keep`, so choosing Multiple again
brought back, say, a cut-and-stack order; and a job printed on Label went back to Size
without giving back the paper Label replaced. open_print now leaves Label through
set_handling (#3564) and resets the layout's own options with the rest of the job's
choices; the binding and sizing stay remembered. The README now says what resets on
each opening and what carries over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: the Print dialog also remembers Print as image and its resolution (#6392)
A printer that needs pictures keeps getting them after a restart: as_image and
dpi are saved with the other print preferences and restored, a resolution the
dialog doesn't offer keeping the default 300 dpi. The bad-settings checks move
to their own test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>