Files
wordcraft/crates/geom/src/pixels.rs
T
3787af0a67 Rendering and PDF check image limits before allocating (#1321) (#2001)
* WIP for #1321

* Images: check size budgets before decoding and cap page rasters (#1321)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Images: charge decode peak, valid-CRC header tests, cap text-line rasters (#1321)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 04:42:17 -04:00

66 lines
2.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Pixel-budget arithmetic shared by every crate that decodes or allocates rasters: sizes are
//! checked before anything is allocated, so a hostile header or request cannot exhaust memory.
/// Bytes a decoded picture may take as RGBA8 (what the `image` crate's default decode budget is).
pub const MAX_DECODED_BYTES: u64 = 512 * 1024 * 1024;
/// Most pixels one page raster may hold (400 MB of RGBA8); larger requests are scaled down.
pub const MAX_RASTER_PIXELS: u64 = 100_000_000;
/// Bytes of a `w`×`h` RGBA8 raster, None on overflow.
pub fn rgba_bytes(w: u32, h: u32) -> Option<u64> {
(w as u64).checked_mul(h as u64)?.checked_mul(4)
}
/// Whether a `w`×`h` RGBA8 raster fits `budget` bytes.
pub fn rgba_fits(w: u32, h: u32, budget: u64) -> bool {
w > 0 && h > 0 && rgba_bytes(w, h).is_some_and(|b| b <= budget)
}
/// Whether decoding a `w`×`h` picture fits `budget` bytes at its peak: the decoder's buffer and
/// the RGBA conversion (or its premultiplied copy) are alive together, so two RGBA buffers.
pub fn decode_fits(w: u32, h: u32, budget: u64) -> bool {
w > 0 && h > 0 && rgba_bytes(w, h).and_then(|b| b.checked_mul(2)).is_some_and(|b| b <= budget)
}
/// `w`×`h` shrunk (aspect kept, each side at least 1) until it holds at most `max_pixels`.
pub fn fit_pixels(w: u32, h: u32, max_pixels: u64) -> (u32, u32) {
let (w, h) = (w.max(1), h.max(1));
let max_pixels = max_pixels.max(1);
let area = w as u64 * h as u64;
if area <= max_pixels {
return (w, h);
}
let f = (max_pixels as f64 / area as f64).sqrt();
let nh = ((h as f64 * f) as u64).clamp(1, h as u64);
let nw = ((w as f64 * f) as u64).clamp(1, w as u64).min(max_pixels / nh).max(1);
let nh = nh.min(max_pixels / nw).max(1);
(nw as u32, nh as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgba_budget_boundary() {
assert_eq!(rgba_bytes(16, 8), Some(512));
assert!(rgba_fits(16, 8, 512));
assert!(!rgba_fits(16, 8, 511));
assert!(!rgba_fits(0, 8, 512), "empty is not a picture");
assert!(decode_fits(16, 8, 1024));
assert!(!decode_fits(16, 8, 1023), "decode + conversion hold two buffers");
assert!(!decode_fits(0, 8, u64::MAX));
}
#[test]
fn fit_pixels_keeps_normal_sizes_and_shrinks_huge_ones() {
assert_eq!(fit_pixels(612, 792, MAX_RASTER_PIXELS), (612, 792));
let (w, h) = fit_pixels(16_000, 16_000, 1_000_000);
assert!(w as u64 * h as u64 <= 1_000_000 && w >= 900, "{w}x{h}");
let (w, h) = fit_pixels(16_000, 1, 100);
assert!(w as u64 * h as u64 <= 100 && h == 1, "{w}x{h}");
assert_eq!(fit_pixels(0, 5, 10).0, 1);
}
}