diff --git a/crates/images/src/jpeg.rs b/crates/images/src/jpeg.rs index 6188a53fc..a4a2a0971 100644 --- a/crates/images/src/jpeg.rs +++ b/crates/images/src/jpeg.rs @@ -100,7 +100,14 @@ pub(crate) fn decode_reduced(bytes: &[u8], shrink: u8) -> Option<(image::RgbaIma } } let reduced = |n: u16| u16::try_from(u32::from(n).div_ceil(1 << shrink)).ok(); - let (w, h) = d.scale(reduced(info.width)?, reduced(info.height)?).ok()?; + let expected = (reduced(info.width)?, reduced(info.height)?); + // The decoder takes the smallest scale that meets either requested side, and a short side + // rounds up to the same size at several scales, so only the long side is requested. + let request = if info.width >= info.height { (expected.0, u16::MAX) } else { (u16::MAX, expected.1) }; + let (w, h) = d.scale(request.0, request.1).ok()?; + if (w, h) != expected { + return None; + } let px = d.decode().ok()?; let pixels = usize::from(w).checked_mul(usize::from(h))?; let rgba: Vec = if colour { diff --git a/crates/images/src/tests.rs b/crates/images/src/tests.rs index 184e3c9c9..3edfa89b9 100644 --- a/crates/images/src/tests.rs +++ b/crates/images/src/tests.rs @@ -596,6 +596,19 @@ fn reduced_jpegs_decode_at_a_half_quarter_or_eighth_rounded_up() { assert!(decode_rgba_reduced(&jpeg, 4).is_none()); } +#[test] +fn a_thin_reduced_jpeg_keeps_its_long_side_at_the_requested_reduction() { + // The short side rounds up to 1 at every reduction, so it cannot pick the scale. + for (w, h) in [(100, 2), (2, 100)] { + let jpeg = gradient_jpeg(w, h, false); + for (shrink, long) in [(1, 50), (2, 25), (3, 13)] { + let img = decode_rgba_reduced(&jpeg, shrink).unwrap(); + let size = if w > h { (long, 1) } else { (1, long) }; + assert_eq!(img.dimensions(), size, "{w} × {h}, shrink {shrink}"); + } + } +} + #[test] fn reduced_decoding_is_for_jpegs_and_survives_damaged_ones() { let gray = decode_rgba_reduced(&gradient_jpeg(64, 64, true), 2).unwrap();