mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Area type and type on a path whose text doesn't fit print without the text past the end of the frame. text.create, text.createInPath, text.setText and text.setStyle report it in their replies, but file.print and print.preview gave no warning, and the Print dialog's Summary showed none. Both commands now add a warning that counts the area type and type on a path that prints and doesn't fit, and gives their ids (the first 20, then how many more). It counts the objects that the fallback font warning counts: visible objects on the layers that Print Layers picks, on the paper of the artboards or tiles that print. The walk that finds the fonts of the printed type also collects these ids. Type in the art of symbols, patterns, brushes, opacity masks and placed documents is not named. Docs: docs/mcp.md (Print, Fitting area type), docs/target-app-parity.md (Type).
128 lines
6.5 KiB
Rust
128 lines
6.5 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"]);
|
||
}
|
||
|
||
#[test]
|
||
fn type_in_a_missing_family_is_named_when_its_artboard_prints() {
|
||
let mut s = session(2);
|
||
let r = s.doc().unwrap().doc.artboards[1].rect;
|
||
s.execute("text.create", &json!({"x": r.x0 + 10.0, "y": r.y0 + 50.0, "text": "Ab", "font": "Nonexistent Sans Probe"})).unwrap();
|
||
let fallback = |v: &Value| v["warnings"].as_array().unwrap().iter().filter(|w| w.as_str().unwrap().contains("fallback font")).count();
|
||
let (_, v) = print_ps(&mut s, json!({}));
|
||
assert_eq!(fallback(&v), 1, "{v}");
|
||
assert!(v["warnings"].to_string().contains("Nonexistent Sans Probe"), "{v}");
|
||
let (_, v) = print_ps(&mut s, json!({"settings": {"artboards": "range", "range": "1"}}));
|
||
assert_eq!(fallback(&v), 0, "{v}");
|
||
// Off the paper: 100 pt wide paper at 100% shows the middle of the 300 pt wide artboard.
|
||
let paper = json!({"artboards": "range", "range": "2", "media": "custom", "width": 100, "height": 300, "autoRotate": false});
|
||
let (_, v) = print_ps(&mut s, json!({ "settings": paper }));
|
||
assert_eq!(fallback(&v), 0, "{v}");
|
||
}
|
||
|
||
#[test]
|
||
fn type_that_does_not_fit_its_frame_is_named_when_its_artboard_prints() {
|
||
let mut s = session(2);
|
||
let r = s.doc().unwrap().doc.artboards[1].rect;
|
||
let p = json!({"x": r.x0 + 10.0, "y": r.y0 + 10.0, "text": "Ab\nCd\nEf\nGh", "size": 12, "area": {"width": 100, "height": 20}});
|
||
let id = s.execute("text.create", &p).unwrap()["id"].clone();
|
||
let over = |v: &Value| v["warnings"].as_array().unwrap().iter().filter(|w| w.as_str().unwrap().contains("fit its frame")).count();
|
||
let (_, v) = print_ps(&mut s, json!({}));
|
||
assert_eq!(over(&v), 1, "{v}");
|
||
assert!(v["warnings"].to_string().contains(&format!("(id {id})")), "{v}");
|
||
let (_, v) = print_ps(&mut s, json!({"settings": {"artboards": "range", "range": "1"}}));
|
||
assert_eq!(over(&v), 0, "{v}");
|
||
}
|