From 1c3285d8a3affaec03949c7fa2b610c4b94dcc34 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 17:55:11 -0500 Subject: [PATCH] Count progressive JPEG buffers in warm budget --- crates/images/src/jpeg.rs | 16 +++++++++++----- crates/images/src/lib.rs | 7 ++++--- crates/render/src/images.rs | 4 ++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/images/src/jpeg.rs b/crates/images/src/jpeg.rs index 8e504d72c..9db1709cc 100644 --- a/crates/images/src/jpeg.rs +++ b/crates/images/src/jpeg.rs @@ -67,7 +67,7 @@ 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)> { +pub(crate) fn reduced_size(bytes: &[u8], shrink: u8) -> Option<(u32, u32, usize)> { use jpeg_decoder::{CodingProcess, PixelFormat}; if !bytes.starts_with(&[0xFF, 0xD8]) || shrink == 0 || shrink > MAX_SHRINK { return None; @@ -84,15 +84,21 @@ pub(crate) fn reduced_size(bytes: &[u8], shrink: u8) -> Option<(u32, u32)> { if crate::over_rgba_budget(u32::from(info.width), u32::from(info.height)) { return None; } - if info.coding_process == CodingProcess::DctProgressive { + let progressive_bytes = 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 { + let bytes = side(info.width) * side(info.height) * components * 2; + if bytes > MAX_BYTES as u64 { return None; } - } + usize::try_from(bytes).ok()? + } else { + 0 + }; let reduced = |n: u16| u32::from(n).div_ceil(1 << shrink); - Some((reduced(info.width), reduced(info.height))) + let (width, height) = (reduced(info.width), reduced(info.height)); + let output_bytes = width as usize * height as usize * 8; + Some((width, height, progressive_bytes.saturating_add(output_bytes))) } /// A YCbCr, RGB or grayscale JPEG decoded at 1/2^`shrink` of its size, each side rounded up, for diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index 07f91a467..9afb009fa 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -510,9 +510,10 @@ 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)> { +/// Dimensions and estimated peak bytes for a JPEG decoded at `1/2^shrink`, without decoding its +/// pixels. Progressive JPEG coefficient buffers are included. `None` when the reduced decoder +/// cannot handle the image. +pub fn reduced_jpeg_size(bytes: &[u8], shrink: u8) -> Option<(u32, u32, usize)> { jpeg::reduced_size(bytes, shrink) } diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index e2d49873c..8e57ef448 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -200,11 +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)?; + let (_, _, decode_peak) = designcraft_images::reduced_jpeg_size(&w.bytes, shrink as u8)?; for cached in levels.iter_mut().take(level + 1).skip(shrink) { *cached = true; } - peak = peak.max(decoded_w as usize * decoded_h as usize * 8); + peak = peak.max(decode_peak); if dims.as_ref().is_none_or(|d: &Vec<(u32, u32)>| d.len() < sizes.len()) { dims = Some(sizes); }