diff --git a/crates/images/src/jpeg.rs b/crates/images/src/jpeg.rs index a4a2a0971..8e504d72c 100644 --- a/crates/images/src/jpeg.rs +++ b/crates/images/src/jpeg.rs @@ -65,6 +65,36 @@ pub(crate) fn decode_cmyk(bytes: &[u8]) -> Option<(CmykRaster, Option>)> /// The largest reduction [`decode_reduced`] reads straight from a JPEG's 8×8 blocks: 1/8. pub(crate) const MAX_SHRINK: u8 = 3; +/// Output size of a JPEG reduction without decoding pixels. `None` when the reduced decoder +/// cannot handle the JPEG, including its full-size resource limits. +pub(crate) fn reduced_size(bytes: &[u8], shrink: u8) -> Option<(u32, u32)> { + use jpeg_decoder::{CodingProcess, PixelFormat}; + if !bytes.starts_with(&[0xFF, 0xD8]) || shrink == 0 || shrink > MAX_SHRINK { + return None; + } + let mut d = jpeg_decoder::Decoder::new(bytes); + d.set_max_decoding_buffer_size(MAX_BYTES); + d.read_info().ok()?; + let info = d.info()?; + let colour = match info.pixel_format { + PixelFormat::RGB24 => true, + PixelFormat::L8 => false, + PixelFormat::L16 | PixelFormat::CMYK32 => return None, + }; + if crate::over_rgba_budget(u32::from(info.width), u32::from(info.height)) { + return None; + } + if info.coding_process == CodingProcess::DctProgressive { + let side = |n: u16| u64::from(n).next_multiple_of(32); + let components: u64 = if colour { 3 } else { 1 }; + if side(info.width) * side(info.height) * components * 2 > MAX_BYTES as u64 { + return None; + } + } + let reduced = |n: u16| u32::from(n).div_ceil(1 << shrink); + Some((reduced(info.width), reduced(info.height))) +} + /// A YCbCr, RGB or grayscale JPEG decoded at 1/2^`shrink` of its size, each side rounded up, for /// `shrink` 1 to [`MAX_SHRINK`]: straight RGBA and whether the image has colour. `None` for CMYK, /// 12-bit and lossless JPEGs, other formats and anything that doesn't decode. `None` as well for a diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index e925de471..07f91a467 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -510,6 +510,12 @@ pub fn decode_rgba_reduced(bytes: &[u8], shrink: u8) -> Option to_display_rgba(px, Some(&icc), &DisplayColour::active()) } +/// Dimensions of a JPEG decoded at `1/2^shrink`, without decoding its pixels. `None` when the +/// reduced decoder cannot handle the image. +pub fn reduced_jpeg_size(bytes: &[u8], shrink: u8) -> Option<(u32, u32)> { + jpeg::reduced_size(bytes, shrink) +} + /// False when `bytes` is a WebP whose `ICCP` chunk declares more bytes than the file holds or than /// [`colour::MAX_ICC_BYTES`]. The decoder sizes its buffer from the declared length before it reads /// anything, so such a file must not reach `icc_profile()`; its pixels decode without it. diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index fd937903d..4df71f42b 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -200,14 +200,11 @@ fn reduced_estimates(wants: &[Want]) -> Option<(usize, usize)> { return None; } let shrink = level.min(usize::from(designcraft_images::MAX_REDUCED_SHRINK)); + let (decoded_w, decoded_h) = designcraft_images::reduced_jpeg_size(&w.bytes, shrink as u8)?; for cached in shrink..=level { levels[cached] = true; } - let denom = 1u32 << shrink; - let (w_px, h_px) = sizes[0]; - // The reduced decoder rounds each side up before the mip is cropped to `sizes[shrink]`. - let decode_pixels = w_px.div_ceil(denom) as usize * h_px.div_ceil(denom) as usize; - peak = peak.max(decode_pixels.saturating_mul(8)); + peak = peak.max(decoded_w as usize * decoded_h as usize * 8); if dims.as_ref().is_none_or(|d: &Vec<(u32, u32)>| d.len() < sizes.len()) { dims = Some(sizes); }