From 7a2efd9c6d5a20bb9df6080f4392154c8eb9652d Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 14 Nov 2026 12:47:39 +0000 Subject: [PATCH] DES-10 review: IDML stored previews are checked before they become proxies A linked graphic's stored preview is decoded, re-encoded within the proxy limits when bigger (1024 px, 512 KiB) and dropped when it doesn't decode. It is used only when it shows the whole raster image; otherwise (a PDF, whose preview shows its crop, or other proportions) the proxy is made from the file, with the preview as the fallback. Graphics that stay embedded keep no proxy. --- crates/engine/src/cmd/interchange.rs | 95 ++++++++++++++++++++++++++-- crates/render/src/prepare.rs | 1 + 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/interchange.rs b/crates/engine/src/cmd/interchange.rs index 8c36bc159..940404aa9 100644 --- a/crates/engine/src/cmd/interchange.rs +++ b/crates/engine/src/cmd/interchange.rs @@ -205,6 +205,9 @@ fn import_report_with_previews( // size differs from the file's, or that share no read) stay embedded. if asset.data.embedded_bytes().is_some_and(|b| b.len() as u64 == len) { link_read_file(std::sync::Arc::make_mut(asset), package); + } else if asset.proxy.is_some() { + // An embedded graphic draws its own bytes. + std::sync::Arc::make_mut(asset).proxy = None; } } } @@ -215,16 +218,61 @@ fn import_report_with_previews( } /// `asset`, whose embedded bytes were just read from the file its link names, as a linked asset -/// ([`super::links::link_to_file`]), drawn with the document's preview while the file can't be -/// read when the importer kept one. +/// ([`super::links::link_to_file`]), drawn while the file can't be read with a proxy: the +/// document's preview of it when that shows the whole image ([`preview_proxy`], +/// [`shows_whole`]), else one made from the file, else the preview. #[cfg(not(target_arch = "wasm32"))] fn link_read_file(asset: &mut designcraft_doc::Asset, package: Option<&std::path::Path>) { let Some(bytes) = asset.data.embedded_bytes().cloned() else { return }; let Some(path) = asset.link.clone() else { return }; - let preview = asset.proxy.take(); + let preview = asset.proxy.take().and_then(|p| preview_proxy(&p)); + let (whole, fallback) = match preview { + Some(p) if shows_whole(asset, &bytes, &p) => (Some(p), None), + p => (None, p), + }; // The fingerprint kept by the read (none for a file just written: its hash tells its bytes). let file = super::links::FileLink::of(std::path::Path::new(&path), asset.link_fingerprint); - super::links::link_to_file(asset, bytes, file, package, preview); + super::links::link_to_file(asset, bytes, file, package, whole); + if asset.storage == designcraft_doc::Storage::Linked && asset.proxy.is_none() { + asset.proxy = fallback; + } +} + +/// A document's stored preview of a linked graphic as a proxy: checked to decode, and made again +/// within the proxy limits ([`designcraft_render::proxy`]) when it is bigger or in another +/// format (a larger one wouldn't be kept by the saved document). `None` when it doesn't decode. +#[cfg(not(target_arch = "wasm32"))] +fn preview_proxy(preview: &designcraft_doc::Proxy) -> Option { + use designcraft_render::proxy::{MAX_BYTES, MAX_SIDE_PX, make_proxy}; + let bytes = preview.data.embedded_bytes()?; + let mime = designcraft_render::image_mime(bytes); + let fits = bytes.len() <= MAX_BYTES + && matches!(mime, "image/jpeg" | "image/png") + && designcraft_render::image_size(bytes).is_some_and(|(w, h)| w.max(h) <= MAX_SIDE_PX); + if fits && let Some(pm) = designcraft_render::decode_pixmap(bytes) { + let pixels = (u32::from(pm.width()), u32::from(pm.height())); + return Some(designcraft_doc::Proxy { mime: mime.into(), pixels, data: preview.data.clone() }); + } + let p = make_proxy(bytes, 0)?; + Some(designcraft_doc::Proxy { mime: p.mime.into(), pixels: p.pixels, data: designcraft_doc::AssetData::embedded(p.data) }) +} + +/// Whether `preview` shows the whole of `asset`'s image (`bytes`, its file's), as a proxy must: +/// a raster whose preview has the image's proportions. A PDF's preview shows its crop, which may +/// not be the page the proxy stands for, and other files' proportions aren't known here. +#[cfg(not(target_arch = "wasm32"))] +fn shows_whole(asset: &designcraft_doc::Asset, bytes: &[u8], preview: &designcraft_doc::Proxy) -> bool { + if designcraft_render::is_pdf(bytes) || asset.mime == "application/pdf" { + return false; + } + let Some((w, h)) = designcraft_render::image_size(bytes) else { return false }; + let (pw, ph) = preview.pixels; + if w == 0 || h == 0 || pw == 0 || ph == 0 { + return false; + } + // Proportions equal but for the preview's rounding to whole pixels. + let (ratio, preview_ratio) = (f64::from(w) / f64::from(h), f64::from(pw) / f64::from(ph)); + (preview_ratio / ratio - 1.0).abs() <= 1.0 / f64::from(pw) + 1.0 / f64::from(ph) + 0.01 } /// Placed PDFs read from IDML name their crop (`PDFCrop`): find where each box sits on its page, @@ -628,6 +676,45 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + /// A document's stored preview stands in for a linked file only when it decodes and shows + /// the whole image, within the proxy limits (made again when bigger); otherwise the proxy is + /// made from the file. + #[test] + fn a_stored_preview_is_the_proxy_only_when_it_decodes_and_shows_the_whole_image() { + use designcraft_render::proxy::{MAX_BYTES, MAX_SIDE_PX}; + let dir = std::env::temp_dir().join(format!("dc-idml-previews-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("photo.png"); + let png = |width: u32, height: u32| designcraft_render::Rendered { width, height, pixels: vec![255; (width * height * 4) as usize] }.to_png(); + let original = png(400, 200); + std::fs::write(&path, &original).unwrap(); + let linked_with = |preview: Vec| { + let mut a = designcraft_doc::Asset { + name: "photo.png".into(), + mime: "image/png".into(), + link: Some(path.to_str().unwrap().into()), + data: designcraft_doc::AssetData::embedded(original.clone()), + pixels: Some((400, 200)), + proxy: Some(designcraft_doc::Proxy { mime: "image/png".into(), pixels: (1, 1), data: designcraft_doc::AssetData::embedded(preview) }), + ..Default::default() + }; + link_read_file(&mut a, None); + assert_eq!(a.storage, designcraft_doc::Storage::Linked); + let proxy = a.proxy.expect("a proxy"); + let bytes = proxy.data.embedded_bytes().expect("embedded").clone(); + let pm = designcraft_render::decode_pixmap(&bytes).expect("the proxy decodes"); + assert_eq!((u32::from(pm.width()), u32::from(pm.height())), proxy.pixels); + assert!(bytes.len() <= MAX_BYTES && proxy.pixels.0.max(proxy.pixels.1) <= MAX_SIDE_PX); + (proxy.pixels, bytes) + }; + let small = png(100, 50); + assert_eq!(linked_with(small.clone()), ((100, 50), Arc::new(small)), "kept as it is"); + assert_eq!(linked_with(png(2048, 1024)).0, (1024, 512), "made again within the limits"); + assert_eq!(linked_with(png(100, 100)).0, (400, 200), "a crop of the image: made from the file"); + assert_eq!(linked_with(b"not an image".to_vec()).0, (400, 200), "undecodable: made from the file"); + std::fs::remove_dir_all(dir).unwrap(); + } + #[test] fn a_link_that_does_not_resolve_keeps_its_path_for_relink() { let (idml, _) = linked_idml("/original-computer/project/logo.png", false).unwrap(); diff --git a/crates/render/src/prepare.rs b/crates/render/src/prepare.rs index 4bcae1c55..b223a5a20 100644 --- a/crates/render/src/prepare.rs +++ b/crates/render/src/prepare.rs @@ -26,6 +26,7 @@ pub(crate) fn job_key(what: impl Hash) -> u64 { /// What [`later`] did with a job. pub(crate) enum Queued { /// Queued, or already running: the frame counts as waiting for images. + #[cfg_attr(target_arch = "wasm32", allow(dead_code))] Yes, /// Done: it ran now (no background thread can run it), or it panicked before and isn't run /// again. What it made, if anything, is where it records it.