Skip to content

Commit

Permalink
Optimize accessing PNG dimensions (#600)
Browse files Browse the repository at this point in the history
I've noticed that a lot of the parsing is spent figuring out the
dimensions of our icons, which very often are PNG images. Apparently to
access the full header of a PNG file a lot of decoding needs to happen,
which is apparently quite expensive. However we don't really need to
access the full header and instead really only the width and height. And
here the PNG specification comes in handy:

https://www.w3.org/TR/2003/REC-PNG-20031110/

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:

```rust
// 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,
```

And with a little bit of bytemuck magic we essentially can access the
width and height with a single read. This improves parsing speed of
entire splits files by up to 30%. Yes, the old code was that slow.
  • Loading branch information
CryZe authored Nov 10, 2022
1 parent c2a4f9c commit 8aea813
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/run/parser/livesplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ fn parse_attempt_history(version: Version, reader: &mut Reader<'_>, run: &mut Ru
pub fn parse(source: &str, path: Option<PathBuf>) -> Result<Run> {
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();

Expand Down
96 changes: 67 additions & 29 deletions src/settings/image/shrinking.rs
Original file line number Diff line number Diff line change
@@ -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<Cow<'_, [u8]>, 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<Cow<'_, [u8]>> {
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
Expand All @@ -30,28 +66,30 @@ fn shrink_inner(data: &[u8], max_dim: u32) -> Result<Cow<'_, [u8]>, 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())
}

0 comments on commit 8aea813

Please sign in to comment.