diff --git a/crates/render/src/paint.rs b/crates/render/src/paint.rs index e3c5ed30b..b9ce01a7b 100644 --- a/crates/render/src/paint.rs +++ b/crates/render/src/paint.rs @@ -189,8 +189,7 @@ fn fits_beside_pinned(bytes: u64) -> bool { /// all: an image that isn't a PNG, JPEG, GIF or WebP; an image of more pixels than the allowance /// has bytes (2,147,483,648 pixels, or 536,870,912 in the web app; 46,341 × 46,341 is more); and an /// image whose decoded samples take more than the allowance, unless it is a PNG that isn't -/// interlaced and is at most the allowance divided by [`ROW_SCRATCH`] pixels wide (read a row at a -/// time). +/// interlaced whose reader's buffers ([`png_row_scratch`]) fit the allowance (read a row at a time). pub fn reduced_size(bytes: &[u8]) -> Option<(u32, u32)> { reduction(bytes, allowance()).map(|r| r.to) } @@ -228,26 +227,44 @@ fn reduction(bytes: &[u8], allowance: u64) -> Option { return None; } let to = fitted_dims(w, h, allowance)?; - // The PNG reader's row buffers take at most `ROW_SCRATCH` bytes a column, also within the allowance. - let rows = format == image::ImageFormat::Png - && u64::from(w).saturating_mul(ROW_SCRATCH) <= allowance - && png_reader(bytes).is_some_and(|r| !r.info().interlaced); - let scratch = if rows { - u64::from(w) * ROW_SCRATCH + // A PNG that isn't interlaced is read a row at a time when the reader's buffers fit the allowance. + let row_scratch = if format == image::ImageFormat::Png { + png_reader(bytes).filter(|r| !r.info().interlaced).and_then(|r| png_row_scratch(&r)).filter(|&s| s <= allowance) } else { + None + }; + let rows = row_scratch.is_some(); + let scratch = row_scratch.unwrap_or_else(|| { let band = u64::from(BAND) * u64::from(w) * (u64::from(d.color_type().bytes_per_pixel()) + 4); d.total_bytes().saturating_add(band) - }; + }); (rows || d.total_bytes() <= allowance).then_some(Reduction { size: (w, h), to, rows, scratch }) } /// How many rows of an image decoded whole [`reduced_pixmap`] converts to RGBA at a time. const BAND: u32 = 16; -/// The most memory a column of a PNG read a row at a time takes in the reader's row buffers: the -/// row as stored (up to 8 bytes a pixel and a filter byte), the row before it, and the row given -/// out (up to 4 bytes a pixel). -const ROW_SCRATCH: u64 = 32; +/// The bytes of rows already read that png 0.18's reader keeps in its stream buffer, at least, +/// before it moves the rest of the buffer back to the start. +const PNG_KEPT_ROWS: u64 = 128 << 10; + +/// The deflate window png 0.18's reader keeps after the rows it has unfiltered, and the step its +/// stream buffer grows by. +const PNG_WINDOW_AND_STEP: u64 = (32 << 10) + (8 << 10); + +/// The most memory `r` takes in its buffers to read its image a row at a time: the stream buffer +/// holds the rows already read up to [`PNG_KEPT_ROWS`] or four rows, whichever is more, the previous +/// and current rows, and [`PNG_WINDOW_AND_STEP`], and may have up to twice that reserved; a copy of +/// a stored row and the row given out come on top. `None` when a row given out doesn't fit memory. +fn png_row_scratch(r: &png::Reader>) -> Option { + let info = r.info(); + // A filter byte and the row's samples, in whole bytes. + let stored = 1 + (u64::from(info.width) * info.color_type.samples() as u64 * info.bit_depth as u64).div_ceil(8); + let out = u64::try_from(r.output_line_size(info.width)?).ok()?; + let kept = (stored * 4).next_multiple_of(64).max(PNG_KEPT_ROWS); + let stream = kept + stored * 2 + PNG_WINDOW_AND_STEP; + Some(stream * 2 + stored + out) +} /// The largest size of at most `w`×`h` pixels, keeping their proportions, whose pixels take at most /// `allowance` bytes as RGBA, with sides of at most `u16::MAX` pixels. diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index e1ded30c8..d97de7a3c 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1648,46 +1648,48 @@ fn encoded(img: image::DynamicImage, format: image::ImageFormat) -> Vec { #[test] fn a_png_past_the_allowance_draws_at_reduced_size() { use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind}; - let img = image::ImageBuffer::, _>::from_fn(64, 48, |x, _| { - if x < 32 { image::Rgba([51400, 10280, 10280, 65535]) } else { image::Rgba([10280, 10280, 51400, 65535]) } + let img = image::ImageBuffer::, _>::from_fn(256, 400, |x, _| { + if x < 128 { image::Rgba([51400, 10280, 10280, 65535]) } else { image::Rgba([10280, 10280, 51400, 65535]) } }); let png = encoded(image::DynamicImage::ImageRgba16(img), image::ImageFormat::Png); - // 64 × 48 is 12,288 bytes as RGBA; 36 × 27 is the largest within 4,096. - assert_eq!(with_allowance(4096, || paint::reduced_size(&png)), Some((36, 27))); + // 256 × 400 is 409,600 bytes as RGBA; 252 × 395 is the largest within 400,000. + assert_eq!(with_allowance(400_000, || paint::reduced_size(&png)), Some((252, 395))); assert_eq!(paint::reduced_size(&png), None, "within the 2 GB allowance"); - let mut d = Document::new(64.0, 48.0); + let mut d = Document::new(256.0, 400.0); d.images.insert("over".into(), ImageBlob::new("image/png", png)); let id = d.alloc_id(); - let im = ImageObject { key: "over".into(), width: 64, height: 48, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None }; + let im = + ImageObject { key: "over".into(), width: 256, height: 400, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None }; d.insert(Some(d.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap(); let failed = decode_failures(); // As on screen, where an image that can't be decoded draws as a crossed box. let screen = RenderOptions { smooth_images: false, mark_undecoded_images: true, ..Default::default() }; - let r = with_allowance(4096, || Renderer::new().render(&d, 64, 48, Affine::IDENTITY, &screen)); + let r = with_allowance(400_000, || Renderer::new().render(&d, 256, 400, Affine::IDENTITY, &screen)); assert_eq!(decode_failures(), failed); - assert_eq!((r.pixel(8, 24), r.pixel(56, 24)), ([200, 40, 40, 255], [40, 40, 200, 255])); - // 16 × 16 at 16 bits a sample is 2,048 bytes decoded and 1,024 as RGBA: past 1,500 as decoded - // and within it as RGBA, it is read a row at a time at its own size. - let small = image::ImageBuffer::, _>::from_pixel(16, 16, image::Rgba([51400, 10280, 10280, 65535])); + assert_eq!((r.pixel(32, 200), r.pixel(224, 200)), ([200, 40, 40, 255], [40, 40, 200, 255])); + // 256 × 256 at 16 bits a sample is 524,288 bytes decoded and 262,144 as RGBA: past 400,000 as + // decoded and within it as RGBA, it is read a row at a time at its own size. + let small = image::ImageBuffer::, _>::from_pixel(256, 256, image::Rgba([51400, 10280, 10280, 65535])); let small = encoded(image::DynamicImage::ImageRgba16(small), image::ImageFormat::Png); - assert_eq!(with_allowance(1500, || paint::reduced_size(&small)), Some((16, 16))); - let pm = with_allowance(1500, || paint::decode_pixmap(&small)).expect("read a row at a time"); + assert_eq!(with_allowance(400_000, || paint::reduced_size(&small)), Some((256, 256))); + let pm = with_allowance(400_000, || paint::decode_pixmap(&small)).expect("read a row at a time"); let c = pm.sample(3, 3); - assert_eq!((pm.width(), pm.height(), [c.r, c.g, c.b, c.a]), (16, 16, [200, 40, 40, 255])); - // A row is read only while the reader's row buffers, 32 bytes a column, fit the allowance: - // 128 columns do within 4,096 bytes, and 129 columns are not decoded (their samples, 8 bytes a - // pixel, are past it too). + assert_eq!((pm.width(), pm.height(), [c.r, c.g, c.b, c.a]), (256, 256, [200, 40, 40, 255])); + // A row is read only while the reader's buffers fit the allowance. Below 4,096 columns of 16-bit + // RGBA they take 344,069 bytes and 44 a column: 1,271 columns take 399,993 bytes, within 400,000, + // and 1,272 columns take 400,037 and are not decoded (their samples, 8 bytes a pixel, are past + // it too). let wide = |w: u32| { - let img = image::ImageBuffer::, _>::from_pixel(w, 6, image::Rgba([51400, 10280, 10280, 65535])); + let img = image::ImageBuffer::, _>::from_pixel(w, 40, image::Rgba([51400, 10280, 10280, 65535])); encoded(image::DynamicImage::ImageRgba16(img), image::ImageFormat::Png) }; - assert_eq!(with_allowance(4096, || paint::reduced_size(&wide(128))), Some((128, 6))); - assert_eq!(with_allowance(4096, || paint::reduced_size(&wide(129))), None); - // An image of more pixels than the allowance has bytes is not read: 128 × 32 8-bit gray pixels - // are drawn at 64 × 16 within 4,096 bytes, and 128 × 33 are not. - let gray = |h: u32| encoded(image::DynamicImage::ImageLuma8(image::GrayImage::from_pixel(128, h, image::Luma([90]))), image::ImageFormat::Png); - assert_eq!(with_allowance(4096, || paint::reduced_size(&gray(32))), Some((64, 16))); - assert_eq!(with_allowance(4096, || paint::reduced_size(&gray(33))), None); + assert_eq!(with_allowance(400_000, || paint::reduced_size(&wide(1271))), Some((1271, 40))); + assert_eq!(with_allowance(400_000, || paint::reduced_size(&wide(1272))), None); + // An image of more pixels than the allowance has bytes is not read: 1,000 × 400 8-bit gray + // pixels are drawn at 500 × 200 within 400,000 bytes, and 1,000 × 401 are not. + let gray = |h: u32| encoded(image::DynamicImage::ImageLuma8(image::GrayImage::from_pixel(1000, h, image::Luma([90]))), image::ImageFormat::Png); + assert_eq!(with_allowance(400_000, || paint::reduced_size(&gray(400))), Some((500, 200))); + assert_eq!(with_allowance(400_000, || paint::reduced_size(&gray(401))), None); } /// A JPEG whose RGB samples are within the allowance but whose RGBA is past it is decoded whole @@ -1730,13 +1732,13 @@ fn a_reduced_decode_counts_what_reading_takes_beside_the_images_paints_hold() { // Decoded whole: 14,400 bytes of RGB and a 16-row band of 8,960 bytes, beside a 73 × 54 result // of 15,768 bytes. let jpeg = encoded(image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(80, 60, image::Rgb([30, 120, 200]))), image::ImageFormat::Jpeg); - // Read a row at a time: 64 columns of 32 bytes in the reader's row buffers, beside a 36 × 27 - // result of 3,888 bytes. + // Read a row at a time: 355,333 bytes in the reader's buffers for 256 columns of 16-bit RGBA, + // beside a 252 × 395 result of 398,160 bytes. let png = encoded( - image::DynamicImage::ImageRgba16(image::ImageBuffer::from_pixel(64, 48, image::Rgba([51400, 10280, 10280, 65535]))), + image::DynamicImage::ImageRgba16(image::ImageBuffer::from_pixel(256, 400, image::Rgba([51400, 10280, 10280, 65535]))), image::ImageFormat::Png, ); - for (bytes, allowance, room, size) in [(&jpeg, 16_000, 15_768 + 14_400 + 8_960, (73, 54)), (&png, 4_096, 3_888 + 2_048, (36, 27))] { + for (bytes, allowance, room, size) in [(&jpeg, 16_000, 15_768 + 14_400 + 8_960, (73, 54)), (&png, 400_000, 398_160 + 355_333, (252, 395))] { let failed = decode_failures(); let none = with_allowance(allowance, || with_room(room - 1, || paint::decode_pixmap_or_why(bytes))); assert_eq!(none.err(), Some(paint::Undecoded::NoRoom), "not decoded with a byte too few, and tried again later");