diff --git a/apps/designcraft-cli/src/stress.rs b/apps/designcraft-cli/src/stress.rs index c9d5b3015..ce3e7ed7d 100644 --- a/apps/designcraft-cli/src/stress.rs +++ b/apps/designcraft-cli/src/stress.rs @@ -169,7 +169,7 @@ pub fn synthetic(spec: &Spec) -> Result { id, name: format!("art-{k}.png"), mime: "image/png".into(), - data: Arc::new(data), + data: designcraft_doc::AssetData::embedded(data), pixels: Some((aw, ah)), ..Default::default() }), diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index e28b1265a..f15ec4de4 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1054,7 +1054,7 @@ fn an_alpha_channel_contour_stops_at_the_clipping_path() { height: 200, outer: rect(75.0, 150.0), holes: PathData::default(), - source: std::sync::Weak::new(), + source: designcraft_doc::SourceKey(0), serial: designcraft_doc::AlphaOutline::next_serial(), })); let cs = compose_story(&d, sid, &ComposeOptions::default()); diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index 74768e86b..86904ccf8 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -254,6 +254,11 @@ pub fn find_command(id: &str) -> Option<&'static CommandSpec> { pub(crate) fn bad(cmd: &str, msg: impl Into) -> EngineError { EngineError::BadParams { cmd: cmd.into(), msg: msg.into() } } + +/// The bytes of `asset`, or why `cmd` can't have them. +pub(crate) fn asset_bytes(cmd: &str, asset: &designcraft_doc::Asset) -> Result>> { + asset.data.load().map_err(|e| bad(cmd, format!("{}: {e}", asset.name))) +} /// `p` with `key` set to `v`. Parameters come from callers (control channel, MCP, scripts) and /// may not be an object: `p[key] = v` panics on an array or a string, so start from `{}` then. pub(crate) fn with_param(p: &Value, key: &str, v: Value) -> Value { diff --git a/crates/engine/src/cmd/alpha_wrap.rs b/crates/engine/src/cmd/alpha_wrap.rs index ea9801f79..b141c57c8 100644 --- a/crates/engine/src/cmd/alpha_wrap.rs +++ b/crates/engine/src/cmd/alpha_wrap.rs @@ -6,7 +6,7 @@ use std::collections::BTreeSet; use std::sync::Arc; -use designcraft_doc::{AlphaOutline, AssetId, ContourType, Document, Item, WrapMode}; +use designcraft_doc::{AlphaOutline, AssetData, AssetId, ContourType, Document, Item, WrapMode}; use designcraft_geom::{BezPath, PathData, Point}; /// Images larger than this many pixels aren't traced (their contour follows the frame). @@ -68,7 +68,7 @@ pub(crate) fn sync(doc: &mut Document) { /// The empty outline standing for an image `trace` found none in: zero-size, which composition /// takes as no outline, and dropped like a traced one when the asset's data changes. -fn no_outline(data: &Arc>, asset: AssetId, threshold: u8) -> AlphaOutline { +fn no_outline(data: &AssetData, asset: AssetId, threshold: u8) -> AlphaOutline { AlphaOutline { asset, threshold, @@ -76,19 +76,20 @@ fn no_outline(data: &Arc>, asset: AssetId, threshold: u8) -> AlphaOutlin height: 0, outer: Default::default(), holes: Default::default(), - source: Arc::downgrade(data), + source: data.key(), serial: AlphaOutline::next_serial(), } } /// The outline of the pixels of `data` more opaque than `threshold`: an image with no /// transparency gives its rectangle. None for an image that can't be read or is too large. -pub(crate) fn trace(data: &Arc>, asset: AssetId, threshold: u8) -> Option { - let (w, h) = designcraft_images::pixel_size(data)?; +pub(crate) fn trace(data: &AssetData, asset: AssetId, threshold: u8) -> Option { + let bytes = data.load().ok()?; + let (w, h) = designcraft_images::pixel_size(&bytes)?; if w == 0 || h == 0 || u64::from(w) * u64::from(h) > MAX_PIXELS { return None; } - let img = designcraft_images::decode_rgba(data)?; + let img = designcraft_images::decode_rgba(&bytes)?; if img.width() != w || img.height() != h { return None; } @@ -121,7 +122,7 @@ pub(crate) fn trace(data: &Arc>, asset: AssetId, threshold: u8) -> Optio } PathData::from_bezpath(&p) }; - let source = Arc::downgrade(data); + let source = data.key(); let serial = AlphaOutline::next_serial(); return Some(AlphaOutline { asset, threshold, width: w, height: h, outer: path(&outer), holes: path(&holes), source, serial }); } @@ -273,7 +274,7 @@ mod tests { #[test] fn a_fragmented_outline_is_traced_coarser_until_composition_can_follow_it() { let dots = png_sized(256, |x, y| x % 4 == 0 && y % 4 == 0); - let o = trace(&Arc::new(dots), AssetId(1), 25).unwrap(); + let o = trace(&AssetData::embedded(dots), AssetId(1), 25).unwrap(); let elements = o.outer.to_bezpath().elements().len() + o.holes.to_bezpath().elements().len(); assert!(elements > 0 && elements <= designcraft_compose::MAX_CONTOUR_POINTS, "{elements} path elements"); } @@ -296,14 +297,14 @@ mod tests { fn traces_outer_boundaries_and_holes_apart() { // A ring: a square with a square hole. let data = png(|x, y| (8..56).contains(&x) && (8..56).contains(&y) && !((24..40).contains(&x) && (24..40).contains(&y))); - let o = trace(&Arc::new(data), AssetId(1), 25).unwrap(); + let o = trace(&AssetData::embedded(data), AssetId(1), 25).unwrap(); assert_eq!((o.width, o.height), (64, 64)); assert_eq!((o.outer.subpaths.len(), o.holes.subpaths.len()), (1, 1)); let (a, b) = (o.outer.bounds().unwrap(), o.holes.bounds().unwrap()); assert_eq!((a.x0, a.y0, a.x1, a.y1), (8.0, 8.0, 56.0, 56.0)); assert_eq!((b.x0, b.y0, b.x1, b.y1), (24.0, 24.0, 40.0, 40.0)); // An opaque image's outline is its rectangle. - let o = trace(&Arc::new(png(|_, _| true)), AssetId(1), 25).unwrap(); + let o = trace(&AssetData::embedded(png(|_, _| true)), AssetId(1), 25).unwrap(); let a = o.outer.bounds().unwrap(); assert_eq!((a.x0, a.y0, a.x1, a.y1, o.holes.subpaths.len()), (0.0, 0.0, 64.0, 64.0, 0)); } @@ -432,7 +433,7 @@ mod tests { else { panic!("a graphic") }; - let narrower = Arc::new(png(|x, _| (24..40).contains(&x))); + let narrower = AssetData::embedded(png(|x, _| (24..40).contains(&x))); s.edit(|d, _| { let a = d.assets.get(&g.asset).unwrap(); let mut a = (**a).clone(); @@ -496,7 +497,7 @@ mod tests { else { panic!("a graphic") }; - let right = Arc::new(png(|x, _| x >= 32)); + let right = AssetData::embedded(png(|x, _| x >= 32)); s.edit(|d, _| { let mut a = (**d.assets.get(&g.asset).unwrap()).clone(); a.data = right.clone(); diff --git a/crates/engine/src/cmd/anchored.rs b/crates/engine/src/cmd/anchored.rs index d55cdf753..bc80b2e1c 100644 --- a/crates/engine/src/cmd/anchored.rs +++ b/crates/engine/src/cmd/anchored.rs @@ -544,7 +544,7 @@ mod text_frame_tests { let d = &s.doc().unwrap().doc; d.check().unwrap(); let mut files: Vec<_> = - d.anchored_items().iter().filter_map(|(_, i)| i.graphic()).map(|g| d.assets.get(&g.asset).map(|a| a.data.clone())).collect(); + d.anchored_items().iter().filter_map(|(_, i)| i.graphic()).map(|g| d.assets.get(&g.asset).map(|a| a.data.load().unwrap())).collect(); files.sort(); files } diff --git a/crates/engine/src/cmd/color_theme.rs b/crates/engine/src/cmd/color_theme.rs index c406c72da..eb84bbae1 100644 --- a/crates/engine/src/cmd/color_theme.rs +++ b/crates/engine/src/cmd/color_theme.rs @@ -37,11 +37,12 @@ fn extract(s: &mut Session, p: &Value) -> Result { }; let graphic = st.doc.item(id).and_then(|it| it.graphic()).ok_or_else(|| bad(ID, "select one placed raster image"))?; let asset = st.doc.assets.get(&graphic.asset).filter(|a| a.is_raster()).ok_or_else(|| bad(ID, "a raster image is required"))?; - let (width, height) = designcraft_images::pixel_size(&asset.data).ok_or_else(|| bad(ID, "cannot read image dimensions"))?; + let bytes = crate::cmd::asset_bytes(ID, asset)?; + let (width, height) = designcraft_images::pixel_size(&bytes).ok_or_else(|| bad(ID, "cannot read image dimensions"))?; if width == 0 || height == 0 || u64::from(width) * u64::from(height) > 16_000_000 { return Err(bad(ID, "the image must contain between 1 and 16 million pixels")); } - let img = designcraft_images::decode_rgba(&asset.data).ok_or_else(|| bad(ID, "cannot decode this image"))?; + let img = designcraft_images::decode_rgba(&bytes).ok_or_else(|| bad(ID, "cannot decode this image"))?; // A regular 128 × 128 grid bounds work and includes both axes even for very thin images. let (nx, ny) = (width.min(128), height.min(128)); let mut samples = Vec::with_capacity((nx * ny) as usize); diff --git a/crates/engine/src/cmd/datamerge/fill.rs b/crates/engine/src/cmd/datamerge/fill.rs index 9d94da7d9..24d097757 100644 --- a/crates/engine/src/cmd/datamerge/fill.rs +++ b/crates/engine/src/cmd/datamerge/fill.rs @@ -181,7 +181,7 @@ pub struct ImageCache { #[derive(Default)] struct ImageCacheInner { - files: std::collections::HashMap>>, + files: std::collections::HashMap, bytes: usize, } @@ -191,14 +191,16 @@ const MAX_CACHED_IMAGE_BYTES: usize = 256 << 20; impl ImageCache { /// The bytes of `path`, reading it once per merge. `read` is called only on a miss. - fn get_or_read(&self, path: &Path, read: impl FnOnce() -> Option>) -> Option>> { + fn get_or_read(&self, path: &Path, read: impl FnOnce() -> Option>) -> Option { if let Some(found) = self.inner.borrow().files.get(path) { return Some(found.clone()); } - let bytes = std::sync::Arc::new(read()?); + let bytes = read()?; + let len = bytes.len(); + let bytes = designcraft_doc::AssetData::embedded(bytes); let mut inner = self.inner.borrow_mut(); - if inner.bytes.saturating_add(bytes.len()) <= MAX_CACHED_IMAGE_BYTES { - inner.bytes = inner.bytes.saturating_add(bytes.len()); + if inner.bytes.saturating_add(len) <= MAX_CACHED_IMAGE_BYTES { + inner.bytes = inner.bytes.saturating_add(len); inner.files.insert(path.to_path_buf(), bytes.clone()); } Some(bytes) @@ -525,7 +527,7 @@ fn create_link(doc: &mut designcraft_doc::Document, anchor: PlaceholderAnchor, d /// An image file named by a cell: where it was found, its bytes, its pixel size and its natural size. struct LoadedImage { path: PathBuf, - bytes: Arc>, + bytes: designcraft_doc::AssetData, pixels: (u32, u32), natural: (f64, f64), } @@ -547,7 +549,8 @@ fn load_image(cell: &str, sources: &Sources) -> Option { })?; // A file that does not decode leaves the search going, so a valid image at a later // candidate is still found. - let (pixels, natural) = designcraft_render::image_size(&bytes).zip(designcraft_render::natural_size(&bytes, 0))?; + let raw = bytes.embedded_bytes()?; + let (pixels, natural) = designcraft_render::image_size(raw).zip(designcraft_render::natural_size(raw, 0))?; Some(LoadedImage { path: p.clone(), bytes, pixels, natural }) }) } @@ -565,7 +568,7 @@ fn add_image_asset(doc: &mut designcraft_doc::Document, image: LoadedImage, fiel doc.add_asset(Asset { id: fresh, name, - mime: designcraft_render::image_mime(&image.bytes).to_string(), + mime: designcraft_render::image_mime(image.bytes.embedded_bytes().map_or(&[][..], |b| b.as_slice())).to_string(), link, // The cached buffer is shared with every other record naming this file, rather than // copied per record (#1595). diff --git a/crates/engine/src/cmd/export/flatten.rs b/crates/engine/src/cmd/export/flatten.rs index 21fec1c5e..6e1d29685 100644 --- a/crates/engine/src/cmd/export/flatten.rs +++ b/crates/engine/src/cmd/export/flatten.rs @@ -31,7 +31,7 @@ pub(super) fn flatten(d: &designcraft_doc::Document, cache: &designcraft_compose id: aid, name: format!("flattened-{}.png", aid.0), mime: "image/png".into(), - data: std::sync::Arc::new(img.to_png()), + data: designcraft_doc::AssetData::embedded(img.to_png()), pixels: Some((w, h)), ..Default::default() }), @@ -310,7 +310,7 @@ fn rasterize_where_with_links( id: aid, name: format!("effects-{}.png", id.0), mime: "image/png".into(), - data: std::sync::Arc::new(img.to_png()), + data: designcraft_doc::AssetData::embedded(img.to_png()), pixels: Some((w, h)), ..Default::default() }), @@ -426,7 +426,7 @@ pub(super) fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option .filter(|it| it.has_pdf_layer_override()) .filter_map(|it| { let g = it.graphic()?; - let a = d.assets.get(&g.asset).filter(|a| designcraft_render::is_pdf(&a.data))?; + let a = d.assets.get(&g.asset).filter(|a| a.data.load().is_ok_and(|b| designcraft_render::is_pdf(&b)))?; Some((it.id, g.clone(), a.clone(), it.pdf_hidden_layers.clone())) }) .collect(); @@ -442,7 +442,8 @@ pub(super) fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option let [l, t, r, b] = shown.unwrap_or([0.0, 0.0, 1.0, 1.0]); let page_side = (g.size.0 / (r - l)).max(g.size.1 / (b - t)); let side = (page_side * 300.0 / 72.0).clamp(64.0, 8000.0) as u32; - let png = designcraft_render::pdf_page_png_options_checked(&a.data, a.page as usize, side, Some(&hidden), shown) + let bytes = a.data.load().map_err(|e| EngineError::Other(format!("{}: {e}", a.name)))?; + let png = designcraft_render::pdf_page_png_options_checked(&bytes, a.page as usize, side, Some(&hidden), shown) .map_err(|e| EngineError::Other(format!("{}: {e}", a.name)))?; let px = designcraft_render::image_size(&png); let aid = designcraft_doc::AssetId(out.alloc()); @@ -452,7 +453,7 @@ pub(super) fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option id: aid, name: format!("{}.png", a.name), mime: "image/png".into(), - data: std::sync::Arc::new(png), + data: designcraft_doc::AssetData::embedded(png), pixels: px, ..Default::default() }), @@ -540,7 +541,7 @@ mod placed_layer_tests { let aid = d.item(id).unwrap().graphic().unwrap().asset; let asset = Arc::make_mut(d.assets.get_mut(&aid).unwrap()); asset.mime = "image/png".into(); - asset.data = Arc::new(designcraft_render::Rendered { width: 1, height: 1, pixels: vec![0, 0, 0, 0] }.to_png()); + asset.data = designcraft_doc::AssetData::embedded(designcraft_render::Rendered { width: 1, height: 1, pixels: vec![0, 0, 0, 0] }.to_png()); let (copy, count) = super::rasterize_layered(d).unwrap(); assert_eq!(count, 0); assert!(copy.is_none()); diff --git a/crates/engine/src/cmd/export/formats.rs b/crates/engine/src/cmd/export/formats.rs index c4f3c628b..60bc923b6 100644 --- a/crates/engine/src/cmd/export/formats.rs +++ b/crates/engine/src/cmd/export/formats.rs @@ -48,17 +48,17 @@ fn eps_previews(d: &designcraft_doc::Document) -> (Cow<'_, designcraft_doc::Docu let mut out = Cow::Borrowed(d); let mut warnings = Vec::new(); for id in used { - let Some(a) = d.assets.get(&id).filter(|a| designcraft_images::is_eps(&a.data)) else { + let Some((a, bytes)) = d.assets.get(&id).and_then(|a| Some((a, a.data.load().ok()?))).filter(|(_, b)| designcraft_images::is_eps(b)) else { continue; }; - let png = designcraft_images::eps_png(&a.data); + let png = designcraft_images::eps_png(&bytes); if png.is_none() { warnings.push(format!("{}: unusable EPS preview; graphic omitted", a.name)); } if let Some(a) = out.to_mut().assets.get_mut(&id) { let a = Arc::make_mut(a); a.mime = "image/png".into(); - a.data = Arc::new(png.unwrap_or_default()); + a.data = designcraft_doc::AssetData::embedded(png.unwrap_or_default()); } } (out, warnings) diff --git a/crates/engine/src/cmd/file/eps_place_tests.rs b/crates/engine/src/cmd/file/eps_place_tests.rs index 2501a31d3..8a78422a3 100644 --- a/crates/engine/src/cmd/file/eps_place_tests.rs +++ b/crates/engine/src/cmd/file/eps_place_tests.rs @@ -167,8 +167,8 @@ fn eps_idml_roundtrip_preserves_source_and_resource_type() { let restored = designcraft_idml::import_idml(&idml).unwrap(); let a = restored.assets.values().next().unwrap(); assert_eq!(a.mime, "application/postscript"); - assert_eq!(a.data.as_slice(), bytes); - assert!(designcraft_render::decode_pixmap(&a.data).is_some()); + assert_eq!(a.data.load().unwrap().as_slice(), bytes); + assert!(designcraft_render::decode_pixmap(&a.data.load().unwrap()).is_some()); // A linked-only EPS keeps its type even when the external file is unavailable; // when the resolver supplies it, sniff the raw source rather than PNG metadata. let mut linked = (*source).clone(); @@ -186,7 +186,7 @@ fn eps_idml_roundtrip_preserves_source_and_resource_type() { .unwrap(); let a = resolved.assets.values().next().unwrap(); assert_eq!(a.mime, "application/postscript"); - assert_eq!(a.data.as_slice(), bytes); + assert_eq!(a.data.load().unwrap().as_slice(), bytes); } } @@ -197,7 +197,7 @@ fn eps_place_cursor_retains_source_and_uses_bounding_box_size() { s.execute("file.new", &json!({})).unwrap(); let result = s.execute("place.load", &json!({"base64": base64_encode(source), "name": "logo.eps"})).unwrap(); let aid = AssetId(result["asset"].as_u64().unwrap()); - assert_eq!(s.doc().unwrap().doc.assets[&aid].data.as_slice(), source); + assert_eq!(s.doc().unwrap().doc.assets[&aid].data.load().unwrap().as_slice(), source); assert_eq!(s.doc().unwrap().doc.assets[&aid].mime, "application/postscript"); assert_eq!(s.doc().unwrap().doc.assets[&aid].pixels, Some((201, 101))); let result = s.execute("place.drop", &json!({"x": 72, "y": 72})).unwrap(); diff --git a/crates/engine/src/cmd/file/place.rs b/crates/engine/src/cmd/file/place.rs index 56f5b646f..6f0aebcc2 100644 --- a/crates/engine/src/cmd/file/place.rs +++ b/crates/engine/src/cmd/file/place.rs @@ -4,8 +4,7 @@ use super::EngineError; use super::bytes::base64_encode; use super::bytes::{base64_decode, from_bytes}; use super::{ - Affine, Arc, Asset, AssetId, Content, Graphic, Item, ItemId, PdfCrop, Rect, Result, Selection, Session, Shape, Value, bad, json, ok, shapes, - str_param, + Affine, Asset, AssetId, Content, Graphic, Item, ItemId, PdfCrop, Rect, Result, Selection, Session, Shape, Value, bad, json, ok, shapes, str_param, }; pub(super) fn file_place(s: &mut Session, p: &Value) -> Result { @@ -96,7 +95,7 @@ pub(super) fn file_place(s: &mut Session, p: &Value) -> Result { mime, link, link_fingerprint: crate::cmd::links::loaded_fingerprint(before, &bytes), - data: Arc::new(bytes), + data: designcraft_doc::AssetData::embedded(bytes), pixels: Some((pw, ph)), pdf_crop, ..Default::default() @@ -199,10 +198,11 @@ pub(super) fn file_place(s: &mut Session, p: &Value) -> Result { /// keeps its crop choice (a missing link relinked later gets its box then). pub(crate) fn crop_pdf(a: &mut Asset) -> Option<(f64, f64)> { a.pdf_box = None; - if a.pdf_crop == PdfCrop::Crop || !designcraft_render::is_pdf(&a.data) { + let bytes = a.data.load().ok()?; + if a.pdf_crop == PdfCrop::Crop || !designcraft_render::is_pdf(&bytes) { return None; } - let (frac, size) = designcraft_render::pdf_crop_box(&a.data, a.page as usize, a.pdf_crop)?; + let (frac, size) = designcraft_render::pdf_crop_box(&bytes, a.page as usize, a.pdf_crop)?; a.pdf_box = Some(frac); Some(size) } @@ -259,7 +259,8 @@ pub(super) fn place_load(s: &mut Session, p: &Value) -> Result { let (pw, ph) = designcraft_render::image_size(&bytes).ok_or_else(|| bad("place.load", "unsupported or corrupt image"))?; let natural = eps_size(&bytes, "place.load")?.or_else(|| designcraft_render::natural_size(&bytes, 0)).unwrap_or((pw as f64, ph as f64)); let mime = designcraft_render::image_mime(&bytes).to_string(); - let asset = Asset { name, mime, link, link_fingerprint, data: Arc::new(bytes), pixels: Some((pw, ph)), ..Default::default() }; + let asset = + Asset { name, mime, link, link_fingerprint, data: designcraft_doc::AssetData::embedded(bytes), pixels: Some((pw, ph)), ..Default::default() }; load_cursor(s, asset, natural) } diff --git a/crates/engine/src/cmd/interchange.rs b/crates/engine/src/cmd/interchange.rs index 8387ff981..a176baf05 100644 --- a/crates/engine/src/cmd/interchange.rs +++ b/crates/engine/src/cmd/interchange.rs @@ -49,7 +49,8 @@ fn export_idml(s: &mut Session, p: &Value) -> Result { /// replaced with another format) is not one, so the package report does not name it. pub(crate) fn has_applicable_pdf_layer_overrides(doc: &Document) -> bool { doc.placed_items().into_iter().any(|it| { - it.has_pdf_layer_override() && it.graphic().and_then(|g| doc.assets.get(&g.asset)).is_some_and(|a| designcraft_render::is_pdf(&a.data)) + it.has_pdf_layer_override() + && it.graphic().and_then(|g| doc.assets.get(&g.asset)).is_some_and(|a| a.data.load().is_ok_and(|b| designcraft_render::is_pdf(&b))) }) } @@ -66,7 +67,8 @@ fn with_pdf_layer_variants(doc: &Document, embed: bool) -> Result Result = - imported.assets.values().filter(|a| designcraft_render::is_pdf(&a.data)).map(|a| designcraft_render::pdf_layers(&a.data)).collect(); + let mut roundtrip: Vec<_> = imported + .assets + .values() + .filter(|a| designcraft_render::is_pdf(&a.data.load().unwrap())) + .map(|a| designcraft_render::pdf_layers(&a.data.load().unwrap())) + .collect(); let mut expected = vec![states(true, false), states(false, true), states(true, true)]; roundtrip.sort(); expected.sort(); @@ -879,8 +890,12 @@ mod nested_layer_tests { doc.check().unwrap(); let result = s.execute("file.exportIdml", &json!({})).unwrap(); let imported = designcraft_idml::import_idml(&base64_decode(result["base64"].as_str().unwrap())).unwrap(); - let mut layers: Vec<_> = - imported.assets.values().filter(|a| designcraft_render::is_pdf(&a.data)).map(|a| designcraft_render::pdf_layers(&a.data)).collect(); + let mut layers: Vec<_> = imported + .assets + .values() + .filter(|a| designcraft_render::is_pdf(&a.data.load().unwrap())) + .map(|a| designcraft_render::pdf_layers(&a.data.load().unwrap())) + .collect(); layers.sort(); assert_eq!(layers, vec![states(false, true), states(true, false)]); } diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index 22e31cf64..6789feb2c 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -157,10 +157,10 @@ pub fn link_missing(a: &designcraft_doc::Asset, document: Option<&Path>) -> bool /// Linked images checked this session: (asset, the identity of its bytes, the file its link /// resolves to) → (the file's size and time when it was read, whether it matched the bytes). The /// resolved file, not the link text, is the key: a relative link names another file once the -/// document is saved elsewhere. The bytes' identity (their `Arc`'s address, as the render cache -/// keys them) changes with every relink, update or undo, so a remembered answer can never -/// describe other bytes. -pub(crate) type ImageChecks = std::collections::HashMap<(AssetId, usize, std::path::PathBuf), (Fingerprint, bool)>; +/// document is saved elsewhere. The bytes' [`designcraft_doc::SourceKey`] (as the render cache +/// keys them) changes with every relink or update and is never reused, so a remembered answer +/// can never describe other bytes. +pub(crate) type ImageChecks = std::collections::HashMap<(AssetId, designcraft_doc::SourceKey, std::path::PathBuf), (Fingerprint, bool)>; /// The size and time to keep with `bytes` just read from a linked file, `before` being the /// file's from before the read. Kept only when they can vouch for the bytes later: the size is @@ -178,7 +178,7 @@ pub(crate) fn loaded_fingerprint(before: Option, bytes: &[u8]) -> O /// new time or no fingerprint leave it to the byte comparison. #[cfg(not(target_arch = "wasm32"))] fn loaded_unchanged(a: &designcraft_doc::Asset, now: Fingerprint) -> bool { - a.link_fingerprint.is_some_and(|loaded| loaded.size == a.data.len() as u64 && loaded == now && settled(&now)) + a.link_fingerprint.is_some_and(|loaded| loaded.size == a.data.len() && loaded == now && settled(&now)) } /// Link status of a placed graphic (`ok`, `modified`, `missing`, `embedded`), reading the file @@ -213,17 +213,17 @@ fn image_status(a: &designcraft_doc::Asset, document: Option<&Path>, checks: &mu if loaded_unchanged(a, now) { return ("ok", None); } - if now.size != a.data.len() as u64 { + if now.size != a.data.len() { return ("modified", None); } - let key = (a.id, Arc::as_ptr(&a.data) as usize, path.clone()); + let key = (a.id, a.data.key(), path.clone()); if let Some((at, same)) = checks.get(&key) && *at == now { return if *same { ("ok", Some(now)) } else { ("modified", None) }; } - let same = match crate::io::read_capped(&path, a.data.len() as u64) { - Ok(b) => b == *a.data, + let same = match crate::io::read_capped(&path, a.data.len()) { + Ok(b) => a.data.load().is_ok_and(|d| b == *d), Err(_) => return ("missing", None), }; // The time from before the read: a change during it is seen next time. @@ -443,7 +443,7 @@ pub fn remember_link_files(s: &mut Session, doc: &Arc, files: &[(Asset if !Arc::ptr_eq(&st.doc, doc) || st.interaction.is_some() { return 0; } - let stale = |a: &designcraft_doc::Asset, f: &Fingerprint| a.link.is_some() && a.data.len() as u64 == f.size && a.link_fingerprint != Some(*f); + let stale = |a: &designcraft_doc::Asset, f: &Fingerprint| a.link.is_some() && a.data.len() == f.size && a.link_fingerprint != Some(*f); let fresh: Vec<(AssetId, Fingerprint)> = files.iter().filter(|(id, f)| st.doc.assets.get(id).is_some_and(|a| stale(a, f))).copied().collect(); if fresh.is_empty() { return 0; @@ -622,7 +622,7 @@ fn relinked_asset(a: &designcraft_doc::Asset, read: &Path, link: &str) -> Result }; let mut a = a.clone(); a.mime = designcraft_render::image_mime(&bytes).to_string(); - a.data = Arc::new(bytes); + a.data = designcraft_doc::AssetData::embedded(bytes); a.pixels = Some(px); a.page = page; a.name = Path::new(link).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| link.to_string()); @@ -963,7 +963,7 @@ mod shared_asset_tests { id: AssetId(id), name: "p.png".into(), mime: "image/png".into(), - data: Arc::new(data), + data: designcraft_doc::AssetData::embedded(data), ..Default::default() }; let mut src = Document::new(&NewDocument::default()); @@ -1018,7 +1018,7 @@ mod shared_asset_tests { name: "p.png".into(), mime: "image/png".into(), link: None, - data: Arc::new(png(8, 4)), + data: designcraft_doc::AssetData::embedded(png(8, 4)), pixels: None, ..Default::default() }), diff --git a/crates/engine/src/cmd/links/tests.rs b/crates/engine/src/cmd/links/tests.rs index 733891dbf..2d156b690 100644 --- a/crates/engine/src/cmd/links/tests.rs +++ b/crates/engine/src/cmd/links/tests.rs @@ -101,7 +101,7 @@ fn a_linked_image_at_its_loaded_size_and_time_is_not_read() { id: AssetId(1), link: Some(path.to_string_lossy().into_owned()), link_fingerprint, - data: Arc::new(b"data".to_vec()), + data: designcraft_doc::AssetData::embedded(b"data".to_vec()), ..Default::default() }; let status = |a: &designcraft_doc::Asset| status_cached(a, None, &mut ImageChecks::default()); @@ -111,7 +111,7 @@ fn a_linked_image_at_its_loaded_size_and_time_is_not_read() { assert_eq!(status(&asset(None)), "modified"); assert_eq!(status(&asset(Some(Fingerprint { mtime: loaded.mtime - 60, ..loaded }))), "modified", "a new time alone is compared"); // Bytes replaced without their fingerprint: it is of another size, so it isn't theirs. - let swapped = designcraft_doc::Asset { data: Arc::new(b"other bytes".to_vec()), ..asset(Some(loaded)) }; + let swapped = designcraft_doc::Asset { data: designcraft_doc::AssetData::embedded(b"other bytes".to_vec()), ..asset(Some(loaded)) }; assert_eq!(status(&swapped), "modified"); // A time too recent to trust: another write in the same second would keep it. set_age(&path, 0); @@ -424,7 +424,7 @@ fn eps_web_exports_convert_tiff_and_skip_bad_used_assets() { assert!(!opf.contains("image/tiff")); assert!(Arc::ptr_eq(&before, &s.doc().unwrap().doc)); s.edit(|d, _| { - Arc::make_mut(d.assets.get_mut(&aid).unwrap()).data = Arc::new(b"%!PS-Adobe-3.0 EPSF-3.0\n%%EOF\n".to_vec()); + Arc::make_mut(d.assets.get_mut(&aid).unwrap()).data = designcraft_doc::AssetData::embedded(b"%!PS-Adobe-3.0 EPSF-3.0\n%%EOF\n".to_vec()); Ok(()) }) .unwrap(); @@ -464,7 +464,7 @@ fn eps_status_tracks_source_bytes_across_save_update_relink_and_embed() { assert!(Arc::ptr_eq(&s.doc().unwrap().doc, &before), "unchanged EPS must not create an edit"); let saved = designcraft_format::save(&before).unwrap(); let reopened = designcraft_format::load(&saved).unwrap(); - assert_eq!(reopened.assets[&aid].data.as_slice(), first); + assert_eq!(reopened.assets[&aid].data.load().unwrap().as_slice(), first); let mut s = Session::new(); s.add_document(crate::DocState::new(reopened, None)); assert_eq!(s.execute("links.list", &json!({})).unwrap()[0]["status"], "ok"); @@ -472,7 +472,7 @@ fn eps_status_tracks_source_bytes_across_save_update_relink_and_embed() { assert_eq!(s.execute("links.list", &json!({})).unwrap()[0]["status"], "modified"); assert_eq!(s.execute("links.update", &json!({})).unwrap()["updated"], 1); assert_eq!(s.execute("links.list", &json!({})).unwrap()[0]["status"], "ok"); - assert_eq!(s.doc().unwrap().doc.assets[&aid].data.as_slice(), changed); + assert_eq!(s.doc().unwrap().doc.assets[&aid].data.load().unwrap().as_slice(), changed); std::fs::remove_file(&path).unwrap(); assert_eq!(s.execute("links.list", &json!({})).unwrap()[0]["status"], "missing"); let replacement = dir.join("replacement.eps"); @@ -489,8 +489,8 @@ fn eps_status_tracks_source_bytes_across_save_update_relink_and_embed() { let embedded = designcraft_format::load(&designcraft_format::save(&s.doc().unwrap().doc).unwrap()).unwrap(); // The asset is embedded, so no link is followed and the document is irrelevant. assert_eq!(status_cached(&embedded.assets[&aid], None, &mut Default::default()), "embedded"); - assert_eq!(embedded.assets[&aid].data.as_slice(), changed); - assert!(designcraft_render::decode_pixmap(&embedded.assets[&aid].data).is_some()); + assert_eq!(embedded.assets[&aid].data.load().unwrap().as_slice(), changed); + assert!(designcraft_render::decode_pixmap(&embedded.assets[&aid].data.load().unwrap()).is_some()); let _ = std::fs::remove_dir_all(dir); } @@ -507,7 +507,7 @@ fn eps_legacy_preview_document_is_upgraded_by_update() { let proxy = designcraft_images::eps_proxy(source).unwrap().0; s.edit(|d, _| { let a = Arc::make_mut(d.assets.get_mut(&aid).unwrap()); - a.data = Arc::new(proxy); + a.data = designcraft_doc::AssetData::embedded(proxy); a.mime = "image/png".into(); Ok(()) }) @@ -517,9 +517,9 @@ fn eps_legacy_preview_document_is_upgraded_by_update() { s.add_document(crate::DocState::new(old, None)); assert_eq!(s.execute("links.update", &json!({})).unwrap()["updated"], 1); assert_eq!(s.execute("links.list", &json!({})).unwrap()[0]["status"], "ok"); - assert_eq!(s.doc().unwrap().doc.assets[&aid].data.as_slice(), source); + assert_eq!(s.doc().unwrap().doc.assets[&aid].data.load().unwrap().as_slice(), source); s.execute("edit.undo", &json!({})).unwrap(); - assert!(designcraft_render::decode_pixmap(&s.doc().unwrap().doc.assets[&aid].data).is_some(), "old preview remains renderable"); + assert!(designcraft_render::decode_pixmap(&s.doc().unwrap().doc.assets[&aid].data.load().unwrap()).is_some(), "old preview remains renderable"); s.execute("edit.redo", &json!({})).unwrap(); assert_eq!(s.execute("links.list", &json!({})).unwrap()[0]["status"], "ok"); let _ = std::fs::remove_dir_all(dir); @@ -542,7 +542,7 @@ fn eps_source_exports_preview_to_pdf_html_and_epub_without_editing_document() { // A document retaining the original EPS rather than the legacy preview-only asset. s.edit(|d, _| { let a = Arc::make_mut(d.assets.get_mut(&aid).unwrap()); - a.data = Arc::new(source.to_vec()); + a.data = designcraft_doc::AssetData::embedded(source.to_vec()); a.mime = "application/postscript".into(); Ok(()) }) @@ -573,7 +573,7 @@ fn eps_source_exports_preview_to_pdf_html_and_epub_without_editing_document() { assert!(opf.contains("media-type=\"image/png\"")); assert!(!opf.contains("application/postscript")); assert!(Arc::ptr_eq(&s.doc().unwrap().doc, &before), "exports must leave the source document intact"); - assert_eq!(before.assets[&aid].data.as_slice(), source); + assert_eq!(before.assets[&aid].data.load().unwrap().as_slice(), source); } #[test] @@ -596,7 +596,7 @@ fn eps_relink_update_and_folder_relink_retain_source() { let a = &s.doc().unwrap().doc.assets[&aid]; assert_eq!(a.pixels, Some((60, 20))); assert_eq!(a.mime, "application/postscript"); - assert_eq!(a.data.as_slice(), eps(30).as_bytes()); + assert_eq!(a.data.load().unwrap().as_slice(), eps(30).as_bytes()); let moved = dir.join("new").join("replacement.eps"); std::fs::rename(&replacement, &moved).unwrap(); assert_eq!(s.execute("links.relinkFolder", &json!({"dir": dir.join("new").to_string_lossy()})).unwrap()["relinked"], 1); @@ -621,7 +621,7 @@ fn pdf_relink_and_update_preserve_selected_page_size() { let a = &s.doc().unwrap().doc.assets[&aid]; assert_eq!(a.page, 1); assert_eq!(a.pixels, Some((50, 60))); - assert!(designcraft_render::render_pdf_page(&a.data, a.page as usize, 64).is_some()); + assert!(designcraft_render::render_pdf_page(&a.data.load().unwrap(), a.page as usize, 64).is_some()); std::fs::write(&replacement, pdf(&[(70, 70), (80, 90)])).unwrap(); assert_eq!(s.execute("links.update", &json!({})).unwrap()["updated"], 1); assert_eq!(s.doc().unwrap().doc.assets[&aid].pixels, Some((80, 90))); @@ -667,7 +667,7 @@ fn pdf_reload_rejects_missing_page_without_edit() { assert_eq!(st.revision, revision); assert_eq!(st.history.undo.len(), undo); assert_eq!(s.journal.len(), journal); - assert!(designcraft_render::render_pdf_page(&st.doc.assets[&aid].data, 1, 64).is_some()); + assert!(designcraft_render::render_pdf_page(&st.doc.assets[&aid].data.load().unwrap(), 1, 64).is_some()); } let _ = std::fs::remove_dir_all(dir); } diff --git a/crates/engine/src/cmd/mathexpr.rs b/crates/engine/src/cmd/mathexpr.rs index 0c8a120c3..44171e098 100644 --- a/crates/engine/src/cmd/mathexpr.rs +++ b/crates/engine/src/cmd/mathexpr.rs @@ -2,8 +2,6 @@ //! text insertion point (an anchored object) or as a frame. The LaTeX is kept as the object's //! alt text, so it can be edited and exports accessibly. -use std::sync::Arc; - use designcraft_doc::{Asset, AssetId, Content, Graphic, Item, ItemId, Shape}; use designcraft_geom::{Affine, Rect, shapes}; use serde_json::{Value, json}; @@ -14,7 +12,13 @@ use crate::{Result, Session}; fn math_item(d: &mut designcraft_doc::Document, latex: &str, size: f64, lid: designcraft_doc::LayerId, at: (f64, f64)) -> Item { let (svg, (w, h)) = crate::math::to_svg(latex, size); let fresh = AssetId(d.alloc()); - let aid = d.add_asset(Asset { id: fresh, name: "math.svg".into(), mime: "image/svg+xml".into(), data: Arc::new(svg), ..Default::default() }); + let aid = d.add_asset(Asset { + id: fresh, + name: "math.svg".into(), + mime: "image/svg+xml".into(), + data: designcraft_doc::AssetData::embedded(svg), + ..Default::default() + }); let id = ItemId(d.alloc()); let r = Rect::new(at.0, at.1, at.0 + w, at.1 + h); let mut it = Item::new(id, lid, Shape::Rectangle, shapes::rectangle(r)); diff --git a/crates/engine/src/cmd/media.rs b/crates/engine/src/cmd/media.rs index eab1027f6..8c7fd0bbe 100644 --- a/crates/engine/src/cmd/media.rs +++ b/crates/engine/src/cmd/media.rs @@ -1,8 +1,6 @@ //! Window › Interactive › Media: placed video and sound (shown as a poster or a placeholder, //! exported as `