vectorcraft/crates/engine/src/tests_printps.rs
bflatastic 0428e56342 M4.92: Print to a PostScript file
file.print {format: "postscript"} (the default for a .ps path) writes the
print job as Level 3 or 2 (level) PostScript instead of a PDF: DSC comments
with %%Pages the page count, one page per sheet with its paper size,
printer's marks in Registration, separations naming their ink (%%PlateColor)
with its halftone screen, a fixed flatness and film negatives. Transparency
is flattened first with flattenerPreset. The answer says which format was
written. In the app, a PostScript job is always saved, never sent to the
printer (the Print dialog's own options are unchanged).

The PDF print's layout is exposed as vectorcraft_pdf::plan (the PDF writer
uses it too); vectorcraft_eps::print draws its sheets with the EPS writer's
document walk, each sheet once however many copies print.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:01:36 +07:00

96 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! File → Print to a PostScript file: `file.print {format: "postscript"}` writes the job the PDF
//! print lays out as DSC-conforming PostScript.
use serde_json::{Value, json};
use super::*;
/// A document of `artboards` 300 × 200 artboards with a rectangle on the first.
fn session(artboards: usize) -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200, "artboards": artboards})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
s
}
/// `file.print {format: "postscript", …p}` → (the PostScript, the answer).
fn print_ps(s: &mut Session, mut p: Value) -> (String, Value) {
p["format"] = json!("postscript");
let v = s.execute("file.print", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
(String::from_utf8(bytes).unwrap(), v)
}
/// The values of DSC comment `key` (`%%Key:`), in order.
fn dsc(ps: &str, key: &str) -> Vec<String> {
let prefix = format!("%%{key}:");
ps.lines().filter_map(|l| l.strip_prefix(&prefix)).map(|v| v.trim().to_string()).collect()
}
#[test]
fn pages_equals_the_page_count() {
let mut s = session(3);
let (ps, v) = print_ps(&mut s, json!({}));
assert_eq!((v["pages"].clone(), v["format"].clone()), (json!(3), json!("postscript")));
assert!(ps.starts_with("%!PS-Adobe-3.0\n") && ps.ends_with("%%EOF\n"));
assert_eq!(dsc(&ps, "Pages"), ["3"]);
assert_eq!(dsc(&ps, "Page").len(), 3);
assert_eq!(dsc(&ps, "LanguageLevel"), ["3"]);
// Copies, a range of artboards, and the level.
let (ps, v) = print_ps(&mut s, json!({"level": 2, "settings": {"copies": 2, "artboards": "range", "range": "1-2"}}));
assert_eq!((v["pages"].clone(), dsc(&ps, "Pages")), (json!(4), vec!["4".to_string()]));
assert_eq!(dsc(&ps, "Page").len(), 4);
assert_eq!(dsc(&ps, "LanguageLevel"), ["2"]);
// Letter paper, turned to the wide artboards.
assert_eq!(dsc(&ps, "PageBoundingBox")[0], "0 0 792 612");
}
#[test]
fn marks_and_separations_are_written() {
let mut s = session(1);
let (ps, _) = print_ps(&mut s, json!({"settings": {"marks": {"trim": true, "registration": true}}}));
// Marks print in Registration: on every ink.
assert!(ps.contains("/All /DeviceCMYK"), "{ps}");
let (none, _) = print_ps(&mut s, json!({}));
assert!(!none.contains("/All /DeviceCMYK"));
// Separations: a page per ink with its screen.
let (ps, v) =
print_ps(&mut s, json!({"settings": {"output": {"mode": "separations", "inks": [{"name": "Cyan", "frequency": 120, "angle": 30}]}}}));
assert_eq!(v["pages"], 4);
assert_eq!(dsc(&ps, "PlateColor"), ["Cyan", "Magenta", "Yellow", "Black"]);
assert!(ps.contains("120 30 {dup mul exch dup mul add 1 exch sub} setscreen"));
// PostScript writes the screens, so no warning says they are left to the device.
assert!(!v["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("screens")), "{v}");
// Negatives and a fixed flatness.
let (ps, _) = print_ps(&mut s, json!({"settings": {"output": {"image": "negative"}, "graphics": {"autoFlatness": false, "flatness": 4}}}));
assert!(ps.contains("{1 exch sub} settransfer") && ps.contains("4 setflat"));
}
#[test]
fn a_ps_path_prints_postscript_and_pdf_stays_the_default() {
let mut s = session(1);
let v = s.execute("file.print", &json!({})).unwrap();
assert_eq!(v["format"], "pdf");
let path = std::env::temp_dir().join(format!("vc-printps-{}.ps", std::process::id()));
let v = s.execute("file.print", &json!({"path": path.to_string_lossy()})).unwrap();
assert_eq!(v["format"], "postscript");
let bytes = std::fs::read(&path).unwrap();
assert!(bytes.starts_with(b"%!PS-Adobe-3.0"));
let _ = std::fs::remove_file(&path);
// The file reads back: the rectangle on the first page.
let back = vectorcraft_eps::import(&bytes).unwrap();
assert!(!back.document.layers[0].children().unwrap().is_empty());
for bad in [json!({"format": "ps2"}), json!({"format": "postscript", "level": 1}), json!({"format": "postscript", "flattenerPreset": "nope"})] {
assert!(matches!(s.execute("file.print", &bad), Err(EngineError::BadParams { .. })), "{bad}");
}
}
#[test]
fn transparency_is_flattened_for_postscript() {
let mut s = session(1);
s.execute("transparency.set", &json!({"opacity": 50})).unwrap();
let (ps, v) = print_ps(&mut s, json!({}));
assert!(v["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("flattened")), "{v}");
assert!(!v["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("opaque: EPS")), "{v}");
assert_eq!(dsc(&ps, "Pages"), ["1"]);
}