//! The CLI opens every readable format (PDF, .ai, images) through the engine's `document.open`. // Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness). #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use std::process::Command; use serde_json::Value; const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); fn tmp(name: &str) -> String { let dir = std::env::temp_dir().join(format!("vectorcraft-cli-fileio-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); 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)); }