//! Placed documents: another VectorCraft file placed linked ([`PlacedDocument`]). //! //! The object keeps the link to the file and the file's bytes, stored like image data in //! [`Document::images`] (with a small preview a save writes instead, as for linked images), so the //! document shows correctly without the file. The art it shows is read from those bytes and never //! joins the object tree, so editing can't reach it: it is locked by design. Drawing and exporting //! expand it like a live object ([`crate::live::expanded`]). //! //! The file is read by the native format's loader, which lives above this crate and registers //! itself ([`set_loader`]); the art read is kept in a small cache by content. Before exporting, //! every exporter calls [`Document::with_placed_art`], which adds the resources the art uses //! (images, symbols, patterns) under names of their own ([`RESOURCE_PREFIX`]) to a temporary copy //! of the document, so they never meet the user's or show in panels. Placed documents inside a //! placed document are read the same way, up to [`MAX_DEPTH`] deep (documents that place each //! other stop there, showing their previews). use std::borrow::Cow; use std::collections::BTreeSet; use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError}; use serde::{Deserialize, Serialize}; use vectorcraft_geom::{Affine, Rect}; use vectorcraft_color::Paint; use crate::{AppearanceItem, Document, ImageBlob, ImageObject, LinkInfo, Node, NodeKind, PatternDef, PlacementOptions, Symbol}; /// A placed document: an artboard of another VectorCraft file, shown as one locked object. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct PlacedDocument { /// The file (its `page` is the artboard shown, 1-based). pub link: LinkInfo, /// Key into [`Document::images`]: the file's bytes (see [`key_for`]). pub key: String, /// The art's bounds rather than the artboard (File › Place's `crop: "bounding"`). #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub bounding: bool, /// The artboard's size (or the art's) at 100% (pt). pub width: f64, pub height: f64, /// Maps the box (0..width, 0..height) into the document. pub xf: Affine, /// Links panel → Placement Options: how the file read again takes this object's place. #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub placement: PlacementOptions, } /// The images key of a file's bytes (`hash`, [`crate::links::hash_bytes`]) placed showing /// artboard `page` (`bounding`: its art's bounds): objects showing the same thing share a blob and /// its preview. pub fn key_for(hash: &str, page: u32, bounding: bool) -> String { format!("{hash}-{page}{}", if bounding { "b" } else { "" }) } impl PlacedDocument { /// The artboard shown (1-based). pub fn page(&self) -> u32 { self.link.page.unwrap_or(1) } /// The box at 100%. pub fn natural(&self) -> Rect { Rect::new(0.0, 0.0, self.width, self.height) } /// The four corners of the box in the document. pub fn corners(&self) -> [vectorcraft_geom::Point; 4] { let r = self.natural(); [(r.x0, r.y0), (r.x1, r.y0), (r.x1, r.y1), (r.x0, r.y1)].map(|(x, y)| self.xf * vectorcraft_geom::Point::new(x, y)) } /// The box in the document (axis-aligned). pub fn bounds(&self) -> Rect { self.xf.transform_rect_bbox(self.natural()) } /// The transform from the file's coordinates (its artboard or art bounds `frame`) to the /// document; `None` when either size is empty. pub fn art_xf(&self, frame: Rect) -> Option { let ok = self.width > 0.0 && self.height > 0.0 && frame.width() > 0.0 && frame.height() > 0.0; ok.then(|| { self.xf * Affine::scale_non_uniform(self.width / frame.width(), self.height / frame.height()) * Affine::translate((-frame.x0, -frame.y0)) }) } /// The art the object shows, as plain objects with `n`'s id: one group, its resources named /// as [`Document::with_placed_art`] adds them (nothing until that has read it). pub fn art(&self, n: &Node) -> Vec { let prefix = prefix(&self.key); read_cached(self).and_then(|e| self.art_node(n, &e, &prefix)).into_iter().collect() } /// Read `e`'s art placed as this object `n` shows it (`n`'s id), its resources renamed with /// `prefix`. fn art_node(&self, n: &Node, e: &ReadDocument, prefix: &str) -> Option { let m = self.art_xf(e.frame)?; let mut g = (*e.art).clone(); rename(&mut g, prefix); g.transform(m, true); each_mut(&mut g, &mut |c| crate::pattern::transform_pattern_paints(c, m)); g.id = n.id; Some(g) } /// An image object showing preview `bytes` (stored under this object's key) in this object's /// box: what it shows and outputs as when the file can't be read. pub fn preview_image(&self, bytes: &[u8]) -> Option { let (w, h) = image_size(bytes).filter(|(w, h)| *w > 0 && *h > 0)?; let px = Affine::scale_non_uniform(self.width / w as f64, self.height / h as f64); Some(ImageObject { key: self.key.clone(), width: w, height: h, xf: self.xf * px, link: None, placement: Default::default() }) } } /// The names the resources of the art stored under `key` take in a prepared document. fn prefix(key: &str) -> String { format!("{RESOURCE_PREFIX}{key}/") } /// `n` and every descendant, opacity-mask art included. fn walk_all<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) { f(n); if let Some(m) = &n.extras().mask { walk_all(&m.art, f); } for c in n.children().into_iter().flatten() { walk_all(c, f); } } /// The roots of `d`'s art: layers, symbol definitions and pattern swatches. fn roots(d: &Document) -> impl Iterator> { d.layers .iter() .chain(d.symbols.iter().map(|s| &s.art)) .chain(d.graph_designs.iter().map(|g| &g.art)) .chain(d.patterns.iter().flat_map(|p| &p.art)) } fn roots_mut(d: &mut Document) -> impl Iterator> { d.layers .iter_mut() .chain(d.symbols.iter_mut().map(|s| &mut s.art)) .chain(d.graph_designs.iter_mut().map(|g| &mut g.art)) .chain(d.patterns.iter_mut().flat_map(|p| &mut p.art)) } impl Document { /// Visit every placed document: in the layers, then in symbol definitions and pattern /// swatches, opacity-mask art included. pub fn visit_placed<'a>(&'a self, mut f: impl FnMut(&'a Node, &'a PlacedDocument)) { for root in roots(self) { walk_all(root, &mut |n| { if let NodeKind::PlacedDocument(p) = &n.kind { f(n, p); } }); } } /// Does the document hold a placed document anywhere? pub fn has_placed(&self) -> bool { let mut any = false; self.visit_placed(|_, _| any = true); any } /// Every placed document (those in the art they show too) replaced by a plain group of that /// art, for apps that don't know them: call it on [`Self::with_placed_art`]'s copy. Only the /// subtrees holding one are copied. pub fn placed_as_groups(&mut self) { fn holds(n: &Node) -> bool { let mut hit = false; walk_all(n, &mut |c| hit |= matches!(c.kind, NodeKind::PlacedDocument(_))); hit } fn update(n: &mut Arc) { if !holds(n) { return; } if matches!(n.kind, NodeKind::PlacedDocument(_)) { let mut g = crate::live::expanded_group(n, None); g.appearance = n.appearance.clone(); *n = Arc::new(g); } // The art shown may hold placed documents too. let n = Arc::make_mut(n); if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) { update(&mut m.art); } for c in n.children_mut().into_iter().flatten() { update(c); } } for root in roots_mut(self) { update(root); } } /// Placed documents whose key is in `keys` replaced by images of their previews (for output /// when their files can't be read). pub fn placed_as_previews(&mut self, keys: &BTreeSet) { fn update(n: &mut Arc, keys: &BTreeSet, images: &std::collections::BTreeMap) { let mut hit = false; walk_all(n, &mut |c| hit |= matches!(&c.kind, NodeKind::PlacedDocument(p) if keys.contains(&p.key))); if !hit { return; } let n = Arc::make_mut(n); if let NodeKind::PlacedDocument(p) = &n.kind && keys.contains(&p.key) { if let Some(im) = images.get(&p.key).and_then(|b| p.preview_image(&b.bytes)) { n.kind = NodeKind::Image(Box::new(im)); } return; } if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) { update(&mut m.art, keys, images); } for c in n.children_mut().into_iter().flatten() { update(c, keys, images); } } let images = self.images.clone(); for root in roots_mut(self) { update(root, keys, &images); } } } // ---------- previews and the files' bytes ---------- /// A placed document's preview: this many pixels per point of its box… pub const PREVIEW_PX_PER_PT: f64 = 1.0; /// …and at most this many on its longer side. pub const PREVIEW_MAX: u32 = 1024; /// How deep placed documents inside placed documents are read; deeper ones show their previews. pub const MAX_DEPTH: usize = 8; /// Reads `p`'s file again (`None`: not found, or changed since it was read). The engine registers /// one with [`set_file_reader`]. pub type FileReader = fn(&PlacedDocument) -> Option>; static FILE_READER: OnceLock = OnceLock::new(); /// Register how files are read again (the first registration stays). pub fn set_file_reader(f: FileReader) { // Already set: the same engine, registered by another session. let _ = FILE_READER.set(f); } /// The most bytes of files read again kept. const FULL_BYTES: usize = 512 << 20; /// Files read again, by key, the most recently read last. type FullFiles = Vec<(String, Arc>)>; static FULL: Mutex = Mutex::new(Vec::new()); fn full() -> MutexGuard<'static, FullFiles> { // Entries are only pushed and removed: a panic while held leaves it whole. FULL.lock().unwrap_or_else(PoisonError::into_inner) } /// The file's bytes when they are at hand: stored (`blob` isn't just the preview) or read again /// already. Never reads it. pub fn full_bytes_cached(p: &PlacedDocument, blob: &ImageBlob) -> Option>> { if !blob.is_proxy() { return Some(blob.bytes.clone()); } full().iter().rev().find(|(k, _)| *k == p.key).map(|(_, b)| b.clone()) } /// The file's bytes: stored, read again already, or read again now ([`set_file_reader`]). `None` /// when only the preview can be had. pub fn full_bytes(p: &PlacedDocument, blob: &ImageBlob) -> Option>> { if let Some(b) = full_bytes_cached(p, blob) { return Some(b); } let b = Arc::new(FILE_READER.get()?(p)?); let mut c = full(); c.push((p.key.clone(), b.clone())); let mut total: usize = c.iter().map(|(_, b)| b.len()).sum(); while total > FULL_BYTES && c.len() > 1 { total = total.saturating_sub(c.remove(0).1.len()); } Some(b) } /// The size in pixels of image bytes `bytes` (a preview), read from its header. pub fn image_size(bytes: &[u8]) -> Option<(u32, u32)> { image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?.into_dimensions().ok() } // ---------- reading files ---------- /// The prefix of the names the resources of placed documents' art take in a prepared document: /// `placed:/`. pub const RESOURCE_PREFIX: &str = "placed:"; /// Reads a placed document's file (its bytes) into what it shows: a document holding the /// resources (no layers), the art as one group in the file's coordinates, and the frame shown /// (the artboard, or the art's bounds). The native format lives above this crate, so the engine /// registers one with [`set_loader`]. pub type Loader = fn(&PlacedDocument, &[u8]) -> Option<(Document, Node, Rect)>; static LOADER: OnceLock = OnceLock::new(); /// Register how files are read (the first registration stays). pub fn set_loader(l: Loader) { // Already set: the same loader, registered by another session. let _ = LOADER.set(l); } /// The most files kept read whatever their size. const CACHE_SIZE: usize = 64; /// Past [`CACHE_SIZE`], the most bytes of files kept read (their own sizes, standing for what /// reading them holds). const CACHE_BYTES: usize = 256 << 20; /// A placed document's file read: the art it shows, ready to draw with its own resources. pub struct ReadDocument { key: String, /// Read from the preview, not the file. preview: bool, /// The bytes read (what tells a cached read is still the one shown). bytes: Arc>, /// The artboard (or art bounds) shown: what the object's box shows. pub frame: Rect, /// The art as one group, in the file's coordinates. Its images, symbols and patterns are /// `doc`'s, under their own names. pub art: Arc, /// The file's resources and setup (no layers: the art is `art`). pub doc: Arc, } struct Entry { e: Arc, used: u64, } #[derive(Default)] struct Cache { entries: std::collections::HashMap<(String, bool), Entry>, clock: u64, } static CACHE: std::sync::LazyLock> = std::sync::LazyLock::new(Default::default); fn cache() -> MutexGuard<'static, Cache> { // A panic while the cache was held leaves it whole (entries are only inserted and removed). CACHE.lock().unwrap_or_else(PoisonError::into_inner) } impl Cache { /// The read of `key` (its preview's when `preview`; made from `bytes` when given), marked used. fn get(&mut self, key: &str, preview: bool, bytes: Option<&Arc>>) -> Option> { self.clock += 1; let clock = self.clock; let e = self.entries.get_mut(&(key.to_string(), preview))?; if bytes.is_some_and(|b| !Arc::ptr_eq(&e.e.bytes, b)) { return None; } e.used = clock; Some(e.e.clone()) } fn insert(&mut self, e: Arc) { self.clock += 1; self.entries.insert((e.key.clone(), e.preview), Entry { e, used: self.clock }); // The least recently used go while over both limits. let mut total: usize = self.entries.values().map(|x| x.e.bytes.len()).sum(); while self.entries.len() > CACHE_SIZE && total > CACHE_BYTES { let Some(k) = self.entries.iter().min_by_key(|(_, x)| x.used).map(|(k, _)| k.clone()) else { break }; if let Some(x) = self.entries.remove(&k) { total = total.saturating_sub(x.e.bytes.len()); } } } } /// `p`'s file (`bytes`) read, from the cache when it was read already: what the renderer draws a /// placed document from. `None` when it can't be read (or no loader is registered). pub fn read(p: &PlacedDocument, bytes: &Arc>) -> Option> { if let Some(e) = cache().get(&p.key, false, Some(bytes)) { return Some(e); } let loader = LOADER.get()?; let (doc, art, frame) = loader(p, bytes)?; let e = Arc::new(ReadDocument { key: p.key.clone(), preview: false, bytes: bytes.clone(), frame, art: Arc::new(art), doc: Arc::new(doc) }); cache().insert(e.clone()); Some(e) } /// `p`'s preview (`bytes`) read as art: one image filling the box. fn read_preview(p: &PlacedDocument, bytes: &Arc>) -> Option> { if let Some(e) = cache().get(&p.key, true, Some(bytes)) { return Some(e); } let (w, h) = image_size(bytes).filter(|(w, h)| *w > 0 && *h > 0)?; let frame = p.natural(); let mut doc = Document::new(frame.width().max(1.0), frame.height().max(1.0)); doc.layers.clear(); doc.swatches.clear(); let mime = if bytes.starts_with(&[0xff, 0xd8]) { "image/jpeg" } else { "image/png" }; doc.images.insert("preview".into(), ImageBlob { mime: mime.into(), bytes: bytes.clone(), proxy: None }); let xf = Affine::scale_non_uniform(frame.width() / w as f64, frame.height() / h as f64); let im = ImageObject { key: "preview".into(), width: w, height: h, xf, link: None, placement: Default::default() }; let art = Node::new(art_id(), NodeKind::Group { children: vec![Arc::new(Node::new(art_id(), NodeKind::Image(Box::new(im))))], clip: false }); let e = Arc::new(ReadDocument { key: p.key.clone(), preview: true, bytes: bytes.clone(), frame, art: Arc::new(art), doc: Arc::new(doc) }); cache().insert(e.clone()); Some(e) } /// `p`'s file as read already (else its preview's), without reading it. pub fn read_cached(p: &PlacedDocument) -> Option> { let mut c = cache(); c.get(&p.key, false, None).or_else(|| c.get(&p.key, true, None)) } /// Keep `read` (what [`Loader`] gives), just read from `bytes` for `p` (when it was placed), so /// drawing it doesn't read it again. pub fn prime(p: &PlacedDocument, bytes: Arc>, (doc, art, frame): (Document, Node, Rect)) { cache().insert(Arc::new(ReadDocument { key: p.key.clone(), preview: false, bytes, frame, art: Arc::new(art), doc: Arc::new(doc) })); } /// The id the read art's own nodes take (their object's, once expanded). fn art_id() -> crate::NodeId { crate::NodeId(0) } /// Visit `n` and every descendant (opacity-mask art included) mutably. fn each_mut(n: &mut Node, f: &mut impl FnMut(&mut Node)) { f(n); if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) { each_mut(Arc::make_mut(&mut m.art), f); } for c in n.children_mut().into_iter().flatten() { each_mut(Arc::make_mut(c), f); } } /// `n`'s references to images, symbols, patterns and placed files prefixed with `prefix`. Colours /// keep their swatch links (a spot colour stays a plate, one with the parent's spot of that name). fn rename(n: &mut Node, prefix: &str) { let paint = |p: &mut Paint| { if let Paint::Pattern { pattern, .. } = p { *pattern = format!("{prefix}{pattern}"); } }; each_mut(n, &mut |n| { for it in &mut n.appearance.items { match it { AppearanceItem::Fill(f) => paint(&mut f.paint), AppearanceItem::Stroke(s) => paint(&mut s.paint), } } match &mut n.kind { NodeKind::Text(t) => { for r in &mut t.runs { paint(&mut r.style.fill); paint(&mut r.style.stroke); } } NodeKind::Image(im) => im.key = format!("{prefix}{}", im.key), NodeKind::PlacedDocument(p) => p.key = format!("{prefix}{}", p.key), NodeKind::SymbolInstance { symbol, .. } => *symbol = format!("{prefix}{symbol}"), _ => {} } }); } impl Document { /// This document ready to export its placed documents: their files read (the file read again /// when only the preview is stored, else the preview), and the resources their art uses added /// under names of their own ([`RESOURCE_PREFIX`]); the same for the placed documents in that /// art, up to [`MAX_DEPTH`] deep. Borrowed when there is none. (The renderer draws them from /// [`read`] instead.) pub fn with_placed_art(&self) -> Cow<'_, Document> { let mut queue: Vec<(PlacedDocument, usize)> = vec![]; let mut seen = BTreeSet::new(); self.visit_placed(|_, p| { if seen.insert(p.key.clone()) { queue.push((p.clone(), 0)); } }); if queue.is_empty() { return Cow::Borrowed(self); } let mut d = self.clone(); let mut found: Vec> = vec![]; while let Some((p, depth)) = queue.pop() { let Some(blob) = d.images.get(&p.key).cloned() else { continue }; let full = if depth < MAX_DEPTH { full_bytes(&p, &blob) } else { None }; if full.and_then(|b| read(&p, &b)).is_none() { let preview = if blob.is_proxy() { Some(blob.bytes.clone()) } else { blob.proxy.clone() }; if let Some(bytes) = preview { read_preview(&p, &bytes); } } // What the art (`PlacedDocument::art`) will show. let Some(e) = read_cached(&p) else { continue }; let prefix = prefix(&p.key); add_resources(&mut d, &e.doc, &prefix); // The placed documents in that art, under their names here. let inner = std::iter::once(&e.art) .chain(e.doc.symbols.iter().map(|s| &s.art)) .chain(e.doc.graph_designs.iter().map(|g| &g.art)) .chain(e.doc.patterns.iter().flat_map(|p| &p.art)); for root in inner { walk_all(root, &mut |n| { if let NodeKind::PlacedDocument(q) = &n.kind { let key = format!("{prefix}{}", q.key); if seen.insert(key.clone()) { queue.push((PlacedDocument { key, ..(**q).clone() }, depth + 1)); } } }); } found.push(e); } // Read last, so all stay read while the art is made (see `PlacedDocument::art`). { let mut c = cache(); for e in &found { c.get(&e.key, e.preview, None); } } Cow::Owned(d) } /// Drop the resources [`Self::with_placed_art`] added under its names (what a save leaves out). pub fn drop_placed_resources(&mut self) { self.symbols.retain(|s| !s.name.starts_with(RESOURCE_PREFIX)); self.patterns.retain(|p| !p.name.starts_with(RESOURCE_PREFIX)); self.images.retain(|k, _| !k.starts_with(RESOURCE_PREFIX)); } } /// The resources of `src` (a read file's) added to `d` under `prefix`: symbols, patterns, images; /// swatches by name, when `d` has none of that name. fn add_resources(d: &mut Document, src: &Document, prefix: &str) { for s in &src.symbols { let name = format!("{prefix}{}", s.name); if !d.symbols.iter().any(|x| x.name == name) { let mut s = s.clone(); rename(Arc::make_mut(&mut s.art), prefix); d.symbols.push(Symbol { name, ..s }); } } for p in &src.patterns { let name = format!("{prefix}{}", p.name); if d.pattern(&name).is_none() { let mut p = p.clone(); p.art.iter_mut().for_each(|a| rename(Arc::make_mut(a), prefix)); d.patterns.push(PatternDef { name, ..p }); } } for (k, b) in &src.images { d.images.entry(format!("{prefix}{k}")).or_insert_with(|| b.clone()); } for w in &src.swatches { if d.swatch(&w.name).is_none() { d.swatches.push(w.clone()); } } } #[cfg(test)] mod tests { use super::*; use crate::NodeId; fn placed(key: &str) -> PlacedDocument { PlacedDocument { link: LinkInfo::new("/art/logo.vectorcraft"), key: key.into(), bounding: false, width: 80.0, height: 40.0, xf: Affine::translate((10.0, 10.0)), placement: Default::default(), } } /// Whatever the bytes: a 40×20 artboard showing a symbol instance and, when the bytes say /// `nest`, a placed document of key `inner`. fn stub_loader(_: &PlacedDocument, bytes: &[u8]) -> Option<(Document, Node, Rect)> { let mut d = Document::new(40.0, 20.0); d.layers.clear(); let art = Node::new( NodeId(50), NodeKind::Image(Box::new(ImageObject { key: "pic".into(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default(), })), ); d.symbols.push(Symbol { name: "Star".into(), art: Arc::new(art) }); d.images.insert("pic".into(), ImageBlob::new("image/png", vec![1, 2, 3])); let mut children = vec![Arc::new(Node::new(NodeId(51), NodeKind::SymbolInstance { symbol: "Star".into(), xf: Affine::IDENTITY }))]; if bytes.starts_with(b"nest") { d.images.insert("inner".into(), ImageBlob::new("application/json", b"leaf".to_vec())); let inner = PlacedDocument { key: "inner".into(), width: 10.0, height: 10.0, xf: Affine::IDENTITY, ..placed("inner") }; children.push(Arc::new(Node::new(NodeId(52), NodeKind::PlacedDocument(Box::new(inner))))); } Some((d, Node::new(NodeId(0), NodeKind::Group { children, clip: false }), Rect::new(0.0, 0.0, 40.0, 20.0))) } #[test] fn round_trips_through_json() { let n = Node::new(NodeId(3), NodeKind::PlacedDocument(Box::new(placed("doc1-1")))); let v = serde_json::to_value(&n).unwrap(); assert_eq!(v["kind"]["type"], "placeddocument"); assert_eq!(v["kind"]["link"]["path"], "/art/logo.vectorcraft"); let back: Node = serde_json::from_value(v).unwrap(); assert_eq!(back, n); assert_eq!(key_for("img00", 2, true), "img00-2b"); } #[test] fn the_art_and_its_resources_join_only_a_prepared_copy_under_their_own_names() { set_loader(stub_loader); let mut d = Document::new(200.0, 200.0); d.symbols.push(Symbol { name: "Star".into(), art: Arc::new(Node::new(NodeId(60), NodeKind::Group { children: vec![], clip: false })) }); d.images.insert("doc1-1".into(), ImageBlob::new("application/json", b"nest".to_vec())); let p = placed("doc1-1"); let layer = d.layers[0].id; d.insert(Some(layer), 0, Node::new(NodeId(7), NodeKind::PlacedDocument(Box::new(p.clone())))).unwrap(); assert!(d.has_placed()); let prepared = d.with_placed_art(); let names: Vec<&str> = prepared.symbols.iter().map(|s| s.name.as_str()).collect(); // The placed document inside is read too, its resources under its own names. assert_eq!(names, ["Star", "placed:doc1-1/Star", "placed:placed:doc1-1/inner/Star"]); assert!(prepared.images.contains_key("placed:doc1-1/pic")); assert_eq!(d.symbols.len(), 1, "the document itself is unchanged"); // The art: a group of the drawing, its resources renamed. let n = d.node(NodeId(7)).unwrap(); let art = p.art(n); let [g] = &art[..] else { panic!("{art:?}") }; let mut refs = vec![]; g.walk(&mut |c| match &c.kind { NodeKind::SymbolInstance { symbol, .. } => refs.push(symbol.clone()), NodeKind::PlacedDocument(q) => { // Its 10×10 box at the file's origin, scaled ×2 into the box at (10, 10). assert_eq!(q.bounds(), Rect::new(10.0, 10.0, 30.0, 30.0)); refs.push(q.key.clone()) } _ => {} }); assert_eq!(refs, ["placed:doc1-1/Star", "placed:doc1-1/inner"]); // For older apps: plain groups all the way down. let mut older = prepared.clone().into_owned(); older.placed_as_groups(); assert!(!older.has_placed()); let mut saved = prepared.into_owned(); saved.drop_placed_resources(); assert_eq!((saved.symbols.len(), saved.images.len()), (1, 1)); } #[test] fn more_files_than_the_cache_holds_all_draw() { set_loader(stub_loader); let mut d = Document::new(200.0, 200.0); let layer = d.layers[0].id; let n = CACHE_SIZE + 10; for i in 0..n { let key = format!("many{i}-1"); d.images.insert(key.clone(), ImageBlob::new("application/json", format!("doc {i}").into_bytes())); d.insert(Some(layer), i, Node::new(NodeId(100 + i as u64), NodeKind::PlacedDocument(Box::new(placed(&key))))).unwrap(); } let _prepared = d.with_placed_art(); let mut empty = 0; d.visit_placed(|n, p| empty += usize::from(p.art(n).is_empty())); assert_eq!(empty, 0, "every file of the document is kept read"); } #[test] fn a_file_that_cant_be_read_shows_its_preview() { set_loader(stub_loader); let mut png = vec![]; image::RgbaImage::from_pixel(8, 4, image::Rgba([0, 200, 0, 255])) .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) .unwrap(); let mut blob = ImageBlob::new("image/png", png); blob.proxy = Some(blob.bytes.clone()); let mut d = Document::new(200.0, 200.0); d.images.insert("gone-1".into(), blob); let layer = d.layers[0].id; d.insert(Some(layer), 0, Node::new(NodeId(7), NodeKind::PlacedDocument(Box::new(placed("gone-1"))))).unwrap(); // No file reader: only the preview can be had. let prepared = d.with_placed_art(); assert!(prepared.images.contains_key("placed:gone-1/preview")); let n = d.node(NodeId(7)).unwrap(); let NodeKind::PlacedDocument(p) = &n.kind else { panic!("not a placed document") }; let art = p.art(n); assert_eq!(art.first().and_then(Node::geometric_bounds), Some(Rect::new(10.0, 10.0, 90.0, 50.0))); } }