//! The CLI opens every readable format (PDF, .ai, images) through the engine's `document.open`. use std::process::Command; use serde_json::Value; const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); /// Returns the path of `name` in `vectorcraft-cli-fileio-` in the temp folder. The first call /// removes that folder and creates it again empty. fn tmp(name: &str) -> String { static DIR: std::sync::OnceLock = std::sync::OnceLock::new(); let dir = DIR.get_or_init(|| { let dir = std::env::temp_dir().join(format!("vectorcraft-cli-fileio-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); dir }); dir.join(name).to_string_lossy().to_string() } fn ok(args: &[&str]) -> String { let out = Command::new(BIN).args(args).output().unwrap(); assert!(out.status.success(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr)); String::from_utf8(out.stdout).unwrap() } /// `run` a new `width`×`height` document with a rectangle and export it to `out`. fn make(out: &str, width: u32, height: u32) { ok(&[ "run", "--cmd", "file.new", "--params", &format!(r#"{{"width":{width},"height":{height},"artboards":2}}"#), "--cmd", "shape.rectangle", "--params", r#"{"x":1,"y":1,"width":2,"height":1}"#, "--export", out, ]); } fn info(file: &str) -> Value { serde_json::from_str(&ok(&["info", file])).unwrap() } #[test] fn convert_pdf_to_svg_and_info_on_ai() { let pdf = tmp("in.pdf"); make(&pdf, 120, 80); let svg = tmp("out.svg"); ok(&["convert", &pdf, &svg]); assert!(std::fs::read_to_string(&svg).unwrap().contains(""#, ) .unwrap(); let png = tmp("missing-link.png"); let out: Value = serde_json::from_str(ok(&["convert", &svg, &png]).trim()).unwrap(); let w = out["importWarnings"].as_array().unwrap_or_else(|| panic!("{out}")); assert!(w.iter().any(|w| w.as_str().unwrap().contains("nowhere-to-be-found.png")), "{out}"); } #[test] fn convert_writes_legacy_ai_with_its_options() { // Two artboards, a shown rectangle and a hidden one. let src = tmp("legacy.vectorcraft"); ok(&[ "run", "--cmd", "file.new", "--params", r#"{"width":120,"height":80,"artboards":2}"#, "--cmd", "shape.rectangle", "--params", r#"{"x":10,"y":10,"width":20,"height":10}"#, "--cmd", "shape.rectangle", "--params", r#"{"x":40,"y":10,"width":20,"height":10}"#, "--cmd", "object.hide", "--export", &src, ]); // One file per artboard, named after it; hidden objects in a hidden layer by default. let out = tmp("legacy.ai"); let r: Value = serde_json::from_str(&ok(&["convert", &src, &out, "--ai-version", "7"])).unwrap(); let files: Vec<&str> = r["files"].as_array().unwrap().iter().filter_map(Value::as_str).collect(); assert_eq!(files.len(), 2, "{r}"); assert!(files.iter().all(|f| std::fs::read(f).unwrap().starts_with(b"%!PS-Adobe-3.0")), "{r}"); assert!(r["warnings"].to_string().contains("hidden layer"), "{r}"); // One artboard is the file itself; the hidden rectangle left out. let one = tmp("legacy-one.ai"); let r: Value = serde_json::from_str(&ok(&[ "convert", &src, &one, "--ai-version", "7", "--range", "1", "--hidden-objects", "omit", "--transparent-images", "omit", ])) .unwrap(); assert!(r["warnings"].to_string().contains("hidden objects are left out"), "{r}"); assert!(std::fs::read(&one).unwrap().starts_with(b"%!PS-Adobe-3.0")); // A value the engine doesn't take is an error. let bad = Command::new(BIN).args(["convert", &src, &tmp("bad.ai"), "--ai-version", "7", "--hidden-objects", "behind"]).output().unwrap(); assert!(!bad.status.success()); assert!(String::from_utf8_lossy(&bad.stderr).contains("hiddenObjects"), "{}", String::from_utf8_lossy(&bad.stderr)); } /// convert and `run --export` write a `.ait` as Save as Template does: a PDF-compatible template of /// every artboard that opens as a new untitled document. #[test] fn convert_and_run_write_ait_templates() { let pdf = tmp("template-in.pdf"); make(&pdf, 120, 80); let (converted, exported) = (tmp("converted.ait"), tmp("exported.ait")); let r: Value = serde_json::from_str(&ok(&["convert", &pdf, &converted])).unwrap(); assert_eq!((&r["format"], &r["path"]), (&Value::from("ait"), &Value::from(converted.as_str())), "{r}"); let r: Value = serde_json::from_str(ok(&["run", "--in", &pdf, "--export", &exported]).lines().nth(1).unwrap()).unwrap(); assert_eq!(r["result"]["format"], "ait", "{r}"); for ait in [&converted, &exported] { assert!(std::fs::read(ait).unwrap().starts_with(b"%PDF"), "{ait}"); let opened: Value = serde_json::from_str(ok(&["run", "--in", ait]).lines().next().unwrap()).unwrap(); let opened = &opened["result"]; assert_eq!((&opened["format"], &opened["restored"]), (&Value::from("ait"), &Value::from(true)), "{opened}"); assert!(opened["title"].as_str().is_some_and(|t| t.starts_with("Untitled-")), "{opened}"); assert_eq!(info(ait)["artboards"].as_array().unwrap().len(), 2, "{ait}"); } // A template holds every artboard: one can't be picked. let one = Command::new(BIN).args(["convert", &pdf, &tmp("one.ait"), "--artboard", "0"]).output().unwrap(); assert!(!one.status.success()); assert!(String::from_utf8_lossy(&one.stderr).contains("every artboard"), "{}", String::from_utf8_lossy(&one.stderr)); }