DES-10 T2: port asset byte users to AssetData

Export, import, links, preflight, alpha wrap and the UI read bytes with
AssetData::load; every asset is still embedded.
This commit is contained in:
aiongg committed 2026-11-14 05:15:03 +00:00
1 parent db16af3a15
commit 55a0c7da3d
48 files changed
+331 -207

No files matched your search

+1 -1
View File
@@ -169,7 +169,7 @@ pub fn synthetic(spec: &Spec) -> Result<Document, String> {
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()
}),
+1 -1
View File
@@ -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());
+5
View File
@@ -254,6 +254,11 @@ pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
pub(crate) fn bad(cmd: &str, msg: impl Into<String>) -> 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<std::sync::Arc<Vec<u8>>> {
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 {
+13 -12
View File
@@ -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<Vec<u8>>, 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<Vec<u8>>, 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<Vec<u8>>, asset: AssetId, threshold: u8) -> Option<AlphaOutline> {
let (w, h) = designcraft_images::pixel_size(data)?;
pub(crate) fn trace(data: &AssetData, asset: AssetId, threshold: u8) -> Option<AlphaOutline> {
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<Vec<u8>>, 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();
+1 -1
View File
@@ -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
}
+3 -2
View File
@@ -37,11 +37,12 @@ fn extract(s: &mut Session, p: &Value) -> Result<Value> {
};
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);
+11 -8
View File
@@ -181,7 +181,7 @@ pub struct ImageCache {
#[derive(Default)]
struct ImageCacheInner {
files: std::collections::HashMap<PathBuf, std::sync::Arc<Vec<u8>>>,
files: std::collections::HashMap<PathBuf, designcraft_doc::AssetData>,
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<Vec<u8>>) -> Option<std::sync::Arc<Vec<u8>>> {
fn get_or_read(&self, path: &Path, read: impl FnOnce() -> Option<Vec<u8>>) -> Option<designcraft_doc::AssetData> {
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<Vec<u8>>,
bytes: designcraft_doc::AssetData,
pixels: (u32, u32),
natural: (f64, f64),
}
@@ -547,7 +549,8 @@ fn load_image(cell: &str, sources: &Sources) -> Option<LoadedImage> {
})?;
// 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).
+7 -6
View File
@@ -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());
+3 -3
View File
@@ -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)
@@ -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();
+7 -6
View File
@@ -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<Value> {
@@ -96,7 +95,7 @@ pub(super) fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
/// 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<Value> {
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)
}
+34 -19
View File
@@ -49,7 +49,8 @@ fn export_idml(s: &mut Session, p: &Value) -> Result<Value> {
/// 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<Option<Documen
.into_iter()
.filter(|it| it.has_pdf_layer_override())
.filter_map(|it| {
let asset = it.graphic().and_then(|g| doc.assets.get(&g.asset)).filter(|a| designcraft_render::is_pdf(&a.data))?;
let asset =
it.graphic().and_then(|g| doc.assets.get(&g.asset)).filter(|a| a.data.load().is_ok_and(|b| designcraft_render::is_pdf(&b)))?;
Some((it.id, asset, it.pdf_hidden_layers.clone(), it.pdf_layer_override))
})
.collect();
@@ -89,11 +91,12 @@ fn with_pdf_layer_variants(doc: &Document, embed: bool) -> Result<Option<Documen
continue;
}
expected.insert(id, signature);
let bytes = designcraft_render::pdf_hide_layers(&asset.data, &hidden)
let source = asset.data.load().map_err(|e| EngineError::Other(format!("{}: {e}", asset.name)))?;
let bytes = designcraft_render::pdf_hide_layers(&source, &hidden)
.ok_or_else(|| EngineError::Other(format!("{}: PDF layer choices could not be applied", asset.name)))?;
let mut variant = (**asset).clone();
variant.id = designcraft_doc::AssetId(out.alloc());
variant.data = std::sync::Arc::new(bytes);
variant.data = designcraft_doc::AssetData::embedded(bytes);
variant.set_link(None, None);
let aid = variant.id;
out.assets.insert(aid, std::sync::Arc::new(variant));
@@ -541,7 +544,11 @@ mod tests {
let doc = import(&idml, Some(&dir.join("x.idml"))).unwrap();
let asset = doc.assets.values().next().unwrap();
assert_eq!(*asset.data, png, "the packaged graphic must render even when its author-machine path no longer exists");
assert_eq!(
**asset.data.embedded_bytes().unwrap(),
png,
"the packaged graphic must render even when its author-machine path no longer exists"
);
let resolved = std::path::absolute(&path).unwrap();
assert_eq!(asset.link.as_deref(), resolved.to_str(), "remember the resolved link so update and preflight use the packaged file");
let expected = super::super::datamerge::fingerprint_from(&std::fs::metadata(&path).unwrap());
@@ -561,7 +568,7 @@ mod tests {
let doc = import(&idml, Some(&dir.join("x.idml"))).unwrap();
let asset = doc.assets.values().next().unwrap();
assert_eq!(*asset.data, png);
assert_eq!(**asset.data.embedded_bytes().unwrap(), png);
assert_eq!(asset.link.as_deref(), std::path::absolute(&path).unwrap().to_str());
assert_eq!(super::super::links::status_cached(asset, None, &mut Default::default()), "ok");
std::fs::remove_dir_all(dir).unwrap();
@@ -577,7 +584,7 @@ mod tests {
let doc = import(&idml, Some(&dir.join("x.idml"))).unwrap();
let asset = doc.assets.values().next().unwrap();
assert_eq!(*asset.data, png);
assert_eq!(**asset.data.embedded_bytes().unwrap(), png);
assert_eq!(asset.link.as_deref(), std::path::absolute(&path).unwrap().to_str());
assert_eq!(super::super::links::status_cached(asset, None, &mut Default::default()), "ok");
std::fs::remove_dir_all(dir).unwrap();
@@ -598,7 +605,7 @@ mod tests {
let doc = import(&idml, Some(&package.join("x.idml"))).unwrap();
let asset = doc.assets.values().next().unwrap();
assert_eq!(std::path::Path::new(asset.link.as_deref().unwrap()), original.as_path(), "an existing original stays authoritative");
assert_eq!(*asset.data, png);
assert_eq!(**asset.data.embedded_bytes().unwrap(), png);
assert_eq!(super::super::links::status_cached(asset, None, &mut Default::default()), "modified");
std::fs::remove_file(&original).unwrap();
@@ -606,7 +613,7 @@ mod tests {
let doc = import(&idml, Some(&package.join("x.idml"))).unwrap();
let asset = doc.assets.values().next().unwrap();
assert_eq!(asset.link.as_deref(), copy.to_str());
assert_eq!(*asset.data, png, "rebinding must not replace embedded pixels");
assert_eq!(**asset.data.embedded_bytes().unwrap(), png, "rebinding must not replace embedded pixels");
assert_eq!(super::super::links::status_cached(asset, None, &mut Default::default()), "modified");
std::fs::remove_file(&copy).unwrap();
@@ -615,7 +622,7 @@ mod tests {
let doc = import(&idml, folder).unwrap();
let asset = doc.assets.values().next().unwrap();
assert_eq!(std::path::Path::new(asset.link.as_deref().unwrap()), original.as_path(), "keep a missing link when there is no candidate");
assert_eq!(*asset.data, png);
assert_eq!(**asset.data.embedded_bytes().unwrap(), png);
assert_eq!(super::super::links::status_cached(asset, None, &mut Default::default()), "missing");
}
let oversized = "a/".repeat(MAX_LINK_PATH_BYTES);
@@ -625,7 +632,7 @@ mod tests {
resolve_packaged_links(&mut doc, Some(&packaged));
let asset = doc.assets.values().next().unwrap();
assert_eq!(asset.link.as_deref(), Some(oversized.as_str()));
assert_eq!(*asset.data, png);
assert_eq!(**asset.data.embedded_bytes().unwrap(), png);
std::fs::remove_dir_all(dir).unwrap();
}
@@ -672,12 +679,12 @@ mod tests {
let (idml, _) = linked_idml(away.to_str().unwrap(), false).unwrap();
let doc = import(&idml, Some(&package)).unwrap();
let asset = doc.assets.values().next().unwrap();
assert_eq!((&*asset.data, asset.link.as_deref()), (&png, away.to_str()));
assert_eq!((&**asset.data.embedded_bytes().unwrap(), asset.link.as_deref()), (&png, away.to_str()));
assert_eq!(super::super::links::status_cached(asset, None, &mut Default::default()), "ok");
std::fs::write(dir.join("Links").join("logo.png"), &png).unwrap();
let (idml, _) = linked_idml("Links/logo.png", false).unwrap();
let doc = import(&idml, Some(&package)).unwrap();
assert_eq!(*doc.assets.values().next().unwrap().data, png);
assert_eq!(**doc.assets.values().next().unwrap().data.embedded_bytes().unwrap(), png);
std::fs::remove_dir_all(base).unwrap();
}
@@ -777,14 +784,14 @@ mod nested_layer_tests {
for (id, expected) in ids.into_iter().zip([states(false, true), states(true, true)]) {
let item = copy.placed_items().into_iter().find(|i| i.id == id).unwrap();
let asset = &copy.assets[&item.graphic().unwrap().asset];
assert_eq!(designcraft_render::pdf_layers(&asset.data), expected, "{location}: placement {id}");
assert_eq!(designcraft_render::pdf_layers(&asset.data.load().unwrap()), expected, "{location}: placement {id}");
assert_ne!(asset.id, original_asset);
assert!(asset.link.is_none());
assert!(!item.pdf_layer_override && item.pdf_hidden_layers.is_empty());
}
assert_eq!(copy.item(control).unwrap().graphic().unwrap().asset, original_asset);
assert!(Arc::ptr_eq(&copy.assets[&original_asset], &source.assets[&original_asset]));
assert_eq!(designcraft_render::pdf_layers(&source.assets[&original_asset].data), states(true, false));
assert_eq!(designcraft_render::pdf_layers(&source.assets[&original_asset].data.load().unwrap()), states(true, false));
assert!(source.placed_items().into_iter().filter(|i| ids.contains(&i.id)).all(|i| i.has_pdf_layer_override()));
}
@@ -794,8 +801,12 @@ mod nested_layer_tests {
let result = session.execute("file.exportIdml", &json!({})).unwrap();
let bytes = base64_decode(result["base64"].as_str().unwrap());
let imported = designcraft_idml::import_idml(&bytes).unwrap();
let mut roundtrip: Vec<_> =
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)]);
}
+13 -13
View File
@@ -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<Fingerprint>, 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<Document>, 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()
}),
+15 -15
View File
@@ -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);
}
+7 -3
View File
@@ -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));
+2 -4
View File
@@ -1,8 +1,6 @@
//! Window › Interactive › Media: placed video and sound (shown as a poster or a placeholder,
//! exported as `<video>` / `<audio>` in EPUB).
use std::sync::Arc;
use designcraft_doc::{Asset, AssetId, Content, Graphic, Item, ItemId, MediaOptions, Shape};
use designcraft_geom::{Affine, Rect, shapes};
use serde_json::{Value, json};
@@ -29,7 +27,7 @@ pub(crate) fn place_media(s: &mut Session, p: &Value, name: String, mime: &str,
let mime = mime.to_string();
s.edit(|d, sel| {
let fresh = AssetId(d.alloc());
let aid = d.add_asset(Asset { id: fresh, name, mime, link, data: Arc::new(bytes), ..Default::default() });
let aid = d.add_asset(Asset { id: fresh, name, mime, link, data: designcraft_doc::AssetData::embedded(bytes), ..Default::default() });
let id = match target {
Some(fid) => {
let it = d.item_mut(fid).ok_or(designcraft_doc::DocError::NoItem(fid))?;
@@ -109,7 +107,7 @@ pub fn specs() -> Vec<CommandSpec> {
name,
mime,
link,
data: Arc::new(bytes),
data: designcraft_doc::AssetData::embedded(bytes),
pixels: Some(px),
..Default::default()
})))
+1 -1
View File
@@ -3164,7 +3164,7 @@ fn clipping_path(s: &mut Session, p: &Value) -> Result<Value> {
let Some(it) = d.item(*id) else { continue };
let Some(g) = it.graphic() else { continue };
let Some(asset) = d.assets.get(&g.asset) else { continue };
let Some(px) = designcraft_render::decode_pixmap_page(&asset.data, asset.page) else { continue };
let Some(px) = asset.data.load().ok().and_then(|b| designcraft_render::decode_pixmap_page(&b, asset.page)) else { continue };
// A placed PDF cropped to a box: trace that box, which graphic space spans.
let px = match designcraft_render::shown_box(asset) {
// A crop too big to trace is skipped, as a graphic that fails to decode is: tracing
+4 -3
View File
@@ -339,12 +339,13 @@ fn write_links(d: &Document, root: &OutputRoot, dir: &Path, only: Option<&[Asset
let Some(a) = d.assets.get(&id) else { continue };
// A missing link has no bytes: an empty file under its name would read as present, so it
// keeps its old (missing) path instead.
if a.data.is_empty() {
// Bytes that can't be had are left like a missing link.
let Some(bytes) = a.data.load().ok().filter(|b| !b.is_empty()) else {
continue;
}
};
let name = link_name(a.link.as_deref().unwrap_or(&a.name), id, &mut taken);
let path = dir.join(&name);
root.write(&path, a.data.as_slice())?;
root.write(&path, bytes.as_slice())?;
let mut na = (**a).clone();
let link = match relative_to {
Some(folder) => format!("{folder}/{name}"),
+1 -1
View File
@@ -112,7 +112,7 @@ fn relocated_packages_rebind_embedded_image_links_on_open() {
let mut opened = Session::new();
opened.execute(command, &json!({"path": moved.join(format!("OwnedPackage.{extension}")).to_string_lossy()})).unwrap();
let asset = opened.doc().unwrap().doc.assets.values().next().unwrap();
assert_eq!(*asset.data, png, "{extension}: the packaged pixels");
assert_eq!(**asset.data.embedded_bytes().unwrap(), png, "{extension}: the packaged pixels");
assert_eq!(asset.link.as_deref(), expected.to_str(), "{extension}: use the relocated Links file");
let links = opened.execute("links.list", &json!({})).unwrap();
assert_eq!(links[0]["status"], "ok", "{extension}");
+2 -2
View File
@@ -181,7 +181,7 @@ mod tests {
let Content::Graphic(g) = &d.item(id).unwrap().content else { panic!("not a graphic") };
let a = &d.assets[&g.asset];
assert_eq!(a.link, None, "embedded, not linked");
a.data.to_vec()
a.data.load().unwrap().to_vec()
}
#[test]
@@ -209,7 +209,7 @@ mod tests {
let idml = s.execute("file.exportIdml", &json!({})).unwrap();
let imported = designcraft_idml::import_idml(&base64_decode(idml["base64"].as_str().unwrap())).unwrap();
assert!(imported.assets.values().any(|a| *a.data == png), "the IDML package carries the pixels");
assert!(imported.assets.values().any(|a| **a.data.embedded_bytes().unwrap() == png), "the IDML package carries the pixels");
}
#[test]
+2 -1
View File
@@ -9,7 +9,8 @@ fn placed_pdf(s: &Session, id: designcraft_doc::ItemId) -> Option<std::sync::Arc
let d = &s.active()?.doc;
let g = d.item(id)?.graphic()?;
let a = d.assets.get(&g.asset)?;
designcraft_render::is_pdf(&a.data).then(|| a.data.clone())
let bytes = a.data.load().ok()?;
designcraft_render::is_pdf(&bytes).then_some(bytes)
}
pub fn specs() -> Vec<CommandSpec> {
+8 -8
View File
@@ -383,10 +383,10 @@ fn placed_pdf_all_on_is_distinct_from_defaults_and_survives_exports() {
let idml = s.execute("file.exportIdml", &json!({})).unwrap();
let bytes = super::super::file::base64_decode(idml["base64"].as_str().unwrap());
let imported = designcraft_idml::import_idml(&bytes).unwrap();
let pdf_asset = imported.assets.values().find(|a| designcraft_render::is_pdf(&a.data)).unwrap();
assert!(designcraft_render::pdf_layers(&pdf_asset.data).iter().all(|(_, on)| *on));
let pdf_asset = imported.assets.values().find(|a| designcraft_render::is_pdf(&a.data.load().unwrap())).unwrap();
assert!(designcraft_render::pdf_layers(&pdf_asset.data.load().unwrap()).iter().all(|(_, on)| *on));
assert!(s.execute("file.exportIdml", &json!({"embedImages":false})).is_err());
assert_eq!(s.doc().unwrap().doc.assets.values().find(|a| a.name == "off.pdf").unwrap().data.as_slice(), pdf);
assert_eq!(s.doc().unwrap().doc.assets.values().find(|a| a.name == "off.pdf").unwrap().data.load().unwrap().as_slice(), pdf);
s.execute("object.layerOptions", &json!({"ids":[id],"hidden":[]})).unwrap();
assert!(!visible(&mut s), "legacy empty list still resets defaults");
s.execute("object.layerOptions", &json!({"ids":[id],"hidden":[],"useDefaults":false})).unwrap();
@@ -412,8 +412,8 @@ fn parent_page_choices_reach_idml_variants_and_raster_export() {
// Embedded IDML: the parent-page placement gets its own variant, like a document one.
let idml = s.execute("file.exportIdml", &json!({})).unwrap();
let imported = designcraft_idml::import_idml(&super::super::file::base64_decode(idml["base64"].as_str().unwrap())).unwrap();
let asset = imported.assets.values().find(|a| designcraft_render::is_pdf(&a.data)).unwrap();
assert!(designcraft_render::pdf_layers(&asset.data).iter().all(|(_, on)| *on), "the embedded copy shows every layer");
let asset = imported.assets.values().find(|a| designcraft_render::is_pdf(&a.data.load().unwrap())).unwrap();
assert!(designcraft_render::pdf_layers(&asset.data.load().unwrap()).iter().all(|(_, on)| *on), "the embedded copy shows every layer");
// An external-only copy would keep the original file's defaults: the choice is rejected.
let err = s.execute("file.exportIdml", &json!({"embedImages": false})).unwrap_err().to_string();
assert!(err.contains("embedded images"), "{err}");
@@ -437,7 +437,7 @@ fn an_anchored_placement_keeps_its_layer_choice_in_saves_and_exports() {
id: asset,
name: "anchored.pdf".into(),
mime: "application/pdf".into(),
data: std::sync::Arc::new(pdf),
data: designcraft_doc::AssetData::embedded(pdf),
..Default::default()
});
let mut item =
@@ -464,10 +464,10 @@ fn an_anchored_placement_keeps_its_layer_choice_in_saves_and_exports() {
// Embedded IDML carries a variant that shows every layer.
let idml = s.execute("file.exportIdml", &json!({})).unwrap();
let imported = designcraft_idml::import_idml(&super::super::file::base64_decode(idml["base64"].as_str().unwrap())).unwrap();
let copies: Vec<_> = imported.assets.values().filter(|a| designcraft_render::is_pdf(&a.data)).collect();
let copies: Vec<_> = imported.assets.values().filter(|a| designcraft_render::is_pdf(&a.data.load().unwrap())).collect();
assert!(!copies.is_empty(), "the anchored placement is exported");
assert!(
copies.iter().all(|a| designcraft_render::pdf_layers(&a.data).iter().all(|(_, on)| *on)),
copies.iter().all(|a| designcraft_render::pdf_layers(&a.data.load().unwrap()).iter().all(|(_, on)| *on)),
"the embedded copy must not fall back to the file's defaults"
);
// An external-only copy would keep the original file's defaults.
+1 -1
View File
@@ -448,7 +448,7 @@ pub fn check_document(d: &Document, cache: &Cache, document: Option<&std::path::
}),
Some(a) => {
// Placed PDF and SVG pages export as vectors; `pixels` is only the preview's.
let vector = designcraft_images::is_pdf(&a.data) || designcraft_images::is_svg(&a.data);
let vector = a.data.load().is_ok_and(|b| designcraft_images::is_pdf(&b) || designcraft_images::is_svg(&b));
if let Some((pw, ph)) = a.pixels.filter(|_| !vector) {
// Measure each image axis in points, not its rotated bounding box or
// cropped frame. Either axis can fall below the minimum resolution.
+1 -1
View File
@@ -309,7 +309,7 @@ fn sample_item(
return None;
}
let asset = d.assets.get(&g.asset).filter(|a| a.is_raster())?;
let img = designcraft_images::decode_rgba(&asset.data)?;
let img = designcraft_images::decode_rgba(&asset.data.load().ok()?)?;
let (pw, ph) = img.dimensions();
let px = ((gp.x / w * f64::from(pw)) as u32).min(pw.checked_sub(1)?);
let py = ((gp.y / h * f64::from(ph)) as u32).min(ph.checked_sub(1)?);
+1 -1
View File
@@ -86,7 +86,7 @@ pub(super) fn place_graphic(s: &mut Session, p: &Value) -> Result<Value> {
name,
mime,
link,
data: std::sync::Arc::new(bytes),
data: designcraft_doc::AssetData::embedded(bytes),
pixels: Some((pw, ph)),
..Default::default()
});
+8 -1
View File
@@ -67,7 +67,14 @@ fn image(d: &mut Document, name: &str, w: u32, h: u32, hue: f32) -> AssetId {
let data = art_png(w, h, hue);
d.assets.insert(
id,
Arc::new(Asset { id, name: name.into(), mime: "image/png".into(), data: Arc::new(data), pixels: Some((w, h)), ..Default::default() }),
Arc::new(Asset {
id,
name: name.into(),
mime: "image/png".into(),
data: designcraft_doc::AssetData::embedded(data),
pixels: Some((w, h)),
..Default::default()
}),
);
id
}
@@ -1,5 +1,3 @@
use std::sync::Arc;
use designcraft_doc::{Asset, AssetId, Content, Document, Graphic, Item, ItemId, Shape, SpreadRef, build::NewDocument};
use designcraft_geom::{Affine, Rect, shapes};
use hayro_syntax::{
@@ -93,9 +91,13 @@ pub fn document(bytes: Vec<u8>, hidden_sets: &[&[&str]]) -> Document {
id: asset,
name: "layers.pdf".into(),
mime: "application/pdf".into(),
data: Arc::new(bytes),
data: designcraft_doc::AssetData::embedded(bytes),
link: None,
link_fingerprint: None,
link_relative: None,
link_hash: None,
storage: Default::default(),
proxy: None,
pixels: None,
page: 0,
pdf_crop: Default::default(),
@@ -103,7 +103,7 @@ fn two_assets(first: Vec<u8>, second: Vec<u8>) -> designcraft_doc::Document {
let mut other = original;
other.id = id;
other.name = "second.pdf".into();
other.data = Arc::new(second);
other.data = designcraft_doc::AssetData::embedded(second);
doc.add_asset(other);
let spread = Arc::make_mut(doc.spreads.first_mut().unwrap());
let item = Arc::make_mut(spread.items.last_mut().unwrap());
+12 -8
View File
@@ -303,14 +303,16 @@ impl<'a, 'f> StoryWriter<'a, 'f> {
let doc = self.doc;
let Content::Graphic(g) = &it.content else { return None };
let a = doc.assets.get(&g.asset)?;
if a.data.is_empty() {
// Linked bytes aren't read here: the graphic is left out like one without data.
let data = a.data.embedded_bytes()?.as_slice();
if data.is_empty() {
return None;
}
let mime = safe_mime(&a.mime);
// Video and sound: an HTML5 player (with the poster, when there is one).
if let Some(kind) = designcraft_doc::media_kind(mime) {
let name = asset_path("media", a, mime);
self.images.insert(name.clone(), (mime, a.data.as_slice()));
self.images.insert(name.clone(), (mime, data));
let m = it.media.clone().unwrap_or_default();
let mut attrs = String::new();
for (on, attr) in [(m.controls, " controls=\"controls\""), (m.play_on_page_load, " autoplay=\"autoplay\""), (m.looping, " loop=\"loop\"")]
@@ -319,25 +321,27 @@ impl<'a, 'f> StoryWriter<'a, 'f> {
attrs.push_str(attr);
}
}
if let Some(pa) = m.poster.and_then(|p| doc.assets.get(&p)) {
if let Some(pa) = m.poster.and_then(|p| doc.assets.get(&p))
&& let Some(poster) = pa.data.embedded_bytes()
{
let poster_mime = safe_mime(&pa.mime);
let pn = asset_path("images", pa, poster_mime);
self.images.insert(pn.clone(), (poster_mime, pa.data.as_slice()));
self.images.insert(pn.clone(), (poster_mime, poster.as_slice()));
if kind == "video" {
attrs.push_str(&format!(" poster=\"{}\"", xml_attr(&img_src(&pn, poster_mime, &pa.data))));
attrs.push_str(&format!(" poster=\"{}\"", xml_attr(&img_src(&pn, poster_mime, poster))));
}
}
let markup = format!("<{kind} src=\"{}\"{attrs}>{}</{kind}>", xml_attr(&img_src(&name, mime, &a.data)), xml_text(&a.name));
let markup = format!("<{kind} src=\"{}\"{attrs}>{}</{kind}>", xml_attr(&img_src(&name, mime, data)), xml_text(&a.name));
return Some((markup, true));
}
let name = asset_path("images", a, mime);
self.images.insert(name.clone(), (mime, a.data.as_slice()));
self.images.insert(name.clone(), (mime, data));
// Alt text from Object Export Options; without one the image counts as decorative
// (`alt=""`): a file name read aloud helps nobody.
if it.alt_text.trim().is_empty() {
self.no_alt.push(a.name.clone());
}
Some((format!("<img src=\"{}\" alt=\"{}\"/>", xml_attr(&img_src(&name, mime, &a.data)), xml_attr(&it.alt_text)), false))
Some((format!("<img src=\"{}\" alt=\"{}\"/>", xml_attr(&img_src(&name, mime, data)), xml_attr(&it.alt_text)), false))
}
/// The footnote whose reference is at byte `pos` of `st`: a note reference in `out`, the note
+4 -4
View File
@@ -1,7 +1,7 @@
use super::*;
use designcraft_doc::build::NewDocument;
use designcraft_doc::geom::{Affine, Rect, shapes};
use designcraft_doc::{Asset, AssetId, Content, Graphic, Item, ItemId, ParaFormat, Shape, SpreadRef, Story, StoryId, Table};
use designcraft_doc::{Asset, AssetData, AssetId, Content, Graphic, Item, ItemId, ParaFormat, Shape, SpreadRef, Story, StoryId, Table};
use std::io::Read;
use std::sync::Arc;
@@ -72,7 +72,7 @@ fn anchored_objects_are_exported_in_place() {
name: "dot.png".into(),
mime: "image/png".into(),
link: None,
data: Arc::new(vec![7]),
data: AssetData::embedded(vec![7]),
pixels: Some((1, 1)),
..Default::default()
}),
@@ -587,7 +587,7 @@ fn images_get_readable_names_and_no_file_name_alt() {
name: name.into(),
mime: mime.into(),
link: None,
data: Arc::new(vec![1]),
data: AssetData::embedded(vec![1]),
pixels: Some((1, 1)),
..Default::default()
}),
@@ -704,7 +704,7 @@ fn metadata_is_serialized_for_its_output_context() {
id: aid,
name: "Cover & Notes".into(),
mime: "image/png; profile=screen".into(),
data: Arc::new(vec![1]),
data: AssetData::embedded(vec![1]),
pixels: Some((1, 1)),
..Default::default()
}),
@@ -91,7 +91,13 @@ fn nested_and_item_rotations_are_combined_before_measuring_graphics() {
mime: "image/svg+xml".into(),
link: None,
link_fingerprint: None,
data: Arc::new(br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" fill="red"/></svg>"#.to_vec()),
link_relative: None,
link_hash: None,
storage: Default::default(),
proxy: None,
data: designcraft_doc::AssetData::embedded(
br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" fill="red"/></svg>"#.to_vec(),
),
pixels: None,
page: 0,
pdf_crop: Default::default(),
+14 -6
View File
@@ -6,7 +6,7 @@ use std::collections::HashMap;
use std::hash::BuildHasher;
use std::sync::Arc;
use designcraft_doc::{AssetId, Document};
use designcraft_doc::{AssetData, AssetId, Document};
#[cfg(test)]
use serde_json::Value;
#[cfg(test)]
@@ -31,7 +31,14 @@ pub fn save_with_preview(doc: &Document, preview: Option<&[u8]>) -> Result<Vec<u
}
let mut compact = doc.clone();
compact.compact_assets();
let owned_assets = compact.assets.values().map(|asset| (asset.id.0.to_string(), asset.mime.clone(), asset.data.clone())).collect::<Vec<_>>();
let owned_assets = compact
.assets
.values()
.map(|asset| {
let data = asset.data.load().map_err(|e| FormatError::Unsaveable(format!("image {}: {e}", asset.name)))?;
Ok((asset.id.0.to_string(), asset.mime.clone(), data))
})
.collect::<Result<Vec<_>, FormatError>>()?;
let assets = owned_assets.iter().map(|(id, mime, data)| craft_designcraft_format::Asset { id, mime, data }).collect::<Vec<_>>();
craft_designcraft_format::save(&compact, concat!("DesignCraft ", env!("CARGO_PKG_VERSION")), preview, &assets)
}
@@ -51,7 +58,8 @@ pub fn write<W: std::io::Write>(out: &mut W, doc: &Document, preview: Option<&[u
#[cfg(test)]
fn required_version(doc: &Document) -> u32 {
let Ok(value) = serde_json::to_value(doc) else { return VERSION };
let owned_assets = doc.assets.values().map(|asset| (asset.id.0.to_string(), asset.mime.clone(), asset.data.clone())).collect::<Vec<_>>();
let owned_assets =
doc.assets.values().map(|asset| (asset.id.0.to_string(), asset.mime.clone(), asset.data.load().unwrap_or_default())).collect::<Vec<_>>();
let assets = owned_assets.iter().map(|(id, mime, data)| craft_designcraft_format::Asset { id, mime, data }).collect::<Vec<_>>();
craft_designcraft_format::required_version(&value, &assets)
}
@@ -61,16 +69,16 @@ pub fn load(bytes: &[u8]) -> Result<Document, FormatError> {
let loaded = craft_designcraft_format::load::<Document>(bytes)?;
let mut doc = loaded.document;
let fingerprint = std::collections::hash_map::RandomState::new();
let mut dedup: HashMap<u64, Vec<Arc<Vec<u8>>>> = HashMap::new();
let mut dedup: HashMap<u64, Vec<AssetData>> = HashMap::new();
for (raw_id, bytes) in loaded.assets {
let Ok(id) = raw_id.parse::<u64>() else { continue };
let asset_id = AssetId(id);
let Some(asset) = doc.assets.get_mut(&asset_id) else { continue };
let same = dedup.entry(fingerprint.hash_one(bytes.as_slice())).or_default();
let data = match same.iter().find(|data| ***data == bytes) {
let data = match same.iter().find(|data| data.embedded_bytes().is_some_and(|b| **b == bytes)) {
Some(data) => data.clone(),
None => {
let data = Arc::new(bytes);
let data = AssetData::embedded(bytes);
same.push(data.clone());
data
}
+32 -14
View File
@@ -3,13 +3,19 @@ use std::io::{Cursor, Read, Write};
use designcraft_doc::build::NewDocument;
use designcraft_doc::geom::Rect;
use designcraft_doc::{Asset, ParaFormat, SpreadRef};
use designcraft_doc::{Asset, AssetData, ParaFormat, SpreadRef};
use serde_json::json;
fn png_asset(d: &mut Document, data: &[u8]) -> AssetId {
let aid = AssetId(d.alloc());
let a =
Asset { id: aid, name: "x.png".into(), mime: "image/png".into(), data: Arc::new(data.to_vec()), pixels: Some((1, 1)), ..Default::default() };
let a = Asset {
id: aid,
name: "x.png".into(),
mime: "image/png".into(),
data: AssetData::embedded(data.to_vec()),
pixels: Some((1, 1)),
..Default::default()
};
d.assets.insert(aid, Arc::new(a));
aid
}
@@ -51,7 +57,7 @@ fn save_uncompacted(d: &Document) -> Vec<u8> {
z.write_all(&serde_json::to_vec(d).unwrap()).unwrap();
for (id, a) in &d.assets {
z.start_file(format!("assets/{}.png", id.0), opts).unwrap();
z.write_all(&a.data).unwrap();
z.write_all(&a.data.load().unwrap()).unwrap();
}
z.finish().unwrap();
buf.into_inner()
@@ -67,7 +73,7 @@ fn explicit_all_on_pdf_layers_require_a_capable_reader_only_when_needed() {
id: aid,
name: "art.pdf".into(),
mime: "application/pdf".into(),
data: Arc::new(b"%PDF-1.7".to_vec()),
data: AssetData::embedded(b"%PDF-1.7".to_vec()),
..Default::default()
}),
);
@@ -98,7 +104,7 @@ fn one_version_4_reader_keeps_both_explicit_all_on_and_graphic_layer_options() {
id: pdf,
name: "art.pdf".into(),
mime: "application/pdf".into(),
data: Arc::new(b"%PDF-1.7".to_vec()),
data: AssetData::embedded(b"%PDF-1.7".to_vec()),
..Default::default()
}),
);
@@ -106,7 +112,13 @@ fn one_version_4_reader_keeps_both_explicit_all_on_and_graphic_layer_options() {
let source = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EOF\n".to_vec();
d.assets.insert(
eps,
Arc::new(Asset { id: eps, name: "logo.eps".into(), mime: "application/postscript".into(), data: Arc::new(source), ..Default::default() }),
Arc::new(Asset {
id: eps,
name: "logo.eps".into(),
mime: "application/postscript".into(),
data: AssetData::embedded(source),
..Default::default()
}),
);
let all_on = place(&mut d, pdf, 0.0);
let layered = place(&mut d, eps, 0.0);
@@ -166,7 +178,7 @@ fn eps_source_requires_new_reader_but_preview_documents_keep_the_older_one() {
name: "logo.eps".into(),
mime: "image/png".into(),
link: None,
data: Arc::new(source.clone()),
data: AssetData::embedded(source.clone()),
pixels: Some((40, 20)),
page: 0,
..Default::default()
@@ -174,8 +186,8 @@ fn eps_source_requires_new_reader_but_preview_documents_keep_the_older_one() {
);
place(&mut d, aid, 0.0);
assert_eq!(version(&d), 3, "sniff source even when legacy metadata says PNG");
assert_eq!(load(&save(&d).unwrap()).unwrap().assets[&aid].data.as_slice(), source);
Arc::make_mut(d.assets.get_mut(&aid).unwrap()).data = Arc::new(b"\x89PNG\r\n\x1a\npreview".to_vec());
assert_eq!(load(&save(&d).unwrap()).unwrap().assets[&aid].data.load().unwrap().as_slice(), source);
Arc::make_mut(d.assets.get_mut(&aid).unwrap()).data = AssetData::embedded(b"\x89PNG\r\n\x1a\npreview".to_vec());
assert_eq!(version(&d), 2, "legacy EPS previews stay compatible");
}
@@ -188,7 +200,13 @@ fn one_place_picks_the_oldest_reader_that_keeps_the_document() {
let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EOF\n".to_vec();
d.assets.insert(
aid,
Arc::new(Asset { id: aid, name: "logo.eps".into(), mime: "application/postscript".into(), data: Arc::new(eps), ..Default::default() }),
Arc::new(Asset {
id: aid,
name: "logo.eps".into(),
mime: "application/postscript".into(),
data: AssetData::embedded(eps),
..Default::default()
}),
);
let id = place(&mut d, aid, 0.0);
assert_eq!(required_version(&d), 3, "EPS sources alone keep the version 3 reader");
@@ -209,7 +227,7 @@ fn roundtrip_with_assets() {
assert!(bytes.starts_with(b"PK"));
let back = load(&bytes).unwrap();
assert_eq!(back.page_count(), 3);
assert_eq!(*back.assets[&aid].data, vec![1, 2, 3, 4]);
assert_eq!(**back.assets[&aid].data.embedded_bytes().unwrap(), vec![1, 2, 3, 4]);
assert_eq!(back.stories, d.stories);
}
@@ -407,8 +425,8 @@ fn equal_size_archive_assets_preserve_content_and_share_only_identical_buffers()
let back = load(&save_uncompacted(&d)).unwrap();
assert_eq!(back.assets.len(), 256);
for (n, (a, b)) in pairs.into_iter().enumerate() {
assert_eq!(back.assets[&a].data[1016..], (n as u64).to_le_bytes());
assert!(Arc::ptr_eq(&back.assets[&a].data, &back.assets[&b].data));
assert_eq!(back.assets[&a].data.embedded_bytes().unwrap()[1016..], (n as u64).to_le_bytes());
assert!(back.assets[&a].data.key() == back.assets[&b].data.key());
assert_eq!(back.assets[&a].link, d.assets[&a].link);
assert_eq!(back.assets[&b].link, d.assets[&b].link);
}
+7 -5
View File
@@ -283,7 +283,9 @@ impl<'a> Ex<'a> {
fn image_el(&mut self, asset: &designcraft_doc::Asset, g: &designcraft_doc::Graphic) -> El {
let is_pdf = asset.mime == "application/pdf";
let is_eps = asset.mime == "application/postscript" || designcraft_images::is_eps(&asset.data);
// Bytes that can't be had are written like a link without data.
let data = asset.data.load().unwrap_or_default();
let is_eps = asset.mime == "application/postscript" || designcraft_images::is_eps(&data);
let mut el = El::new(if is_eps {
"EPS"
} else if is_pdf {
@@ -308,10 +310,10 @@ impl<'a> Ex<'a> {
el.set("FillColor", id.clone());
el.set("FillTint", pct(g.fill.tint as f64));
}
let wanted = !asset.data.is_empty() && (self.opts.embed_images || asset.link.is_none());
let wanted = !data.is_empty() && (self.opts.embed_images || asset.link.is_none());
// An image with no link of its own has nowhere else to live, so it is always embedded
// (dropping its data would lose it); only an image with a real link can fall back to it.
let embed = wanted && (asset.link.is_none() || self.embedded.saturating_add(asset.data.len()) <= self.embed_limit);
let embed = wanted && (asset.link.is_none() || self.embedded.saturating_add(data.len()) <= self.embed_limit);
if wanted && !embed {
self.warnings.push(format!(
"image {} is written as a link, not embedded: the package already holds {} bytes of images (limit {})",
@@ -320,9 +322,9 @@ impl<'a> Ex<'a> {
}
let mut props = Vec::new();
if embed {
self.embedded = self.embedded.saturating_add(asset.data.len());
self.embedded = self.embedded.saturating_add(data.len());
let mut c = El::new("Contents");
c.children.push(Node::CData(base64_encode(&asset.data)));
c.children.push(Node::CData(base64_encode(&data)));
props.push(c);
}
props.push(El::new("GraphicBounds").attr("Left", "0").attr("Top", "0").attr("Right", num(g.size.0)).attr("Bottom", num(g.size.1)));
+2 -2
View File
@@ -302,7 +302,7 @@ impl<'r> Importer<'r> {
// file once.
let key = (p.clone(), page, pdf_crop);
if let Some(id) = self.linked_assets.get(&key) {
let size = self.assets.get(id).and_then(|a| craft_svg::sniff::viewbox_size(&a.data)).unwrap_or(size);
let size = self.assets.get(id).and_then(|a| craft_svg::sniff::viewbox_size(a.data.embedded_bytes()?)).unwrap_or(size);
return graphic(*id, size);
}
if let Some(d) = (self.read_link)(p) {
@@ -334,7 +334,7 @@ impl<'r> Importer<'r> {
name,
mime,
link: link_path,
data: preview.cloned().unwrap_or_else(|| Arc::new(data)),
data: designcraft_doc::AssetData::embedded(preview.cloned().unwrap_or_else(|| Arc::new(data))),
pixels,
pdf_crop,
..Default::default()
+6 -3
View File
@@ -4478,7 +4478,7 @@ fn missing_linked_graphics_show_their_preview_and_keep_their_link() {
let d = crate::import_idml_report_with_previews(&fixture(), &|_| None, &previews).unwrap().document;
let img = d.spreads.iter().flat_map(|sp| sp.items.iter()).find(|i| i.graphic().is_some()).unwrap();
let a = &d.assets[&img.graphic().unwrap().asset];
assert_eq!(a.data.as_slice(), png.as_slice(), "the preview shows");
assert_eq!(a.data.load().unwrap().as_slice(), png.as_slice(), "the preview shows");
assert_eq!(a.mime, "image/jpeg", "the link's format, not the preview's");
assert_eq!(a.link.as_deref(), Some("/definitely/missing dir/photo.jpg"), "relinking still finds the file");
// Without a preview it stays empty, as before.
@@ -4522,7 +4522,10 @@ fn a_preview_keeps_the_links_format_and_pixel_size_and_is_shared() {
// 40 × 30 pt at 300 ppi, from the link, not the preview's 4 × 3.
assert_eq!((photo.mime.as_str(), photo.pixels), ("image/jpeg", Some((167, 125))));
assert_eq!(logo.mime, "application/pdf", "a placed PDF stays a PDF, for IDML export");
assert!(std::sync::Arc::ptr_eq(&photo.data, &png) && std::sync::Arc::ptr_eq(&logo.data, &png), "the preview's bytes are not copied");
assert!(
std::sync::Arc::ptr_eq(photo.data.embedded_bytes().unwrap(), &png) && std::sync::Arc::ptr_eq(logo.data.embedded_bytes().unwrap(), &png),
"the preview's bytes are not copied"
);
}
#[test]
@@ -5229,7 +5232,7 @@ fn exports_graphic_in_table_cell() {
id: asset,
name: "tick.png".into(),
mime: "image/png".into(),
data: std::sync::Arc::new(vec![1, 2, 3]),
data: designcraft_doc::AssetData::embedded(vec![1, 2, 3]),
..Default::default()
});
let mut t = designcraft_doc::Table::new(d.alloc(), 2, 2, 0, 0, 200.0);
+1 -1
View File
@@ -430,5 +430,5 @@ fn an_unlinked_image_is_embedded_past_the_embed_limit() {
let (out, warnings) = crate::export::export_with_limit(&d, &ExportOptions { embed_images: true }, 100);
assert!(warnings.is_empty(), "{warnings:?}");
let back = import_idml_with(&out, &|_: &str| None).unwrap();
assert!(!back.assets.is_empty() && back.assets.values().all(|a| a.data.as_slice() == bytes.as_slice()));
assert!(!back.assets.is_empty() && back.assets.values().all(|a| a.data.load().unwrap().as_slice() == bytes.as_slice()));
}
+27 -16
View File
@@ -65,17 +65,25 @@ impl Exporter<'_> {
return i.clone();
}
let asset = self.doc.assets.get(&id)?.clone();
let source = match asset.data.load() {
Ok(bytes) => bytes,
Err(e) => {
self.warn(format!("image `{}` was skipped: {e}", asset.name));
self.images.insert(id, None);
return None;
}
};
// Use the same EPS preview as the renderer, preserving CMYK TIFF inks below. The
// content cache is keyed on the bytes actually converted, so equal previews share one.
let data = if designcraft_images::is_eps(&asset.data) {
let Some((proxy, _)) = designcraft_images::eps_proxy(&asset.data) else {
let data = if designcraft_images::is_eps(&source) {
let Some((proxy, _)) = designcraft_images::eps_proxy(&source) else {
self.warn(format!("image `{}` could not be decoded and was skipped", asset.name));
self.images.insert(id, None);
return None;
};
Arc::new(proxy)
} else {
asset.data.clone()
source
};
if let Some(converted) = self.images_by_content.get(&data).cloned() {
if let Some(warning) = converted.warning {
@@ -190,7 +198,10 @@ impl Exporter<'_> {
/// A placed PDF goes in as vectors (the page as a form XObject). `false` when `g` is not one.
fn graphic_placed_pdf(&mut self, s: &mut Surface, g: &designcraft_doc::Graphic, hidden: Option<&[String]>) -> bool {
let doc = self.doc;
let Some(asset) = doc.assets.get(&g.asset).filter(|asset| asset.data.starts_with(b"%PDF")) else { return false };
let Some((asset, bytes)) = doc.assets.get(&g.asset).and_then(|a| Some((a, a.data.load().ok()?))).filter(|(_, b)| b.starts_with(b"%PDF"))
else {
return false;
};
// Cropped to a box: the whole page, placed so that the box spans graphic space.
let shown = asset.shown_box();
let [l, t, r, b] = shown.unwrap_or([0.0, 0.0, 1.0, 1.0]);
@@ -204,7 +215,7 @@ impl Exporter<'_> {
{
self.pdf_preparations += 1;
}
match crate::optional_content::prepare(&asset.data, hidden) {
match crate::optional_content::prepare(&bytes, hidden) {
Ok(variant) => {
self.variants.insert(request, variant.clone());
variant
@@ -259,19 +270,19 @@ impl Exporter<'_> {
/// A placed SVG goes in as vectors too. `false` when `g` is not one.
fn graphic_placed_svg(&mut self, s: &mut Surface, g: &designcraft_doc::Graphic) -> bool {
let Some(asset) = self.doc.assets.get(&g.asset).filter(|asset| designcraft_images::is_svg(&asset.data)) else { return false };
let Some((asset, bytes)) =
self.doc.assets.get(&g.asset).and_then(|a| Some((a, a.data.load().ok()?))).filter(|(_, b)| designcraft_images::is_svg(b))
else {
return false;
};
let Some(size) = Size::from_wh(g.size.0.max(1e-3) as f32, g.size.1.max(1e-3) as f32) else { return true };
let tree = self
.svgs
.entry(g.asset)
.or_insert_with(|| {
craft_pdf::write::svg::Document::parse(
&asset.data,
craft_pdf::write::svg::Limits::default(),
crate::svg_options::svg_options_for_pdf(),
)
.ok()
.map(Arc::new)
craft_pdf::write::svg::Document::parse(&bytes, craft_pdf::write::svg::Limits::default(), crate::svg_options::svg_options_for_pdf())
.ok()
.map(Arc::new)
})
.clone();
match tree {
@@ -295,10 +306,10 @@ impl Exporter<'_> {
if g.fill.is_none() {
return false;
}
let Some(asset) = self.doc.assets.get(&g.asset) else { return false };
let Some(tone) = designcraft_images::tone(&asset.data) else { return false };
let Some(bytes) = self.doc.assets.get(&g.asset).and_then(|a| a.data.load().ok()) else { return false };
let Some(tone) = designcraft_images::tone(&bytes) else { return false };
let Some(paint) = self.swatch_color(&g.fill.swatch, g.fill.tint) else { return false };
let Some(coverage) = designcraft_images::decode_rgba(&asset.data).and_then(|img| CoverageImage::new(&img)) else { return false };
let Some(coverage) = designcraft_images::decode_rgba(&bytes).and_then(|img| CoverageImage::new(&img)) else { return false };
let interpolate = RasterMode::for_standard(self.opts.standard).interpolate;
let Ok(mask) = Image::from_custom(coverage, interpolate) else { return false };
let size = Size::from_wh(g.size.0.max(1e-3) as f32, g.size.1.max(1e-3) as f32);
+20 -11
View File
@@ -208,7 +208,16 @@ mod image_content_cache_tests {
}
fn asset(doc: &mut Document, bytes: Vec<u8>, name: &str) -> AssetId {
let id = AssetId(doc.alloc());
doc.assets.insert(id, Arc::new(Asset { id, name: name.into(), mime: "image/png".into(), data: Arc::new(bytes), ..Default::default() }));
doc.assets.insert(
id,
Arc::new(Asset {
id,
name: name.into(),
mime: "image/png".into(),
data: designcraft_doc::AssetData::embedded(bytes),
..Default::default()
}),
);
id
}
fn exporter<'a>(doc: &'a Document, cache: &'a Cache, opts: &'a PdfOptions) -> Exporter<'a> {
@@ -283,7 +292,7 @@ mod image_content_cache_tests {
// a, b, and a with its layer hidden (a copy of its own).
assert_eq!(ex.pdfs.len(), 3);
let variant = ex.variants.get(&(a, None)).unwrap();
assert!(Arc::ptr_eq(&variant.data, &doc.assets[&a].data));
assert!(Arc::ptr_eq(&variant.data, doc.assets[&a].data.embedded_bytes().unwrap()));
surface.finish();
page.finish();
assert!(writer.finish().is_ok());
@@ -298,14 +307,14 @@ mod image_content_cache_tests {
let b1 = asset(&mut d, blue.clone(), "b1");
let a2 = asset(&mut d, red, "a2");
let b2 = asset(&mut d, blue, "b2");
assert!(!Arc::ptr_eq(&d.assets[&a1].data, &d.assets[&a2].data));
assert!(!Arc::ptr_eq(d.assets[&a1].data.embedded_bytes().unwrap(), d.assets[&a2].data.embedded_bytes().unwrap()));
let cache = Cache::new();
let opts = PdfOptions::default();
let mut ex = exporter(&d, &cache, &opts);
let mut h1 = ex.images_by_content.hasher().build_hasher();
h1.write(&d.assets[&a1].data);
h1.write(d.assets[&a1].data.embedded_bytes().unwrap());
let mut h2 = ex.images_by_content.hasher().build_hasher();
h2.write(&d.assets[&b1].data);
h2.write(d.assets[&b1].data.embedded_bytes().unwrap());
assert_eq!(h1.finish(), h2.finish());
assert_ne!(d.assets[&a1].data, d.assets[&b1].data);
for (id, count, size) in [(a1, 1, (2, 1)), (b1, 2, (1, 2)), (a2, 2, (2, 1)), (a1, 2, (2, 1)), (b2, 2, (1, 2))] {
@@ -314,8 +323,8 @@ mod image_content_cache_tests {
}
assert_eq!(ex.images.len(), 4);
assert_eq!(ex.images_by_content.len(), 2);
let first_key = ex.images_by_content.keys().find(|key| key.as_slice() == d.assets[&a1].data.as_slice()).unwrap();
assert!(Arc::ptr_eq(first_key, &d.assets[&a1].data), "cache keys share the original encoded payload");
let first_key = ex.images_by_content.keys().find(|key| key.as_slice() == d.assets[&a1].data.embedded_bytes().unwrap().as_slice()).unwrap();
assert!(Arc::ptr_eq(first_key, d.assets[&a1].data.embedded_bytes().unwrap()), "cache keys share the original encoded payload");
ex.images_by_content.clear();
assert_eq!(ex.load_image(a1).unwrap().size(), (2, 1));
assert_eq!(ex.image_conversions, 2, "the AssetId cache remains the first lookup");
@@ -446,8 +455,8 @@ mod image_content_cache_tests {
let bytes = png(2, 1, [20, 30, 40, 255]);
let a = asset(&mut d, bytes.clone(), "a.png");
let b = asset(&mut d, bytes, "b.png");
let payload = Arc::downgrade(&d.assets[&a].data);
let duplicate = Arc::downgrade(&d.assets[&b].data);
let payload = Arc::downgrade(d.assets[&a].data.embedded_bytes().unwrap());
let duplicate = Arc::downgrade(d.assets[&b].data.embedded_bytes().unwrap());
let cache = Cache::new();
let opts = PdfOptions::default();
let mut ex = exporter(&d, &cache, &opts);
@@ -455,7 +464,7 @@ mod image_content_cache_tests {
assert!(ex.load_image(b).is_some());
assert_eq!(duplicate.strong_count(), 1, "a content hit must not retain the second encoded allocation");
let key = ex.images_by_content.keys().next().unwrap();
assert!(Arc::ptr_eq(key, &d.assets[&a].data));
assert!(Arc::ptr_eq(key, d.assets[&a].data.embedded_bytes().unwrap()));
drop(ex);
drop(d);
assert!(payload.upgrade().is_none(), "no converted image or encoded key outlives this exporter");
@@ -494,7 +503,7 @@ mod eps_tests {
name: "cmyk.eps".into(),
mime: "application/postscript".into(),
link: None,
data: Arc::new(eps),
data: designcraft_doc::AssetData::embedded(eps),
pixels: Some((2, 1)),
page: 0,
..Default::default()
+4 -2
View File
@@ -50,9 +50,11 @@ pub(crate) fn collect_media(doc: &Document, sheets: &[Sheet]) -> Vec<PlacedMedia
let mut out = Vec::new();
visit(doc, sheets, |pi, r, it| {
if let (Some(m), designcraft_doc::Content::Graphic(g)) = (&it.media, &it.content)
&& let Some(a) = doc.assets.get(&g.asset).filter(|a| !a.data.is_empty())
&& let Some(a) = doc.assets.get(&g.asset)
&& let Ok(data) = a.data.load()
&& !data.is_empty()
{
out.push((pi, r, a.name.clone(), a.mime.clone(), a.data.clone(), m.clone()));
out.push((pi, r, a.name.clone(), a.mime.clone(), data, m.clone()));
}
});
out
+8 -3
View File
@@ -1,13 +1,18 @@
use super::{
Affine, Arc, AssetId, BookletOptions, Cache, Content, Document, Item, PdfOptions, Rect, SpreadRef, Standard, export_booklet, export_pdf,
merge_pdfs,
Affine, AssetId, BookletOptions, Cache, Content, Document, Item, PdfOptions, Rect, SpreadRef, Standard, export_booklet, export_pdf, merge_pdfs,
};
fn placed_document(source: Vec<u8>) -> Document {
use designcraft_doc::{Asset, Graphic, ItemId, Shape};
let mut doc = Document::new(&Default::default());
let asset = AssetId(doc.alloc());
doc.add_asset(Asset { id: asset, name: "placed.pdf".into(), mime: "application/pdf".into(), data: Arc::new(source), ..Default::default() });
doc.add_asset(Asset {
id: asset,
name: "placed.pdf".into(),
mime: "application/pdf".into(),
data: designcraft_doc::AssetData::embedded(source),
..Default::default()
});
let mut item =
Item::new(ItemId(doc.alloc()), doc.default_layer(), Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)));
item.content = Content::Graphic(Graphic {
@@ -25,7 +25,17 @@ fn place(d: &mut Document, data: Vec<u8>, x: f64) {
fn place_named(d: &mut Document, data: Vec<u8>, x: f64, name: &str, mime: &str, page: u32) {
let aid = AssetId(d.alloc());
d.assets.insert(aid, Arc::new(Asset { id: aid, name: name.into(), mime: mime.into(), data: Arc::new(data), page, ..Default::default() }));
d.assets.insert(
aid,
Arc::new(Asset {
id: aid,
name: name.into(),
mime: mime.into(),
data: designcraft_doc::AssetData::embedded(data),
page,
..Default::default()
}),
);
let id = ItemId(d.alloc());
let mut item = Item::new(id, d.default_layer(), Shape::Rectangle, shapes::rectangle(Rect::new(x, 36.0, x + 20.0, 56.0)));
item.stroke.weight = 0.0;
+3 -1
View File
@@ -560,7 +560,9 @@ impl<'a> Exporter<'a> {
fn convert(&mut self, asset: AssetId) -> Option<(String, Option<String>)> {
let a = self.doc.assets.get(&asset)?.clone();
let data: &[u8] = &a.data;
// Bytes that can't be had are left out like a missing file.
let bytes = a.data.load().unwrap_or_default();
let data: &[u8] = &bytes;
if data.is_empty() {
self.warn(format!("`{}` is missing (linked file not found); left out", a.name));
return None;
+1 -1
View File
@@ -202,7 +202,7 @@ fn pictures_are_cropped_to_their_frames() {
name: "red.png".into(),
mime: "image/png".into(),
link: None,
data: Arc::new(png(200, 100)),
data: designcraft_doc::AssetData::embedded(png(200, 100)),
pixels: Some((200, 100)),
..Default::default()
}),
+6 -2
View File
@@ -1772,7 +1772,11 @@ fn eyedropper_samples_a_placed_image_and_matches_other_objects() {
mime: "image/png".into(),
link: None,
link_fingerprint: None,
data: std::sync::Arc::new(vec![]),
link_relative: None,
link_hash: None,
storage: Default::default(),
proxy: None,
data: designcraft_doc::AssetData::embedded(vec![]),
pixels: Some((100, 100)),
page: 0,
pdf_crop: Default::default(),
@@ -2151,7 +2155,7 @@ fn eyedropper_ignores_inactive_states_of_a_multi_state_object() {
name: "photo.png".into(),
mime: "image/png".into(),
link: None,
data: std::sync::Arc::new(vec![]),
data: designcraft_doc::AssetData::embedded(vec![]),
pixels: Some((100, 100)),
..Default::default()
}),
+1 -1
View File
@@ -610,7 +610,7 @@ pub(crate) mod browser_import_tests {
assert_eq!(app.session.active_index(), Some(0));
let asset = app.session.doc().unwrap().doc.assets.values().next().unwrap();
assert_eq!(asset.page, 1);
assert_eq!(*asset.data, bytes);
assert_eq!(**asset.data.embedded_bytes().unwrap(), bytes);
assert!(app.pending_pdf.is_none());
assert!(app.session.documents()[1].doc.assets.is_empty());
app.run("place.drop", json!({"x": 36, "y": 36})).unwrap();
+2 -2
View File
@@ -442,14 +442,14 @@ fn loaded_thumb_texture(ctx: &egui::Context, doc: &Document, asset: designcraft_
pub(super) fn proxy_texture(
ctx: &egui::Context,
name: &str,
data: &std::sync::Arc<Vec<u8>>,
data: &designcraft_doc::AssetData,
page: u32,
width_pt: f64,
scale: f64,
max_side: usize,
) -> Option<egui::TextureHandle> {
use designcraft_render::images::{Mip, generation, mip_now};
let key = egui::Id::unique((name, std::sync::Arc::as_ptr(data) as usize, data.len(), page));
let key = egui::Id::unique((name, data.key(), page));
// `None`: the level asked for; else the generation a stand-in was drawn at.
let cached = ctx.data(|d| d.get_temp::<(egui::TextureHandle, Option<u64>)>(key));
if let Some((t, stand_in)) = &cached
@@ -39,7 +39,7 @@ fn linked_graphics(paths: &[Option<&str>]) -> (DesignApp, Vec<designcraft_doc::I
name: "photo.svg".into(),
mime: "image/svg+xml".into(),
link: path.map(str::to_owned),
data: Arc::new(
data: designcraft_doc::AssetData::embedded(
br##"<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32" fill="#4488cc"/></svg>"##
.to_vec(),
),