mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
When the desktop app opened a document on a thread of its own, that thread parsed it, but open_read, which the app ran on the UI thread, read the document's linked files, decoding BMP, PSD and RGB TIFF files and storing them again as PNG, and laid out the type. In release builds, a document with 12 linked 3000 x 2000 TIFFs kept the UI thread busy for 1.04 s, and one with 300 Auto Size area type objects, every second one in three columns, kept it busy for 0.85 s. read_for_open now reads the linked files and lays out the type on the opening thread, with the path and the Update Links preference that opening::start passes it, and open_read only adds the finished document. open_read now takes 0.02 ms and 0.1 ms on those documents. For the measured documents, the opened documents, results and undo states are the same as on main. Docs: docs/target-app-parity.md.
449 lines
21 KiB
Rust
449 lines
21 KiB
Rust
//! Linked images: what a link records, the preview saved in place of the pixels, finding files
|
||
//! that moved, missing and modified files (`document.open`, `links.check`, `links.update`,
|
||
//! `links.relink`).
|
||
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_doc::{Document, ImageObject, LinkInfo, NodeKind};
|
||
use vectorcraft_geom::Rect;
|
||
|
||
use super::*;
|
||
|
||
/// A fresh folder for one test (removed when dropped).
|
||
pub(crate) struct Folder(pub(crate) PathBuf);
|
||
|
||
impl Folder {
|
||
pub(crate) fn new(name: &str) -> Self {
|
||
let dir = std::env::temp_dir().join(format!("vectorcraft-links-{name}-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
Self(dir)
|
||
}
|
||
pub(crate) fn file(&self, name: &str) -> String {
|
||
self.0.join(name).to_string_lossy().into_owned()
|
||
}
|
||
}
|
||
|
||
impl Drop for Folder {
|
||
fn drop(&mut self) {
|
||
let _ = std::fs::remove_dir_all(&self.0);
|
||
}
|
||
}
|
||
|
||
/// A `w`×`h` PNG of one colour.
|
||
pub(crate) fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
|
||
let mut out = vec![];
|
||
let [r, g, b] = rgb;
|
||
image::RgbaImage::from_pixel(w, h, image::Rgba([r, g, b, 255])).write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
|
||
out
|
||
}
|
||
|
||
pub(crate) const RED: [u8; 3] = [230, 20, 20];
|
||
pub(crate) const BLUE: [u8; 3] = [20, 20, 230];
|
||
|
||
pub(crate) fn write(path: &str, bytes: &[u8]) {
|
||
if let Some(dir) = Path::new(path).parent() {
|
||
std::fs::create_dir_all(dir).unwrap();
|
||
}
|
||
std::fs::write(path, bytes).unwrap();
|
||
}
|
||
|
||
pub(crate) fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||
s
|
||
}
|
||
|
||
/// Place the file at `path` (linked) → its id.
|
||
pub(crate) 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())
|
||
}
|
||
|
||
pub(crate) fn image(s: &Session, id: NodeId) -> ImageObject {
|
||
match &s.doc().unwrap().doc.node(id).unwrap().kind {
|
||
NodeKind::Image(im) => (**im).clone(),
|
||
k => panic!("not an image: {k:?}"),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn bounds(s: &Session, id: NodeId) -> Rect {
|
||
s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap()
|
||
}
|
||
|
||
/// The colour at the centre of the active document's artboard.
|
||
pub(crate) fn centre_colour(doc: &Document) -> [u8; 3] {
|
||
let img = vectorcraft_render::Renderer::new().render_region(doc, Rect::new(0.0, 0.0, 400.0, 300.0), 1.0, true);
|
||
let [r, g, b, _] = img.pixel(200, 150);
|
||
[r, g, b]
|
||
}
|
||
|
||
pub(crate) fn near(a: [u8; 3], b: [u8; 3]) -> bool {
|
||
a.iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 6)
|
||
}
|
||
|
||
/// The first image object's JSON in a saved document.
|
||
fn saved_image(v: &mut Value) -> &mut Value {
|
||
&mut v["document"]["layers"][0]["kind"]["children"][0]["kind"]
|
||
}
|
||
|
||
pub(crate) fn save(s: &mut Session, path: &str) {
|
||
s.execute("document.save", &json!({"path": path})).unwrap();
|
||
}
|
||
|
||
pub(crate) fn open(s: &mut Session, path: &str) -> Value {
|
||
s.execute("document.open", &json!({"path": path})).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn a_placed_link_records_the_file_and_the_document_saves_only_a_preview() {
|
||
let dir = Folder::new("record");
|
||
let pic = dir.file("art/photo.png");
|
||
let bytes = png(600, 300, RED);
|
||
write(&pic, &bytes);
|
||
let mut s = session();
|
||
let id = place(&mut s, &pic);
|
||
let im = image(&s, id);
|
||
let link = im.link.clone().unwrap();
|
||
assert_eq!((link.path.as_str(), link.size), (pic.as_str(), Some(bytes.len() as u64)));
|
||
assert_eq!(link.hash.as_deref(), Some(vectorcraft_doc::links::hash_bytes(&bytes).as_str()));
|
||
assert!(link.modified.is_some_and(|m| m > 1_600_000_000_000), "{link:?}");
|
||
let blob = &s.doc().unwrap().doc.images[&im.key];
|
||
assert!(blob.proxy.is_some() && !blob.is_proxy(), "full pixels, with a preview to save");
|
||
let doc_path = dir.file("poster.vectorcraft");
|
||
save(&mut s, &doc_path);
|
||
let mut saved: Value = serde_json::from_slice(&std::fs::read(&doc_path).unwrap()).unwrap();
|
||
let entry = &saved["images"][&im.key];
|
||
assert_eq!(entry["proxy"], true);
|
||
assert!(entry["data"].as_str().unwrap().len() * 3 / 4 < bytes.len(), "the preview is smaller than the file");
|
||
let saved_link = saved_image(&mut saved)["link"].clone();
|
||
assert_eq!((saved_link["path"].as_str(), saved_link["relative"].as_str()), (Some(pic.as_str()), Some("art/photo.png")));
|
||
// Embedding keeps the pixels, and no preview is saved for them.
|
||
s.execute("file.place", &json!({"path": pic, "link": false})).unwrap();
|
||
save(&mut s, &doc_path);
|
||
let saved: Value = serde_json::from_slice(&std::fs::read(&doc_path).unwrap()).unwrap();
|
||
assert_eq!(saved["images"][&im.key]["proxy"], Value::Null, "an embedded image shows it too");
|
||
}
|
||
|
||
/// A flat `w`×`h` Photoshop document of one colour (raw RGB, no layers).
|
||
fn psd(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
|
||
let mut f = b"8BPS\0\x01\0\0\0\0\0\0\0\x03".to_vec();
|
||
f.extend(h.to_be_bytes());
|
||
f.extend(w.to_be_bytes());
|
||
f.extend([0, 8, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||
for v in rgb {
|
||
f.extend(std::iter::repeat_n(v, (w * h) as usize));
|
||
}
|
||
f
|
||
}
|
||
|
||
/// #918: a Photoshop document placed linked shows its merged image, and updates when it changes.
|
||
#[test]
|
||
fn a_linked_photoshop_document_places_and_updates() {
|
||
let dir = Folder::new("psd");
|
||
let pic = dir.file("art/photo.psd");
|
||
write(&pic, &psd(600, 300, RED));
|
||
let mut s = session();
|
||
let id = place(&mut s, &pic);
|
||
let link = image(&s, id).link.unwrap();
|
||
assert_eq!(link.path, pic);
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
|
||
write(&pic, &psd(400, 400, BLUE));
|
||
assert_eq!(s.execute("links.update", &json!({})).unwrap(), json!({"updated": [id.0], "missing": []}));
|
||
let im = image(&s, id);
|
||
assert_eq!((im.width, im.height), (400, 400));
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
|
||
}
|
||
|
||
#[test]
|
||
fn the_relative_path_finds_the_file_after_the_folder_moves() {
|
||
let a = Folder::new("move-a");
|
||
let b = Folder::new("move-b");
|
||
let pic = a.file("art/photo.png");
|
||
write(&pic, &png(600, 300, RED));
|
||
let mut s = session();
|
||
let id = place(&mut s, &pic);
|
||
save(&mut s, &a.file("poster.vectorcraft"));
|
||
// Move the document with its links: the old folder is gone.
|
||
std::fs::remove_dir_all(&b.0).unwrap();
|
||
std::fs::rename(&a.0, &b.0).unwrap();
|
||
let r = open(&mut s, &b.file("poster.vectorcraft"));
|
||
assert_eq!((r["missingLinks"].clone(), r["modifiedLinks"].clone()), (json!([]), json!([])), "{r}");
|
||
let moved = b.file("art/photo.png");
|
||
let im = image(&s, id);
|
||
assert_eq!(Path::new(&im.link.unwrap().path), Path::new(&moved), "the link follows the file");
|
||
assert!(!s.doc().unwrap().doc.images[&im.key].is_proxy(), "the file's pixels were read");
|
||
let c = s.execute("links.check", &json!({})).unwrap();
|
||
assert_eq!((c["links"][0]["status"].as_str(), c["missing"].as_u64()), (Some("ok"), Some(0)), "{c}");
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
|
||
}
|
||
|
||
#[test]
|
||
fn a_missing_link_renders_its_preview_and_relinks() {
|
||
let dir = Folder::new("missing");
|
||
let pic = dir.file("photo.png");
|
||
write(&pic, &png(600, 300, RED));
|
||
let mut s = session();
|
||
let id = place(&mut s, &pic);
|
||
let placed = bounds(&s, id);
|
||
let doc_path = dir.file("poster.vectorcraft");
|
||
save(&mut s, &doc_path);
|
||
std::fs::remove_file(&pic).unwrap();
|
||
let r = open(&mut s, &doc_path);
|
||
assert_eq!(r["missingLinks"], json!([{"name": "photo.png", "path": pic, "ids": [id.0]}]));
|
||
let doc = &s.doc().unwrap().doc;
|
||
assert!(doc.images[&image(&s, id).key].is_proxy(), "the preview stands in");
|
||
assert!(near(centre_colour(doc), RED), "missing still renders");
|
||
let c = s.execute("links.check", &json!({})).unwrap();
|
||
assert_eq!((c["links"][0]["status"].as_str(), c["links"][0]["preview"].as_bool(), c["missing"].as_u64()), (Some("missing"), Some(true), Some(1)));
|
||
assert_eq!(s.execute("links.update", &json!({})).unwrap(), json!({"updated": [], "missing": [id.0]}));
|
||
// Replace: another file, smaller; the image keeps its bounds.
|
||
let other = dir.file("other/blue.png");
|
||
write(&other, &png(300, 100, BLUE));
|
||
let r = s.execute("links.relink", &json!({"ids": [id.0], "path": other})).unwrap();
|
||
assert_eq!(r, json!({"relinked": [id.0], "notFound": []}));
|
||
let im = image(&s, id);
|
||
assert_eq!((im.width, im.height, im.link.unwrap().path), (300, 100, other.clone()));
|
||
let b = bounds(&s, id);
|
||
assert!((b.x0 - placed.x0).abs() < 1e-6 && (b.y1 - placed.y1).abs() < 1e-6, "{b:?} vs {placed:?}");
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED), "one undo step");
|
||
// Relink to Folder: the file of the link's name there.
|
||
write(&dir.file("found/photo.png"), &png(600, 300, BLUE));
|
||
let r = s.execute("links.relink", &json!({"folder": dir.file("found")})).unwrap();
|
||
assert_eq!(r, json!({"relinked": [id.0], "notFound": []}), "nothing selected: every missing link");
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
|
||
assert!(s.execute("links.relink", &json!({"ids": [id.0]})).is_err(), "path or folder");
|
||
}
|
||
|
||
#[test]
|
||
fn a_modified_link_follows_the_update_links_preference() {
|
||
let dir = Folder::new("modified");
|
||
let pic = dir.file("photo.png");
|
||
write(&pic, &png(600, 300, RED));
|
||
let mut s = session();
|
||
let id = place(&mut s, &pic);
|
||
let placed = bounds(&s, id);
|
||
let doc_path = dir.file("poster.vectorcraft");
|
||
save(&mut s, &doc_path);
|
||
write(&pic, &png(400, 400, BLUE));
|
||
let c = s.execute("links.check", &json!({})).unwrap();
|
||
assert_eq!((c["links"][0]["status"].as_str(), c["modified"].as_u64()), (Some("modified"), Some(1)), "{c}");
|
||
|
||
// Ask When Modified (the default): reported, left as it was.
|
||
let r = open(&mut s, &doc_path);
|
||
assert_eq!((r["modifiedLinks"][0]["ids"].clone(), r["updatedLinks"].clone()), (json!([id.0]), json!([])), "{r}");
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED), "the preview until updated");
|
||
let r = s.execute("links.update", &json!({})).unwrap();
|
||
assert_eq!(r, json!({"updated": [id.0], "missing": []}));
|
||
let im = image(&s, id);
|
||
assert_eq!((im.width, im.height), (400, 400));
|
||
let b = bounds(&s, id);
|
||
assert!((b.width() - placed.width()).abs() < 1e-6 && (b.height() - placed.height()).abs() < 1e-6, "keeps its bounds: {b:?}");
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
|
||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Update Links");
|
||
let c = s.execute("links.check", &json!({})).unwrap();
|
||
assert_eq!(c["links"][0]["status"], "ok");
|
||
assert_eq!(s.execute("links.update", &json!({})).unwrap()["updated"], json!([]), "nothing left to update");
|
||
|
||
// 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["modifiedLinks"].clone(), r["updatedLinks"][0]["ids"].clone()), (json!([]), json!([id.0])), "{r}");
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), BLUE));
|
||
assert!(s.doc().unwrap().history.undo.is_empty(), "opening is not an undo step");
|
||
}
|
||
|
||
/// The half of opening that the desktop app runs on a thread of its own (`read_for_open`) reads
|
||
/// the linked files and lays out the type. The half left for the UI thread (`open_read`) reads no
|
||
/// file: with a linked file moved away in between, it opens the document that `document.open` opens.
|
||
#[test]
|
||
fn a_file_read_for_opening_has_its_links_read_and_its_type_laid_out() {
|
||
let dir = Folder::new("read-for-open");
|
||
let tif = dir.file("art/photo.tif");
|
||
let mut tiff = vec![];
|
||
image::RgbImage::from_pixel(600, 300, image::Rgb(RED)).write_to(&mut std::io::Cursor::new(&mut tiff), image::ImageFormat::Tiff).unwrap();
|
||
write(&tif, &tiff);
|
||
let (gone, changed) = (dir.file("gone.png"), dir.file("changed.png"));
|
||
write(&gone, &png(300, 100, BLUE));
|
||
write(&changed, &png(300, 100, BLUE));
|
||
let mut s = session();
|
||
let photo = place(&mut s, &tif);
|
||
place(&mut s, &gone);
|
||
place(&mut s, &changed);
|
||
let area = json!({"width": 150, "height": 20, "fit": "autoHeight"});
|
||
let r = s.execute("text.create", &json!({"x": 20, "y": 40, "text": "Linked files and type. ".repeat(12), "area": area})).unwrap();
|
||
let text = NodeId(r["id"].as_u64().unwrap());
|
||
s.execute("text.areaOptions", &json!({"ids": [text.0], "columns": 2})).unwrap();
|
||
let doc_path = dir.file("poster.vectorcraft");
|
||
save(&mut s, &doc_path);
|
||
std::fs::remove_file(&gone).unwrap();
|
||
write(&changed, &png(200, 200, RED));
|
||
let bytes = std::fs::read(&doc_path).unwrap();
|
||
let away = dir.file("away.tif");
|
||
for update in ["askWhenModified", "automatically"] {
|
||
let mut at_once = session();
|
||
at_once.execute("prefs.set", &json!({"key": "updateLinks", "value": update})).unwrap();
|
||
let expected = open(&mut at_once, &doc_path);
|
||
let rows = if update == "automatically" { &expected["updatedLinks"] } else { &expected["modifiedLinks"] };
|
||
assert!(expected["missingLinks"][0]["name"] == "gone.png" && rows[0]["name"] == "changed.png", "{update}: {expected}");
|
||
let file = cmd::fileio::read_for_open(&doc_path, &bytes, Some(doc_path.clone()), &Value::Null, update == "automatically").unwrap();
|
||
let doc = file.document();
|
||
let NodeKind::Image(im) = &doc.node(photo).unwrap().kind else { panic!("not an image") };
|
||
assert!(!doc.images[&im.key].is_proxy(), "{update}: the TIFF is read, not the preview saved for it");
|
||
let NodeKind::Text(t) = &doc.node(text).unwrap().kind else { panic!("not type") };
|
||
assert!(t.cached_bounds.is_some(), "{update}: the type is laid out");
|
||
std::fs::rename(&tif, &away).unwrap();
|
||
let mut opened = session();
|
||
let r = cmd::fileio::open_read(&mut opened, file).unwrap();
|
||
std::fs::rename(&away, &tif).unwrap();
|
||
assert_eq!(r, expected, "{update}");
|
||
let (a, b) = (at_once.doc().unwrap(), opened.doc().unwrap());
|
||
assert!(*b.doc == *a.doc, "{update}: the same document");
|
||
assert!(b.history.undo.is_empty() && !b.is_dirty(), "{update}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn links_from_older_files_load_and_keep_their_pixels() {
|
||
let dir = Folder::new("legacy");
|
||
let pic = dir.file("photo.png");
|
||
let bytes = png(600, 300, RED);
|
||
write(&pic, &bytes);
|
||
let mut s = session();
|
||
s.execute("file.place", &json!({"path": pic, "at": [200, 150], "link": false})).unwrap();
|
||
// As older versions saved a link: the path alone, the pixels embedded.
|
||
let mut v: Value = serde_json::from_slice(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||
saved_image(&mut v)["link"] = json!(pic);
|
||
v["format"] = json!("drawcraft");
|
||
let legacy = serde_json::to_vec(&v).unwrap();
|
||
let doc_path = dir.file("old.drawcraft");
|
||
write(&doc_path, &legacy);
|
||
let r = open(&mut s, &doc_path);
|
||
assert_eq!((r["missingLinks"].clone(), r["modifiedLinks"].clone()), (json!([]), json!([])), "{r}");
|
||
let st = s.doc().unwrap();
|
||
let id = st.doc.layers[0].children().unwrap()[0].id;
|
||
let im = image(&s, id);
|
||
let link = im.link.unwrap();
|
||
assert_eq!((link.path.as_str(), link.size), (pic.as_str(), Some(bytes.len() as u64)), "the details are filled in");
|
||
assert!(s.doc().unwrap().doc.images[&im.key].proxy.is_some(), "a preview to save from now on");
|
||
// Gone, the embedded pixels still show.
|
||
std::fs::remove_file(&pic).unwrap();
|
||
let r = open(&mut s, &doc_path);
|
||
assert_eq!(r["missingLinks"][0]["name"], "photo.png");
|
||
assert!(!s.doc().unwrap().doc.images[&im.key].is_proxy());
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
|
||
}
|
||
|
||
#[test]
|
||
fn without_a_file_system_the_saved_preview_shows() {
|
||
// What the web does: the document arrives as bytes and no linked file can be read.
|
||
let dir = Folder::new("web");
|
||
let pic = dir.file("photo.png");
|
||
write(&pic, &png(600, 300, RED));
|
||
let mut s = session();
|
||
let id = place(&mut s, &pic);
|
||
let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false);
|
||
std::fs::remove_file(&pic).unwrap();
|
||
let doc = vectorcraft_format::load(&saved).unwrap();
|
||
assert!(doc.images.values().all(|b| b.is_proxy()));
|
||
assert!(near(centre_colour(&doc), RED), "the preview renders");
|
||
let r = s.execute("document.open", &json!({"name": "poster.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&saved)})).unwrap();
|
||
assert_eq!(r["missingLinks"][0]["ids"], json!([id.0]));
|
||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
|
||
// Exports carry the preview.
|
||
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
|
||
assert!(svg["text"].as_str().unwrap().contains("data:image/png;base64,"));
|
||
}
|
||
|
||
#[test]
|
||
fn relative_paths_are_written_against_the_saved_file() {
|
||
let mut d = Document::new(10.0, 10.0);
|
||
let (lid, nid) = (d.layers[0].id, d.alloc_id());
|
||
let root = if cfg!(windows) { "C:\\work" } else { "/work" };
|
||
let sep = std::path::MAIN_SEPARATOR;
|
||
let im = ImageObject {
|
||
key: "k".into(),
|
||
width: 1,
|
||
height: 1,
|
||
xf: Default::default(),
|
||
link: Some(LinkInfo::new(format!("{root}{sep}art{sep}a.png"))),
|
||
placement: Default::default(),
|
||
};
|
||
d.insert(Some(lid), 0, vectorcraft_doc::Node::new(nid, NodeKind::Image(Box::new(im)))).unwrap();
|
||
let rel = |dest: String| {
|
||
let d = cmd::links::with_relative_paths(&d, &dest).unwrap_or_else(|| d.clone());
|
||
match &d.node(nid).unwrap().kind {
|
||
NodeKind::Image(im) => im.link.as_ref().unwrap().relative.clone(),
|
||
_ => None,
|
||
}
|
||
};
|
||
assert_eq!(rel(format!("{root}{sep}doc.vectorcraft")).as_deref(), Some("art/a.png"));
|
||
assert_eq!(rel(format!("{root}{sep}docs{sep}deep{sep}doc.vectorcraft")).as_deref(), Some("../../art/a.png"));
|
||
assert_eq!(rel(format!("{root}{sep}art{sep}doc.vectorcraft")).as_deref(), Some("a.png"));
|
||
if cfg!(windows) {
|
||
assert_eq!(rel("D:\\other\\doc.vectorcraft".into()), None, "another drive");
|
||
assert_eq!(rel("c:\\WORK\\doc.vectorcraft".into()).as_deref(), Some("art/a.png"), "without case");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_placed_document_brings_its_links_with_the_files_pixels() {
|
||
let dir = Folder::new("place-doc");
|
||
let pic = dir.file("photo.png");
|
||
write(&pic, &png(600, 300, RED));
|
||
let mut s = session();
|
||
place(&mut s, &pic);
|
||
let doc_path = dir.file("part.vectorcraft");
|
||
save(&mut s, &doc_path);
|
||
let mut s = session();
|
||
// As an editable copy (placed linked, it stays one locked object).
|
||
s.execute("file.place", &json!({"path": doc_path, "link": false})).unwrap();
|
||
let doc = &s.doc().unwrap().doc;
|
||
let mut placed = vec![];
|
||
doc.visit_images(|_, im| placed.push(im.clone()));
|
||
let [im] = &placed[..] else { panic!("one image: {placed:?}") };
|
||
assert_eq!(im.link.as_ref().map(|l| l.path.as_str()), Some(pic.as_str()), "still linked");
|
||
assert!(!doc.images[&im.key].is_proxy(), "the file's pixels, not the preview");
|
||
}
|
||
|
||
/// The placed (linked) files of a `.ai` file come in with the preview the file keeps of them: one
|
||
/// not found shows it, one found is read at once into the same bounds, which isn't a change.
|
||
#[test]
|
||
fn links_of_an_imported_file_are_read_where_they_are_found() {
|
||
use vectorcraft_testkit::ai;
|
||
let dir = Folder::new("imported");
|
||
let pic = dir.file("photo.png").replace('\\', "/");
|
||
// A 2×1 preview (premultiplied ARGB: a blue at half opacity) of a file of 600 × 300 px placed at
|
||
// 60 × 30 pt, which the PDF part draws there too.
|
||
let place = format!(
|
||
"%AI5_BeginPlace\n[0.1 0 0 -0.1 20 80] 0 0 600 300 4 4 0\n({pic})`\n%AI26_BeginPlacedObjectPreview\n2 1 8 32 17 \x7f\x10\x10\x60\x7f\x10\x10\x60\n%AI26_EndPlacedObjectPreview\n~\n%AI5_EndPlace\n"
|
||
);
|
||
let doc_path = dir.file("poster.ai");
|
||
write(&doc_path, &ai::ai(ai::editing_data(200.0, 100.0, &ai::layer("Art", &place)).as_bytes(), "0.56 0.56 0.87 rg 20 50 60 30 re f"));
|
||
let placed = |s: &Session| {
|
||
let mut found = vec![];
|
||
s.doc().unwrap().doc.visit_images(|n, im| found.push((n.geometric_bounds().unwrap(), im.clone())));
|
||
let [one] = &found[..] else { panic!("one image: {found:?}") };
|
||
one.clone()
|
||
};
|
||
let in_place = |b: Rect| [b.x0 - 20.0, b.y0 - 20.0, b.x1 - 80.0, b.y1 - 50.0].iter().all(|d| d.abs() < 1e-6);
|
||
let mut s = session();
|
||
|
||
let r = open(&mut s, &doc_path);
|
||
assert_eq!((r["missingLinks"][0]["name"].as_str(), r["missingLinks"][0]["path"].as_str()), (Some("photo.png"), Some(pic.as_str())), "{r}");
|
||
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("low-resolution preview")), "{r}");
|
||
let (b, im) = placed(&s);
|
||
assert!(in_place(b) && im.width == 2 && s.doc().unwrap().doc.images[&im.key].is_proxy(), "{b:?} {im:?}");
|
||
|
||
write(&pic, &png(600, 300, RED));
|
||
let r = open(&mut s, &doc_path);
|
||
assert_eq!((&r["missingLinks"], &r["modifiedLinks"], &r["updatedLinks"]), (&json!([]), &json!([]), &json!([])), "{r}");
|
||
let (b, im) = placed(&s);
|
||
assert!(in_place(b) && (im.width, im.height) == (600, 300) && im.link.as_ref().is_some_and(|l| l.hash.is_some()), "{b:?} {im:?}");
|
||
assert!(!s.doc().unwrap().doc.images[&im.key].is_proxy());
|
||
}
|