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}");
+}