From edbea6036f88fe176be7a5b83f9b6670a9483b74 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 12:28:30 +0000 Subject: [PATCH 1/3] fix: bound recent-file preview decoding and PDF crop allocation --- crates/engine/src/cmd/file/open.rs | 10 ++++++++++ crates/render/src/pdf_raster.rs | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 4e90f30fb..86de7c369 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -16,11 +16,21 @@ pub struct RecentPreview { /// The picture a `.designcraft` file saved of its first page; None when the file is gone, isn't /// ours, or was saved before files had one (it gets one when saved again). +/// The most pixels a recent file's preview may have to be shown (its PNG is bounded by +/// `designcraft_format::MAX_PREVIEW_BYTES`, but a small PNG can claim a huge picture). +const MAX_PREVIEW_PIXELS: u64 = 1 << 24; + pub fn recent_preview(path: &str) -> Option { #[cfg(not(target_arch = "wasm32"))] { // Only the zip directory and the preview entry are read, not the whole document. let png = designcraft_format::read_preview(std::io::BufReader::new(crate::io::open(path).ok()?))?; + // A preview is a small picture: one whose header claims more pixels than that isn't + // decoded (a decode is bounded, but not at thumbnail size). + let (w, h) = designcraft_images::pixel_size(&png)?; + if u64::from(w).saturating_mul(u64::from(h)) > MAX_PREVIEW_PIXELS { + return None; + } let img = designcraft_images::decode_rgba(&png)?; let (width, height) = img.dimensions(); Some(RecentPreview { width, height, rgba: img.into_raw() }) diff --git a/crates/render/src/pdf_raster.rs b/crates/render/src/pdf_raster.rs index 76160be96..e660e1657 100644 --- a/crates/render/src/pdf_raster.rs +++ b/crates/render/src/pdf_raster.rs @@ -251,6 +251,11 @@ pub fn crop_pixmap(pm: &Pixmap, frac: [f64; 4]) -> Option { return None; } let (w, h) = (w as u16, h as u16); + // A box reaching far outside the page would mostly be transparent padding: it gets no more + // pixels than a page raster may have. + if u64::from(w) * u64::from(h) > crate::MAX_PAGE_PIXELS { + return None; + } let mut out = Pixmap::new(w, h); let src = pm.data(); let dst = out.data_mut(); @@ -272,3 +277,17 @@ pub fn crop_pixmap(pm: &Pixmap, frac: [f64; 4]) -> Option { } Some(out) } + +#[cfg(test)] +mod crop_tests { + use super::*; + + /// A crop box reaching far outside the page gets no bigger a pixmap than a page may have. + #[test] + fn crop_outside_the_page_is_bounded() { + let pm = Pixmap::new(10, 10); + assert!(crop_pixmap(&pm, [-0.5, -0.5, 1.5, 1.5]).is_some_and(|c| (c.width(), c.height()) == (20, 20))); + // 60,000 × 60,000 pixels: within u16 each way, far past the page raster budget. + assert!(crop_pixmap(&pm, [-3000.0, -3000.0, 3000.0, 3000.0]).is_none()); + } +} From acf478e6e78abfc0b85c89a920b03815091b5162 Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:35:07 +0000 Subject: [PATCH 2/3] Apply review fixes - crates/engine/src/cmd/file/open.rs --- crates/engine/src/cmd/file/open.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 86de7c369..41df29723 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -14,12 +14,12 @@ pub struct RecentPreview { pub rgba: Vec, } -/// The picture a `.designcraft` file saved of its first page; None when the file is gone, isn't -/// ours, or was saved before files had one (it gets one when saved again). /// The most pixels a recent file's preview may have to be shown (its PNG is bounded by /// `designcraft_format::MAX_PREVIEW_BYTES`, but a small PNG can claim a huge picture). const MAX_PREVIEW_PIXELS: u64 = 1 << 24; +/// The picture a `.designcraft` file saved of its first page; None when the file is gone, isn't +/// ours, or was saved before files had one (it gets one when saved again). pub fn recent_preview(path: &str) -> Option { #[cfg(not(target_arch = "wasm32"))] { From 4071bd40e6de7e8d656f30c0baedc87e1d9219e4 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 4 Nov 2026 00:39:05 +0000 Subject: [PATCH 3/3] address review: skip a graphic whose PDF crop is too large to trace --- crates/engine/src/cmd/object.rs | 41 ++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index aac0d0eb1..d4584b6c4 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -2836,7 +2836,12 @@ fn clipping_path(s: &mut Session, p: &Value) -> Result { let Some(px) = designcraft_render::decode_pixmap_page(&asset.data, 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) { - Some(frac) => designcraft_render::crop_pixmap(&px, frac).unwrap_or(px), + // A crop too big to trace is skipped, as a graphic that fails to decode is: tracing + // the whole page instead would trace something else than the box. + Some(frac) => match designcraft_render::crop_pixmap(&px, frac) { + Some(cropped) => cropped, + None => continue, + }, None => px, }; let (pw, ph) = (px.width() as usize, px.height() as usize); @@ -4637,6 +4642,40 @@ mod clipping_tests { assert!(s.execute("object.clippingPath", &json!({"ids": [f], "threshold": 255})).is_err()); } + /// A placed PDF whose crop box is too large to trace is skipped, not traced from the whole page. + #[test] + fn clipping_path_skips_a_pdf_whose_crop_is_too_large_to_trace() { + let content = "0 0 0 rg 10 10 50 50 re f"; + let objs = [ + "<>".to_string(), + "<>".into(), + "<>".into(), + format!("<>\nstream\n{content}\nendstream", content.len()), + ]; + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, body) in objs.iter().enumerate() { + offsets.push(bytes.len()); + bytes.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", i + 1).as_bytes()); + } + let xref = bytes.len(); + bytes.extend_from_slice(b"xref\n0 5\n0000000000 65535 f\r\n"); + for at in offsets { + bytes.extend_from_slice(format!("{at:010} 00000 n\r\n").as_bytes()); + } + bytes.extend_from_slice(format!("trailer\n<>\nstartxref\n{xref}\n%%EOF\n").as_bytes()); + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let f = s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap(); + let placed = s + .execute("file.place", &json!({"base64": super::super::file::base64_encode(&bytes), "name": "wide.pdf", "frame": f, "pdfCrop": "media"})); + assert!(placed.is_ok(), "{placed:?}"); + let before = s.doc().unwrap().doc.item(ItemId(f)).unwrap().shape; + let e = s.execute("object.clippingPath", &json!({"ids": [f], "type": "alpha"})); + assert!(e.is_err(), "the graphic is skipped, so there is nothing to trace: {e:?}"); + assert_eq!(s.doc().unwrap().doc.item(ItemId(f)).unwrap().shape, before, "the frame keeps its shape"); + } + #[test] fn clipping_path_options_report_invert_and_remove_the_clip() { let mut s = Session::new();