mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 05:42:52 +00:00
Print: the printer's own paper sizes in the Paper list, named as the job's media (#2672)
* 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>
This commit is contained in:
1 parent
3be126f9bb
commit
e317ccae44
4 files changed
+390
-32
No files matched your search
@@ -757,7 +757,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
t(
|
||||
"printer_papers",
|
||||
"List printer paper sizes",
|
||||
"The media sizes (names and dimensions in points) and default paper supported by a printer driver. Pass printer: a name from printers, or \"default\".",
|
||||
"The media sizes (names and dimensions in points) and default paper supported by a printer driver (CUPS: its PPD's, by the driver's names, the default being its *DefaultPageSize; a job on one, or on a standard size of one, is fed it under the PPD's own keyword). Pass printer: a name from printers, or \"default\".",
|
||||
)
|
||||
.ro()
|
||||
.with(schema(json!({ "printer": { "type": "string" } }), &["printer"])),
|
||||
|
||||
+234
-30
@@ -194,6 +194,35 @@ pub(crate) fn output_within(command: &mut std::process::Command, limit: std::tim
|
||||
}
|
||||
}
|
||||
|
||||
/// The queue of a CUPS destination (`queue` or `queue/instance`); `None` when either part is not
|
||||
/// a plain name. Neither part is a path: dot components, backslashes and any further separator
|
||||
/// are refused before I/O. A leading `-` is refused too: `lpoptions` parses its argv with getopt,
|
||||
/// so `-p -E -l` would read `-E` as an option rather than as the destination, and `--` cannot
|
||||
/// help because it ends option parsing before the `-p` that names the queue.
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
fn queue_of(printer: &str) -> Option<&str> {
|
||||
let (queue, instance) = printer.split_once('/').map_or((printer, None), |(q, i)| (q, Some(i)));
|
||||
let valid = |name: &str| {
|
||||
!name.is_empty()
|
||||
&& name.len() <= 64
|
||||
&& !name.starts_with('-')
|
||||
&& !matches!(name, "." | "..")
|
||||
&& name.bytes().all(|b| b.is_ascii_graphic() && !matches!(b, b'/' | b'\\' | b':' | b'=' | b'"'))
|
||||
};
|
||||
(valid(queue) && instance.is_none_or(valid)).then_some(queue)
|
||||
}
|
||||
|
||||
/// The queue's PPD in `ppd_dir`, at most 16 MiB of it; `None` when the queue has none.
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
fn read_ppd(ppd_dir: &std::path::Path, queue: &str) -> Option<Vec<u8>> {
|
||||
use std::io::Read as _;
|
||||
const PPD_CAP: u64 = 16 << 20;
|
||||
let file = std::fs::File::open(ppd_dir.join(format!("{queue}.ppd"))).ok()?;
|
||||
let mut bytes = Vec::new();
|
||||
file.take(PPD_CAP).read_to_end(&mut bytes).ok()?;
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
/// Read a destination's options with the PPD directory and command supplied, so tests need
|
||||
/// neither a system queue nor a physical printer. CUPS instances share their queue's PPD but
|
||||
/// have their own defaults, queried with the complete `queue/instance` destination. `limit` is
|
||||
@@ -206,28 +235,8 @@ pub(crate) fn printer_options_via_within(
|
||||
limit: std::time::Duration,
|
||||
lang: &str,
|
||||
) -> Vec<PrinterOption> {
|
||||
let (queue, instance) = printer.split_once('/').map_or((printer, None), |(q, i)| (q, Some(i)));
|
||||
// Retain the conservative name checks, applied to each component. Neither component is
|
||||
// a path: reject dot components, backslashes and any additional separator before I/O. A
|
||||
// leading `-` is refused too: `lpoptions` parses its argv with getopt, so `-p -E -l` would
|
||||
// read `-E` as an option rather than as the destination, and `--` cannot help because it
|
||||
// ends option parsing before the `-p` that names the queue.
|
||||
let valid = |name: &str| {
|
||||
!name.is_empty()
|
||||
&& name.len() <= 64
|
||||
&& !name.starts_with('-')
|
||||
&& !matches!(name, "." | "..")
|
||||
&& name.bytes().all(|b| b.is_ascii_graphic() && !matches!(b, b'/' | b'\\' | b':' | b'=' | b'"'))
|
||||
};
|
||||
if !valid(queue) || instance.is_some_and(|name| !valid(name)) {
|
||||
return Vec::new();
|
||||
}
|
||||
const PPD_CAP: u64 = 16 << 20;
|
||||
let ppd = std::fs::File::open(ppd_dir.join(format!("{queue}.ppd"))).ok().and_then(|f| {
|
||||
use std::io::Read as _;
|
||||
let mut bytes = Vec::new();
|
||||
f.take(PPD_CAP).read_to_end(&mut bytes).ok().map(|_| bytes)
|
||||
});
|
||||
let Some(queue) = queue_of(printer) else { return Vec::new() };
|
||||
let ppd = read_ppd(ppd_dir, queue);
|
||||
let mut options = ppd.as_deref().map(|b| parse_ppd(&ppd_text(b))).unwrap_or_default();
|
||||
let lpoptions_out = output_within(lpoptions.args(["-p", printer, "-l"]), limit).ok();
|
||||
let current = lpoptions_out.as_deref().map(|out| parse_lpoptions(&String::from_utf8_lossy(out))).unwrap_or_default();
|
||||
@@ -390,6 +399,8 @@ pub(crate) fn with_job_info(job: &Job, owner: Option<&str>) -> Job {
|
||||
|
||||
/// The job as the system spooler takes it.
|
||||
fn spooled(job: &Job) -> Job {
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
let job = &with_ppd_media(job, |printer| ppd_papers_in(printer, std::path::Path::new("/etc/cups/ppd")));
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
with_job_info(job, std::env::var("USER").ok().as_deref())
|
||||
@@ -492,10 +503,206 @@ fn printer_papers_internal(printer: &str) -> Vec<Paper> {
|
||||
papers
|
||||
}
|
||||
|
||||
/// The paper sizes supported by `printer` (fallback for non-Windows platforms).
|
||||
/// The paper sizes supported by `printer` on CUPS: its PPD's (see [`cups_paper_info`]); none on
|
||||
/// the web.
|
||||
#[cfg(not(windows))]
|
||||
pub fn printer_papers(_printer: &str) -> Vec<Paper> {
|
||||
Vec::new()
|
||||
pub fn printer_papers(printer: &str) -> Vec<Paper> {
|
||||
cups_paper_info(printer).0
|
||||
}
|
||||
|
||||
/// CUPS: the papers of the queue's PPD, each named by the driver's label, and its
|
||||
/// `*DefaultPageSize` one (the stock a label printer is set up for, an office printer's tray
|
||||
/// paper); none for a queue without a PPD and on the web. A job on one of them is fed it under
|
||||
/// the PPD's own keyword ([`with_ppd_media`]).
|
||||
#[cfg(not(windows))]
|
||||
fn cups_paper_info(printer: &str) -> (Vec<Paper>, Option<Paper>) {
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
{
|
||||
cups_paper_info_in(printer, std::path::Path::new("/etc/cups/ppd"))
|
||||
}
|
||||
#[cfg(not(all(unix, not(target_arch = "wasm32"))))]
|
||||
{
|
||||
let _ = printer;
|
||||
(Vec::new(), None)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`cups_paper_info`] with the PPD directory supplied, so tests need no system queue.
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn cups_paper_info_in(printer: &str, ppd_dir: &std::path::Path) -> (Vec<Paper>, Option<Paper>) {
|
||||
let papers = ppd_papers_in(printer, ppd_dir);
|
||||
let default = papers.iter().find(|p| p.default).map(|p| (p.label.clone(), p.size));
|
||||
(papers.into_iter().map(|p| (p.label, p.size)).collect(), default)
|
||||
}
|
||||
|
||||
/// A paper of a PPD: one `*PaperDimension` entry.
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct PpdPaper {
|
||||
/// The driver's keyword for it (`Letter`, `w288h432`), which a job's `media` names.
|
||||
pub key: String,
|
||||
/// What the driver calls it (`US Letter`); the keyword when it gives no name.
|
||||
pub label: String,
|
||||
/// Width and height in points as the driver feeds it: a label's width is across the feed,
|
||||
/// so a 2.25 × 1.25 in label and a 1.25 × 2.25 in one are different stock.
|
||||
pub size: (f64, f64),
|
||||
/// Whether it is the PPD's `*DefaultPageSize`.
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
/// The papers of the queue's PPD in `ppd_dir`; none when the destination is not a plain name or
|
||||
/// its queue has no PPD.
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
fn ppd_papers_in(printer: &str, ppd_dir: &std::path::Path) -> Vec<PpdPaper> {
|
||||
queue_of(printer).and_then(|queue| read_ppd(ppd_dir, queue)).map(|ppd| parse_ppd_papers(&ppd_text(&ppd))).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The largest side a PPD paper may have, in points: PDF's own page limit (200 inches).
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
const MAX_PAPER_SIDE: f64 = 14_400.0;
|
||||
/// How many papers a PPD may list (PPDs are text files from the printer vendor).
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
const MAX_PAPERS: usize = 400;
|
||||
|
||||
/// The paper sizes a PPD offers: its `*PaperDimension key/label: "width height"` entries in
|
||||
/// file order and as written, each named by its own label, else its `*PageSize` label, else the
|
||||
/// keyword, the `*DefaultPageSize` one marked as the default. Custom-size placeholders,
|
||||
/// translations (`*fr.PaperDimension`), repeated keywords, keywords that could not be passed as
|
||||
/// a CUPS option value, and sizes outside 1 pt to 200 in are left out. Lenient: malformed lines
|
||||
/// are skipped.
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
pub(crate) fn parse_ppd_papers(ppd: &str) -> Vec<PpdPaper> {
|
||||
// A keyword goes to `lp -o media=<keyword>`: no space, quote, separator or `=`.
|
||||
let keyword = |s: &str| {
|
||||
!s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_graphic() && !matches!(b, b'/' | b':' | b'=' | b'"' | b'\'' | b'\\' | b','))
|
||||
};
|
||||
// `*Prefix key/label: value` → (key, label, value), the label falling back to the key.
|
||||
let entry = |line: &str, prefix: &str| -> Option<(String, String, String)> {
|
||||
let (head, value) = line.trim_end_matches('\r').strip_prefix(prefix)?.split_once(':')?;
|
||||
let head = head.trim();
|
||||
let (key, label) = match head.split_once('/') {
|
||||
Some((k, l)) if !l.trim().is_empty() => (k.trim(), l.trim()),
|
||||
Some((k, _)) => (k.trim(), k.trim()),
|
||||
None => (head, head),
|
||||
};
|
||||
keyword(key).then(|| (key.to_string(), label.to_string(), value.trim().to_string()))
|
||||
};
|
||||
let mut names: std::collections::BTreeMap<String, String> = Default::default();
|
||||
for line in ppd.lines() {
|
||||
if let Some((key, label, _)) = entry(line, "*PageSize ")
|
||||
&& label != key
|
||||
{
|
||||
names.entry(key).or_insert(label);
|
||||
}
|
||||
}
|
||||
let default_key = ppd
|
||||
.lines()
|
||||
.find_map(|line| line.trim_end_matches('\r').strip_prefix("*DefaultPageSize:").map(str::trim))
|
||||
.filter(|k| keyword(k))
|
||||
.map(str::to_string);
|
||||
let fits = |v: f64| v.is_finite() && (1.0..=MAX_PAPER_SIDE).contains(&v);
|
||||
let mut out: Vec<PpdPaper> = Vec::new();
|
||||
for line in ppd.lines() {
|
||||
if out.len() >= MAX_PAPERS {
|
||||
break;
|
||||
}
|
||||
let Some((key, label, value)) = entry(line, "*PaperDimension ") else { continue };
|
||||
if key.starts_with("Custom") || out.iter().any(|p| p.key == key) {
|
||||
continue;
|
||||
}
|
||||
let mut sides = value.trim_matches('"').split_whitespace().map(|v| v.parse::<f64>().ok());
|
||||
let (Some(Some(w)), Some(Some(h))) = (sides.next(), sides.next()) else { continue };
|
||||
if !fits(w) || !fits(h) {
|
||||
continue;
|
||||
}
|
||||
let label = if label == key { names.get(&key).cloned().unwrap_or(label) } else { label };
|
||||
let default = default_key.as_deref() == Some(key.as_str());
|
||||
out.push(PpdPaper { key, label, size: (w, h), default });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The paper of `papers` of this size, within half a point a side: the one fed that way round
|
||||
/// first (a label's width is its width across the feed), else the one fed the other way round.
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
pub(crate) fn ppd_paper_of_size(papers: &[PpdPaper], (w, h): (f64, f64)) -> Option<&PpdPaper> {
|
||||
let same = |p: &&PpdPaper, (w, h): (f64, f64)| (p.size.0 - w).abs() <= 0.5 && (p.size.1 - h).abs() <= 0.5;
|
||||
papers.iter().find(|p| same(p, (w, h))).or_else(|| papers.iter().find(|p| same(p, (h, w))))
|
||||
}
|
||||
|
||||
/// How far a side of a PPD paper may be from a standard name's nominal size and still be that
|
||||
/// paper: Adobe's 5 points, which CUPS also uses to find the PPD size for a media name. A driver
|
||||
/// may measure a stock in millimetres (a "4 × 6 in" label of 102 × 152 mm is 1.1 pt off), and
|
||||
/// neighbouring label sizes differ by more.
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
const NOMINAL_SIZE_EPSILON: f64 = 5.0;
|
||||
|
||||
/// The paper of `papers` nearest a nominal size, within [`NOMINAL_SIZE_EPSILON`] a side, fed
|
||||
/// either way round; on a tie the one fed that way round, then the first.
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
pub(crate) fn ppd_paper_near_size(papers: &[PpdPaper], (w, h): (f64, f64)) -> Option<&PpdPaper> {
|
||||
let off = |p: &PpdPaper, (w, h): (f64, f64)| {
|
||||
let (dw, dh) = ((p.size.0 - w).abs(), (p.size.1 - h).abs());
|
||||
(dw <= NOMINAL_SIZE_EPSILON && dh <= NOMINAL_SIZE_EPSILON).then_some(dw + dh)
|
||||
};
|
||||
let fed_as_is = papers.iter().filter_map(|p| Some((off(p, (w, h))?, 0, p)));
|
||||
let turned = papers.iter().filter_map(|p| Some((off(p, (h, w))?, 1, p)));
|
||||
fed_as_is.chain(turned).min_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1))).map(|(_, _, p)| p)
|
||||
}
|
||||
|
||||
/// The size in points that a standard media name stands for: one of [`crate::cups_media`]'s
|
||||
/// names, a PPD-style `w<width>h<height>` in points, or a PWG 5101.1 self-describing name
|
||||
/// (`na_index-4x6_4x6in`, `iso_a6_105x148mm`). `None` for any other name.
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
pub(crate) fn nominal_media_size(media: &str) -> Option<(f64, f64)> {
|
||||
let sides = |s: &str, x: char, scale: f64| {
|
||||
let (w, h) = s.split_once(x)?;
|
||||
let (w, h) = (w.parse::<f64>().ok()? * scale, h.parse::<f64>().ok()? * scale);
|
||||
(w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0).then_some((w, h))
|
||||
};
|
||||
if let Some(index) = (0..crate::PAPERS.len()).find(|&i| crate::cups_media(i).is_some_and(|m| m.eq_ignore_ascii_case(media))) {
|
||||
return crate::PAPERS.get(index).map(|(_, size)| *size);
|
||||
}
|
||||
if let Some(size) = media.strip_prefix('w').and_then(|s| sides(s, 'h', 1.0)) {
|
||||
return Some(size);
|
||||
}
|
||||
let (class_name, dims) = media.rsplit_once('_')?;
|
||||
if !class_name.contains('_') {
|
||||
return None;
|
||||
}
|
||||
if let Some(dims) = dims.strip_suffix("in") {
|
||||
return sides(dims, 'x', 72.0);
|
||||
}
|
||||
sides(dims.strip_suffix("mm")?, 'x', 72.0 / 25.4)
|
||||
}
|
||||
|
||||
/// CUPS: the job with its `media` named by the printer's own PPD keyword when it names one of
|
||||
/// the PPD's papers by its label (as [`printer_papers`] lists them), as `Custom.<w>x<h>` points
|
||||
/// of one of their sizes ([`crate::custom_media`]), or by a standard name of one of their sizes
|
||||
/// ([`nominal_media_size`], such as the `w288h432` of PdfCraft's 4 × 6 in label). Fed under the
|
||||
/// PPD's keyword, the paper is the driver's own: a driver may take no custom sizes, and a label
|
||||
/// printer's stock is set up under its keyword. A keyword the PPD has, any other value, a job
|
||||
/// without `media` and one for the default destination go as they are.
|
||||
#[cfg(any(test, all(unix, not(target_arch = "wasm32"))))]
|
||||
pub(crate) fn with_ppd_media(job: &Job, papers: impl FnOnce(&str) -> Vec<PpdPaper>) -> Job {
|
||||
let mut job = job.clone();
|
||||
let Some(printer) = job.printer.clone() else { return job };
|
||||
let Some(media) = job.options.iter_mut().rev().find(|(k, _)| k.eq_ignore_ascii_case("media")) else { return job };
|
||||
let papers = papers(&printer);
|
||||
if papers.iter().any(|p| p.key == media.1) {
|
||||
return job;
|
||||
}
|
||||
let custom =
|
||||
media.1.strip_prefix("Custom.").and_then(|s| s.split_once('x')).and_then(|(w, h)| Some((w.parse::<f64>().ok()?, h.parse::<f64>().ok()?)));
|
||||
let paper = papers
|
||||
.iter()
|
||||
.find(|p| p.label == media.1)
|
||||
.or_else(|| custom.and_then(|size| ppd_paper_of_size(&papers, size)))
|
||||
.or_else(|| nominal_media_size(&media.1).and_then(|size| ppd_paper_near_size(&papers, size)));
|
||||
if let Some(paper) = paper {
|
||||
media.1 = paper.key.clone();
|
||||
}
|
||||
job
|
||||
}
|
||||
|
||||
/// Match `paper_pts` against detected driver `papers` within a 1.0 pt tolerance.
|
||||
@@ -528,7 +735,7 @@ pub fn printer_paper_info(printer: &str) -> (Vec<Paper>, Option<Paper>) {
|
||||
#[cfg(windows)]
|
||||
let papers = printer_papers_internal(printer);
|
||||
#[cfg(not(windows))]
|
||||
let papers = printer_papers(printer);
|
||||
let (papers, cups_default) = cups_paper_info(printer);
|
||||
#[cfg(windows)]
|
||||
let default_paper = {
|
||||
let settings = craft_print::rgb::default_settings(printer).ok();
|
||||
@@ -536,10 +743,7 @@ pub fn printer_paper_info(printer: &str) -> (Vec<Paper>, Option<Paper>) {
|
||||
page.map(|p| resolve_paper_name(&papers, p.paper))
|
||||
};
|
||||
#[cfg(not(windows))]
|
||||
let default_paper = {
|
||||
let _ = &papers;
|
||||
None
|
||||
};
|
||||
let default_paper = cups_default;
|
||||
(papers, default_paper)
|
||||
}
|
||||
|
||||
|
||||
@@ -801,6 +801,137 @@ fn a_job_that_names_its_orientation_keeps_it() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A label printer's PPD: `*PageSize` labels, `*PaperDimension` sizes, and the lines that
|
||||
/// must not become papers (custom placeholders, repeats, translations, nonsense, malformed).
|
||||
const LABEL_PPD: &str = "*PPD-Adobe: \"4.3\"\n\
|
||||
*OpenUI *PageSize/Media Size: PickOne\n\
|
||||
*DefaultPageSize: w288h432\n\
|
||||
*PageSize Letter/US Letter: \"<</PageSize[612 792]>>setpagedevice\"\n\
|
||||
*PageSize w288h432/4 x 6 in: \"\"\n\
|
||||
*PageSize Env10/#10 Envelope: \"\"\n\
|
||||
*PageSize Wide/Wide: \"\"\n\
|
||||
*PageSize Custom.WIDTHxHEIGHT/Custom: \"\"\n\
|
||||
*fr.PageSize Letter/Lettre US: \"\"\n\
|
||||
*CloseUI: *PageSize\n\
|
||||
*PaperDimension Letter: \"612.0000 792.0000\"\r\n\
|
||||
*PaperDimension w288h432: \"288 432\"\n\
|
||||
*PaperDimension Env10/Envelope #10: \"297 684\"\n\
|
||||
*PaperDimension Wide: \"1224 792\"\n\
|
||||
*PaperDimension Custom: \"0 0\"\n\
|
||||
*PaperDimension Letter: \"1 1\"\n\
|
||||
*fr.PaperDimension Letter/Lettre: \"612 792\"\n\
|
||||
*PaperDimension Huge: \"20000 300\"\n\
|
||||
*PaperDimension Tiny: \"0.5 300\"\n\
|
||||
*PaperDimension Broken: \"abc 300\"\n\
|
||||
*PaperDimension Short: \"300\"\n\
|
||||
*PaperDimension NoColon \"1 2\"\n\
|
||||
*PaperDimension Bad/Key: \"300 300\"\n\
|
||||
*PaperDimension Two Words: \"300 300\"\n\
|
||||
*PaperDimension media=x: \"300 300\"\n\
|
||||
*PaperDimension Quo\"te: \"300 300\"\n\
|
||||
*PaperDimension a,b: \"300 300\"\n";
|
||||
|
||||
#[test]
|
||||
fn ppd_papers_come_from_the_ppd() {
|
||||
let papers = spool::parse_ppd_papers(LABEL_PPD);
|
||||
let listed: Vec<(&str, &str, (f64, f64))> = papers.iter().map(|p| (p.key.as_str(), p.label.as_str(), p.size)).collect();
|
||||
assert_eq!(
|
||||
listed,
|
||||
[
|
||||
("Letter", "US Letter", (612.0, 792.0)),
|
||||
("w288h432", "4 x 6 in", (288.0, 432.0)),
|
||||
("Env10", "Envelope #10", (297.0, 684.0)),
|
||||
("Wide", "Wide", (1224.0, 792.0)),
|
||||
("Bad", "Key", (300.0, 300.0)),
|
||||
],
|
||||
"a *PageSize label names a size, its own label wins, sizes stay as the driver feeds them; placeholders, repeats, translations, impossible sizes, malformed lines and keywords that would break a CUPS option (a space, `=`, a quote, a comma) are left out"
|
||||
);
|
||||
assert_eq!(papers.iter().map(|p| p.default).collect::<Vec<_>>(), [false, true, false, false, false], "the *DefaultPageSize one is the default");
|
||||
let key = |size| spool::ppd_paper_of_size(&papers, size).map(|p| p.key.as_str());
|
||||
assert_eq!(key((432.0, 288.0)), Some("w288h432"), "the other way round too");
|
||||
assert_eq!(key((612.4, 792.4)), Some("Letter"), "within half a point");
|
||||
assert_eq!(key((613.0, 792.0)), None);
|
||||
assert_eq!(key((f64::NAN, 792.0)), None);
|
||||
// A label fed either way is different stock: the exact way round wins when both exist.
|
||||
let labels = spool::parse_ppd_papers("*PaperDimension w90h162/1.25x2.25: \"90 162\"\n*PaperDimension w162h90/2.25x1.25: \"162 90\"\n");
|
||||
let key = |size| spool::ppd_paper_of_size(&labels, size).map(|p| p.key.as_str());
|
||||
assert_eq!((key((162.0, 90.0)), key((90.0, 162.0))), (Some("w162h90"), Some("w90h162")));
|
||||
assert!(spool::parse_ppd_papers("").is_empty());
|
||||
assert!(spool::parse_ppd_papers("*PaperDimension").is_empty());
|
||||
let many: String = (0..500).map(|i| format!("*PaperDimension P{i}: \"100 200\"\n")).collect();
|
||||
assert_eq!(spool::parse_ppd_papers(&many).len(), 400, "capped");
|
||||
}
|
||||
|
||||
/// A CUPS job on one of the PPD's papers, as the Print dialog and `doc_print` name it (by its
|
||||
/// label, or as custom points of its size), is fed it under the PPD's keyword; anything else
|
||||
/// goes as it is.
|
||||
#[test]
|
||||
fn a_job_on_a_ppd_paper_names_the_ppd_keyword() {
|
||||
let papers = |printer: &str| if printer == "Label" { spool::parse_ppd_papers(LABEL_PPD) } else { Vec::new() };
|
||||
let media = |printer: Option<&str>, options: &[(&str, &str)]| {
|
||||
let job = Job {
|
||||
printer: printer.map(Into::into),
|
||||
options: options.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
|
||||
..Job::default()
|
||||
};
|
||||
spool::with_ppd_media(&job, papers).options.into_iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(media(Some("Label"), &[("media", "Custom.288x432")]), ["media=w288h432"], "custom points of a PPD size");
|
||||
assert_eq!(media(Some("Label"), &[("media", "Custom.432x288")]), ["media=w288h432"], "turned");
|
||||
assert_eq!(media(Some("Label"), &[("media", "Custom.162x90")]), ["media=Custom.162x90"], "a size the PPD lacks");
|
||||
assert_eq!(media(Some("Label"), &[("media", "Envelope #10")]), ["media=Env10"], "by label");
|
||||
assert_eq!(media(Some("Label"), &[("media", "Wide")]), ["media=Wide"], "a keyword it has");
|
||||
assert_eq!(media(Some("Label"), &[("media", "na_letter_8.5x11in")]), ["media=Letter"], "a PWG name of a PPD size");
|
||||
assert_eq!(media(Some("Label"), &[("media", "iso_a4_210x297mm")]), ["media=iso_a4_210x297mm"], "a PWG name the PPD lacks");
|
||||
assert_eq!(media(Some("Label"), &[("InputSlot", "Tray2")]), ["InputSlot=Tray2"], "no media");
|
||||
assert_eq!(media(None, &[("media", "Custom.288x432")]), ["media=Custom.288x432"], "the default destination");
|
||||
assert_eq!(media(Some("Office"), &[("media", "Custom.288x432")]), ["media=Custom.288x432"], "a queue without the PPD");
|
||||
// The last `media` is the one CUPS uses, so that is the one named.
|
||||
assert_eq!(media(Some("Label"), &[("media", "Letter"), ("MEDIA", "Custom.288x432")]), ["media=Letter", "MEDIA=w288h432"]);
|
||||
}
|
||||
|
||||
/// A driver that measures its label stock in millimetres, under keywords of its own.
|
||||
const METRIC_LABEL_PPD: &str = "*PPD-Adobe: \"4.3\"\n\
|
||||
*PageSize 102x152/4\" x 6\": \"\"\n\
|
||||
*PageSize 57x32/57 x 32 mm: \"\"\n\
|
||||
*PageSize 58x32/58 x 32 mm: \"\"\n\
|
||||
*PaperDimension 102x152: \"289.13 430.87\"\n\
|
||||
*PaperDimension 57x32: \"161.57 90.71\"\n\
|
||||
*PaperDimension 58x32: \"164.41 90.71\"\n";
|
||||
|
||||
#[test]
|
||||
fn a_job_on_a_standard_size_names_the_ppd_keyword_of_that_size() {
|
||||
let media = |value: &str| {
|
||||
let job = Job { printer: Some("Label".into()), options: vec![("media".into(), value.into())], ..Job::default() };
|
||||
let job = spool::with_ppd_media(&job, |_| spool::parse_ppd_papers(METRIC_LABEL_PPD));
|
||||
job.options.into_iter().map(|(_, v)| v).collect::<Vec<_>>()
|
||||
};
|
||||
let label = cups_media(LABEL_PAPER).unwrap_or_default();
|
||||
assert_eq!(media(label), ["102x152"], "PdfCraft's 4 × 6 in label on the driver's 102 × 152 mm stock");
|
||||
assert_eq!(media("na_index-4x6_4x6in"), ["102x152"], "a PWG name in inches");
|
||||
assert_eq!(media("oe_4x6-label_4x6in"), ["102x152"]);
|
||||
assert_eq!(media("w432h288"), ["102x152"], "turned");
|
||||
assert_eq!(media("w162h90"), ["57x32"], "the nearest of two sizes in reach");
|
||||
assert_eq!(media("om_small-label_57x32mm"), ["57x32"], "a PWG name in millimetres");
|
||||
assert_eq!(media("w288h440"), ["w288h440"], "more than 5 pt off");
|
||||
assert_eq!(media("Custom.288x432"), ["Custom.288x432"], "custom points match within half a point or not at all");
|
||||
for name in ["w288", "wxh432", "w-288h432", "na_x_4x6", "na_x_axbin", "x_4x6in", "winfhinf"] {
|
||||
assert_eq!(media(name), [name], "{name} names no size");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nominal_media_sizes() {
|
||||
assert_eq!(spool::nominal_media_size("Letter"), Some((612.0, 792.0)));
|
||||
assert_eq!(spool::nominal_media_size("a4"), Some((595.28, 841.89)), "cups_media's names, in any case");
|
||||
assert_eq!(spool::nominal_media_size("w288h432"), Some((288.0, 432.0)));
|
||||
assert_eq!(spool::nominal_media_size("na_index-4x6_4x6in"), Some((288.0, 432.0)));
|
||||
let a6 = spool::nominal_media_size("iso_a6_105x148mm").unwrap_or_default();
|
||||
assert!((a6.0 - 297.64).abs() < 0.01 && (a6.1 - 419.53).abs() < 0.01, "{a6:?}");
|
||||
assert_eq!(spool::nominal_media_size("Roll80mm"), None);
|
||||
assert_eq!(spool::nominal_media_size("w0h432"), None);
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
mod cups_options {
|
||||
use crate::spool;
|
||||
@@ -917,6 +1048,25 @@ mod cups_options {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// `printer_papers` and `printer_default_paper` on CUPS: the queue's PPD papers by label and
|
||||
/// its `*DefaultPageSize`, instances sharing their queue's PPD; nothing for a queue without
|
||||
/// one or a destination that isn't a plain name.
|
||||
#[test]
|
||||
fn printer_papers_are_read_from_the_queue_ppd() {
|
||||
let dir = folder();
|
||||
std::fs::write(dir.join("Label.ppd"), super::LABEL_PPD).unwrap();
|
||||
for destination in ["Label", "Label/draft"] {
|
||||
let (papers, default) = spool::cups_paper_info_in(destination, &dir);
|
||||
let names: Vec<&str> = papers.iter().map(|(name, _)| name.as_str()).collect();
|
||||
assert_eq!(names, ["US Letter", "4 x 6 in", "Envelope #10", "Wide", "Key"], "{destination}");
|
||||
assert_eq!(default, Some(("4 x 6 in".to_string(), (288.0, 432.0))), "{destination}");
|
||||
}
|
||||
for destination in ["Missing", "../Label", "Label/a/b", "Label\\draft", "", ".", "-Label"] {
|
||||
assert_eq!(spool::cups_paper_info_in(destination, &dir), (Vec::new(), None), "{destination:?}");
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_destinations_are_rejected_before_running_lpoptions() {
|
||||
let dir = folder();
|
||||
|
||||
@@ -8409,6 +8409,10 @@ evidence = [
|
||||
"crates/ui-egui/tests/it/print.rs::printer_papers_and_properties_selection",
|
||||
"crates/automation/tests/it/automation.rs::printer_papers_through_tools",
|
||||
"crates/print/src/tests.rs::printer_papers_and_default_paper_api",
|
||||
"crates/print/src/tests.rs::ppd_papers_come_from_the_ppd",
|
||||
"crates/print/src/tests.rs::a_job_on_a_ppd_paper_names_the_ppd_keyword",
|
||||
"crates/print/src/tests.rs::a_job_on_a_standard_size_names_the_ppd_keyword_of_that_size",
|
||||
"crates/print/src/tests.rs::printer_papers_are_read_from_the_queue_ppd",
|
||||
"crates/print/src/tests.rs::overlapping_paper_discovery_two_sided_and_queue_queries",
|
||||
"crates/ui-egui/tests/it/print.rs::a_custom_paper_prints_at_its_size_and_its_printer_keeps_it",
|
||||
"crates/ui-egui/src/workflows/print_paper.rs::the_paper_list_is_the_standard_sizes_then_the_custom_ones",
|
||||
@@ -8417,7 +8421,7 @@ evidence = [
|
||||
"crates/ui-egui/tests/it/print_custom_paper_names.rs::a_custom_paper_cannot_take_a_standard_name",
|
||||
"crates/automation/tests/it/automation.rs::doc_print_takes_a_paper_size_of_its_own",
|
||||
]
|
||||
notes = "In the Print dialog: Custom… beside Paper adds a named size in the preferences' unit (1 to 200 inches a side), kept with the settings and removable. A printer printed to on a custom paper starts on it next time (#2001). The Page Setup button opens the driver's page setup to choose paper size and orientation. printer_papers enumerates driver-reported papers and default paper headlessly. doc_print takes sizes such as 8.5x14in or 210x297mm or driver papers. Missing: margins."
|
||||
notes = "In the Print dialog: Custom… beside Paper adds a named size in the preferences' unit (1 to 200 inches a side), kept with the settings and removable. A printer printed to on a custom paper starts on it next time (#2001). The Page Setup button opens the driver's page setup to choose paper size and orientation. printer_papers enumerates driver-reported papers and default paper headlessly. On CUPS they are the queue's PPD papers by the driver's names and its *DefaultPageSize, and a job on one, or on a standard size of one (such as the 4 x 6 in label), is fed it under the PPD's keyword. doc_print takes sizes such as 8.5x14in or 210x297mm or driver papers. Missing: margins."
|
||||
|
||||
[[feature]]
|
||||
id = "print.print-ready-pdf"
|
||||
|
||||
Reference in new issue
Block a user