Files
cadcraft/crates/engine/tests/plotters.rs
T
2026-10-11 13:25:00 -04:00

175 lines
9.9 KiB
Rust

//! Plotter configurations (issue #564): PLOTTERMANAGER keeps user devices in the host's folder,
//! PAGESETUP takes a device and its papers with their printable areas, and PLOT hands the
//! plotter the device's output, paper and margins.
use cadcraft_doc::{Drawing, Space};
use cadcraft_engine::Session;
use cadcraft_engine::cmd::file::{IoHooks, base64_decode, set_io};
use cadcraft_engine::cmd::plotters::{PlotterStore, set_plotter_store};
use serde_json::{Value, json};
/// The plot hook echoes the options it gets.
fn echo_plot(_: &Drawing, _: &Space, opts: &Value) -> Result<Vec<u8>, String> {
Ok(opts.to_string().into_bytes())
}
fn session() -> Session {
set_plotter_store(PlotterStore::memory());
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Err("no writer in this test".into()), plot: Some(echo_plot) });
let mut s = Session::new();
s.cmdline("line 0,0 100,0").unwrap();
s.cmdline("").unwrap();
s
}
fn names(s: &mut Session) -> Vec<String> {
let r = s.execute("plottermanager", &json!({})).unwrap();
r["devices"].as_array().unwrap().iter().map(|d| d["name"].as_str().unwrap().to_string()).collect()
}
/// The options the plot hook got.
fn plotted(r: Value) -> Value {
let bytes = base64_decode(r["data"].as_str().unwrap()).unwrap();
serde_json::from_slice(&bytes).unwrap()
}
#[test]
fn manage_devices() {
let mut s = session();
let n = names(&mut s);
assert_eq!(n[..3], ["CADCraft PDF", "CADCraft PNG", "CADCraft SVG"], "built-in devices first");
let pdf = s.execute("plottermanager", &json!({"action": "get", "name": "cadcraft pdf"})).unwrap();
assert!(pdf["builtin"].as_bool().unwrap());
let papers = pdf["papers"].as_array().unwrap();
assert!(papers.iter().any(|p| p["name"] == "ISO B1 (707.00 x 1000.00 MM)"));
let a4 = papers.iter().find(|p| p["name"] == "ISO A4 (210.00 x 297.00 MM)").unwrap();
assert_eq!(a4["margins"], json!([5.0, 5.0, 5.0, 5.0]));
let letter = papers.iter().find(|p| p["name"] == "ANSI A (8.50 x 11.00 Inches)").unwrap();
assert_eq!(letter["margins"], json!([6.35, 6.35, 6.35, 6.35]));
// A user device: a PNG copy with its own borderless paper.
s.execute("plottermanager", &json!({"action": "new", "name": "Office Raster", "output": "png", "resolution": 150})).unwrap();
let paper = json!({"name": "A3 borderless", "width": 420, "height": 297, "margins": [0, 0, 0, 0]});
let r = s.execute("plottermanager", &json!({"action": "set", "name": "office raster", "paper": paper, "defaultPaper": "A3 borderless"})).unwrap();
assert_eq!(r["defaultPaper"], "A3 borderless");
assert_eq!(r["resolution"], 150);
assert!(names(&mut s).contains(&"Office Raster".to_string()));
assert!(s.execute("plottermanager", &json!({"action": "new", "name": "OFFICE RASTER"})).is_err(), "duplicate");
assert!(s.execute("plottermanager", &json!({"action": "set", "name": "CADCraft PDF", "resolution": 100})).is_err(), "built-ins are fixed");
assert!(s.execute("plottermanager", &json!({"action": "delete", "name": "CADCraft SVG"})).is_err());
let bad_margins = json!({"name": "x", "width": 100, "height": 100, "margins": [60, 0, 60, 0]});
assert!(s.execute("plottermanager", &json!({"action": "set", "name": "Office Raster", "paper": bad_margins})).is_err());
// Save as text, load under another name, delete.
let text = s.execute("plottermanager", &json!({"action": "save", "name": "Office Raster"})).unwrap()["text"].as_str().unwrap().to_string();
assert!(text.contains("\"cadcraftPlotter\": 1"), "{text}");
s.execute("plottermanager", &json!({"action": "load", "text": text, "name": "Office Copy"})).unwrap();
let copy = s.execute("plottermanager", &json!({"action": "get", "name": "Office Copy"})).unwrap();
assert_eq!(copy["output"], "png");
assert!(s.execute("plottermanager", &json!({"action": "load", "text": "{}"})).is_err());
s.execute("plottermanager", &json!({"action": "delete", "name": "office copy"})).unwrap();
assert!(!names(&mut s).contains(&"Office Copy".to_string()));
assert!(s.execute("plottermanager", &json!({"action": "nope"})).is_err());
}
#[test]
fn page_setup_takes_a_device_and_its_papers() {
let mut s = session();
s.execute("plottermanager", &json!({"action": "new", "name": "Plan Printer", "output": "pdf"})).unwrap();
let paper = json!({"name": "Strip", "width": 200, "height": 600, "margins": [10, 20, 10, 20]});
s.execute("plottermanager", &json!({"action": "set", "name": "Plan Printer", "paper": paper})).unwrap();
let r = s.execute("pagesetup", &json!({"layout": "Layout1", "device": "plan printer", "paper": "strip", "landscape": false})).unwrap();
assert_eq!(r["page"]["device"], "Plan Printer");
assert_eq!(r["page"]["paper"], "Strip");
let page = &s.doc().unwrap().layout("Layout1").unwrap().page;
assert_eq!((page.width_mm, page.height_mm, page.margins_mm), (200.0, 600.0, [10.0, 20.0, 10.0, 20.0]));
// The printable area shown in the layout follows the device paper.
let sheet = cadcraft_render::sheet(s.doc().unwrap(), "Layout1").unwrap();
let mm = |v: f64| v * sheet.unit_mm;
let got = [mm(sheet.printable.min.x), mm(sheet.printable.min.y), mm(sheet.printable.max.x), mm(sheet.printable.max.y)];
assert!(got.iter().zip([10.0, 20.0, 190.0, 580.0]).all(|(a, b)| (a - b).abs() < 1e-9), "{got:?}");
// Switching to a built-in device keeps a paper it has, else takes its default (A4).
s.execute("pagesetup", &json!({"layout": "Layout1", "device": "CADCraft PDF"})).unwrap();
let page = s.doc().unwrap().layout("Layout1").unwrap().page.clone();
assert_eq!((page.paper.as_str(), page.margins_mm), ("ISO A4 (210.00 x 297.00 MM)", [5.0; 4]));
s.execute("pagesetup", &json!({"layout": "Layout1", "paper": "A3"})).unwrap();
let page = s.doc().unwrap().layout("Layout1").unwrap().page.clone();
assert_eq!((page.width_mm, page.height_mm), (297.0, 420.0));
assert!(s.execute("pagesetup", &json!({"layout": "Layout1", "device": "Nope"})).is_err());
assert!(
s.execute("pagesetup", &json!({"layout": "Layout1", "device": "Plan Printer", "paper": "Tabloid Extra"})).is_err(),
"not a paper of the device"
);
s.execute("pagesetup", &json!({"layout": "Layout1", "device": "None", "paper": "Letter"})).unwrap();
let page = s.doc().unwrap().layout("Layout1").unwrap().page.clone();
assert_eq!((page.device.as_str(), page.paper.as_str()), ("None", "ANSI A (8.50 x 11.00 Inches)"));
let report = s.execute("pagesetup", &Value::Null).unwrap();
assert!(report["devices"].as_array().unwrap().iter().any(|d| d == "Plan Printer"));
}
#[test]
fn plot_uses_the_device() {
let mut s = session();
s.execute("plottermanager", &json!({"action": "new", "name": "Sketch PNG", "output": "png", "resolution": 96})).unwrap();
let o = plotted(s.execute("plot", &json!({"layout": "Model", "device": "Sketch PNG", "paper": "A3"})).unwrap());
assert_eq!(o["output"], "png");
assert_eq!(o["resolution"], 96);
assert_eq!((o["width"].as_f64(), o["height"].as_f64()), (Some(297.0), Some(420.0)));
assert_eq!(o["margins"], json!([5.0, 5.0, 5.0, 5.0]));
assert_eq!(o["landscape"], true, "the device's default orientation");
assert!(o.get("paper").is_none() && o.get("device").is_none());
// EXPORTPDF always writes PDF.
let o = plotted(s.execute("exportpdf", &json!({"layout": "Model", "device": "Sketch PNG"})).unwrap());
assert_eq!(o["output"], "pdf");
// A layout plots with its page setup's device and paper.
s.execute("pagesetup", &json!({"layout": "Layout1", "device": "Sketch PNG", "paper": "A4"})).unwrap();
let o = plotted(s.execute("plot", &json!({"layout": "Layout1"})).unwrap());
assert_eq!(o["output"], "png");
assert!(o.get("width").is_none(), "the page setup already holds the paper");
// No device: options pass through unchanged.
let o = plotted(s.execute("plot", &json!({"layout": "Model", "paper": "A4"})).unwrap());
assert!(o.get("output").is_none());
assert!(s.execute("plot", &json!({"layout": "Model", "device": "Nope"})).is_err());
assert!(s.execute("plot", &json!({"layout": "Model", "device": "Sketch PNG", "paper": "Nope"})).is_err());
}
#[test]
fn typed_plotter_manager() {
let mut s = session();
for line in ["-plottermanager", "New", "Typed Plotter", "Svg"] {
s.cmdline(line).unwrap();
}
let d = s.execute("plottermanager", &json!({"action": "get", "name": "Typed Plotter"})).unwrap();
assert_eq!(d["output"], "svg");
for line in ["-plottermanager", "Paper", "Typed Plotter", "Card", "100,150"] {
s.cmdline(line).unwrap();
}
let d = s.execute("plottermanager", &json!({"action": "get", "name": "Typed Plotter"})).unwrap();
assert!(d["papers"].as_array().unwrap().iter().any(|p| p["name"] == "Card" && p["width"] == 100.0));
for line in ["-plottermanager", "Delete", "Typed Plotter"] {
s.cmdline(line).unwrap();
}
assert!(!names(&mut s).contains(&"Typed Plotter".to_string()));
}
#[test]
fn hostile_names_and_values_are_refused() {
let mut s = session();
let r = s.execute("plottermanager", &json!({"action": "new", "name": "../../etc/passwd"})).unwrap();
assert_eq!(r["name"], "../../etc/passwd", "the name is kept; only the file name is cleaned");
for p in [
json!({"action": "new", "name": ""}),
json!({"action": "new", "name": "x", "output": "dwf"}),
json!({"action": "new", "name": "y", "resolution": 1e12}),
json!({"action": "set", "name": "../../etc/passwd", "paper": {"name": "p", "width": f64::MAX, "height": 1}}),
json!({"action": "set", "name": "../../etc/passwd", "paper": {"name": "p", "width": -1, "height": 1}}),
json!({"action": "set", "name": "../../etc/passwd", "removePaper": "nope"}),
json!({"action": "load", "text": "x".repeat(2 << 20)}),
json!({"action": "get"}),
] {
assert!(s.execute("plottermanager", &p).is_err(), "{p}");
}
}