From e01b9a20d95845d9bdebce3591f71ef51b0a490b Mon Sep 17 00:00:00 2001 From: fastmikey99 <218057203+fastmikey99@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:05:40 +1300 Subject: [PATCH] Crop Image trim: decode print-size rasters, and leave CMYK images alone Trim decoded the image with the decoder's default 512 MB allocation limit, so a 300 ppi roll (6614 x 59055 px, 1.56 GB as RGBA) that opens fine failed with "the image's pixels can't be read". It now decodes with the allowance opening already has (MAX_RASTER_ALLOC: 2 GB, 512 MB on the web). A CMYK image, kept as its ink amounts, has no alpha: trim leaves it alone (trimmed: false) instead of failing to decode it. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/fileio/load.rs | 2 +- crates/engine/src/cmd/fileio/mod.rs | 1 + crates/engine/src/cmd/menucmds.rs | 13 ++++++++++++- crates/engine/src/tests_menucmds.rs | 12 ++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index 64fde0e5..449b9637 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -392,7 +392,7 @@ pub(super) fn new_from_template(s: &mut Session, p: &Value) -> Result { /// The most memory decoding one image may take: a 60 in print sheet at 300 ppi (18000 × 6600 px) /// is ~475 MB as RGBA, past the decoder's 512 MB default once it is converted. The web app keeps /// that default: wasm32 has 4 GB to address and aborts when an allocation fails. -const MAX_RASTER_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 }; +pub(crate) const MAX_RASTER_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 }; /// Decode an image's header (and, for formats stored as PNG, its pixels). CMYK TIFFs are kept as /// they are, with their ink amounts ([`ImageBlob::cmyk`]); CMYK TIFFs with an alpha channel, which diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index ce0fd295..7a13657c 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -28,6 +28,7 @@ pub mod eps; mod export; pub(crate) mod imagemap; mod load; +pub(crate) use load::MAX_RASTER_ALLOC; mod metafile; mod native; pub mod pdf; diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index c84d78a2..fa78bc67 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -625,7 +625,18 @@ fn trim_image(s: &mut Session, id: NodeId, im: ImageObject) -> Result { const C: &str = "object.cropImage"; let st = s.doc()?; let blob = st.doc.images.get(&im.key).ok_or_else(|| bad(C, "the image's pixels are missing"))?; - let px = image::load_from_memory(&blob.bytes).map_err(|_| bad(C, "the image's pixels can't be read"))?.to_rgba8(); + // A CMYK image (kept as its ink amounts) has no alpha: nothing to cut. + if blob.is_cmyk() { + return Ok(json!({ "id": id.0, "width": im.width, "height": im.height, "trimmed": false })); + } + // Print rasters run to gigabytes decoded: the same allowance opening them has. + let mut reader = image::ImageReader::new(std::io::Cursor::new(blob.bytes.as_slice())) + .with_guessed_format() + .map_err(|_| bad(C, "the image's pixels can't be read"))?; + let mut limits = image::Limits::default(); + limits.max_alloc = Some(crate::cmd::fileio::MAX_RASTER_ALLOC); + reader.limits(limits); + let px = reader.decode().map_err(|e| bad(C, format!("the image's pixels can't be read: {e}")))?.to_rgba8(); let (w, h) = px.dimensions(); let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0, 0); for (x, y, p) in px.enumerate_pixels() { diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index 3a23dda1..76f0e7a3 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -1826,3 +1826,15 @@ fn crop_image_trim_cuts_to_the_opaque_pixels_as_they_are() { assert_eq!((r["trimmed"].as_bool(), r["width"].as_u64()), (Some(false), Some(30)), "{r}"); assert_eq!(bounds(&s, img), b); } + +#[test] +fn crop_image_trim_leaves_a_cmyk_image_alone() { + // CMYK images keep their ink amounts and have no alpha: nothing to cut, and not an error. + let inks = vectorcraft_doc::cmyk::Inks::new(4, 2, [255, 0, 0, 0].repeat(8)).unwrap(); + let tiff = vectorcraft_doc::ImageBlob::cmyk_tiff(&inks).unwrap(); + let mut s = Session::new(); + s.execute("document.open", &json!({"name": "inks.tif", "dataBase64": vectorcraft_format::base64_encode(&tiff.bytes)})).unwrap(); + s.execute("select.all", &json!({})).unwrap(); + let r = s.execute("object.cropImage", &json!({"trim": true})).unwrap(); + assert_eq!((r["trimmed"].as_bool(), r["width"].as_u64(), r["height"].as_u64()), (Some(false), Some(4), Some(2)), "{r}"); +}