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")?;