diff --git a/Cargo.toml b/Cargo.toml index fcc45e18..5fe02335 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ flate2 = "1.0" miniz_oxide = { version = "0.7.1", features = ["simd"] } [dev-dependencies] +byteorder = "1.5.0" clap = { version = "3.0", features = ["derive"] } criterion = "0.3.1" getopts = "0.2.14" diff --git a/benches/decoder.rs b/benches/decoder.rs index a078c9a0..419f555d 100644 --- a/benches/decoder.rs +++ b/benches/decoder.rs @@ -3,6 +3,8 @@ use std::fs; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use png::Decoder; +mod png_generator; + fn load_all(c: &mut Criterion) { for entry in fs::read_dir("tests/benches/").unwrap().flatten() { match entry.path().extension() { @@ -13,14 +15,29 @@ fn load_all(c: &mut Criterion) { let data = fs::read(entry.path()).unwrap(); bench_file(c, data, entry.file_name().into_string().unwrap()); } + + bench_noncompressed_png(c, 8); + bench_noncompressed_png(c, 128); } criterion_group!(benches, load_all); criterion_main!(benches); +fn bench_noncompressed_png(c: &mut Criterion, width: u32) { + let mut data = Vec::new(); + png_generator::write_noncompressed_png(&mut data, width); + bench_file( + c, + data, + format!("png_generator::noncompressed-{width}x{width}.png"), + ); +} + fn bench_file(c: &mut Criterion, data: Vec, name: String) { let mut group = c.benchmark_group("decode"); - group.sample_size(20); + if data.len() > 100000 { + group.sample_size(20); + } let decoder = Decoder::new(&*data); let mut reader = decoder.read_info().unwrap(); diff --git a/benches/png_generator.rs b/benches/png_generator.rs new file mode 100644 index 00000000..7c2acd01 --- /dev/null +++ b/benches/png_generator.rs @@ -0,0 +1,93 @@ +use byteorder::WriteBytesExt; +use std::io::Write; + +/// Generates a store-only, non-compressed image: +/// +/// * `00` compression mode (i.e.`BTYPE` = `00` = no compression) is used +/// * No filter is applied to the image rows +/// +/// Currently the image always has the following properties: +/// +/// * Single `IDAT` chunk +/// * Zlib chunks of maximum possible size +/// * 8-bit RGBA +/// +/// These images are somewhat artificial, but may be useful for benchmarking performance of parts +/// outside of `fdeflate` crate and/or the `unfilter` function (e.g. these images were originally +/// used to evaluate changes to minimize copying of image pixels between various buffers - see +/// [this +/// discussion](https://github.com/image-rs/image-png/discussions/416#discussioncomment-7436871) +/// for more details). +pub fn write_noncompressed_png(w: &mut impl Write, width: u32) { + write_png_sig(w); + write_ihdr(w, width); + write_noncompressed_idat(w, width); + write_iend(w); +} + +fn write_png_sig(w: &mut impl Write) { + const SIG: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; + w.write_all(&SIG).unwrap(); +} + +fn write_chunk(w: &mut impl Write, chunk_type: &[u8], data: &[u8]) { + let crc = { + let input = chunk_type + .iter() + .copied() + .chain(data.iter().copied()) + .collect::>(); + crc32fast::hash(input.as_slice()) + }; + w.write_u32::(data.len() as u32) + .unwrap(); + w.write_all(chunk_type).unwrap(); + w.write_all(data).unwrap(); + w.write_u32::(crc).unwrap(); +} + +fn write_ihdr(w: &mut impl Write, width: u32) { + let mut data = Vec::new(); + data.write_u32::(width).unwrap(); + data.write_u32::(width).unwrap(); // height + data.write_u8(8).unwrap(); // bit depth = always 8-bits per channel + data.write_u8(6).unwrap(); // color type = color + alpha + data.write_u8(0).unwrap(); // compression method (0 is the only allowed value) + data.write_u8(0).unwrap(); // filter method (0 is the only allowed value) + data.write_u8(0).unwrap(); // interlace method = no interlacing + write_chunk(w, b"IHDR", &data); +} + +fn write_noncompressed_idat(w: &mut impl Write, width: u32) { + // Generate arbitrary test pixels. + let image_pixels = { + let mut row = Vec::new(); + row.write_u8(0).unwrap(); // filter = no filter + + let row_pixels = (0..width) + .map(|i| { + let color: u8 = (i * 255 / width) as u8; + let alpha: u8 = 0xff; + [color, 255 - color, color / 2, alpha] + }) + .flatten(); + row.extend(row_pixels); + + std::iter::repeat(row) + .take(width as usize) + .flatten() + .collect::>() + }; + + let mut zlib_data = Vec::new(); + let mut store_only_compressor = + fdeflate::StoredOnlyCompressor::new(std::io::Cursor::new(&mut zlib_data)).unwrap(); + store_only_compressor.write_data(&image_pixels).unwrap(); + store_only_compressor.finish().unwrap(); + + write_chunk(w, b"IDAT", &zlib_data); +} + +fn write_iend(w: &mut impl Write) { + write_chunk(w, b"IEND", &[]); +} diff --git a/tests/benches/README.md b/tests/benches/README.md index 8591fca8..c5843fda 100644 --- a/tests/benches/README.md +++ b/tests/benches/README.md @@ -1,12 +1,13 @@ Copyrights: -Lohengrin_-_Illustrated_Sporting_and_Dramatic_News.png: Public Domain, according to Wikimedia -kodim*.png: Eastman Kodak Company, released for unrestricted use -Transparency.png: Public Domain, according to Wikimedia +* `Lohengrin_-_Illustrated_Sporting_and_Dramatic_News.png`: Public Domain, according to Wikimedia +* `kodim*.png`: Eastman Kodak Company, released for unrestricted use +* `Transparency.png`: Public Domain, according to Wikimedia The images use different filtering: -Lohengrin: no filtering -kodim02: mixed -kodim07: mainly paeth -kodim17: mainly sub -kodim23: mixed + +* Lohengrin: no filtering +* kodim02: mixed +* kodim07: mainly paeth +* kodim17: mainly sub +* kodim23: mixed