From 57c03d36fc83486fde669a3dc94d6b7d868ccc0c Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Thu, 15 Oct 2026 01:28:10 +0200 Subject: [PATCH] Render: decode a cropped picture for the whole image's size, not its box's (#762) A picture was decoded at the size of its box on screen. A cropped picture shows only part of the image, magnified, so it was decoded at a fraction of the resolution it needs and stretched back up: Merge Shapes > Intersect, which crops the picture to the new smaller box, left it visibly blurred. Size the decode from the rectangle the whole image occupies instead. --- crates/render/src/paint.rs | 22 ++++++++++++---------- crates/render/src/tests.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/crates/render/src/paint.rs b/crates/render/src/paint.rs index 8011f1bb..ee572fd9 100644 --- a/crates/render/src/paint.rs +++ b/crates/render/src/paint.rs @@ -188,16 +188,6 @@ pub fn picture(ctx: &mut RenderContext, pres: &Presentation, pf: &PictureFill, b return; }; let px_scale = crate::scale_of(m); - let want = (bounds.width().max(bounds.height()) * px_scale).max(1.0); - let frame = gif_time.and_then(|t| images::gif_pixmap(&item.data, &pf.adjust, t)); - let Some(pm) = frame.or_else(|| images::decode_for(&item.data, want, &pf.adjust)) else { - missing(ctx, bounds); - return; - }; - let (iw, ih) = (pm.width() as f64, pm.height() as f64); - if iw < 1.0 || ih < 1.0 { - return; - } let dest = match &pf.mode { PictureMode::Stretch { fill_rect } => { let [l, t, r, b] = *fill_rect; @@ -211,6 +201,18 @@ pub fn picture(ctx: &mut RenderContext, pres: &Presentation, pf: &PictureFill, b } PictureMode::Tile { .. } => bounds, }; + // Decode for the whole image's size on screen, not the box's: a cropped picture shows only part + // of it, magnified (#762). + let want = (dest.width().abs().max(dest.height().abs()) * px_scale).max(1.0); + let frame = gif_time.and_then(|t| images::gif_pixmap(&item.data, &pf.adjust, t)); + let Some(pm) = frame.or_else(|| images::decode_for(&item.data, want, &pf.adjust)) else { + missing(ctx, bounds); + return; + }; + let (iw, ih) = (pm.width() as f64, pm.height() as f64); + if iw < 1.0 || ih < 1.0 { + return; + } let alpha = pf.alpha.unwrap_or(1.0).clamp(0.0, 1.0); let image = vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm.clone()), diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index c85a1e29..b1e58f93 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -2509,3 +2509,35 @@ fn single_series_column_chart_varies_colours_by_point() { }; assert_ne!(render(true), render(false)); } + +/// A 1024×1024 PNG of 2 px wide black and white vertical stripes. +fn fine_stripes_png() -> Vec { + let img = + image::RgbaImage::from_fn(1024, 1024, |x, _| if (x / 2) % 2 == 0 { image::Rgba([0, 0, 0, 255]) } else { image::Rgba([255, 255, 255, 255]) }); + let mut out = std::io::Cursor::new(Vec::new()); + image::DynamicImage::ImageRgba8(img).write_to(&mut out, image::ImageFormat::Png).expect("png"); + out.into_inner() +} + +#[test] +fn a_cropped_picture_is_decoded_at_the_resolution_of_the_whole_image() { + // The top-left sixteenth of the image in a 64 pt box: 64 image pixels over 64 output pixels. + // Decoding at the box's size instead of the whole image's blurs the stripes to grey (#762). + let mut p = Presentation::default(); + let media = p.add_media("stripes.png", "image/png", fine_stripes_png()).expect("media"); + let fill = deckcraft_model::style::PictureFill { media, crop: [0.0, 0.0, 15.0 / 16.0, 15.0 / 16.0], ..Default::default() }; + let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(100.0, 100.0, 64.0, 64.0)), kind: ShapeKind::Picture { fill }, ..Default::default() }; + std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); + let (mut sharp, mut total) = (0, 0); + for y in 110..154 { + for x in 110..154 { + let v = img.pixel(x, y)[0]; + total += 1; + if !(48..=208).contains(&v) { + sharp += 1; + } + } + } + assert!(sharp * 10 >= total * 9, "the stripes stay sharp: {sharp} of {total} pixels black or white"); +}