From 13bd24ba7952481f3a663e9bfb3f0d8fd029c3d6 Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Tue, 13 Oct 2026 05:50:08 +0200 Subject: [PATCH] Open by a relative path finds relative links; convert reports import warnings document.open (and file.newFromTemplate) with a path relative to the working directory took the SVG's folder from Path::parent(), which is "" for a bare file name and was then dropped, so relatively linked images were "not found" (and, linked by their bare name, not listed in missingLinks either). A relative path is now made absolute first: its folder is the working directory, as for the absolute path. vectorcraft-cli convert printed only the export's warnings; what reading the input left out now comes as importWarnings. Fixes #1604 --- apps/vectorcraft-cli/src/main.rs | 18 ++++++++++------ apps/vectorcraft-cli/tests/fileio.rs | 16 +++++++++++++++ crates/engine/src/cmd/fileio/load.rs | 8 ++++++-- crates/engine/tests/open_relative_path.rs | 25 +++++++++++++++++++++++ 4 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 crates/engine/tests/open_relative_path.rs diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 8c595421..63df4511 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -93,7 +93,8 @@ USAGE: precision (1–7, default 3). OUT may not be IN when reading IN left things out (hidden text, art or layers it could not read; the notes say which) unless --replace-lossy is given. --editing-data opens a .ai from its editing copy even where that - draws differently from its PDF part (which can be older), as long as it can be read. + draws differently from its PDF part (which can be older), as long as it can be read. The + result has the export's warnings, and importWarnings for what reading IN left out. vectorcraft-cli info FILE Print a JSON summary: the import warnings (what didn't come in as it was, such as an EPS @@ -237,19 +238,24 @@ fn convert(args: &[String]) -> Result<(), String> { } let [input, output] = <[String; 2]>::try_from(files).map_err(|_| "convert needs IN and OUT")?; let mut h = Headless::new(); - if editing_data { + let opened = if editing_data { let params = json!({"path": input, "editingData": "always"}); - h.call("engine.execute", json!({"command": "document.open", "params": params})).map_err(|e| format!("open {input}: {e}"))?; + h.call("engine.execute", json!({"command": "document.open", "params": params})).map_err(|e| format!("open {input}: {e}"))? } else { - h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))?; - } + h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))? + }; // A conversion keeps hidden layers and objects, written hidden, where the format can (SVG, // PSD). let mut params = json!({"path": output, "scale": scale, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy}); if let Some(decimals) = decimals { params["decimals"] = json!(decimals); } - let r = h.call("engine.execute", json!({"command": "document.export", "params": params})).map_err(|e| format!("export {output}: {e}"))?; + let mut r = h.call("engine.execute", json!({"command": "document.export", "params": params})).map_err(|e| format!("export {output}: {e}"))?; + // What reading the input left out (a missing linked file, an unsupported filter…) matters as + // much as what writing the output did. + if let (Some(w), Some(out)) = (opened.get("warnings").filter(|w| w.as_array().is_some_and(|a| !a.is_empty())), r.as_object_mut()) { + out.insert("importWarnings".into(), w.clone()); + } outln!("{r}"); Ok(()) } diff --git a/apps/vectorcraft-cli/tests/fileio.rs b/apps/vectorcraft-cli/tests/fileio.rs index f23ab514..7c40493e 100644 --- a/apps/vectorcraft-cli/tests/fileio.rs +++ b/apps/vectorcraft-cli/tests/fileio.rs @@ -99,3 +99,19 @@ fn info_reports_why_an_eps_read_partly() { assert!(w.contains("`frobnicate`"), "{v}"); assert_eq!(v["kinds"]["Path"], 1, "{v}"); } + +/// #1604: convert reports what reading the input left out (here a missing linked image) as +/// importWarnings, next to the export's own warnings. +#[test] +fn convert_reports_import_warnings() { + let svg = tmp("missing-link.svg"); + std::fs::write( + &svg, + r#""#, + ) + .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}"); +} diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index 374825d1..d7433759 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -382,12 +382,16 @@ pub(crate) fn source<'a>(p: &'a Value, cmd: &str) -> Result> { pub(super) fn open(s: &mut Session, p: &Value) -> Result { let src = source(p, "document.open")?; - open_bytes_with(s, src.name, &src.bytes, src.path.map(str::to_string), p) + // A relative path is the file in the working directory: its folder is where relative links + // are found (a bare file name has no folder of its own). + let path = src.path.map(super::absolute_path); + open_bytes_with(s, path.as_deref().unwrap_or(src.name), &src.bytes, path.clone(), p) } pub(super) fn new_from_template(s: &mut Session, p: &Value) -> Result { let src = source(p, "file.newFromTemplate")?; - open_template(s, src.name, &src.bytes, src.path) + let path = src.path.map(super::absolute_path); + open_template(s, path.as_deref().unwrap_or(src.name), &src.bytes, path.as_deref()) } /// The most memory decoding one image may take: a 60 in print sheet at 300 ppi (18000 × 6600 px) diff --git a/crates/engine/tests/open_relative_path.rs b/crates/engine/tests/open_relative_path.rs new file mode 100644 index 00000000..f0c63a0f --- /dev/null +++ b/crates/engine/tests/open_relative_path.rs @@ -0,0 +1,25 @@ +//! #1604: a document opened by a path relative to the working directory finds its relatively +//! linked images, as when opened by its absolute path. Its own test binary: it changes the +//! process's working directory. +// 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 serde_json::json; +use vectorcraft_engine::Session; + +#[test] +fn a_relative_path_finds_relative_links() { + let dir = std::env::temp_dir().join(format!("vectorcraft-relative-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let mut png = Vec::new(); + image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 0, 0, 255])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + std::fs::write(dir.join("ext.png"), &png).unwrap(); + let svg = r#""#; + std::fs::write(dir.join("linked.svg"), svg).unwrap(); + std::env::set_current_dir(&dir).unwrap(); + let mut s = Session::new(); + let r = s.execute("document.open", &json!({"path": "linked.svg"})).unwrap(); + let _ = std::fs::remove_dir_all(&dir); + assert_eq!(r["warnings"], json!([]), "{r}"); + assert_eq!(r["missingLinks"], json!([]), "{r}"); +}