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>
96 lines
4.7 KiB
Rust
96 lines
4.7 KiB
Rust
//! 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"]);
|
||
}
|