diff --git a/src/run/parser/livesplit.rs b/src/run/parser/livesplit.rs index 44d7cf3e..c6b3bbe9 100644 --- a/src/run/parser/livesplit.rs +++ b/src/run/parser/livesplit.rs @@ -433,7 +433,7 @@ fn parse_attempt_history(version: Version, reader: &mut Reader<'_>, run: &mut Ru pub fn parse(source: &str, path: Option) -> Result { let reader = &mut Reader::new(source); - let mut image_buf = Vec::with_capacity(4096); + let mut image_buf = Vec::new(); let mut run = Run::new(); diff --git a/src/settings/image/shrinking.rs b/src/settings/image/shrinking.rs index 9efec268..1f00c909 100644 --- a/src/settings/image/shrinking.rs +++ b/src/settings/image/shrinking.rs @@ -1,26 +1,62 @@ use alloc::borrow::Cow; +use bytemuck::{Pod, Zeroable}; use image::{ - codecs::{bmp, farbfeld, hdr, ico, jpeg, png, pnm, tga, tiff, webp}, - guess_format, load_from_memory_with_format, ImageDecoder, ImageEncoder, ImageError, - ImageFormat, + codecs::{farbfeld, hdr, ico, jpeg, pnm, tga, tiff, webp}, + guess_format, load_from_memory_with_format, ImageDecoder, ImageEncoder, ImageFormat, }; use std::io::Cursor; -fn shrink_inner(data: &[u8], max_dim: u32) -> Result, ImageError> { - let format = guess_format(data)?; +use crate::util::byte_parsing::{big_endian::U32, strip_pod}; + +fn shrink_inner(data: &[u8], max_dim: u32) -> Option> { + let format = guess_format(data).ok()?; - let cursor = Cursor::new(data); let (width, height) = match format { - ImageFormat::Png => png::PngDecoder::new(cursor)?.dimensions(), - ImageFormat::Jpeg => jpeg::JpegDecoder::new(cursor)?.dimensions(), - ImageFormat::WebP => webp::WebPDecoder::new(cursor)?.dimensions(), - ImageFormat::Pnm => pnm::PnmDecoder::new(cursor)?.dimensions(), - ImageFormat::Tiff => tiff::TiffDecoder::new(cursor)?.dimensions(), - ImageFormat::Tga => tga::TgaDecoder::new(cursor)?.dimensions(), - ImageFormat::Bmp => bmp::BmpDecoder::new(cursor)?.dimensions(), - ImageFormat::Ico => ico::IcoDecoder::new(cursor)?.dimensions(), - ImageFormat::Hdr => hdr::HdrAdapter::new(cursor)?.dimensions(), - ImageFormat::Farbfeld => farbfeld::FarbfeldDecoder::new(cursor)?.dimensions(), + ImageFormat::Png => { + // We encounter a lot of PNG images in splits files and decoding + // them with image's PNG decoder seems to decode way more than + // necessary. We really just need to find the width and height. The + // specification is here: + // https://www.w3.org/TR/2003/REC-PNG-20031110/ + // + // And it says the following: + // + // "A valid PNG datastream shall begin with a PNG signature, + // immediately followed by an IHDR chunk". + // + // Each chunk is encoded as a length and type and then its chunk + // encoding. An IHDR chunk immediately starts with the width and + // height. This means we can model the beginning of a PNG file as + // follows: + #[derive(Copy, Clone, Pod, Zeroable)] + #[repr(C)] + struct BeginningOfPng { + // 5.2 PNG signature + png_signature: [u8; 8], + // 5.3 Chunk layout + chunk_len: [u8; 4], + // 11.2.2 IHDR Image header + chunk_type: [u8; 4], + width: U32, + height: U32, + } + + // This improves parsing speed of entire splits files by up to 30%. + + let beginning_of_png: &BeginningOfPng = strip_pod(&mut &*data)?; + (beginning_of_png.width.get(), beginning_of_png.height.get()) + } + ImageFormat::Jpeg => jpeg::JpegDecoder::new(data).ok()?.dimensions(), + ImageFormat::WebP => webp::WebPDecoder::new(data).ok()?.dimensions(), + ImageFormat::Pnm => pnm::PnmDecoder::new(data).ok()?.dimensions(), + ImageFormat::Tiff => tiff::TiffDecoder::new(Cursor::new(data)).ok()?.dimensions(), + ImageFormat::Tga => tga::TgaDecoder::new(Cursor::new(data)).ok()?.dimensions(), + // We always want to re-encode BMP images so we might as well skip the + // dimension checking. + ImageFormat::Bmp => (0, 0), + ImageFormat::Ico => ico::IcoDecoder::new(Cursor::new(data)).ok()?.dimensions(), + ImageFormat::Hdr => hdr::HdrAdapter::new(data).ok()?.dimensions(), + ImageFormat::Farbfeld => farbfeld::FarbfeldDecoder::new(data).ok()?.dimensions(), // FIXME: For GIF we would need to properly shrink the whole animation. // The image crate can't properly handle this at this point in time. // Some properties are not translated over properly it seems. We could @@ -30,28 +66,30 @@ fn shrink_inner(data: &[u8], max_dim: u32) -> Result, ImageError> // AVIF uses C bindings, so it's not portable. // The OpenEXR code in the image crate doesn't compile on Big Endian targets. // And the image format is non-exhaustive. - _ => return Ok(data.into()), + _ => return Some(data.into()), }; let is_too_large = width > max_dim || height > max_dim; - if is_too_large || format == ImageFormat::Bmp { - let mut image = load_from_memory_with_format(data, format)?; + Some(if is_too_large || format == ImageFormat::Bmp { + let mut image = load_from_memory_with_format(data, format).ok()?; if is_too_large { image = image.thumbnail(max_dim, max_dim); } let mut data = Vec::new(); - png::PngEncoder::new(&mut data).write_image( - image.as_bytes(), - image.width(), - image.height(), - image.color(), - )?; - Ok(data.into()) + image::codecs::png::PngEncoder::new(&mut data) + .write_image( + image.as_bytes(), + image.width(), + image.height(), + image.color(), + ) + .ok()?; + data.into() } else { - Ok(data.into()) - } + data.into() + }) } pub fn shrink(data: &[u8], max_dim: u32) -> Cow<'_, [u8]> { - shrink_inner(data, max_dim).unwrap_or_else(|_| data.into()) + shrink_inner(data, max_dim).unwrap_or_else(|| data.into()) }