Fix #1717: sampling and recolouring check an image's RGBA size before decoding

The decoder's 512 MB limit is on the file's samples; a grey image within it
is four times that once converted to RGBA for the Eyedropper or a recolour,
which the web build can't allocate. The size is now read from the header and
held to the allowance the importers and the renderer use.
This commit is contained in:
bflatastic committed 2026-10-14 08:47:10 +07:00
1 parent ea30e411e7
commit 0cb55bc0aa
3 files changed
+32 -3

No files matched your search

+1
View File
@@ -31,6 +31,7 @@ pub mod overprint;
pub mod pattern;
pub mod perspective;
mod pixels;
pub use pixels::MAX_RASTER_ALLOC;
pub mod placed_document;
pub mod profiles;
pub mod puppet;
+30 -2
View File
@@ -6,6 +6,22 @@ use std::sync::Arc;
use crate::ImageBlob;
/// The most memory the decoded RGBA pixels of one image may take: the allowance the importers
/// and the renderer decode within (a 60 in sheet at 300 ppi is ~475 MB). The web app has 4 GB to
/// address and aborts when an allocation fails, so it keeps the decoder's own 512 MB.
pub const MAX_RASTER_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 };
/// The pixels of encoded image `bytes` as RGBA; `None` when they can't be decoded or would take
/// more than `max` bytes as RGBA. The size is read from the header first: the decoder's own limit
/// is on the file's samples, and a grey image is a quarter of what it becomes here (#1717).
fn rgba_within(bytes: &[u8], max: u64) -> Option<image::RgbaImage> {
let (w, h) = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().ok()?.into_dimensions().ok()?;
if u64::from(w) * u64::from(h) * 4 > max {
return None;
}
Some(image::load_from_memory(bytes).ok()?.to_rgba8())
}
impl ImageBlob {
/// A PNG blob of `png` bytes.
pub fn png(png: Vec<u8>) -> Self {
@@ -21,7 +37,7 @@ impl ImageBlob {
/// pixels left alone, re-encoded as PNG. `f` runs once per distinct colour. `None` when the
/// image can't be decoded or no pixel changed.
pub fn map_rgb(&self, mut f: impl FnMut([u8; 3]) -> [u8; 3]) -> Option<ImageBlob> {
let mut img = image::load_from_memory(&self.bytes).ok()?.to_rgba8();
let mut img = rgba_within(&self.bytes, MAX_RASTER_ALLOC)?;
let mut memo: HashMap<[u8; 3], [u8; 3]> = HashMap::new();
let mut changed = false;
for px in img.pixels_mut().filter(|p| p.0[3] > 0) {
@@ -67,7 +83,7 @@ impl ImageBlob {
LAST.with_borrow_mut(|last| match last {
Some((k, img)) if *k == key => Some(img.clone()),
_ => {
let img = Arc::new(image::load_from_memory(&self.bytes).ok()?.to_rgba8());
let img = Arc::new(rgba_within(&self.bytes, MAX_RASTER_ALLOC)?);
*last = Some((key, img.clone()));
Some(img)
}
@@ -115,6 +131,18 @@ mod tests {
image::load_from_memory(&b.bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect()
}
/// #1717: the RGBA size is checked from the header, before anything is decoded: a grey image
/// the decoder's own limit lets through is four times that as RGBA.
#[test]
fn pixels_past_the_allowance_are_not_decoded() {
let grey = image::GrayImage::from_raw(4, 2, vec![7; 8]).unwrap();
let mut png = Vec::new();
grey.write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
assert!(rgba_within(&png, 31).is_none(), "8 grey bytes are 32 as RGBA");
assert_eq!(rgba_within(&png, 32).map(|i| i.dimensions()), Some((4, 2)));
assert!(rgba_within(b"not an image", u64::MAX).is_none());
}
#[test]
fn samples_a_pixel_or_the_average_around_it() {
let b = blob(&[[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [9, 9, 9, 0]], 4, 1);
+1 -1
View File
@@ -419,7 +419,7 @@ pub(super) fn new_from_template(s: &mut Session, p: &Value) -> Result<Value> {
/// The most memory decoding one image may take: a 60 in print sheet at 300 ppi (18000 × 6600 px)
/// is ~475 MB as RGBA, past the decoder's 512 MB default once it is converted. The web app keeps
/// that default: wasm32 has 4 GB to address and aborts when an allocation fails.
pub(crate) const MAX_RASTER_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 };
pub(crate) use vectorcraft_doc::MAX_RASTER_ALLOC;
/// What to say of encoded image `bytes` that need more than [`MAX_RASTER_ALLOC`] to decode (its
/// header says so; the pixels aren't read): the renderer, which has the same allowance, draws