diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index c629771c3..13d4cb4bc 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -67,6 +67,11 @@ fn image_format(f: image::ImageFormat) -> Option<&'static Format> { f.extensions_str().iter().find_map(|e| format(e)).filter(|f| f.read && f.raster) } +/// Why an SVG carrying editing data opened as plain SVG: it was changed after it was saved. +pub const EDITING_STALE: &str = "this SVG was changed in another app after it was saved with its editing data: it opened as plain SVG"; +/// Why an SVG carrying editing data opened as plain SVG: the data can't be read. +pub const EDITING_DAMAGED: &str = "this SVG's editing data can't be read: it opened as plain SVG"; + /// Read a file of any readable format (`name`: its file name or path, for the extension and title). pub fn load(name: &str, bytes: &[u8]) -> Result { let format = detect(name, bytes).ok_or_else(|| err(format!("can't open `{name}`: not a format VectorCraft reads (see document.formats)")))?; @@ -76,12 +81,22 @@ pub fn load(name: &str, bytes: &[u8]) -> Result { "vectorcraft" => vectorcraft_format::load(bytes).map_err(err)?, "svg" | "svgz" => { let text = vectorcraft_svg::text_of(bytes).map_err(err)?; - // An SVG saved with Preserve Editing carries the native document: open that. - match vectorcraft_svg::editing_data(&text).and_then(|b64| vectorcraft_format::base64_decode(&b64)) { - Some(native) => vectorcraft_format::load(&native).map_err(err)?, + // An SVG saved with Preserve Editing carries the native document: open that, unless + // the SVG was edited since (or the data is damaged). + let editing = vectorcraft_svg::editing(&text); + let native = editing + .as_ref() + .filter(|e| e.intact) + .and_then(|e| vectorcraft_format::base64_decode(&e.data)) + .and_then(|b| vectorcraft_format::load(&b).ok()); + match native { + Some(d) => d, None => { let (d, w) = vectorcraft_svg::import_with_report(&text).map_err(err)?; warnings = w; + if let Some(e) = editing { + warnings.insert(0, (if e.intact { EDITING_DAMAGED } else { EDITING_STALE }).to_string()); + } d } } diff --git a/crates/engine/src/cmd/fileio/tests_svgedit.rs b/crates/engine/src/cmd/fileio/tests_svgedit.rs index 07f8dfca2..5e2e54cce 100644 --- a/crates/engine/src/cmd/fileio/tests_svgedit.rs +++ b/crates/engine/src/cmd/fileio/tests_svgedit.rs @@ -1,7 +1,9 @@ -//! SVG files that reopen as they were: Save keeps hidden layers and linked images stay linked. +//! SVG files that reopen as they were: Preserve Editing restores the document exactly (unless the +//! SVG was edited elsewhere), Save keeps hidden layers, symbols share one def, and linked images +//! stay linked. use serde_json::{Value, json}; -use vectorcraft_doc::NodeKind; +use vectorcraft_doc::{Document, NodeKind}; use super::tests_svg::{image_session, svg}; use super::*; @@ -14,10 +16,75 @@ fn rect(s: &mut Session, x: f64, y: f64) -> u64 { new_id(&s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": 20, "height": 20})).unwrap()) } +fn select(s: &mut Session, ids: &[u64]) { + s.execute("select.set", &json!({ "ids": ids })).unwrap(); +} + +/// A roughened square, three instances of a symbol and a square filled with a pattern. +fn rich() -> Session { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap(); + let rough = rect(&mut s, 10.0, 10.0); + s.execute("effect.apply", &json!({"effect": "distort.roughen", "ids": [rough]})).unwrap(); + let mark = rect(&mut s, 60.0, 10.0); + s.execute("symbol.new", &json!({"name": "Mark", "ids": [mark]})).unwrap(); + for (x, y) in [(150.0, 50.0), (200.0, 120.0)] { + s.execute("symbol.place", &json!({"name": "Mark", "x": x, "y": y})).unwrap(); + } + let dot = rect(&mut s, 0.0, 150.0); + s.execute("paint.setFill", &json!({"ids": [dot], "color": "#ff0000"})).unwrap(); + select(&mut s, &[dot]); + s.execute("object.pattern.make", &json!({"name": "Dots", "width": 30, "height": 30})).unwrap(); + s.execute("object.pattern.done", &json!({})).unwrap(); + let filled = rect(&mut s, 100.0, 150.0); + s.execute("paint.setFill", &json!({"ids": [filled], "swatch": "Dots"})).unwrap(); + s +} + fn open(s: &mut Session, name: &str, text: &str) -> Value { s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(text.as_bytes())})).unwrap() } +/// `d` without what opening a file sets anew. +fn comparable(d: &Document) -> Document { + let mut d = d.clone(); + d.title.clear(); + d +} + +#[test] +fn preserve_editing_restores_effects_symbols_and_patterns_exactly() { + let mut s = rich(); + let before = comparable(&s.doc().unwrap().doc); + assert!(before.symbols.iter().any(|x| x.name == "Mark") && before.pattern("Dots").is_some()); + let text = svg(&mut s, json!({"preserveEditing": true, "styling": "css"})); + assert_eq!(text.matches("