mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
The Links panel lists every image (thumbnail, name, page, missing/modified/embedded badge) with Show filters, sorting and thumbnail sizes; its buttons and flyout relink (to a file or a folder), go to, update, edit the original, embed and unembed, show in folder and open Placement Options. Link Info shows format, colour space, location, ppi and effective ppi, dimensions, scale, rotation, size and dates. Edit > Edit Original and the Properties panel's image section reach the same commands. Engine: links.list, links.goTo, links.embed, links.unembed, links.info and links.placementOptions; links.relink and links.update now place the new file as the image's Placement Options say (transforms, bounds, file dimensions, fit, fill, the alignment point, clip to the old bounds). Model: ImageObject.placement. UI: links.editOriginal, links.reveal, ui.placementOptionsDialog; Services.open_file and pick_folder on the desktop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
3.1 KiB
Rust
64 lines
3.1 KiB
Rust
//! Linked images in `.vectorcraft` files: their link details round-trip, and an image only linked
|
||
//! images show is saved as its preview.
|
||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||
|
||
use std::sync::Arc;
|
||
|
||
use serde_json::Value;
|
||
use vectorcraft_doc::{Document, ImageBlob, ImageObject, LinkInfo, Node, NodeKind};
|
||
use vectorcraft_format::{base64_decode, load, save};
|
||
use vectorcraft_geom::Affine;
|
||
|
||
fn image(d: &mut Document, key: &str, link: Option<LinkInfo>) {
|
||
let (id, layer) = (d.alloc_id(), d.layers[0].id);
|
||
let im = ImageObject { key: key.into(), width: 600, height: 300, xf: Affine::IDENTITY, link, placement: Default::default() };
|
||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(im))).unwrap();
|
||
}
|
||
|
||
fn blob(full: &[u8], proxy: Option<&[u8]>) -> ImageBlob {
|
||
ImageBlob { proxy: proxy.map(|p| Arc::new(p.to_vec())), ..ImageBlob::new("image/jpeg", full.to_vec()) }
|
||
}
|
||
|
||
#[test]
|
||
fn link_details_round_trip_and_linked_only_images_save_their_preview() {
|
||
let mut d = Document::new(100.0, 100.0);
|
||
let link = LinkInfo {
|
||
relative: Some("art/photo.jpg".into()),
|
||
modified: Some(1_700_000_000_000),
|
||
size: Some(4),
|
||
hash: Some("img0123456789abcdef".into()),
|
||
..LinkInfo::new("/work/art/photo.jpg")
|
||
};
|
||
d.images.insert("linked".into(), blob(&[1, 2, 3, 4], Some(&[9, 9])));
|
||
d.images.insert("both".into(), blob(&[5, 6, 7], Some(&[8])));
|
||
d.images.insert("small".into(), blob(&[4, 4], None));
|
||
image(&mut d, "linked", Some(link.clone()));
|
||
image(&mut d, "both", Some(LinkInfo::new("/work/b.jpg")));
|
||
image(&mut d, "both", None);
|
||
image(&mut d, "small", Some(LinkInfo::new("/work/s.jpg")));
|
||
let bytes = save(&d, false);
|
||
let v: Value = serde_json::from_slice(&bytes).unwrap();
|
||
let saved = |k: &str| {
|
||
(
|
||
v["images"][k]["mime"].as_str().unwrap().to_string(),
|
||
base64_decode(v["images"][k]["data"].as_str().unwrap()).unwrap(),
|
||
v["images"][k]["proxy"].clone(),
|
||
)
|
||
};
|
||
assert_eq!(saved("linked"), ("image/png".into(), vec![9, 9], Value::Bool(true)), "only linked images show it: the preview");
|
||
assert_eq!(saved("both"), ("image/jpeg".into(), vec![5, 6, 7], Value::Null), "an embedded image shows it too: the full bytes");
|
||
assert_eq!(saved("small"), ("image/jpeg".into(), vec![4, 4], Value::Null), "no preview: the full bytes");
|
||
|
||
let back = load(&bytes).unwrap();
|
||
let linked = &back.images["linked"];
|
||
assert_eq!((linked.bytes.as_slice(), linked.is_proxy()), (&[9u8, 9][..], true), "the preview stands in until the file is read");
|
||
assert!(!back.images["both"].is_proxy() && back.images["both"].proxy.is_none());
|
||
let mut links = vec![];
|
||
back.visit_images(|_, im| links.push(im.link.clone()));
|
||
assert!(links.contains(&Some(link)), "{links:?}");
|
||
// Saved again unread, the preview stays the preview.
|
||
let again: Value = serde_json::from_slice(&save(&back, false)).unwrap();
|
||
assert_eq!(again["images"]["linked"], v["images"]["linked"]);
|
||
}
|