mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 21:11:52 +00:00
fix(print): landscape sheets stay landscape on CUPS (#2713)
On macOS and Linux the job sent no orientation, so CUPS left a landscape sheet unrotated and shrank it into the portrait paper named by `media` (Letter, A4, ...). Printing in Landscape therefore came out as a small landscape page on a portrait sheet. Add `pdfcraft_print::cups_orientation`, which gives `orientation-requested=4` when every sheet is landscape, and attach it to the job in two places: - the Print dialog (`print_now`); - the headless `doc_print` tool, which builds its own spooler job, so the automation tool and MCP server print the same way. An `orientation-requested` the caller already chose in `options` stands. Neither applies on Windows, whose print ticket sets the orientation. A job that mixes orientations gets nothing, since the option applies to the whole job. Record the new test as evidence on the `print.orientation` parity entry, saying what is still only checked by hand, and mention the option in the print crate README's CUPS paragraph. Checked against Apple's CUPS filters with a generic PostScript queue: a 792x612 sheet sent to Letter without the option stays unrotated at about 77% scale; with it the page is turned 90 degrees to fill the sheet. The new test fails with the fix stubbed out.
This commit is contained in:
1 parent
01b696df2c
commit
e3f91e9c0b
6 files changed
+56
-6
No files matched your search
@@ -202,7 +202,8 @@ impl Automation {
|
||||
};
|
||||
let settings = print::Settings { pages, paper, orientation, layout, content, raster, crops: Vec::new() };
|
||||
let sizes: Vec<(f64, f64)> = self.doc(a)?.info.pages.iter().map(|p| (p.width as f64, p.height as f64)).collect();
|
||||
let sheets = print::layout(&sizes, &settings).map_err(|e| bad(e.to_string()))?.len();
|
||||
let laid_out = print::layout(&sizes, &settings).map_err(|e| bad(e.to_string()))?;
|
||||
let sheets = laid_out.len();
|
||||
let bytes = self.session.print_pdf(id, &settings).map_err(failed)?;
|
||||
let mut out = json!({ "sheets": sheets, "pages": settings.pages.len(), "bytes": bytes.len() });
|
||||
match (a.opt_str("path")?, a.opt_str("printer")?) {
|
||||
@@ -212,7 +213,12 @@ impl Automation {
|
||||
out["path"] = json!(path.to_string_lossy());
|
||||
}
|
||||
(None, Some(printer)) => {
|
||||
let job = spool_job(a, printer, self.doc(a)?.name.clone(), duplex, paper_index)?;
|
||||
let mut job = spool_job(a, printer, self.doc(a)?.name.clone(), duplex, paper_index)?;
|
||||
// CUPS is told which way the sheets run, as the Print dialog does (#2713); Windows'
|
||||
// print ticket already is. An orientation the caller chose in `options` stands.
|
||||
if !cfg!(windows) && !job.options.iter().any(|(k, _)| k == "orientation-requested") {
|
||||
job.options.extend(print::cups_orientation(&laid_out));
|
||||
}
|
||||
out["job"] = json!(spool::submit(&bytes, &job).map_err(|e| failed(e.to_string()))?);
|
||||
}
|
||||
_ => return Err(bad("pass either path (save the print-ready PDF) or printer (a name from printers, or \"default\")")),
|
||||
|
||||
@@ -46,7 +46,9 @@ Headless `doc_print` defaults are unchanged.
|
||||
`spool` delegates discovery, driver options, preferences and submission to
|
||||
[`craft-print`](https://github.com/storytold/craft-libs/tree/main/crates/print).
|
||||
PdfCraft retains page selection, imposition, annotations and print-permission checks.
|
||||
CUPS jobs stream through stdin with `fit-to-page=false`. Windows renders sheets through
|
||||
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. 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
|
||||
|
||||
@@ -361,6 +361,18 @@ pub fn media_option(index: usize) -> Option<(String, String)> {
|
||||
cups_media(index).map(|m| ("media".to_string(), m.to_string()))
|
||||
}
|
||||
|
||||
/// The CUPS job option that keeps landscape sheets landscape (`orientation-requested` 4).
|
||||
///
|
||||
/// The print-ready PDF's sheets are already laid out on their paper, but the paper is named
|
||||
/// portrait (`Letter`), and without this option CUPS leaves a landscape sheet unrotated and
|
||||
/// shrinks it into the portrait paper instead of turning it to fill it (#2713). It is a property
|
||||
/// of the whole job, so it is given only when *every* sheet is landscape; a job that mixes
|
||||
/// orientations is left to the spooler, as portrait sheets would be turned wrongly.
|
||||
pub fn cups_orientation(sheets: &[Sheet]) -> Option<(String, String)> {
|
||||
let landscape = !sheets.is_empty() && sheets.iter().all(|s| s.size.0 > s.size.1);
|
||||
landscape.then(|| ("orientation-requested".to_string(), "4".to_string()))
|
||||
}
|
||||
|
||||
/// Whether most of page `index`'s text runs bottom-to-top as the page displays (a label laid
|
||||
/// sideways on a letter or A4 sheet), counting each text-showing operator once; form XObjects
|
||||
/// are followed a few levels deep.
|
||||
|
||||
@@ -509,6 +509,28 @@ fn printer_option_defaults_and_job_arguments() {
|
||||
assert!(spool::printer_options("../../etc/passwd").is_empty(), "not a queue name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_sheets_ask_cups_to_turn_the_page() {
|
||||
let landscape = |size: (f64, f64)| Sheet { size, ..Sheet::default() };
|
||||
let ask = Some(("orientation-requested".to_string(), "4".to_string()));
|
||||
// A landscape Size sheet, and a booklet (always landscape sheets), are both turned.
|
||||
let page = layout(&[(300.0, 200.0)], &settings(vec![0], Layout::Size(SizeMode::Fit))).unwrap();
|
||||
assert_eq!(cups_orientation(&page), ask);
|
||||
let booklet =
|
||||
layout(&[(200.0, 300.0); 4], &settings(vec![0, 1, 2, 3], Layout::Booklet { subset: BookletSubset::BothSides, binding: Binding::Left }))
|
||||
.unwrap();
|
||||
assert_eq!(cups_orientation(&booklet), ask);
|
||||
// Portrait sheets, mixed jobs and nothing at all say nothing.
|
||||
let portrait = layout(&[(200.0, 300.0)], &settings(vec![0], Layout::Size(SizeMode::Fit))).unwrap();
|
||||
assert_eq!(cups_orientation(&portrait), None);
|
||||
assert_eq!(cups_orientation(&[landscape((792.0, 612.0)), landscape((612.0, 792.0))]), None, "a mixed job is not turned as a whole");
|
||||
assert_eq!(cups_orientation(&[landscape((612.0, 612.0))]), None, "a square sheet has no orientation");
|
||||
assert_eq!(cups_orientation(&[]), None);
|
||||
// `lp` is given it, among the job's other options.
|
||||
let job = Job { options: ask.into_iter().collect(), ..Job::default() };
|
||||
assert!(lp_args(&job).join(" ").ends_with("-o fit-to-page=false -o orientation-requested=4"));
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
mod cups_options {
|
||||
use crate::spool;
|
||||
|
||||
@@ -653,7 +653,15 @@ impl PdfCraftApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(printer) => self.send_to_printer(printer, bytes, self.print_draft.job(&name)),
|
||||
Some(printer) => {
|
||||
let mut job = self.print_draft.job(&name);
|
||||
// CUPS is told which way the sheets run; Windows' print ticket already is.
|
||||
if !cfg!(windows) {
|
||||
let sizes: Vec<(f64, f64)> = doc.info.pages.iter().map(|p| (f64::from(p.width), f64::from(p.height))).collect();
|
||||
job.options.extend(print::layout(&sizes, &settings).ok().and_then(|sheets| print::cups_orientation(&sheets)));
|
||||
}
|
||||
self.send_to_printer(printer, bytes, job)
|
||||
}
|
||||
None => self.save_print_pdf(&name, bytes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8144,8 +8144,8 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::size_modes", "crates/print/src/tests.rs::architectural_sheets_preserve_full_scale", "crates/automation/tests/it/automation.rs::architectural_and_iso_papers_through_tools", "crates/ui-egui/tests/it/print.rs::architectural_page_can_be_matched_at_100_percent"]
|
||||
notes = "auto portrait/landscape"
|
||||
evidence = ["crates/print/src/tests.rs::size_modes", "crates/print/src/tests.rs::architectural_sheets_preserve_full_scale", "crates/automation/tests/it/automation.rs::architectural_and_iso_papers_through_tools", "crates/ui-egui/tests/it/print.rs::architectural_page_can_be_matched_at_100_percent", "crates/print/src/tests.rs::landscape_sheets_ask_cups_to_turn_the_page"]
|
||||
notes = "auto portrait/landscape. CUPS jobs carry orientation-requested=4 when every sheet is landscape, so a landscape sheet fills its portrait paper instead of being shrunk into it (#2713); a job that mixes orientations is left to the spooler, and Windows sets orientation in its own print ticket. The CUPS option is checked against Apple's filters and one physical printer, not a test that runs a spooler."
|
||||
|
||||
[[feature]]
|
||||
id = "print.comments-and-forms"
|
||||
|
||||
Reference in new issue
Block a user