Merge pull request #3925 from cmain/fix/print-landscape-spool

Print: landscape pages print upright on macOS print queues
This commit is contained in:
BFlat
2026-10-24 17:52:30 +07:00
committed by GitHub
7 changed files with 155 additions and 4 deletions
+16
View File
@@ -23,6 +23,11 @@ impl PrintService for SystemPrint {
fn shared_print(&self) -> Option<vectorcraft_ui_egui::print::SharedPrintFn> {
Some(std::sync::Arc::new(|job| SystemPrint.print(job)))
}
fn turns_landscape(&self) -> bool {
// 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
}
@@ -127,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"));
}
}
+4 -2
View File
@@ -40,7 +40,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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 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
),
@@ -187,7 +187,9 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
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 }));
}
+12
View File
@@ -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::<String>::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::<Vec<_>>();
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);
}
+26 -2
View File
@@ -67,6 +67,11 @@ 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 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,
}
/// A printed job.
@@ -375,7 +380,7 @@ pub fn plan<'a>(doc: &'a Document, opts: &PrintOptions) -> Result<PrintPlan<'a>,
}
art
});
Some(PlannedSheet {
let sheet = PlannedSheet {
size: l.size,
trim: l.page_trim,
bleed: l.page_bleed,
@@ -387,13 +392,32 @@ pub fn plan<'a>(doc: &'a Document, opts: &PrintOptions) -> Result<PrintPlan<'a>,
negative: set.output.image == PrintImage::Negative,
plate: *plate,
ink: ink.cloned(),
})
};
// 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 })
}
/// 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 {
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<PrintReport, PdfError> {
craft_pdf::write::guard(|| leaving_out_undecodable(doc, |left_out| print_inner(doc, opts, left_out)))
+54
View File
@@ -441,6 +441,60 @@ 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));
// 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]
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::<Vec<_>>();
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,
+34
View File
@@ -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<Printer> {
vec![]
}
fn print(&mut self, job: &PrintJob) -> Result<String, String> {
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::<Vec<_>>();
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();
+9
View File
@@ -42,6 +42,12 @@ pub trait PrintService {
fn shared_print(&self) -> Option<SharedPrintFn> {
None
}
/// 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
}
/// 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<Value, String> {
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")?;