From 41430cc1712f2b2b7f47fc1747922a8eb91ec85e Mon Sep 17 00:00:00 2001 From: Chris Main Date: Fri, 23 Oct 2026 21:10:01 -0400 Subject: [PATCH 1/2] Print: landscape pages print upright on macOS print queues A landscape print job was a page wider than tall, and `lp` on macOS puts each page on the paper as it is: on upright Letter it printed unturned, cut off on the right (checked with cupsfilter through an AirPrint driver). Saving the PDF and printing it from Preview worked because Preview turns the page. `file.print` takes `forPrinter`: a page wider than tall is written upright, the art turned a quarter anticlockwise as a printer driver turns a landscape job. Page by page, so auto-rotated jobs that mix wide and tall artboards print right too. The desktop print service asks for it (`PrintService::turns_landscape`); saved PDFs, PostScript, the preview and the browser's print dialog keep landscape pages as they are. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/vectorcraft/src/printing.rs | 4 +++ crates/engine/src/cmd/print.rs | 6 ++-- crates/engine/src/tests_print.rs | 12 +++++++ crates/pdf/src/print/mod.rs | 25 ++++++++++++-- crates/pdf/src/tests_print.rs | 40 +++++++++++++++++++++++ crates/ui-egui/src/dialogs/tests_print.rs | 34 +++++++++++++++++++ crates/ui-egui/src/print.rs | 9 +++++ 7 files changed, 126 insertions(+), 4 deletions(-) diff --git a/apps/vectorcraft/src/printing.rs b/apps/vectorcraft/src/printing.rs index 643e64c69..8f8d30a34 100644 --- a/apps/vectorcraft/src/printing.rs +++ b/apps/vectorcraft/src/printing.rs @@ -23,6 +23,10 @@ impl PrintService for SystemPrint { fn shared_print(&self) -> Option { Some(std::sync::Arc::new(|job| SystemPrint.print(job))) } + fn turns_landscape(&self) -> bool { + // `lp` doesn't turn a landscape page to the paper (macOS cuts it off on upright paper). + false + } fn has_setup(&self) -> bool { true } diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index 76f52f4b8..a19ef1ae2 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -40,7 +40,7 @@ pub fn specs() -> Vec { "Print", [], Some("Cmd+P"), - "{preset?: name (as print.setup's: its settings instead of the document's; the saved settings stay), settings?: {…settings of print.setup} (over the document's or the preset's), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device", + "{preset?: name (as print.setup's: its settings instead of the document's; the saved settings stay), settings?: {…settings of print.setup} (over the document's or the preset's), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out), forPrinter?: bool (the PDF goes to a print queue: a page wider than tall is written upright on the paper, the art turned a quarter anticlockwise, as a printer driver turns a landscape job; macOS prints a landscape page as it is, cut off on upright paper)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device", has_doc, print ), @@ -187,7 +187,9 @@ fn print(s: &mut Session, p: &Value) -> Result { if !postscript { let printed = prepared.as_ref().unwrap_or(doc); let typed = type_warnings(&full, which, &areas(&vectorcraft_pdf::preview(printed, &set).map_err(|e| pdf_error(PRINT, e))?)); - let mut r = vectorcraft_pdf::print(printed, &PrintOptions { settings: set, ..Default::default() }).map_err(|e| pdf_error(PRINT, e))?; + let mut r = + vectorcraft_pdf::print(printed, &PrintOptions { settings: set, for_printer: bool_or(p, "forPrinter", false), ..Default::default() }) + .map_err(|e| pdf_error(PRINT, e))?; r.warnings.extend(typed); return write_or_return(path, &r.bytes, json!({ "pages": r.pages, "format": "pdf", "warnings": r.warnings })); } diff --git a/crates/engine/src/tests_print.rs b/crates/engine/src/tests_print.rs index c92e3bf5a..4ba587d45 100644 --- a/crates/engine/src/tests_print.rs +++ b/crates/engine/src/tests_print.rs @@ -212,3 +212,15 @@ fn type_that_does_not_fit_its_frame_is_named() { let (_, v) = print(&mut s, json!({})); assert_eq!(over(&v), Vec::::new()); } + +#[test] +fn for_a_printer_a_landscape_page_is_written_upright() { + let (mut s, _) = session(); + let size = |pdf: &[u8]| vectorcraft_pdf::info(pdf, None).unwrap().pages.iter().map(|p| (p.width, p.height)).collect::>(); + let settings = json!({"autoRotate": false, "orientation": "landscape"}); + let (saved, _) = print(&mut s, json!({"settings": settings})); + assert_eq!(size(&saved), [(792.0, 612.0)]); + let (spooled, v) = print(&mut s, json!({"settings": settings, "forPrinter": true})); + assert_eq!(size(&spooled), [(612.0, 792.0)]); + assert_eq!(v["pages"], 1); +} diff --git a/crates/pdf/src/print/mod.rs b/crates/pdf/src/print/mod.rs index 56c8a2525..9ef36ff2e 100644 --- a/crates/pdf/src/print/mod.rs +++ b/crates/pdf/src/print/mod.rs @@ -67,6 +67,10 @@ pub struct PrintOptions { /// The job is written as PostScript, which carries halftone screens and flatness: no /// warnings that they are left to the device. pub postscript: bool, + /// The job goes to a print queue, which puts each page on the paper as it is (macOS does + /// not turn a landscape page to the paper): a page wider than tall is written upright, the + /// art turned a quarter anticlockwise, as a printer driver turns a landscape job. + pub for_printer: bool, } /// A printed job. @@ -375,7 +379,7 @@ pub fn plan<'a>(doc: &'a Document, opts: &PrintOptions) -> Result, } art }); - Some(PlannedSheet { + let sheet = PlannedSheet { size: l.size, trim: l.page_trim, bleed: l.page_bleed, @@ -387,13 +391,30 @@ pub fn plan<'a>(doc: &'a Document, opts: &PrintOptions) -> Result, negative: set.output.image == PrintImage::Negative, plate: *plate, ink: ink.cloned(), - }) + }; + Some(if opts.for_printer { upright(sheet) } else { sheet }) }) .collect(); let order = job.order(set); Ok(PrintPlan { plates, sheets, order, title, created, warnings: job.warnings, doc: job.doc }) } +/// `s` upright on the paper ([`PrintOptions::for_printer`]): a page wider than tall turns a +/// quarter anticlockwise, its top to the paper's left edge. +fn upright(mut s: PlannedSheet) -> PlannedSheet { + let (w, h) = s.size; + if w <= h { + return s; + } + // y down: the top-right corner (w, 0) goes to the top-left, the top-left to the bottom-left. + let turn = Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, w]); + s.size = (h, w); + s.view = turn * s.view; + s.trim = turn.transform_rect_bbox(s.trim); + s.bleed = turn.transform_rect_bbox(s.bleed); + s +} + /// Print `doc` to a PDF as `opts` say. pub fn print(doc: &Document, opts: &PrintOptions) -> Result { craft_pdf::write::guard(|| leaving_out_undecodable(doc, |left_out| print_inner(doc, opts, left_out))) diff --git a/crates/pdf/src/tests_print.rs b/crates/pdf/src/tests_print.rs index d969dcacc..a37ea408e 100644 --- a/crates/pdf/src/tests_print.rs +++ b/crates/pdf/src/tests_print.rs @@ -441,6 +441,46 @@ fn turns_mirror_and_negative() { assert!(c[0] < 10 && c[1] > 245 && c[2] > 245, "{c:?}"); } +#[test] +fn for_a_printer_landscape_pages_print_upright_turned_anticlockwise() { + let d = doc(100.0, 100.0, &[(Rect::new(0.0, 0.0, 50.0, 100.0), rgb(1.0, 0.0, 0.0))]); + let s = json!({"placement": {"origin": "topLeft"}, "autoRotate": false, "orientation": "landscape"}); + let spooled = |v| { + let opts = PrintOptions { settings: settings(v), created: Some(1_791_200_000), uncompressed: true, for_printer: true, ..Default::default() }; + pages(&print(&d, &opts).unwrap().bytes).remove(0) + }; + // Saved, the page stays landscape. + assert_eq!(pages(&job(&d, s.clone()).bytes)[0].0, (792.0, 612.0)); + // For a printer, Letter upright: the art's top at the paper's left edge, its left half at the bottom. + let p = spooled(s.clone()); + assert_eq!(p.0, (612.0, 792.0)); + assert!(near(p.1[0].0, Rect::new(0.0, 742.0, 100.0, 792.0)), "{:?}", p.1); + // A portrait page and a transverse one (already upright) don't turn. + let p = spooled(json!({"placement": {"origin": "topLeft"}, "autoRotate": false})); + assert_eq!(p.0, (612.0, 792.0)); + assert!(near(p.1[0].0, Rect::new(0.0, 0.0, 50.0, 100.0)), "{:?}", p.1); + let mut t = s; + t["transverse"] = json!(true); + assert_eq!(spooled(t).0, (612.0, 792.0)); +} + +#[test] +fn for_a_printer_auto_rotated_artboards_all_print_upright() { + // A wide artboard and a tall one, the paper turned to each. + let mut d = doc(100.0, 100.0, &[(Rect::new(0.0, 0.0, 10.0, 10.0), rgb(1.0, 0.0, 0.0))]); + let mut wide = artboard(1, 0.0); + wide.rect = Rect::new(0.0, 0.0, 300.0, 100.0); + let mut tall = artboard(2, 400.0); + tall.rect = Rect::new(400.0, 0.0, 500.0, 300.0); + d.artboards = vec![wide, tall]; + let set = settings(json!({"autoRotate": true})); + let saved = print(&d, &PrintOptions { settings: set.clone(), ..Default::default() }).unwrap(); + let sizes = |bytes: &[u8]| pages(bytes).into_iter().map(|p| p.0).collect::>(); + assert_eq!(sizes(&saved.bytes), [(792.0, 612.0), (612.0, 792.0)]); + let spooled = print(&d, &PrintOptions { settings: set, for_printer: true, ..Default::default() }).unwrap(); + assert_eq!(sizes(&spooled.bytes), [(612.0, 792.0), (612.0, 792.0)]); +} + #[test] fn golden_page_one() { // 300 × 200: red left half, blue right half, a black frame; on Letter, turned landscape, diff --git a/crates/ui-egui/src/dialogs/tests_print.rs b/crates/ui-egui/src/dialogs/tests_print.rs index 490cbf677..bf213c789 100644 --- a/crates/ui-egui/src/dialogs/tests_print.rs +++ b/crates/ui-egui/src/dialogs/tests_print.rs @@ -148,6 +148,40 @@ fn done_keeps_the_settings_without_printing_and_bad_settings_keep_the_dialog() { assert!(!crate::menus::enabled(&app, "print.printerSetup"), "the mock has no settings to open"); } +/// A print queue that puts each page on the paper as it is, as `lp` does. +struct UprightQueue(Jobs); + +impl PrintService for UprightQueue { + fn printers(&mut self) -> Vec { + vec![] + } + + fn print(&mut self, job: &PrintJob) -> Result { + MockPrint(self.0.clone()).print(job) + } + + fn turns_landscape(&self) -> bool { + false + } +} + +#[test] +fn a_landscape_job_goes_upright_to_a_queue_that_does_not_turn_it() { + let sizes = |pdf: &[u8]| vectorcraft_pdf::info(pdf, None).unwrap().pages.iter().map(|p| (p.width, p.height)).collect::>(); + let landscape = json!({"settings": {"autoRotate": false, "orientation": "landscape", "artboards": "range", "range": "1"}}); + // The desktop's queue: Letter upright, the art turned on it. + let jobs = Jobs::default(); + let services = Services { print: Some(Box::new(UprightQueue(jobs.clone()))), ..Default::default() }; + let mut desktop = VectorcraftApp::new(Session::new(), services); + desktop.run("file.new", json!({"width": 120, "height": 90})).unwrap(); + desktop.run("file.print", landscape.clone()).unwrap(); + assert_eq!(sizes(&jobs.borrow()[0].2), [(612.0, 792.0)]); + // A service that turns pages itself (the browser's dialog) gets the landscape page. + let (mut app, jobs) = app(); + app.run("file.print", landscape).unwrap(); + assert_eq!(sizes(&jobs.borrow()[0].2), [(792.0, 612.0)]); +} + #[test] fn without_a_print_service_print_saves_a_pdf() { let written: Written = Rc::default(); diff --git a/crates/ui-egui/src/print.rs b/crates/ui-egui/src/print.rs index 0fcb163da..1eeea2846 100644 --- a/crates/ui-egui/src/print.rs +++ b/crates/ui-egui/src/print.rs @@ -42,6 +42,12 @@ pub trait PrintService { fn shared_print(&self) -> Option { None } + /// Whether the system turns a page wider than tall to fit the paper. The desktop's print + /// queues put each page on the paper as it is, so their jobs are written upright + /// (`file.print`'s `forPrinter`); the browser's print dialog turns them itself. + fn turns_landscape(&self) -> bool { + true + } /// Whether Setup… can open the system's printer settings. fn has_setup(&self) -> bool { false @@ -91,6 +97,9 @@ pub fn run(app: &mut VectorcraftApp, p: &Value) -> Result { v["printed"] = json!(false); return Ok(v); } + if app.services.print.as_ref().is_some_and(|s| !s.turns_landscape()) { + q["forPrinter"] = json!(true); + } let mut v = app.session.execute("file.print", &q).map_err(|e| e.to_string())?; let data = v.as_object_mut().and_then(|o| o.remove("dataBase64")); let pdf = data.as_ref().and_then(Value::as_str).and_then(vectorcraft_format::base64_decode).ok_or("the print job has no data")?; From 61504c84fbb519625809660e69b2bd68b24881ec Mon Sep 17 00:00:00 2001 From: Chris Main Date: Sat, 24 Oct 2026 05:39:36 -0400 Subject: [PATCH 2/2] Print: turn only landscape paper, and only for macOS queues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #3925: the spooled job turned every sheet wider than tall, so custom paper that is wide in Portrait (a 4 × 2 in label) went to the queue tall, and a Portrait + Transverse page was turned back. Only the sheets whose paper the layout turned to landscape (not transverse) are written upright now. The desktop print service asked for upright pages on every platform, but craft-print already turns each sheet to its own orientation on Windows: only macOS asks for them now; Linux keeps main's behaviour. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/vectorcraft/src/printing.rs | 16 ++++++++++++++-- crates/engine/src/cmd/print.rs | 2 +- crates/pdf/src/print/mod.rs | 13 ++++++++----- crates/pdf/src/tests_print.rs | 14 ++++++++++++++ crates/ui-egui/src/print.rs | 6 +++--- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/apps/vectorcraft/src/printing.rs b/apps/vectorcraft/src/printing.rs index 8f8d30a34..c9e6539a2 100644 --- a/apps/vectorcraft/src/printing.rs +++ b/apps/vectorcraft/src/printing.rs @@ -24,8 +24,9 @@ impl PrintService for SystemPrint { Some(std::sync::Arc::new(|job| SystemPrint.print(job))) } fn turns_landscape(&self) -> bool { - // `lp` doesn't turn a landscape page to the paper (macOS cuts it off on upright paper). - false + // macOS's `lp` doesn't turn a landscape page to the paper: it cuts it off on upright + // paper. craft-print turns each sheet to its own orientation on Windows. + !cfg!(target_os = "macos") } fn has_setup(&self) -> bool { true @@ -131,3 +132,14 @@ mod tests { assert_eq!(args, ["ms-settings:printers"]); } } + +#[cfg(test)] +mod turn_tests { + use super::SystemPrint; + use vectorcraft_ui_egui::print::PrintService; + + #[test] + fn only_macos_queues_need_landscape_pages_turned() { + assert_eq!(SystemPrint.turns_landscape(), !cfg!(target_os = "macos")); + } +} diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index a19ef1ae2..be17271c0 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -40,7 +40,7 @@ pub fn specs() -> Vec { "Print", [], Some("Cmd+P"), - "{preset?: name (as print.setup's: its settings instead of the document's; the saved settings stay), settings?: {…settings of print.setup} (over the document's or the preset's), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out), forPrinter?: bool (the PDF goes to a print queue: a page wider than tall is written upright on the paper, the art turned a quarter anticlockwise, as a printer driver turns a landscape job; macOS prints a landscape page as it is, cut off on upright paper)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device", + "{preset?: name (as print.setup's: its settings instead of the document's; the saved settings stay), settings?: {…settings of print.setup} (over the document's or the preset's), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out), forPrinter?: bool (the PDF goes to a print queue: a page whose paper is turned to landscape is written upright, the art turned a quarter anticlockwise, as a printer driver turns a landscape job; macOS prints a landscape page as it is, cut off on upright paper; paper wide in Portrait and transverse pages stay as they are)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device", has_doc, print ), diff --git a/crates/pdf/src/print/mod.rs b/crates/pdf/src/print/mod.rs index 9ef36ff2e..a8e54b324 100644 --- a/crates/pdf/src/print/mod.rs +++ b/crates/pdf/src/print/mod.rs @@ -68,8 +68,9 @@ pub struct PrintOptions { /// warnings that they are left to the device. pub postscript: bool, /// The job goes to a print queue, which puts each page on the paper as it is (macOS does - /// not turn a landscape page to the paper): a page wider than tall is written upright, the - /// art turned a quarter anticlockwise, as a printer driver turns a landscape job. + /// not turn a landscape page to the paper): a page whose paper the layout turned to landscape + /// is written upright, the art turned a quarter anticlockwise, as a printer driver turns a + /// landscape job. Paper that is wide in Portrait, and transverse pages, stay as they are. pub for_printer: bool, } @@ -392,15 +393,17 @@ pub fn plan<'a>(doc: &'a Document, opts: &PrintOptions) -> Result, plate: *plate, ink: ink.cloned(), }; - Some(if opts.for_printer { upright(sheet) } else { sheet }) + // Only the paper the layout turned: custom paper can be wide in Portrait. + let turned = l.orientation.landscape() && !set.transverse; + Some(if opts.for_printer && turned { upright(sheet) } else { sheet }) }) .collect(); let order = job.order(set); Ok(PrintPlan { plates, sheets, order, title, created, warnings: job.warnings, doc: job.doc }) } -/// `s` upright on the paper ([`PrintOptions::for_printer`]): a page wider than tall turns a -/// quarter anticlockwise, its top to the paper's left edge. +/// Landscape sheet `s` upright on the paper ([`PrintOptions::for_printer`]): a page wider than +/// tall turns a quarter anticlockwise, its top to the paper's left edge. fn upright(mut s: PlannedSheet) -> PlannedSheet { let (w, h) = s.size; if w <= h { diff --git a/crates/pdf/src/tests_print.rs b/crates/pdf/src/tests_print.rs index a37ea408e..cd1232a4a 100644 --- a/crates/pdf/src/tests_print.rs +++ b/crates/pdf/src/tests_print.rs @@ -462,6 +462,20 @@ fn for_a_printer_landscape_pages_print_upright_turned_anticlockwise() { let mut t = s; t["transverse"] = json!(true); assert_eq!(spooled(t).0, (612.0, 792.0)); + // Transverse in Portrait is wide on purpose: it stays wide. + assert_eq!(spooled(json!({"autoRotate": false, "transverse": true})).0, (792.0, 612.0)); +} + +#[test] +fn for_a_printer_paper_wide_in_portrait_stays_as_it_is() { + // A 4 × 2 in label in Portrait: the queue's media is wide too. + let d = doc(288.0, 144.0, &[(Rect::new(0.0, 0.0, 144.0, 144.0), rgb(1.0, 0.0, 0.0))]); + let v = + json!({"media": "custom", "width": 288, "height": 144, "orientation": "portrait", "autoRotate": false, "placement": {"origin": "topLeft"}}); + let opts = PrintOptions { settings: settings(v), uncompressed: true, for_printer: true, ..Default::default() }; + let p = pages(&print(&d, &opts).unwrap().bytes).remove(0); + assert_eq!(p.0, (288.0, 144.0)); + assert!(near(p.1[0].0, Rect::new(0.0, 0.0, 144.0, 144.0)), "{:?}", p.1); } #[test] diff --git a/crates/ui-egui/src/print.rs b/crates/ui-egui/src/print.rs index 1eeea2846..499f6f260 100644 --- a/crates/ui-egui/src/print.rs +++ b/crates/ui-egui/src/print.rs @@ -42,9 +42,9 @@ pub trait PrintService { fn shared_print(&self) -> Option { None } - /// Whether the system turns a page wider than tall to fit the paper. The desktop's print - /// queues put each page on the paper as it is, so their jobs are written upright - /// (`file.print`'s `forPrinter`); the browser's print dialog turns them itself. + /// Whether the system turns a landscape page to fit the paper. macOS's print queues put + /// each page on the paper as it is, so their jobs are written upright (`file.print`'s + /// `forPrinter`); Windows and the browser's print dialog turn them themselves. fn turns_landscape(&self) -> bool { true }