Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize accessing PNG dimensions #600

Merged
merged 1 commit into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also snuck this in. Nowadays we know exactly how much memory we need for each image in here when we encounter it, so there's no need to preallocate anything if we are going to reallocate the memory anyway the moment we encounter an image.


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())
}