Files
vectorcraft/crates/engine/src/tests_placed_document.rs
T
dotCooCoo 781e62b9cc Place: Link keeps a placed PDF or .ai page linked, as it does images
The File Handling preference Link Placed PDF and .ai Files
(linkPlacedPdf) is removed. File › Place with Link now places a page of
a PDF, .ai or .ait file read as PDF pages as a linked placed document,
as it links a raster image, and link: false embeds the page. Link is on
by default in the Place dialog and in file.place, so a PDF or .ai page
placed from a file, which used to be embedded, is now linked unless
Link is off. A password or textAs: "outlines" still embeds, and so do
EPS and PostScript .ai files.

The Place dialog's Link tooltip describes a PDF or .ai file as a
document.

A .ai file read from its editing data (its PDF part only a placeholder)
finds the files its art places by name, beside it. Package now copies
those files into the same folder as the .ai file, and warns when one has
to be copied under another name.
2026-10-22 21:52:23 +07:00

1049 lines
54 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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). A PDF
//! or `.ai` page places the same way.
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<u8> {
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("<image") && (text.contains("<path") || text.contains("<rect")), "drawn as paths: {text}");
let (images, paths) = pdf_contents(&decode(&s.execute("document.serialize", &json!({"format": "pdf"})).unwrap()));
assert_eq!(images, 0, "no image in the PDF");
assert!(paths > 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("<image") && (text.contains("<path") || text.contains("<rect")), "vectors");
// So does saving with Include Linked Files, and the document stays as it was.
let full = file_json(&decode(&s.execute("document.serialize", &json!({"format": "vectorcraft", "includeLinked": true})).unwrap()));
assert_eq!(full["images"][&key]["mime"], "application/json");
assert!(s.doc().unwrap().doc.images[&key].is_proxy());
}
#[test]
fn a_changed_file_is_modified_and_update_links_reads_it_again() {
let dir = Folder::new("placed-update");
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 before = bounds(&s, id);
let c = s.execute("links.check", &json!({})).unwrap();
assert_eq!((c["modified"].as_u64(), c["links"][0]["status"].as_str()), (Some(0), Some("ok")), "{c}");
let list = s.execute("links.list", &json!({})).unwrap();
let row = &list["links"][0];
assert_eq!((row["format"].as_str(), row["document"].as_bool(), row["status"].as_str()), (Some("VectorCraft"), Some(true), Some("ok")), "{row}");
// A blue document, wider, and a different file size.
source(&src, 200.0, 50.0, BLUE, 1);
assert_eq!(s.execute("links.check", &json!({})).unwrap()["links"][0]["status"], "modified");
// Until updated, it shows (and outputs) the file as it read it.
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
assert_eq!(svg["warnings"], json!([]));
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
let u = s.execute("links.update", &json!({})).unwrap();
assert_eq!(u["updated"], json!([id.0]), "{u}");
assert_eq!(placed(&s, id).width, 200.0);
assert_eq!(bounds(&s, id), before, "keeps its bounds");
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
assert_eq!(s.execute("links.check", &json!({})).unwrap()["modified"], 0);
// One undo step.
s.execute("edit.undo", &json!({})).unwrap();
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
}
#[test]
fn update_links_on_open_reads_a_modified_file_again() {
let dir = Folder::new("placed-open");
let src = dir.file("logo.vectorcraft");
source(&src, 100.0, 50.0, RED, 0);
let mut s = session();
place(&mut s, &src);
let doc_path = dir.file("poster.vectorcraft");
save(&mut s, &doc_path);
source(&src, 100.0, 50.0, BLUE, 1);
// Ask When Modified (the default): reported, left as it was.
let r = open(&mut s, &doc_path);
assert_eq!(r["modifiedLinks"].as_array().map(Vec::len), Some(1), "{r}");
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
// Automatically: read again as the document opens.
s.execute("prefs.set", &json!({"key": "updateLinks", "value": "automatically"})).unwrap();
let r = open(&mut s, &doc_path);
assert_eq!(r["updatedLinks"].as_array().map(Vec::len), Some(1), "{r}");
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
}
#[test]
fn relink_points_a_placed_document_at_another_file() {
let dir = Folder::new("placed-relink");
let (a, b) = (dir.file("a.vectorcraft"), dir.file("b.vectorcraft"));
source(&a, 100.0, 50.0, RED, 0);
source(&b, 100.0, 50.0, BLUE, 0);
let mut s = session();
let id = place(&mut s, &a);
let r = s.execute("links.relink", &json!({"ids": [id.0], "path": b})).unwrap();
assert_eq!(r["relinked"], json!([id.0]), "{r}");
assert_eq!(placed(&s, id).link.path, b);
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
}
/// A file made from scratch at `path` in `format` (`pdf`, or `ai`: a PDF-compatible .ai): one
/// 100×50 page per color of `pages`, filled with it (`extra` more rectangles on each change the
/// file's size).
fn pdf_source(path: &str, format: &str, pages: &[[u8; 3]], extra: usize) {
let mut d = Document::new(100.0, 50.0);
let layer = d.layers[0].id;
for (i, rgb) in pages.iter().enumerate() {
let x = i as f64 * 150.0;
let page = Rect::new(x, 0.0, x + 100.0, 50.0);
if i > 0 {
let mut b = d.artboards[0].clone();
(b.id, b.rect) = (i as u32 + 1, page);
d.artboards.push(b);
}
for _ in 0..=extra {
let id = d.alloc_id();
d.insert(Some(layer), usize::MAX, Node::path(id, shapes::rectangle(page), look(*rgb))).unwrap();
}
}
let p = if format == "ai" { json!({"pdfCompatible": true}) } else { json!({"range": "all", "preserveEditing": false}) };
write(path, &cmd::fileio::encode_all(&d, format, &p).unwrap().files[0].1);
}
#[test]
fn a_pdf_page_placed_without_link_is_embedded() {
let dir = Folder::new("placed-pdf-embedded");
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 0);
let mut s = session();
let r = s.execute("file.place", &json!({"path": src, "link": false, "at": [200, 150]})).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");
assert!(!s.doc().unwrap().doc.has_placed());
// Text asked for as outlines is embedded with Link too.
let r = s.execute("file.place", &json!({"path": src, "textAs": "outlines"})).unwrap();
assert_eq!(r["linked"], false, "{r}");
assert!(!s.doc().unwrap().doc.has_placed());
}
#[test]
fn with_link_a_pdf_page_places_linked() {
let dir = Folder::new("placed-pdf-linked");
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 0);
let mut s = session();
let id = place(&mut s, &src);
let p = placed(&s, id);
assert_eq!((p.link.path.as_str(), p.link.page, p.bounding), (src.as_str(), Some(1), false), "{p:?}");
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!(near(centre_colour(&s.doc().unwrap().doc), RED));
let row = &s.execute("links.list", &json!({})).unwrap()["links"][0];
assert_eq!((row["format"].as_str(), row["document"].as_bool(), row["status"].as_str()), (Some("PDF"), Some(true), Some("ok")), "{row}");
assert_eq!(s.doc().unwrap().doc.images[&p.key].mime, "application/pdf");
// Break Link gives the group placing it without Link gives.
let r = s.execute("links.embed", &json!({"ids": [id.0]})).unwrap();
assert_eq!(r["embedded"], json!([id.0]), "{r}");
assert_eq!(s.doc().unwrap().doc.node(id).unwrap().kind_label(), "Clip Group");
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
}
#[test]
fn a_changed_pdf_is_modified_and_update_links_reads_it_again() {
let dir = Folder::new("placed-pdf-update");
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 0);
let mut s = session();
let id = place(&mut s, &src);
pdf_source(&src, "pdf", &[BLUE], 1);
assert_eq!(s.execute("links.check", &json!({})).unwrap()["links"][0]["status"], "modified");
assert!(near(centre_colour(&s.doc().unwrap().doc), RED), "shows the page as it read it");
let u = s.execute("links.update", &json!({})).unwrap();
assert_eq!(u["updated"], json!([id.0]), "{u}");
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
assert_eq!(s.execute("links.check", &json!({})).unwrap()["modified"], 0);
// Relinked to another PDF, it shows that file's page.
let other = dir.file("other.pdf");
pdf_source(&other, "pdf", &[RED], 2);
let r = s.execute("links.relink", &json!({"ids": [id.0], "path": other})).unwrap();
assert_eq!(r["relinked"], json!([id.0]), "{r}");
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
// A file it can't show is refused.
let notes = dir.file("notes.txt");
write(&notes, b"not a page");
let e = s.execute("links.relink", &json!({"ids": [id.0], "path": notes})).unwrap_err().to_string();
assert!(e.contains("isn't a VectorCraft document, a PDF or a PDF-compatible .ai file"), "{e}");
assert_eq!(placed(&s, id).link.path, other);
}
#[test]
fn a_linked_pdf_page_outputs_as_vectors_after_the_document_reopens() {
let dir = Folder::new("placed-pdf-outputs");
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 3);
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");
// Output reads the file again: the page's paths, not the preview.
let pdf = s.execute("document.serialize", &json!({"format": "pdf"})).unwrap();
assert_eq!(pdf["warnings"], json!([]), "{}", pdf["warnings"]);
let (images, paths) = pdf_contents(&decode(&pdf));
assert_eq!(images, 0, "no image in the PDF");
assert!(paths > 0);
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
assert!(!svg["text"].as_str().unwrap().contains("<image"), "vectors");
// Include Linked Files keeps the PDF 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/pdf")));
}
#[test]
fn saving_and_reopening_keeps_a_linked_pdf_pages_number_and_box() {
let dir = Folder::new("placed-pdf-reopen");
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED, BLUE], 4);
let mut s = session();
let r = s.execute("file.place", &json!({"path": src, "page": 2, "crop": "media", "at": [200, 150]})).unwrap();
assert_eq!(r["linked"], true, "{r}");
let id = NodeId(r["ids"][0].as_u64().unwrap());
let before = placed(&s, id);
assert_eq!((before.link.page, before.page_box), (Some(2), vectorcraft_doc::PageBox::Media), "{before:?}");
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE), "page 2");
let doc_path = dir.file("poster.vectorcraft");
save(&mut s, &doc_path);
let saved = file_json(&std::fs::read(&doc_path).unwrap());
let kind = &saved["document"]["layers"][0]["kind"]["children"][0]["kind"];
assert_eq!((kind["type"].as_str(), kind["page_box"].as_str(), kind["link"]["page"].as_u64()), (Some("placeddocument"), Some("media"), Some(2)));
let r = open(&mut s, &doc_path);
assert_eq!((r["missingLinks"].clone(), r["modifiedLinks"].clone()), (json!([]), json!([])), "{r}");
let mut reopened = placed(&s, id);
assert_eq!(reopened.link.relative.take().as_deref(), Some("flyer.pdf"), "written on save");
assert_eq!(reopened, before);
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
// Read again from the file, it shows the same page and box.
pdf_source(&src, "pdf", &[RED, RED], 5);
s.execute("links.update", &json!({})).unwrap();
let after = placed(&s, id);
assert_eq!((after.link.page, after.page_box), (Some(2), vectorcraft_doc::PageBox::Media));
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
}
#[test]
fn a_pdf_compatible_ai_file_places_linked_like_a_pdf() {
let dir = Folder::new("placed-ai-linked");
let src = dir.file("badge.ai");
pdf_source(&src, "ai", &[RED], 0);
let mut s = session();
let id = place(&mut s, &src);
assert_eq!(placed(&s, id).link.path, src);
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
let row = &s.execute("links.list", &json!({})).unwrap()["links"][0];
assert_eq!(row["format"], "PDF-compatible .ai", "{row}");
pdf_source(&src, "ai", &[BLUE], 1);
let u = s.execute("links.update", &json!({})).unwrap();
assert_eq!(u["updated"], json!([id.0]), "{u}");
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
let (images, _) = pdf_contents(&decode(&s.execute("document.serialize", &json!({"format": "pdf"})).unwrap()));
assert_eq!(images, 0);
}
#[test]
fn a_placed_document_inside_a_placed_document_outputs_from_its_own_file() {
let dir = Folder::new("placed-nested");
let (logo, card) = (dir.file("logo.vectorcraft"), dir.file("card.vectorcraft"));
source(&logo, 100.0, 50.0, RED, 0);
// The card places the logo, and is saved with the logo's preview only.
let mut s = session();
place(&mut s, &logo);
save(&mut s, &card);
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
place(&mut s, &card);
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
// The logo inside is read from its file: vectors, not its preview.
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
let text = svg["text"].as_str().unwrap();
assert!(!text.contains("<image"), "the logo's file, not its preview: {text}");
assert_eq!(pdf_contents(&decode(&s.execute("document.serialize", &json!({"format": "pdf"})).unwrap())).0, 0);
}
#[test]
fn documents_that_place_each_other_dont_loop() {
let dir = Folder::new("placed-loop");
let (a, b) = (dir.file("a.vectorcraft"), dir.file("b.vectorcraft"));
source(&a, 100.0, 50.0, RED, 0);
// B places A; A then places B.
let mut s = session();
place(&mut s, &a);
save(&mut s, &b);
s.execute("document.open", &json!({"path": a})).unwrap();
place(&mut s, &b);
save(&mut s, &a);
s.execute("document.open", &json!({"path": b})).unwrap();
let u = s.execute("links.update", &json!({})).unwrap();
assert_eq!(u["updated"].as_array().map(Vec::len), Some(1), "{u}");
// Drawn and output a few levels deep, then their previews.
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
for format in ["svg", "pdf", "png"] {
s.execute("document.serialize", &json!({"format": format})).unwrap();
}
}
#[test]
fn what_cant_be_placed_linked_is_an_error() {
let dir = Folder::new("placed-errors");
let src = dir.file("logo.vectorcraft");
source(&src, 100.0, 50.0, RED, 0);
let mut s = session();
let place = |s: &mut Session, p: Value| s.execute("file.place", &p).unwrap_err().to_string();
let e = place(&mut s, json!({"path": src, "page": 2}));
assert!(e.contains("page 2") && e.contains("1 artboard"), "{e}");
let e = place(&mut s, json!({"path": src, "crop": "art"}));
assert!(e.contains("crop"), "{e}");
let empty = dir.file("empty.vectorcraft");
write(&empty, &vectorcraft_format::save_file(&Document::new(100.0, 50.0)));
let e = place(&mut s, json!({"path": empty, "crop": "bounding"}));
assert!(e.contains("no art to place"), "{e}");
// Junk params are errors, not crashes.
for v in [json!(null), json!(true), json!(-1), json!(1e308), json!("x"), json!([1, 2]), json!({"a": {"b": []}})] {
for k in ["link", "page", "crop", "at", "rect"] {
let _ = s.execute("file.place", &json!({"path": src, k: v}));
}
}
}
#[test]
fn a_placed_documents_resources_stay_its_own() {
let dir = Folder::new("placed-symbols");
let src = dir.file("badge.vectorcraft");
star_source(&src, "star");
let mut s = session();
parent_star(&mut s);
place(&mut s, &src);
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE), "the file's own Star");
let doc = &s.doc().unwrap().doc;
assert_eq!(doc.symbols.len(), 1, "nothing joins the document");
let prefix = vectorcraft_doc::placed_document::RESOURCE_PREFIX;
assert!(doc.images.keys().all(|k| !k.starts_with(prefix)));
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
assert!(!svg["text"].as_str().unwrap().contains("<image"));
// Nor its saves.
let saved = String::from_utf8(decode(&s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap())).unwrap();
assert!(!saved.contains(prefix), "{saved}");
// Saved for older apps: the art and its resources, as plain objects.
let old = vectorcraft_format::load(&decode(&s.execute("document.serialize", &json!({"format": "vectorcraft", "version": 2})).unwrap())).unwrap();
assert!(!old.has_placed());
assert!(near(centre_colour(&old), BLUE));
}
#[test]
fn break_link_gives_the_editable_copy_placing_without_link_gives() {
let dir = Folder::new("placed-break");
let src = dir.file("badge.vectorcraft");
star_source(&src, "break");
let mut s = session();
parent_star(&mut s);
// The copy placing without Link gives.
let r = s.execute("file.place", &json!({"path": src, "link": false, "at": [200, 150]})).unwrap();
let copy = s.doc().unwrap().doc.node(NodeId(r["ids"][0].as_u64().unwrap())).unwrap().clone();
s.execute("edit.undo", &json!({})).unwrap();
let id = place(&mut s, &src);
let r = s.execute("links.embed", &json!({"ids": [id.0]})).unwrap();
assert_eq!(r["embedded"], json!([id.0]), "{r}");
let doc = &s.doc().unwrap().doc;
let n = doc.node(id).unwrap();
assert!(!doc.has_placed() && n.kind_label() == "Clip Group", "{}", n.kind_label());
assert_eq!((n.name.as_deref(), n.geometric_bounds()), (copy.name.as_deref(), copy.geometric_bounds()));
let kinds = |n: &Node| {
let mut k = vec![];
n.walk(&mut |c| k.push(c.kind_label()));
k
};
assert_eq!(kinds(n), kinds(&copy));
assert!(near(centre_colour(doc), BLUE), "the art as it showed");
let names: Vec<&str> = 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", &params).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, &params).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("<image"), missing, "{format}/{command}: {text}");
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();
let opened = restored
.execute("document.open", &json!({"name": format!("editable.{format}"), "dataBase64": vectorcraft_format::base64_encode(&bytes)}))
.unwrap();
assert_eq!(opened["restored"], true, "{opened}");
eprintln!("SVG_EDITING_RESTORED_READY missing={missing} format={format} command={command}");
assert_eq!(placed(&restored, id), original, "{format}/{command}: link or transform lost");
assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing));
assert_eq!(s.execute("document.json", &json!({})).unwrap(), before, "source unchanged");
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]));
assert_eq!(placed(&restored, id).link.path, replacement);
}
}
let out = s.execute("document.serialize", &json!({"format": format, "preserveEditing": false})).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());
assert_eq!(vectorcraft_svg::text_of(&bytes).unwrap().contains("<image"), missing);
let mut reopened = Session::new();
assert_eq!(
reopened
.execute("document.open", &json!({"name": format!("plain.{format}"), "dataBase64": vectorcraft_format::base64_encode(&bytes)}))
.unwrap()["restored"],
false
);
}
// Dot and case aliases must reach the same original/prepared document boundary.
for alias in ["SVG", ".svg", "SVGZ"] {
let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"preserveEditing": true})).unwrap();
let format = cmd::fileio::format(alias).unwrap().id;
let mut restored = Session::new();
restored
.execute(
"document.open",
&json!({"name": format!("alias.{format}"), "dataBase64": vectorcraft_format::base64_encode(&enc.files[0].1)}),
)
.unwrap();
assert_eq!(placed(&restored, id), original, "{alias}");
}
}
}