//! Placed documents: a VectorCraft document placed linked (`file.place`'s default for one read //! from a file), drawn and output as vectors, saved with its preview, checked, updated and //! relinked when the file changes, and turned into an editable copy (Break Link, Expand). use serde_json::{Value, json}; use vectorcraft_color::{Color, Paint}; use vectorcraft_doc::{Appearance, Document, Node, NodeKind, PlacedDocument}; use vectorcraft_geom::{Rect, shapes}; use super::tests_links::{BLUE, Folder, RED, centre_colour, near, open, save, session, write}; use super::*; /// A VectorCraft document at `path`: one `w`×`h` artboard filled with `rgb` (and `extra` more /// rectangles, which change the file's size). fn source(path: &str, w: f64, h: f64, rgb: [u8; 3], extra: usize) { let mut d = Document::new(w, h); let layer = d.layers[0].id; let [r, g, b] = rgb; for i in 0..=extra { let id = d.alloc_id(); let look = Appearance::basic(Paint::solid(Color::rgb8(r, g, b)), Paint::None, 0.0); d.insert(Some(layer), i, Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, w, h)), look)).unwrap(); } write(path, &vectorcraft_format::save_file(&d)); } /// A document drawing symbol `Star` (a blue 100×50 rectangle) at `path`, titled `title` (tests /// that remove the file give it content of its own: files read are cached by content). fn star_source(path: &str, title: &str) { let mut d = Document::new(100.0, 50.0); d.title = title.into(); let layer = d.layers[0].id; let art = Node::path(vectorcraft_doc::NodeId(900), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)), look(BLUE)); d.symbols.push(vectorcraft_doc::Symbol { name: "Star".into(), art: std::sync::Arc::new(art) }); let id = d.alloc_id(); d.insert(Some(layer), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Star".into(), xf: vectorcraft_geom::Affine::IDENTITY })).unwrap(); write(path, &vectorcraft_format::save_file(&d)); } fn look(c: [u8; 3]) -> Appearance { Appearance::basic(Paint::solid(Color::rgb8(c[0], c[1], c[2])), Paint::None, 0.0) } /// The active document gets a red symbol named `Star` of its own. fn parent_star(s: &mut Session) { let star = Node::path(vectorcraft_doc::NodeId(901), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), look(RED)); std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc) .symbols .push(vectorcraft_doc::Symbol { name: "Star".into(), art: std::sync::Arc::new(star) }); } /// Place `path` (linked: the default), centred on the artboard → its id. fn place(s: &mut Session, path: &str) -> NodeId { let r = s.execute("file.place", &json!({"path": path, "at": [200, 150]})).unwrap(); assert_eq!(r["linked"], true, "{r}"); NodeId(r["ids"][0].as_u64().unwrap()) } fn placed(s: &Session, id: NodeId) -> PlacedDocument { match &s.doc().unwrap().doc.node(id).unwrap().kind { NodeKind::PlacedDocument(p) => (**p).clone(), k => panic!("not a placed document: {k:?}"), } } fn bounds(s: &Session, id: NodeId) -> Rect { s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap() } fn decode(v: &Value) -> Vec { vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap() } /// Images and paths in `bytes`, a PDF, read back. fn pdf_contents(bytes: &[u8]) -> (usize, usize) { // The page as drawn (not the VectorCraft document the PDF carries). let (doc, _) = super::cmd::fileio::page_document(bytes, 0, &Default::default()).unwrap(); let (mut images, mut paths) = (0, 0); doc.walk(|n| match n.kind { NodeKind::Image(_) => images += 1, NodeKind::Path { .. } | NodeKind::Compound { .. } => paths += 1, _ => {} }); (images, paths) } /// The JSON of a `.vectorcraft` file (the data after it left out). fn file_json(bytes: &[u8]) -> Value { let end = bytes.windows(vectorcraft_format::BLOB_MAGIC.len()).position(|w| w == vectorcraft_format::BLOB_MAGIC).unwrap_or(bytes.len()); serde_json::from_slice(&bytes[..end]).unwrap() } #[test] fn a_document_places_linked_by_default_at_its_artboard_size() { let dir = Folder::new("placed-basic"); let src = dir.file("logo.vectorcraft"); source(&src, 100.0, 50.0, RED, 0); let mut s = session(); let id = place(&mut s, &src); let p = placed(&s, id); assert!((p.width - 100.0).abs() < 1e-6 && (p.height - 50.0).abs() < 1e-6, "{p:?}"); assert_eq!(bounds(&s, id), Rect::new(150.0, 125.0, 250.0, 175.0)); assert_eq!((p.link.path.as_str(), p.link.page), (src.as_str(), Some(1))); let n = s.doc().unwrap().doc.node(id).unwrap(); assert_eq!((n.name.as_deref(), n.kind_label()), (Some("logo.vectorcraft"), "Placed Document")); assert!(near(centre_colour(&s.doc().unwrap().doc), RED)); // It moves and scales like any object. s.execute("object.move", &json!({"dx": 10, "dy": 0})).unwrap(); assert_eq!(bounds(&s, id), Rect::new(160.0, 125.0, 260.0, 175.0)); // Its art's bounds instead of the artboard. let mut d = Document::new(100.0, 50.0); let layer = d.layers[0].id; let id2 = d.alloc_id(); d.insert(Some(layer), 0, Node::path(id2, shapes::rectangle(Rect::new(10.0, 10.0, 40.0, 30.0)), look(RED))).unwrap(); let small = dir.file("small.vectorcraft"); write(&small, &vectorcraft_format::save_file(&d)); let r = s.execute("file.place", &json!({"path": small, "crop": "bounding", "at": [200, 150]})).unwrap(); let p = placed(&s, NodeId(r["ids"][0].as_u64().unwrap())); assert!(p.bounding && (p.width - 30.0).abs() < 1e-6 && (p.height - 20.0).abs() < 1e-6, "{p:?}"); } #[test] fn without_link_or_a_file_a_document_places_as_an_editable_copy() { let dir = Folder::new("placed-copy"); let src = dir.file("logo.vectorcraft"); source(&src, 100.0, 50.0, RED, 0); let mut s = session(); let data = vectorcraft_format::base64_encode(&std::fs::read(&src).unwrap()); for p in [json!({"path": src, "link": false}), json!({"name": "logo.vectorcraft", "dataBase64": data})] { let r = s.execute("file.place", &p).unwrap(); assert_eq!(r["linked"], false, "{r}"); let n = s.doc().unwrap().doc.node(NodeId(r["ids"][0].as_u64().unwrap())).unwrap(); assert_eq!(n.kind_label(), "Clip Group", "{p}"); } assert!(!s.doc().unwrap().doc.has_placed()); } #[test] fn a_placed_document_is_vectors_in_every_output() { let dir = Folder::new("placed-outputs"); let src = dir.file("logo.vectorcraft"); source(&src, 100.0, 50.0, RED, 0); let mut s = session(); place(&mut s, &src); let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap(); let text = svg["text"].as_str().unwrap(); assert!(!text.contains(" 0); let png = decode(&s.execute("document.serialize", &json!({"format": "png"})).unwrap()); let [r, g, b, _] = image::load_from_memory(&png).unwrap().to_rgba8().get_pixel(200, 150).0; assert!(near([r, g, b], RED), "{:?}", [r, g, b]); for format in ["eps", "emf", "dxf"] { let out = s.execute("document.serialize", &json!({"format": format})).unwrap(); assert!(out.to_string().len() > 200, "{format}: {out}"); } let info = s.execute("document.info", &json!({})).unwrap(); assert!(info.to_string().contains("\"placedDocuments\""), "{info}"); } #[test] fn the_preview_is_saved_so_the_document_shows_it_without_its_file() { let dir = Folder::new("placed-saved"); let src = dir.file("logo.vectorcraft"); // Content of its own: files read are cached by content, and this one is removed. source(&src, 100.0, 50.0, RED, 2); let mut s = session(); let id = place(&mut s, &src); let doc_path = dir.file("poster/poster.vectorcraft"); std::fs::create_dir_all(dir.file("poster")).unwrap(); save(&mut s, &doc_path); let saved: Value = serde_json::from_slice(&std::fs::read(&doc_path).unwrap()).unwrap(); let kind = &saved["document"]["layers"][0]["kind"]["children"][0]["kind"]; assert_eq!(kind["type"], "placeddocument"); assert_eq!(kind["link"]["relative"], "../logo.vectorcraft"); let key = kind["key"].as_str().unwrap(); let entry = &saved["images"][key]; assert_eq!((entry["proxy"].as_bool(), entry["mime"].as_str()), (Some(true), Some("image/jpeg")), "the preview, as JPEG (opaque)"); // Include Linked Files keeps the file itself. let full = file_json(&decode(&s.execute("document.serialize", &json!({"format": "vectorcraft", "includeLinked": true})).unwrap())); assert_eq!((full["images"][key]["proxy"].clone(), full["images"][key]["mime"].as_str()), (Value::Null, Some("application/json"))); std::fs::remove_file(&src).unwrap(); let r = open(&mut s, &doc_path); assert_eq!(r["missingLinks"].as_array().map(Vec::len), Some(1), "{r}"); assert_eq!(placed(&s, id).link.path, src); assert!(near(centre_colour(&s.doc().unwrap().doc), RED), "shown from the saved preview"); assert_eq!(s.execute("links.check", &json!({})).unwrap()["missing"], 1); // Output with the file gone: the preview, with a warning. let png = s.execute("document.serialize", &json!({"format": "png"})).unwrap(); assert!(png["warnings"].to_string().contains("output as their previews"), "{png}"); let [r, g, b, _] = image::load_from_memory(&decode(&png)).unwrap().to_rgba8().get_pixel(200, 150).0; assert!(near([r, g, b], RED), "{:?}", [r, g, b]); // Saved for older apps: the art it shows (here its preview), as plain objects. let old: Value = serde_json::from_slice(&decode(&s.execute("document.serialize", &json!({"format": "vectorcraft", "version": 2})).unwrap())).unwrap(); let old = old["document"]["layers"][0]["kind"]["children"][0].to_string(); assert!(old.contains("\"image\"") && !old.contains("placeddocument"), "{old}"); } #[test] fn ai_export_preserves_placed_links_while_drawing_full_art_or_previews() { for missing in [false, true] { let dir = Folder::new(&format!("placed-ai-editable-{missing}")); let src = dir.file("logo.vectorcraft"); star_source(&src, &format!("placed-ai-editable-{missing}")); let mut s = session(); let id = place(&mut s, &src); let parent = dir.file("parent.vectorcraft"); save(&mut s, &parent); if missing { std::fs::remove_file(&src).unwrap(); } open(&mut s, &parent); let original = placed(&s, id); for pdf_compatible in [true, false] { for (command, include_linked) in [("document.export", false), ("document.export", true), ("document.serialize", false), ("document.serialize", true)] { let out = s.execute(command, &json!({"format": "ai", "pdfCompatible": pdf_compatible, "includeLinked": include_linked})).unwrap(); let bytes = decode(&out); let editing = vectorcraft_pdf::editing(&bytes).unwrap(); let native = vectorcraft_format::load(&editing.data).unwrap(); assert_eq!( native.images[&original.key].is_proxy(), missing || !include_linked, "Include Linked Files retains available source bytes" ); if pdf_compatible { let (images, paths) = pdf_contents(&bytes); if missing { assert!(images > 0, "the missing file draws its preview"); assert!(out["warnings"].to_string().contains("output as their previews")); } else { assert_eq!(images, 0, "the PDF uses the file's vectors"); assert!(paths > 0); } } let mut restored = session(); let result = restored .execute("document.open", &json!({"name": "export.ai", "dataBase64": vectorcraft_format::base64_encode(&bytes)})) .unwrap(); assert_eq!(result["restored"], true); assert_eq!(placed(&restored, id), original, "link metadata stays editable"); assert_eq!(placed(&s, id), original, "export leaves the source intact"); if missing { assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], 1); let replacement = dir.file("replacement.vectorcraft"); source(&replacement, 100.0, 50.0, BLUE, 1); let relinked = restored.execute("links.relink", &json!({"ids": [id.0], "path": replacement})).unwrap(); assert_eq!(relinked["relinked"], json!([id.0])); assert_eq!(placed(&restored, id).link.path, replacement); } } } } } #[test] fn ai_export_public_aliases_preserve_missing_placed_links() { let dir = Folder::new("placed-ai-public-aliases-latest"); let src = dir.file("logo.vectorcraft"); star_source(&src, "placed-ai-public-aliases-latest-unique"); let mut s = session(); let id = place(&mut s, &src); let parent = dir.file("parent.vectorcraft"); save(&mut s, &parent); std::fs::remove_file(&src).unwrap(); open(&mut s, &parent); let original = placed(&s, id); assert!(s.doc().unwrap().doc.images[&original.key].is_proxy()); for alias in ["vectorcraft", ".vectorcraft", "VECTORCRAFT", "drawcraft", ".DRAWCRAFT", "template", "vctemplate", ".VCTEMPLATE", "ai", ".ai", "AI", ".AI"] { let canonical = cmd::fileio::format(alias).unwrap().id; let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"pdfCompatible": false, "includeLinked": false})).unwrap(); let bytes = &enc.files[0].1; let raw = if canonical == "ai" { vectorcraft_pdf::editing(bytes).unwrap().data } else { bytes.clone() }; let native = vectorcraft_format::load(&raw).unwrap(); assert!( matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{alias}: preparation discarded the editable placed link" ); assert!(native.images[&original.key].is_proxy(), "{alias}"); assert_eq!(placed(&s, id), original, "{alias}: source changed"); } } /// Include Linked Files must retain relinkable objects when only their previews are available. #[test] fn native_included_links_keep_missing_placed_documents_editable() { for missing in [true, false] { let dir = Folder::new(&format!("native-included-links-{missing}")); let src = dir.file("logo.vectorcraft"); star_source(&src, &format!("native-included-links-{missing}")); let source_bytes = std::fs::read(&src).unwrap(); let mut s = session(); let id = place(&mut s, &src); let parent = decode(&s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap()); if missing { std::fs::remove_file(&src).unwrap(); } s.execute("document.open", &json!({"name": "parent.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&parent)})).unwrap(); let original = placed(&s, id); assert!(s.doc().unwrap().doc.images[&original.key].is_proxy()); let before = s.execute("document.json", &json!({})).unwrap(); for format in ["vectorcraft", "template"] { for command in ["document.serialize", "document.export", "document.save"] { for include_linked in [true, false] { let out = s.execute(command, &json!({"format": format, "includeLinked": include_linked, "compress": false})).unwrap(); let bytes = decode(&out); let native = vectorcraft_format::load(&bytes).unwrap(); assert_eq!(file_json(&bytes)["version"], vectorcraft_format::VERSION); assert!( matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{command}/{format}/{missing}/{include_linked}: discarded the editable link" ); let blob = &native.images[&original.key]; assert_eq!(blob.is_proxy(), missing || !include_linked); if !missing && include_linked { assert_eq!(blob.bytes.as_slice(), source_bytes.as_slice()); } assert_eq!(s.execute("document.json", &json!({})).unwrap(), before); if missing && include_linked { let mut restored = Session::new(); restored .execute( "document.open", &json!({"name": "included.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&bytes)}), ) .unwrap(); assert_eq!(placed(&restored, id), original); assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], 1); let replacement = dir.file("replacement.vectorcraft"); source(&replacement, 100.0, 50.0, BLUE, 1); assert_eq!( restored.execute("links.relink", &json!({"ids": [id.0], "path": replacement})).unwrap()["relinked"], json!([id.0]) ); assert_eq!(placed(&restored, id).link.path, replacement); } } } } } } #[test] fn a_document_saved_with_previews_outputs_its_files_art() { let dir = Folder::new("placed-preview-output"); let src = dir.file("logo.vectorcraft"); source(&src, 100.0, 50.0, RED, 0); let mut s = session(); let id = place(&mut s, &src); let doc_path = dir.file("poster.vectorcraft"); save(&mut s, &doc_path); open(&mut s, &doc_path); let key = placed(&s, id).key; assert!(s.doc().unwrap().doc.images[&key].is_proxy(), "opened with the preview only"); assert!(near(centre_colour(&s.doc().unwrap().doc), RED)); // Output reads the file again: vectors, no warning. let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap(); assert_eq!(svg["warnings"], json!([]), "{}", svg["warnings"]); let text = svg["text"].as_str().unwrap(); assert!(!text.contains(" = doc.symbols.iter().map(|s| s.name.as_str()).collect(); assert_eq!(names.len(), 2, "the file's Star joined, renamed: {names:?}"); assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"], json!([])); assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Embed"); s.execute("edit.undo", &json!({})).unwrap(); assert!(s.doc().unwrap().doc.has_placed()); // A file that can't be read stays linked. std::fs::remove_file(&src).unwrap(); let doc_path = dir.file("poster.vectorcraft"); save(&mut s, &doc_path); open(&mut s, &doc_path); let r = s.execute("links.embed", &json!({"ids": [id.0]})).unwrap(); assert_eq!((r["embedded"].clone(), r["missing"].clone()), (json!([]), json!([id.0])), "{r}"); } #[test] fn expand_turns_a_placed_document_into_editable_art_with_its_resources() { let dir = Folder::new("placed-expand"); let src = dir.file("badge.vectorcraft"); star_source(&src, "star"); let mut s = session(); parent_star(&mut s); let id = place(&mut s, &src); s.execute("object.scale", &json!({"sx": 200})).unwrap(); let before = bounds(&s, id); s.execute("object.expand", &json!({})).unwrap(); let doc = &s.doc().unwrap().doc; let n = doc.node(id).unwrap(); assert!(n.is_container() && !doc.has_placed(), "{:?}", n.kind_label()); let after = n.geometric_bounds().unwrap(); assert!((after.width() - before.width()).abs() < 0.5 && (after.height() - before.height()).abs() < 0.5, "{after:?} vs {before:?}"); assert!(near(centre_colour(doc), BLUE)); let saved = vectorcraft_format::save_file(doc); assert!(near(centre_colour(&vectorcraft_format::load(&saved).unwrap()), BLUE), "saved with its resources"); let info = s.execute("object.expand.info", &json!({})).unwrap(); assert_eq!(info["object"], false, "nothing left to expand: {info}"); } #[test] fn saving_a_document_refreshes_the_open_documents_that_place_it() { let dir = Folder::new("placed-refresh"); let src = dir.file("logo.vectorcraft"); source(&src, 100.0, 50.0, RED, 0); let mut s = session(); let id = place(&mut s, &src); let parent = s.active_index().unwrap(); // Open the file, recolour it and save it. s.execute("document.open", &json!({"path": src})).unwrap(); s.execute("select.all", &json!({})).unwrap(); s.execute("paint.setFill", &json!({"color": "#1414e6"})).unwrap(); s.execute("document.save", &json!({})).unwrap(); let child = s.active_index().unwrap(); assert_ne!(parent, child); let doc = &s.documents()[parent]; assert_eq!(doc.history.undo.last().unwrap().label, "Update Links"); assert!(near(centre_colour(&doc.doc), BLUE), "shows the saved version"); assert!(matches!(doc.doc.node(id).map(|n| &n.kind), Some(NodeKind::PlacedDocument(_)))); assert_eq!(s.active_index(), Some(child), "the saved document stays active"); } #[test] fn link_info_gives_the_artboards_size_not_pixels() { let dir = Folder::new("placed-info"); let src = dir.file("logo.vectorcraft"); source(&src, 100.0, 50.0, RED, 0); let mut s = session(); let id = place(&mut s, &src); s.execute("object.scale", &json!({"sx": 200})).unwrap(); let i = s.execute("links.info", &json!({"id": id.0})).unwrap(); assert_eq!((i["pageWidth"].as_f64(), i["pageHeight"].as_f64(), i["document"].as_bool()), (Some(100.0), Some(50.0), Some(true)), "{i}"); assert!(i.get("pixelWidth").is_none() && i.get("ppi").is_none(), "no pixels: {i}"); assert_eq!( (i["status"].as_str(), i["fileName"].as_str(), i["scale"].clone()), (Some("ok"), Some("logo.vectorcraft"), json!([200.0, 200.0])), "{i}" ); } /// Save As reads its defaults from the catalog; using them must retain current editable links. #[test] fn native_save_catalog_defaults_keep_placed_documents_editable() { for missing in [false, true] { let dir = Folder::new(if missing { "native-default-missing" } else { "native-default-linked" }); let src = dir.file("logo.vectorcraft"); star_source(&src, if missing { "native-default-missing" } else { "native-default-linked" }); let mut s = session(); let id = place(&mut s, &src); let original = placed(&s, id); if missing { std::fs::remove_file(&src).unwrap(); } let options = s.execute("file.formatOptions", &json!({"format": "vectorcraft"})).unwrap(); let saved = dir.file("default-options.vectorcraft"); s.execute("document.save", &json!({"path": saved, "version": options["options"]["version"]["value"]})).unwrap(); let bytes = std::fs::read(&saved).unwrap(); let native = vectorcraft_format::load(&bytes).unwrap(); let mut written = original.clone(); // Native saves record a sibling link relative to the destination as well as its // absolute path. This is relocation metadata, not a change to the placed object. written.link.relative = Some("logo.vectorcraft".into()); assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == written), "source missing: {missing}"); assert_eq!(file_json(&bytes)["version"], vectorcraft_format::VERSION); assert_eq!(placed(&s, id), original, "saving does not rewrite the live link"); let mut reopened = Session::new(); open(&mut reopened, &saved); assert_eq!(placed(&reopened, id).link.path, src); assert_eq!(reopened.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing)); } } /// Generic and dedicated PDF exports share editable links and prepared drawn pages. #[test] fn pdf_editing_keeps_placed_links_separate_from_drawn_previews() { for missing in [true, false] { let dir = Folder::new(&format!("pdf-editable-links-{missing}")); let src = dir.file("logo.vectorcraft"); star_source(&src, &format!("pdf-editable-links-{missing}")); let mut s = session(); parent_star(&mut s); let id = place(&mut s, &src); s.execute("artboard.new", &json!({"x":600,"y":0,"width":100,"height":100})).unwrap(); let parent = decode(&s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap()); if missing { std::fs::remove_file(&src).unwrap(); } s.execute("document.open", &json!({"name": "parent.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&parent)})).unwrap(); let original = placed(&s, id); assert!(s.doc().unwrap().doc.images[&original.key].is_proxy()); let before = s.execute("document.json", &json!({})).unwrap(); for command in ["document.serialize", "document.export", "document.save", "document.exportPdf"] { let out = s.execute(command, &json!({"format":"pdf","preserveEditing":true,"range":"all","thumbnails":false})).unwrap(); let bytes = decode(&out); let editing = vectorcraft_pdf::editing(&bytes).unwrap(); assert!(editing.intact); let native = vectorcraft_format::load(&editing.data).unwrap(); assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{command}: discarded editable link"); let (images, paths) = pdf_contents(&bytes); assert_eq!(images > 0, missing, "{command}: pages still draw previews or vectors"); if !missing { assert!(paths > 0); } assert_eq!( out["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("placed document(s) output as their previews")), missing, "{out}" ); let mut restored = Session::new(); assert_eq!( restored.execute("document.open", &json!({"name":"editable.pdf","dataBase64":vectorcraft_format::base64_encode(&bytes)})).unwrap()["restored"], true ); assert_eq!(placed(&restored, id), original); assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing)); assert_eq!(s.execute("document.json", &json!({})).unwrap(), before); if missing { let replacement = dir.file("replacement.vectorcraft"); source(&replacement, 100.0, 50.0, BLUE, 1); assert_eq!(restored.execute("links.relink", &json!({"ids":[id.0],"path":replacement})).unwrap()["relinked"], json!([id.0])); } } for alias in ["PDF", ".pdf"] { let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"preserveEditing":true})).unwrap(); let editing = vectorcraft_pdf::editing(&enc.files[0].1).unwrap(); let native = vectorcraft_format::load(&editing.data).unwrap(); assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{alias}"); } for params in [json!({"format":"pdf","preserveEditing":false}), json!({"format":"pdf","preserveEditing":true,"range":"1"})] { let out = s.execute("document.serialize", ¶ms).unwrap(); let bytes = decode(&out); assert!(vectorcraft_pdf::editing(&bytes).is_none(), "{params}"); assert_eq!(pdf_contents(&bytes).0 > 0, missing); } } } /// EPS draws hydrated artwork but carries the original link metadata with requested source bytes. #[test] fn eps_editing_keeps_placed_links_and_requested_source_bytes() { for missing in [true, false] { let dir = Folder::new(&format!("eps-editable-links-{missing}")); let src = dir.file("logo.vectorcraft"); star_source(&src, &format!("eps-editable-links-{missing}")); let source_bytes = std::fs::read(&src).unwrap(); let mut s = session(); parent_star(&mut s); let id = place(&mut s, &src); let parent = decode(&s.execute("document.serialize", &json!({"format":"vectorcraft"})).unwrap()); if missing { std::fs::remove_file(&src).unwrap(); } s.execute("document.open", &json!({"name":"parent.vectorcraft","dataBase64":vectorcraft_format::base64_encode(&parent)})).unwrap(); let original = placed(&s, id); assert!(s.doc().unwrap().doc.images[&original.key].is_proxy()); let before = s.execute("document.json", &json!({})).unwrap(); for include_linked in [false, true] { let params = json!({"format":"eps","includeLinkedFiles":include_linked,"previewFormat":"none","thumbnails":false}); for command in ["document.serialize", "document.export", "document.exportEps"] { let out = s.execute(command, ¶ms).unwrap(); let bytes = decode(&out); let raw = vectorcraft_eps::native(&bytes).unwrap(); let native = vectorcraft_format::load(&raw).unwrap(); assert!( matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{command}/{missing}/{include_linked}: editable link lost" ); let blob = &native.images[&original.key]; assert_eq!(blob.is_proxy(), missing || !include_linked); if !missing && include_linked { assert_eq!(blob.bytes.as_slice(), source_bytes.as_slice()); } assert_eq!( out["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("placed document(s) output as their previews")), missing, "{out}" ); // Read the printed page, independently of the native editing attachment. let drawn = vectorcraft_eps::import_with(&bytes, false).unwrap(); assert!(!drawn.preview); let (mut images, mut paths) = (0, 0); drawn.document.walk(|n| match n.kind { NodeKind::Image(_) => images += 1, NodeKind::Path { .. } | NodeKind::Compound { .. } => paths += 1, _ => {} }); assert_eq!(images > 0, missing, "{command}: drawn output uses vectors or the preview"); if !missing { assert!(paths > 0); } let mut restored = Session::new(); assert_eq!( restored .execute("document.open", &json!({"name":"editable.eps","dataBase64":vectorcraft_format::base64_encode(&bytes)})) .unwrap()["restored"], true ); assert_eq!(placed(&restored, id), original); assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing)); assert_eq!(s.execute("document.json", &json!({})).unwrap(), before); if missing { let replacement = dir.file("replacement.vectorcraft"); source(&replacement, 100.0, 50.0, BLUE, 1); assert_eq!(restored.execute("links.relink", &json!({"ids":[id.0],"path":replacement})).unwrap()["relinked"], json!([id.0])); } } } for alias in ["EPS", ".eps"] { let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"previewFormat":"none","thumbnails":false})).unwrap(); let native = vectorcraft_format::load(&vectorcraft_eps::native(&enc.files[0].1).unwrap()).unwrap(); assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{alias}"); } } } /// An SVG's editing attachment keeps links even when its visible output must use previews. #[test] fn svg_editing_keeps_placed_links_separate_from_drawn_previews() { for missing in [true, false] { let dir = Folder::new(&format!("svg-editable-links-{missing}")); let src = dir.file("logo.vectorcraft"); star_source(&src, &format!("svg-editable-links-{missing}")); let mut s = session(); parent_star(&mut s); let id = place(&mut s, &src); let parent = decode(&s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap()); if missing { std::fs::remove_file(&src).unwrap(); } s.execute("document.open", &json!({"name": "parent.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&parent)})).unwrap(); let original = placed(&s, id); assert!(s.doc().unwrap().doc.images[&original.key].is_proxy()); let before = s.execute("document.json", &json!({})).unwrap(); for format in ["svg", "svgz"] { for command in ["document.serialize", "document.export", "document.save"] { let out = s.execute(command, &json!({"format": format, "svg": {"preserveEditing": true, "range": "1"}})).unwrap(); let bytes = out .get("dataBase64") .map(|b| vectorcraft_format::base64_decode(b.as_str().unwrap()).unwrap()) .unwrap_or_else(|| out["text"].as_str().unwrap().as_bytes().to_vec()); let text = vectorcraft_svg::text_of(&bytes).unwrap(); assert_eq!(text.contains("