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 <noreply@anthropic.com>
This commit is contained in:
Denis GomesandClaude Opus 5.5 committed 2026-11-06 02:37:04 -05:00
1 parent 8eb0b87646
commit bea214ebb0
2 files changed
+23 -5

No files matched your search

+8 -5
View File
@@ -129,9 +129,6 @@ fn clipboard_png(app: &mut FigApp) -> Option<(Vec<u8>, 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<u8>, f64, f64)> {
const MAX_PASTED_IMAGE_PIXELS: u64 = 100_000_000;
fn with_pixel_size(png: Vec<u8>) -> Option<(Vec<u8>, (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<Vec<u8>> {
+15
View File
@@ -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));
}