From bea214ebb05b57efa0a8a9992d37944130882dd0 Mon Sep 17 00:00:00 2001 From: Denis Gomes <6731020+denisg0mes@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:35:30 -0400 Subject: [PATCH] M7: decode a clipboard PNG before pasting it with_pixel_size read only the PNG's header, so a PNG with a whole IHDR and a cut-off body passed as the one to paste. Since #1724 image.place refuses it as damaged, and nothing was pasted: the decoded clipboard image, which paste falls back to for an unreadable PNG, was never tried. The PNG is now decoded in full before it is accepted, so the paste falls back as it does for any other unreadable PNG. The zero-size and pixel-count checks run ahead of that decode, so an image past the limit is refused before it is decoded. Raised by CodeRabbit on #1360 after the merge. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/menus/clipboard.rs | 13 ++++++++----- crates/ui-egui/src/paste_image_tests.rs | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/menus/clipboard.rs b/crates/ui-egui/src/menus/clipboard.rs index ff065a48f..bae7aaca9 100644 --- a/crates/ui-egui/src/menus/clipboard.rs +++ b/crates/ui-egui/src/menus/clipboard.rs @@ -129,9 +129,6 @@ fn clipboard_png(app: &mut FigApp) -> Option<(Vec, f64, f64)> { Some(readable) => readable, None => with_pixel_size(encoded_clipboard_image(app)?)?, }; - if w == 0 || h == 0 || u64::from(w) * u64::from(h) > MAX_PASTED_IMAGE_PIXELS { - return None; - } let density = figcraft_format::pixel_density(&png); let (w, h) = figcraft_format::placed_size(w, h, density, figcraft_engine::cmd::create::PLACED_IMAGE_MAX_SIDE); Some((png, w, h)) @@ -140,8 +137,14 @@ fn clipboard_png(app: &mut FigApp) -> Option<(Vec, f64, f64)> { const MAX_PASTED_IMAGE_PIXELS: u64 = 100_000_000; fn with_pixel_size(png: Vec) -> Option<(Vec, (u32, u32))> { - let size = image::ImageReader::new(std::io::Cursor::new(&png)).with_guessed_format().ok()?.into_dimensions().ok()?; - Some((png, size)) + use image::ImageDecoder as _; + let decoder = image::ImageReader::new(std::io::Cursor::new(&png)).with_guessed_format().ok()?.into_decoder().ok()?; + let (w, h) = decoder.dimensions(); + if w == 0 || h == 0 || u64::from(w) * u64::from(h) > MAX_PASTED_IMAGE_PIXELS { + return None; + } + image::DynamicImage::from_decoder(decoder).ok()?; + Some((png, (w, h))) } fn encoded_clipboard_image(app: &mut FigApp) -> Option> { diff --git a/crates/ui-egui/src/paste_image_tests.rs b/crates/ui-egui/src/paste_image_tests.rs index e70ea0968..756c93067 100644 --- a/crates/ui-egui/src/paste_image_tests.rs +++ b/crates/ui-egui/src/paste_image_tests.rs @@ -549,3 +549,18 @@ fn a_clipboard_png_past_the_byte_limit_falls_back_to_the_decoded_image() { let pasted = doc.nodes().find(|n| n.name == "Pasted image").expect("pasted from the decoded image"); assert_eq!((pasted.width, pasted.height), (2.0, 2.0)); } + +#[test] +fn a_clipboard_png_with_a_cut_off_body_falls_back_to_the_decoded_image() { + let mut w = Win::new(); + let mut png = std::io::Cursor::new(Vec::new()); + image::RgbaImage::new(4, 4).write_to(&mut png, image::ImageFormat::Png).unwrap(); + let png = png.into_inner(); + let cut_off = png[..png.len() - 20].to_vec(); + assert_eq!(image::ImageReader::new(std::io::Cursor::new(&cut_off)).with_guessed_format().unwrap().into_dimensions().unwrap(), (4, 4)); + *w.clip.borrow_mut() = Clip { png: Some(cut_off), image: Some((2, 2, vec![255; 16])), ..Clip::default() }; + w.cmd_v(); + let doc = w.app.session.doc().unwrap(); + let pasted = doc.nodes().find(|n| n.name == "Pasted image").expect("pasted from the decoded image"); + assert_eq!((pasted.width, pasted.height), (2.0, 2.0)); +}