mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Squash of PR #570 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
175 lines
9.9 KiB
Rust
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}");
|
|
}
|
|
}
|