From a41d7de348367e33a17300af73876c7008bb4a3b Mon Sep 17 00:00:00 2001 From: andrews05 Date: Sun, 20 Nov 2022 02:49:25 +1300 Subject: [PATCH] New filter strategies (#461) * Refactor filters as enum * Include filter byte in filter output * Add entropy filter * Add bigrams filter * Add bigram entropy filter * Add brute filter * Replace bit-vec * Add tests and benches * Show filters in help * Use FxHasher in color to palette * Use windows function for minor improvement --- Cargo.lock | 54 ++++++- Cargo.toml | 5 +- benches/filters.rs | 62 ++++---- benches/strategies.rs | 58 ++++++++ src/evaluate.rs | 9 +- src/filters.rs | 291 ++++++++++++++++++++++--------------- src/interlace.rs | 40 ++--- src/lib.rs | 28 ++-- src/main.rs | 29 +++- src/png/mod.rs | 183 +++++++++++++++++------ src/reduction/bit_depth.rs | 15 +- src/reduction/color.rs | 10 +- tests/filters.rs | 138 +++++++++--------- tests/flags.rs | 8 +- tests/interlaced.rs | 4 +- tests/interlacing.rs | 202 +++++++++++++++++++++++++ tests/reduction.rs | 4 +- tests/regression.rs | 6 +- tests/strategies.rs | 124 ++++++++++++++++ 19 files changed, 938 insertions(+), 332 deletions(-) create mode 100644 benches/strategies.rs create mode 100644 tests/interlacing.rs create mode 100644 tests/strategies.rs diff --git a/Cargo.lock b/Cargo.lock index d7941738..f36931b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,18 +31,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "bytemuck" version = "1.12.1" @@ -194,6 +200,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "glob" version = "0.3.0" @@ -362,7 +374,7 @@ checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" name = "oxipng" version = "6.0.1" dependencies = [ - "bit-vec", + "bitvec", "clap", "crossbeam-channel", "filetime", @@ -373,6 +385,7 @@ dependencies = [ "log", "rayon", "rgb", + "rustc-hash", "rustc_version", "stderrlog", "wild", @@ -391,6 +404,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rayon" version = "1.5.3" @@ -433,6 +452,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -472,6 +497,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.1.3" @@ -585,6 +616,15 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "wyz" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +dependencies = [ + "tap", +] + [[package]] name = "zopfli" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index c087d365..e80291c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ path = "src/main.rs" required-features = ["binary"] [dependencies] -bit-vec = "0.6.3" itertools = "0.10.3" zopfli = { version = "0.7.1", optional = true } rgb = "0.8.33" @@ -32,6 +31,8 @@ libdeflater = "0.11.0" log = "0.4.17" stderrlog = { version = "0.5.3", optional = true, default-features = false } crossbeam-channel = "0.5.6" +bitvec = "1.0.1" +rustc-hash = "1.1.0" [dependencies.filetime] optional = true @@ -73,5 +74,5 @@ opt-level = 2 [profile.release] lto = "thin" -[profile.dev.package.bit-vec] +[profile.dev.package.bitvec] opt-level = 3 diff --git a/benches/filters.rs b/benches/filters.rs index 27a330b8..687f9709 100644 --- a/benches/filters.rs +++ b/benches/filters.rs @@ -3,7 +3,7 @@ extern crate oxipng; extern crate test; -use oxipng::internal_tests::*; +use oxipng::{internal_tests::*, RowFilter}; use std::path::PathBuf; use test::Bencher; @@ -13,7 +13,7 @@ fn filters_16_bits_filter_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(0); + png.raw.filter_image(RowFilter::None); }); } @@ -23,7 +23,7 @@ fn filters_8_bits_filter_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(0); + png.raw.filter_image(RowFilter::None); }); } @@ -35,7 +35,7 @@ fn filters_4_bits_filter_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(0); + png.raw.filter_image(RowFilter::None); }); } @@ -47,7 +47,7 @@ fn filters_2_bits_filter_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(0); + png.raw.filter_image(RowFilter::None); }); } @@ -59,7 +59,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(0); + png.raw.filter_image(RowFilter::None); }); } @@ -69,7 +69,7 @@ fn filters_16_bits_filter_1(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(1); + png.raw.filter_image(RowFilter::Sub); }); } @@ -79,7 +79,7 @@ fn filters_8_bits_filter_1(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(1); + png.raw.filter_image(RowFilter::Sub); }); } @@ -91,7 +91,7 @@ fn filters_4_bits_filter_1(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(1); + png.raw.filter_image(RowFilter::Sub); }); } @@ -103,7 +103,7 @@ fn filters_2_bits_filter_1(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(1); + png.raw.filter_image(RowFilter::Sub); }); } @@ -115,7 +115,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(1); + png.raw.filter_image(RowFilter::Sub); }); } @@ -125,7 +125,7 @@ fn filters_16_bits_filter_2(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(2); + png.raw.filter_image(RowFilter::Up); }); } @@ -135,7 +135,7 @@ fn filters_8_bits_filter_2(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(2); + png.raw.filter_image(RowFilter::Up); }); } @@ -147,7 +147,7 @@ fn filters_4_bits_filter_2(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(2); + png.raw.filter_image(RowFilter::Up); }); } @@ -159,7 +159,7 @@ fn filters_2_bits_filter_2(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(2); + png.raw.filter_image(RowFilter::Up); }); } @@ -171,7 +171,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(2); + png.raw.filter_image(RowFilter::Up); }); } @@ -181,7 +181,7 @@ fn filters_16_bits_filter_3(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(3); + png.raw.filter_image(RowFilter::Average); }); } @@ -191,7 +191,7 @@ fn filters_8_bits_filter_3(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(3); + png.raw.filter_image(RowFilter::Average); }); } @@ -203,7 +203,7 @@ fn filters_4_bits_filter_3(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(3); + png.raw.filter_image(RowFilter::Average); }); } @@ -215,7 +215,7 @@ fn filters_2_bits_filter_3(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(3); + png.raw.filter_image(RowFilter::Average); }); } @@ -227,7 +227,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(3); + png.raw.filter_image(RowFilter::Average); }); } @@ -237,7 +237,7 @@ fn filters_16_bits_filter_4(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(4); + png.raw.filter_image(RowFilter::Paeth); }); } @@ -247,7 +247,7 @@ fn filters_8_bits_filter_4(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(4); + png.raw.filter_image(RowFilter::Paeth); }); } @@ -259,7 +259,7 @@ fn filters_4_bits_filter_4(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(4); + png.raw.filter_image(RowFilter::Paeth); }); } @@ -271,7 +271,7 @@ fn filters_2_bits_filter_4(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(4); + png.raw.filter_image(RowFilter::Paeth); }); } @@ -283,7 +283,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(4); + png.raw.filter_image(RowFilter::Paeth); }); } @@ -293,7 +293,7 @@ fn filters_16_bits_filter_5(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(5); + png.raw.filter_image(RowFilter::MinSum); }); } @@ -303,7 +303,7 @@ fn filters_8_bits_filter_5(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(5); + png.raw.filter_image(RowFilter::MinSum); }); } @@ -315,7 +315,7 @@ fn filters_4_bits_filter_5(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(5); + png.raw.filter_image(RowFilter::MinSum); }); } @@ -327,7 +327,7 @@ fn filters_2_bits_filter_5(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(5); + png.raw.filter_image(RowFilter::MinSum); }); } @@ -339,6 +339,6 @@ fn filters_1_bits_filter_5(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - png.raw.filter_image(5); + png.raw.filter_image(RowFilter::MinSum); }); } diff --git a/benches/strategies.rs b/benches/strategies.rs new file mode 100644 index 00000000..a8b6c091 --- /dev/null +++ b/benches/strategies.rs @@ -0,0 +1,58 @@ +#![feature(test)] + +extern crate oxipng; +extern crate test; + +use oxipng::{internal_tests::*, RowFilter}; +use std::path::PathBuf; +use test::Bencher; + +#[bench] +fn filters_minsum(b: &mut Bencher) { + let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + png.raw.filter_image(RowFilter::MinSum); + }); +} + +#[bench] +fn filters_entropy(b: &mut Bencher) { + let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + png.raw.filter_image(RowFilter::Entropy); + }); +} + +#[bench] +fn filters_bigrams(b: &mut Bencher) { + let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + png.raw.filter_image(RowFilter::Bigrams); + }); +} + +#[bench] +fn filters_bigent(b: &mut Bencher) { + let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + png.raw.filter_image(RowFilter::BigEnt); + }); +} + +#[bench] +fn filters_brute(b: &mut Bencher) { + let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + png.raw.filter_image(RowFilter::Brute); + }); +} diff --git a/src/evaluate.rs b/src/evaluate.rs index 96bca0fb..b4d31919 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -3,10 +3,9 @@ use crate::atomicmin::AtomicMin; use crate::deflate; +use crate::filters::RowFilter; use crate::png::PngData; use crate::png::PngImage; -use crate::png::STD_COMPRESSION; -use crate::png::STD_FILTERS; #[cfg(not(feature = "parallel"))] use crate::rayon; use crate::Deadline; @@ -19,9 +18,13 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering::SeqCst; use std::sync::Arc; +/// Must use normal (lazy) compression, as faster ones (greedy) are not representative +const STD_COMPRESSION: u8 = 5; +const STD_FILTERS: [RowFilter; 2] = [RowFilter::None, RowFilter::MinSum]; + struct Candidate { image: PngData, - filter: u8, + filter: RowFilter, // first wins tie-breaker nth: usize, } diff --git a/src/filters.rs b/src/filters.rs index c0c10b10..ec62e8fe 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,130 +1,191 @@ +use std::{fmt::Display, mem::transmute}; + use crate::error::PngError; -pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8], buf: &mut Vec) { - assert!(data.len() >= bpp); - assert!(last_line.is_empty() || data.len() == last_line.len()); - buf.reserve(data.len()); - match filter { - 0 => { - buf.extend_from_slice(data); +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub enum RowFilter { + // Standard filter types + None, + Sub, + Up, + Average, + Paeth, + // Heuristic strategies + MinSum, + Entropy, + Bigrams, + BigEnt, + Brute, +} + +impl TryFrom for RowFilter { + type Error = (); + + fn try_from(value: u8) -> Result { + if value > Self::LAST { + return Err(()); } - 1 => { - buf.extend_from_slice(&data[0..bpp]); - buf.extend( - data.iter() - .skip(bpp) - .zip(data.iter()) - .map(|(cur, last)| cur.wrapping_sub(*last)), - ); - } - 2 => { - if last_line.is_empty() { - buf.extend_from_slice(data); - } else { - assert_eq!(data.len(), last_line.len()); - buf.extend( - data.iter() - .zip(last_line.iter()) - .map(|(cur, last)| cur.wrapping_sub(*last)), - ); - }; - } - 3 => { - for (i, byte) in data.iter().enumerate() { - if last_line.is_empty() { - buf.push(match i.checked_sub(bpp) { - Some(x) => byte.wrapping_sub(data[x] >> 1), - None => *byte, - }); - } else { - buf.push(match i.checked_sub(bpp) { - Some(x) => byte.wrapping_sub( - ((u16::from(data[x]) + u16::from(last_line[i])) >> 1) as u8, - ), - None => byte.wrapping_sub(last_line[i] >> 1), - }); - }; - } - } - 4 => { - for (i, byte) in data.iter().enumerate() { - if last_line.is_empty() { - buf.push(match i.checked_sub(bpp) { - Some(x) => byte.wrapping_sub(data[x]), - None => *byte, - }); - } else { - buf.push(match i.checked_sub(bpp) { - Some(x) => { - byte.wrapping_sub(paeth_predictor(data[x], last_line[i], last_line[x])) - } - None => byte.wrapping_sub(last_line[i]), - }); - }; - } - } - _ => unreachable!(), + unsafe { transmute(value as i8) } } } -pub fn unfilter_line( - filter: u8, - bpp: usize, - data: &[u8], - last_line: &[u8], - buf: &mut Vec, -) -> Result<(), PngError> { - buf.clear(); - buf.reserve(data.len()); - assert!(data.len() >= bpp); - assert_eq!(data.len(), last_line.len()); - match filter { - 0 => { - buf.extend_from_slice(data); - } - 1 => { - for (i, &cur) in data.iter().enumerate() { - let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied()); - buf.push(match prev_byte { - Some(b) => cur.wrapping_add(b), - None => cur, - }); +impl Display for RowFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:8}", + match *self { + Self::None => "None", + Self::Sub => "Sub", + Self::Up => "Up", + Self::Average => "Average", + Self::Paeth => "Paeth", + Self::MinSum => "MinSum", + Self::Entropy => "Entropy", + Self::Bigrams => "Bigrams", + Self::BigEnt => "BigEnt", + Self::Brute => "Brute", } - } - 2 => { - buf.extend( - data.iter() - .zip(last_line) - .map(|(&cur, &last)| cur.wrapping_add(last)), - ); - } - 3 => { - for (i, (&cur, &last)) in data.iter().zip(last_line).enumerate() { - let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied()); - buf.push(match prev_byte { - Some(b) => cur.wrapping_add(((u16::from(b) + u16::from(last)) >> 1) as u8), - None => cur.wrapping_add(last >> 1), - }); + ) + } +} + +impl RowFilter { + pub const LAST: u8 = Self::Brute as u8; + pub const STANDARD: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth]; + pub const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub]; + + pub fn filter_line(self, bpp: usize, data: &[u8], last_line: &[u8], buf: &mut Vec) { + assert!(data.len() >= bpp); + assert!(last_line.is_empty() || data.len() == last_line.len()); + buf.clear(); + buf.reserve(data.len() + 1); + buf.push(self as u8); + match self { + Self::None => { + buf.extend_from_slice(data); } - } - 4 => { - for (i, (&cur, &up)) in data.iter().zip(last_line).enumerate() { - buf.push( - match i - .checked_sub(bpp) - .map(|x| (buf.get(x).copied(), last_line.get(x).copied())) - { - Some((Some(left), Some(left_up))) => { - cur.wrapping_add(paeth_predictor(left, up, left_up)) - } - _ => cur.wrapping_add(up), - }, + Self::Sub => { + buf.extend_from_slice(&data[0..bpp]); + buf.extend( + data.iter() + .skip(bpp) + .zip(data.iter()) + .map(|(cur, last)| cur.wrapping_sub(*last)), ); } + Self::Up => { + if last_line.is_empty() { + buf.extend_from_slice(data); + } else { + assert_eq!(data.len(), last_line.len()); + buf.extend( + data.iter() + .zip(last_line.iter()) + .map(|(cur, last)| cur.wrapping_sub(*last)), + ); + }; + } + Self::Average => { + for (i, byte) in data.iter().enumerate() { + if last_line.is_empty() { + buf.push(match i.checked_sub(bpp) { + Some(x) => byte.wrapping_sub(data[x] >> 1), + None => *byte, + }); + } else { + buf.push(match i.checked_sub(bpp) { + Some(x) => byte.wrapping_sub( + ((u16::from(data[x]) + u16::from(last_line[i])) >> 1) as u8, + ), + None => byte.wrapping_sub(last_line[i] >> 1), + }); + }; + } + } + Self::Paeth => { + for (i, byte) in data.iter().enumerate() { + if last_line.is_empty() { + buf.push(match i.checked_sub(bpp) { + Some(x) => byte.wrapping_sub(data[x]), + None => *byte, + }); + } else { + buf.push(match i.checked_sub(bpp) { + Some(x) => byte.wrapping_sub(paeth_predictor( + data[x], + last_line[i], + last_line[x], + )), + None => byte.wrapping_sub(last_line[i]), + }); + }; + } + } + _ => unreachable!(), } - _ => return Err(PngError::InvalidData), } - Ok(()) + + pub fn unfilter_line( + self, + bpp: usize, + data: &[u8], + last_line: &[u8], + buf: &mut Vec, + ) -> Result<(), PngError> { + buf.clear(); + buf.reserve(data.len()); + assert!(data.len() >= bpp); + assert_eq!(data.len(), last_line.len()); + match self { + Self::None => { + buf.extend_from_slice(data); + } + Self::Sub => { + for (i, &cur) in data.iter().enumerate() { + let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied()); + buf.push(match prev_byte { + Some(b) => cur.wrapping_add(b), + None => cur, + }); + } + } + Self::Up => { + buf.extend( + data.iter() + .zip(last_line) + .map(|(&cur, &last)| cur.wrapping_add(last)), + ); + } + Self::Average => { + for (i, (&cur, &last)) in data.iter().zip(last_line).enumerate() { + let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied()); + buf.push(match prev_byte { + Some(b) => cur.wrapping_add(((u16::from(b) + u16::from(last)) >> 1) as u8), + None => cur.wrapping_add(last >> 1), + }); + } + } + Self::Paeth => { + for (i, (&cur, &up)) in data.iter().zip(last_line).enumerate() { + buf.push( + match i + .checked_sub(bpp) + .map(|x| (buf.get(x).copied(), last_line.get(x).copied())) + { + Some((Some(left), Some(left_up))) => { + cur.wrapping_add(paeth_predictor(left, up, left_up)) + } + _ => cur.wrapping_add(up), + }, + ); + } + } + _ => return Err(PngError::InvalidData), + } + Ok(()) + } } fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 { diff --git a/src/interlace.rs b/src/interlace.rs index 81928c7d..33332724 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -1,47 +1,47 @@ use crate::headers::IhdrData; use crate::png::PngImage; -use bit_vec::BitVec; +use bitvec::prelude::*; #[must_use] pub fn interlace_image(png: &PngImage) -> PngImage { - let mut passes: Vec = vec![BitVec::new(); 7]; + let mut passes: Vec> = vec![BitVec::new(); 7]; let bits_per_pixel = png.ihdr.bpp(); for (index, line) in png.scan_lines().enumerate() { match index % 8 { // Add filter bytes to passes that will be in the output image 0 => { - passes[0].extend(BitVec::from_elem(8, false)); + passes[0].extend_from_raw_slice(&[0]); if png.ihdr.width >= 5 { - passes[1].extend(BitVec::from_elem(8, false)); + passes[1].extend_from_raw_slice(&[0]); } if png.ihdr.width >= 3 { - passes[3].extend(BitVec::from_elem(8, false)); + passes[3].extend_from_raw_slice(&[0]); } if png.ihdr.width >= 2 { - passes[5].extend(BitVec::from_elem(8, false)); + passes[5].extend_from_raw_slice(&[0]); } } 4 => { - passes[2].extend(BitVec::from_elem(8, false)); + passes[2].extend_from_raw_slice(&[0]); if png.ihdr.width >= 3 { - passes[3].extend(BitVec::from_elem(8, false)); + passes[3].extend_from_raw_slice(&[0]); } if png.ihdr.width >= 2 { - passes[5].extend(BitVec::from_elem(8, false)); + passes[5].extend_from_raw_slice(&[0]); } } 2 | 6 => { - passes[4].extend(BitVec::from_elem(8, false)); + passes[4].extend_from_raw_slice(&[0]); if png.ihdr.width >= 2 { - passes[5].extend(BitVec::from_elem(8, false)); + passes[5].extend_from_raw_slice(&[0]); } } _ => { - passes[6].extend(BitVec::from_elem(8, false)); + passes[6].extend_from_raw_slice(&[0]); } } - let bit_vec = BitVec::from_bytes(line.data); - for (i, bit) in bit_vec.iter().enumerate() { + let bit_vec = line.data.view_bits::(); + for (i, bit) in bit_vec.iter().by_vals().enumerate() { // Avoid moving padded 0's into new image if i >= (png.ihdr.width * u32::from(bits_per_pixel)) as usize { break; @@ -79,7 +79,7 @@ pub fn interlace_image(png: &PngImage) -> PngImage { let mut output = Vec::with_capacity(png.data.len()); for pass in &passes { - output.extend(pass.to_bytes()); + output.extend_from_slice(pass.as_raw_slice()); } PngImage { @@ -99,19 +99,19 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage { let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr.width as usize; // Initialize each output line with a starting filter byte of 0 // as well as some blank data - let mut lines: Vec = - vec![BitVec::from_elem(bits_per_line, false); png.ihdr.height as usize]; + let mut lines: Vec> = + vec![bitvec![u8, Msb0; 0; bits_per_line]; png.ihdr.height as usize]; let mut current_pass = 1; let mut pass_constants = interlaced_constants(current_pass); let mut current_y: usize = pass_constants.y_shift as usize; for line in png.scan_lines() { - let bit_vec = BitVec::from_bytes(line.data); + let bit_vec = line.data.view_bits::(); let bits_in_line = ((png.ihdr.width - u32::from(pass_constants.x_shift) + u32::from(pass_constants.x_step) - 1) / u32::from(pass_constants.x_step)) as usize * bits_per_pixel as usize; - for (i, bit) in bit_vec.iter().enumerate() { + for (i, bit) in bit_vec.iter().by_vals().enumerate() { // Avoid moving padded 0's into new image if i >= bits_in_line { break; @@ -157,7 +157,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage { while line.len() % 8 != 0 { line.push(false); } - output.extend(line.to_bytes()); + output.extend_from_slice(line.as_raw_slice()); } PngImage { data: output, diff --git a/src/lib.rs b/src/lib.rs index fc57e16f..899640da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ use std::time::{Duration, Instant}; pub use crate::colors::AlphaOptim; pub use crate::deflate::Deflaters; pub use crate::error::PngError; +pub use crate::filters::RowFilter; pub use crate::headers::Headers; pub use indexmap::{IndexMap, IndexSet}; @@ -149,7 +150,7 @@ pub struct Options { /// Which filters to try on the file (0-5) /// /// Default: `0,5` - pub filter: IndexSet, + pub filter: IndexSet, /// Whether to change the interlacing type of the file. /// /// `None` will not change the current interlacing type. @@ -253,9 +254,10 @@ impl Options { } fn apply_preset_3(mut self) -> Self { - for i in 1..=4 { - self.filter.insert(i); - } + self.filter.insert(RowFilter::Sub); + self.filter.insert(RowFilter::Up); + self.filter.insert(RowFilter::Average); + self.filter.insert(RowFilter::Paeth); self } @@ -292,8 +294,8 @@ impl Default for Options { fn default() -> Options { // Default settings based on -o 2 from the CLI interface let mut filter = IndexSet::new(); - filter.insert(0); - filter.insert(5); + filter.insert(RowFilter::None); + filter.insert(RowFilter::MinSum); let mut compression = IndexSet::new(); compression.insert(11); // We always need NoOp to be present @@ -465,7 +467,7 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult> { #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] /// Defines options to be used for a single compression trial struct TrialOptions { - pub filter: u8, + pub filter: RowFilter, pub compression: u8, } @@ -511,9 +513,9 @@ fn optimize_png( let use_filter = if png.raw.ihdr.bit_depth.as_u8() >= 8 && png.raw.ihdr.color_type != colors::ColorType::Indexed { - 5 + RowFilter::MinSum } else { - 0 + RowFilter::None }; if filter.is_empty() { filter.insert(use_filter); @@ -570,7 +572,7 @@ fn optimize_png( info!("Trying: {} combinations", results.len()); - let filters: IndexMap> = filter + let filters: IndexMap> = filter .par_iter() .with_max_len(1) .map(|f| { @@ -601,7 +603,7 @@ fn optimize_png( Ok(n) => n, Err(PngError::DeflatedDataTooLong(max)) => { debug!( - " zc = {} f = {} >{} bytes", + " zc = {} f = {} >{} bytes", trial.compression, trial.filter, max, ); return None; @@ -614,7 +616,7 @@ fn optimize_png( best_size.set_min(new_size); debug!( - " zc = {} f = {} {} bytes", + " zc = {} f = {} {} bytes", trial.compression, trial.filter, new_idat.len() @@ -638,7 +640,7 @@ fn optimize_png( png.idat_data = idat_data; info!("Found better combination:"); info!( - " zc = {} f = {} {} bytes", + " zc = {} f = {} {} bytes", opts.compression, opts.filter, png.idat_data.len() diff --git a/src/main.rs b/src/main.rs index 1ac6f70f..969b856d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ use oxipng::AlphaOptim; use oxipng::Deflaters; use oxipng::Headers; use oxipng::Options; +use oxipng::RowFilter; use oxipng::{InFile, OutFile}; use std::fs::DirBuilder; #[cfg(feature = "zopfli")] @@ -168,11 +169,15 @@ fn main() { ) .arg( Arg::new("filters") - .help("PNG delta filters (0-5) - Default: 0,5") + .help(&*format!( + "PNG delta filters (0-{}) - Default: 0,{}", + RowFilter::LAST, + RowFilter::MinSum as u8 + )) .short('f') .long("filters") .takes_value(true) - .validator(|x| match parse_numeric_range_opts(x, 0, 5) { + .validator(|x| match parse_numeric_range_opts(x, 0, RowFilter::LAST) { Ok(_) => Ok(()), Err(_) => Err("Invalid option for filters".to_owned()), }), @@ -264,7 +269,20 @@ fn main() { -o max => (stable alias for the max compression) Manually specifying a compression option (zc, f, etc.) will override the optimization preset, - regardless of the order you write the arguments.", + regardless of the order you write the arguments. + +PNG delta filters: + 0 => None + 1 => Sub + 2 => Up + 3 => Average + 4 => Paeth +Heuristic filter selection strategies: + 5 => MinSum Minimum sum of absolute differences + 6 => Entropy Highest Shannon entropy + 7 => Bigrams Lowest count of distinct bigrams + 8 => BigEnt Highest Shannon entropy of bigrams + 9 => Brute Smallest compressed size (slow)", ) .get_matches_from(wild::args()); @@ -381,7 +399,10 @@ fn parse_opts_into_struct( } if let Some(x) = matches.value_of("filters") { - opts.filter = parse_numeric_range_opts(x, 0, 5).unwrap(); + opts.filter.clear(); + for f in parse_numeric_range_opts(x, 0, RowFilter::LAST).unwrap() { + opts.filter.insert(RowFilter::try_from(f).unwrap()); + } } if let Some(x) = matches.value_of("timeout") { diff --git a/src/png/mod.rs b/src/png/mod.rs index 2d1adace..147c1c9b 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -4,23 +4,27 @@ use crate::error::PngError; use crate::filters::*; use crate::headers::*; use crate::interlace::{deinterlace_image, interlace_image}; +use bitvec::bitarr; use indexmap::IndexMap; +use libdeflater::{CompressionLvl, Compressor}; use rgb::ComponentSlice; use rgb::RGBA8; +use rustc_hash::FxHashMap; use std::fs::File; use std::io::{BufReader, Read, Write}; use std::iter::Iterator; use std::path::Path; use std::sync::Arc; -/// Must use normal (lazy) compression, as faster ones (greedy) are not representative -pub(crate) const STD_COMPRESSION: u8 = 5; -pub(crate) const STD_FILTERS: [u8; 2] = [0, 5]; - pub(crate) mod scan_lines; use self::scan_lines::{ScanLines, ScanLinesMut}; +/// Compression level to use for the Brute filter strategy +const BRUTE_LEVEL: i32 = 1; // 1 is fastest, 2-4 are not useful, 5 is slower but more effective +/// Number of lines to compress with the Brute filter strategy +const BRUTE_LINES: usize = 4; // Values over 8 are generally not useful + #[derive(Debug, Clone)] pub struct PngImage { /// The headers stored in the IHDR chunk @@ -292,7 +296,8 @@ impl PngImage { last_pass = line.pass; } last_line.resize(line.data.len(), 0); - unfilter_line(line.filter, bpp, line.data, &last_line, &mut unfiltered_buf)?; + let filter = RowFilter::try_from(line.filter).map_err(|_| PngError::InvalidData)?; + filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf)?; unfiltered.push(0); unfiltered.extend_from_slice(&unfiltered_buf); std::mem::swap(&mut last_line, &mut unfiltered_buf); @@ -302,61 +307,141 @@ impl PngImage { } /// Apply the specified filter type to all rows in the image - /// 0: None - /// 1: Sub - /// 2: Up - /// 3: Average - /// 4: Paeth - /// 5: All (heuristically pick the best filter for each line) - pub fn filter_image(&self, filter: u8) -> Vec { + pub fn filter_image(&self, filter: RowFilter) -> Vec { let mut filtered = Vec::with_capacity(self.data.len()); let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; let mut last_line: &[u8] = &[]; let mut last_pass: Option = None; let mut f_buf = Vec::new(); for line in self.scan_lines() { - f_buf.clear(); if last_pass != line.pass { last_line = &[]; } - match filter { - 0 | 1 | 2 | 3 | 4 => { - let filter = if last_pass == line.pass || filter <= 1 { - filter - } else { - 0 - }; - filtered.push(filter); - filter_line(filter, bpp, line.data, last_line, &mut f_buf); - filtered.extend_from_slice(&f_buf); - } - 5 => { - // Heuristically guess best filter per line - // Uses MSAD algorithm mentioned in libpng reference docs - // http://www.libpng.org/pub/png/book/chapter09.html - let mut best_filter = 0; - let mut best_line = Vec::new(); - let mut best_size = u64::MAX; - // Avoid vertical filtering on first line of each interlacing pass - for filter in if last_pass == line.pass { 0..5 } else { 0..2 } { - filter_line(filter, bpp, line.data, last_line, &mut f_buf); - let size = f_buf.iter().fold(0_u64, |acc, &x| { - let signed = x as i8; - acc + i16::from(signed).unsigned_abs() as u64 - }); - if size < best_size { - best_size = size; - best_filter = filter; - std::mem::swap(&mut best_line, &mut f_buf); + if filter <= RowFilter::Paeth { + // Standard filters + let filter = if last_pass == line.pass || filter <= RowFilter::Sub { + filter + } else { + RowFilter::None + }; + filter.filter_line(bpp, line.data, last_line, &mut f_buf); + filtered.extend_from_slice(&f_buf); + } else { + // Heuristic filter selection strategies + let mut best_line = Vec::new(); + // Avoid vertical filtering on first line of each interlacing pass + let try_filters = if last_pass == line.pass { + RowFilter::STANDARD.iter() + } else { + RowFilter::SINGLE_LINE.iter() + }; + match filter { + RowFilter::MinSum => { + // MSAD algorithm mentioned in libpng reference docs + // http://www.libpng.org/pub/png/book/chapter09.html + let mut best_size = usize::MAX; + for try_filter in try_filters { + try_filter.filter_line(bpp, line.data, last_line, &mut f_buf); + let size = f_buf.iter().fold(0, |acc, &x| { + let signed = x as i8; + acc + signed.unsigned_abs() as usize + }); + if size < best_size { + best_size = size; + std::mem::swap(&mut best_line, &mut f_buf); + } } - f_buf.clear() //discard buffer, and start again } - filtered.push(best_filter); - filtered.extend_from_slice(&best_line); + RowFilter::Entropy => { + // Shannon entropy algorithm, from LodePNG + // https://github.com/lvandeve/lodepng + let mut best_size = i32::MIN; + for try_filter in try_filters { + try_filter.filter_line(bpp, line.data, last_line, &mut f_buf); + let mut counts = vec![0; 0x100]; + for &i in f_buf.iter() { + counts[i as usize] += 1; + } + let size = counts.into_iter().fold(0, |acc, x| { + if x == 0 { + return acc; + } + acc + ilog2i(x) + }) as i32; + if size > best_size { + best_size = size; + std::mem::swap(&mut best_line, &mut f_buf); + } + } + } + RowFilter::Bigrams => { + // Count distinct bigrams, from pngwolf + // https://bjoern.hoehrmann.de/pngwolf/ + let mut best_size = usize::MAX; + for try_filter in try_filters { + try_filter.filter_line(bpp, line.data, last_line, &mut f_buf); + let mut set = bitarr![0; 0x10000]; + for pair in f_buf.windows(2) { + let bigram = (pair[0] as usize) << 8 | pair[1] as usize; + set.set(bigram, true); + } + let size = set.count_ones(); + if size < best_size { + best_size = size; + std::mem::swap(&mut best_line, &mut f_buf); + } + } + } + RowFilter::BigEnt => { + // Bigram entropy, combined from Entropy and Bigrams filters + let mut best_size = i32::MIN; + // FxHasher is the fastest rust hasher currently available for this purpose + let mut counts = FxHashMap::::default(); + for try_filter in try_filters { + try_filter.filter_line(bpp, line.data, last_line, &mut f_buf); + counts.clear(); + for pair in f_buf.windows(2) { + let bigram = (pair[0] as u16) << 8 | pair[1] as u16; + counts.entry(bigram).and_modify(|e| *e += 1).or_insert(1); + } + let size = counts.values().fold(0, |acc, &x| acc + ilog2i(x)) as i32; + if size > best_size { + best_size = size; + std::mem::swap(&mut best_line, &mut f_buf); + } + } + } + RowFilter::Brute => { + // Brute force by compressing each filter attempt + // Similar to that of LodePNG but includes some previous lines for context + let mut best_size = usize::MAX; + let line_start = filtered.len(); + filtered.resize(filtered.len() + line.data.len() + 1, 0); + let mut compressor = + Compressor::new(CompressionLvl::new(BRUTE_LEVEL).unwrap()); + let limit = filtered.len().min((line.data.len() + 1) * BRUTE_LINES); + let capacity = compressor.zlib_compress_bound(limit); + let mut dest = vec![0; capacity]; + + for try_filter in try_filters { + try_filter.filter_line(bpp, line.data, last_line, &mut f_buf); + filtered[line_start..].copy_from_slice(&f_buf); + let size = compressor + .zlib_compress(&filtered[filtered.len() - limit..], &mut dest) + .unwrap_or(usize::MAX); + if size < best_size { + best_size = size; + std::mem::swap(&mut best_line, &mut f_buf); + } + } + filtered.resize(line_start, 0); + } + _ => unreachable!(), } - _ => unreachable!(), + filtered.extend_from_slice(&best_line); } + last_line = line.data; last_pass = line.pass; } @@ -373,3 +458,9 @@ fn write_png_block(key: &[u8], header: &[u8], output: &mut Vec) { output.append(&mut header_data); output.extend_from_slice(&crc.to_be_bytes()); } + +// Integer approximation for i * log2(i) - much faster than float calculations +fn ilog2i(i: u32) -> u32 { + let log = 32 - i.leading_zeros() - 1; + i * log + ((i - (1 << log)) << 1) +} diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 12c837f6..e29a7075 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -1,7 +1,7 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; -use bit_vec::BitVec; +use bitvec::prelude::*; const ONE_BIT_PERMUTATIONS: [u8; 2] = [0b0000_0000, 0b1111_1111]; const TWO_BIT_PERMUTATIONS: [u8; 4] = [0b0000_0000, 0b0101_0101, 0b1010_1010, 0b1111_1111]; @@ -108,8 +108,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op } } } else { - let bit_vec = BitVec::from_bytes(line.data); - for byte in bit_vec.to_bytes() { + for &byte in line.data { while minimum_bits < bit_depth { let permutations: &[u8] = if minimum_bits == 1 { &ONE_BIT_PERMUTATIONS @@ -129,11 +128,11 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op } } - let mut reduced = BitVec::with_capacity(png.data.len() * 8); + let mut reduced = BitVec::::with_capacity(png.data.len() * 8); for line in png.scan_lines() { - reduced.extend(BitVec::from_bytes(&[line.filter])); - let bit_vec = BitVec::from_bytes(line.data); - for (i, bit) in bit_vec.iter().enumerate() { + reduced.extend_from_raw_slice(&[line.filter]); + let bit_vec = line.data.view_bits::(); + for (i, bit) in bit_vec.iter().by_vals().enumerate() { let bit_index = bit_depth - (i % bit_depth); if bit_index <= minimum_bits { reduced.push(bit); @@ -146,7 +145,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op } Some(PngImage { - data: reduced.to_bytes(), + data: reduced.as_raw_slice().to_vec(), ihdr: IhdrData { bit_depth: BitDepth::from_u8(minimum_bits as u8), ..png.ihdr diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 078b01c7..5284c94e 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -4,7 +4,10 @@ use crate::png::PngImage; use indexmap::IndexMap; use itertools::Itertools; use rgb::{FromSlice, RGB8, RGBA8}; -use std::hash::Hash; +use rustc_hash::FxHasher; +use std::hash::{BuildHasherDefault, Hash}; + +type FxIndexMap = IndexMap>; #[must_use] pub fn reduce_rgba_to_grayscale_alpha(png: &PngImage) -> Option { @@ -78,7 +81,7 @@ pub fn reduce_rgba_to_grayscale_alpha(png: &PngImage) -> Option { fn reduce_scanline_to_palette( iter: impl IntoIterator, - palette: &mut IndexMap, + palette: &mut FxIndexMap, reduced: &mut Vec, ) -> bool where @@ -107,7 +110,8 @@ pub fn reduced_color_to_palette(png: &PngImage) -> Option { return None; } let mut raw_data = Vec::with_capacity(png.data.len()); - let mut palette = IndexMap::with_capacity(257); + let mut palette = FxIndexMap::default(); + palette.reserve(257); let transparency_pixel = png .transparency_pixel .as_ref() diff --git a/tests/filters.rs b/tests/filters.rs index 4da00a8c..e1dbad40 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -1,5 +1,5 @@ use indexmap::IndexSet; -use oxipng::internal_tests::*; +use oxipng::{internal_tests::*, RowFilter}; use oxipng::{InFile, OutFile}; use std::fs::remove_file; use std::path::Path; @@ -11,7 +11,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { ..Default::default() }; let mut filter = IndexSet::new(); - filter.insert(0); + filter.insert(RowFilter::None); options.filter = filter; ( @@ -22,7 +22,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn test_it_converts( input: &str, - filter: u8, + filter: RowFilter, color_type_in: ColorType, bit_depth_in: BitDepth, color_type_out: ColorType, @@ -67,7 +67,7 @@ fn test_it_converts( fn filter_0_for_rgba_16() { test_it_converts( "tests/files/filter_0_for_rgba_16.png", - 0, + RowFilter::None, ColorType::RGBA, BitDepth::Sixteen, ColorType::RGBA, @@ -79,7 +79,7 @@ fn filter_0_for_rgba_16() { fn filter_1_for_rgba_16() { test_it_converts( "tests/files/filter_1_for_rgba_16.png", - 1, + RowFilter::Sub, ColorType::RGBA, BitDepth::Sixteen, ColorType::RGBA, @@ -91,7 +91,7 @@ fn filter_1_for_rgba_16() { fn filter_2_for_rgba_16() { test_it_converts( "tests/files/filter_2_for_rgba_16.png", - 2, + RowFilter::Up, ColorType::RGBA, BitDepth::Sixteen, ColorType::RGBA, @@ -103,7 +103,7 @@ fn filter_2_for_rgba_16() { fn filter_3_for_rgba_16() { test_it_converts( "tests/files/filter_3_for_rgba_16.png", - 3, + RowFilter::Average, ColorType::RGBA, BitDepth::Sixteen, ColorType::RGBA, @@ -115,7 +115,7 @@ fn filter_3_for_rgba_16() { fn filter_4_for_rgba_16() { test_it_converts( "tests/files/filter_4_for_rgba_16.png", - 4, + RowFilter::Paeth, ColorType::RGBA, BitDepth::Sixteen, ColorType::RGBA, @@ -127,7 +127,7 @@ fn filter_4_for_rgba_16() { fn filter_5_for_rgba_16() { test_it_converts( "tests/files/filter_5_for_rgba_16.png", - 5, + RowFilter::MinSum, ColorType::RGBA, BitDepth::Sixteen, ColorType::RGBA, @@ -139,7 +139,7 @@ fn filter_5_for_rgba_16() { fn filter_0_for_rgba_8() { test_it_converts( "tests/files/filter_0_for_rgba_8.png", - 0, + RowFilter::None, ColorType::RGBA, BitDepth::Eight, ColorType::RGBA, @@ -151,7 +151,7 @@ fn filter_0_for_rgba_8() { fn filter_1_for_rgba_8() { test_it_converts( "tests/files/filter_1_for_rgba_8.png", - 1, + RowFilter::Sub, ColorType::RGBA, BitDepth::Eight, ColorType::RGBA, @@ -163,7 +163,7 @@ fn filter_1_for_rgba_8() { fn filter_2_for_rgba_8() { test_it_converts( "tests/files/filter_2_for_rgba_8.png", - 2, + RowFilter::Up, ColorType::RGBA, BitDepth::Eight, ColorType::RGBA, @@ -175,7 +175,7 @@ fn filter_2_for_rgba_8() { fn filter_3_for_rgba_8() { test_it_converts( "tests/files/filter_3_for_rgba_8.png", - 3, + RowFilter::Average, ColorType::RGBA, BitDepth::Eight, ColorType::RGBA, @@ -187,7 +187,7 @@ fn filter_3_for_rgba_8() { fn filter_4_for_rgba_8() { test_it_converts( "tests/files/filter_4_for_rgba_8.png", - 4, + RowFilter::Paeth, ColorType::RGBA, BitDepth::Eight, ColorType::RGBA, @@ -199,7 +199,7 @@ fn filter_4_for_rgba_8() { fn filter_5_for_rgba_8() { test_it_converts( "tests/files/filter_5_for_rgba_8.png", - 5, + RowFilter::MinSum, ColorType::RGBA, BitDepth::Eight, ColorType::RGBA, @@ -211,7 +211,7 @@ fn filter_5_for_rgba_8() { fn filter_0_for_rgb_16() { test_it_converts( "tests/files/filter_0_for_rgb_16.png", - 0, + RowFilter::None, ColorType::RGB, BitDepth::Sixteen, ColorType::RGB, @@ -223,7 +223,7 @@ fn filter_0_for_rgb_16() { fn filter_1_for_rgb_16() { test_it_converts( "tests/files/filter_1_for_rgb_16.png", - 1, + RowFilter::Sub, ColorType::RGB, BitDepth::Sixteen, ColorType::RGB, @@ -235,7 +235,7 @@ fn filter_1_for_rgb_16() { fn filter_2_for_rgb_16() { test_it_converts( "tests/files/filter_2_for_rgb_16.png", - 2, + RowFilter::Up, ColorType::RGB, BitDepth::Sixteen, ColorType::RGB, @@ -247,7 +247,7 @@ fn filter_2_for_rgb_16() { fn filter_3_for_rgb_16() { test_it_converts( "tests/files/filter_3_for_rgb_16.png", - 3, + RowFilter::Average, ColorType::RGB, BitDepth::Sixteen, ColorType::RGB, @@ -259,7 +259,7 @@ fn filter_3_for_rgb_16() { fn filter_4_for_rgb_16() { test_it_converts( "tests/files/filter_4_for_rgb_16.png", - 4, + RowFilter::Paeth, ColorType::RGB, BitDepth::Sixteen, ColorType::RGB, @@ -271,7 +271,7 @@ fn filter_4_for_rgb_16() { fn filter_5_for_rgb_16() { test_it_converts( "tests/files/filter_5_for_rgb_16.png", - 5, + RowFilter::MinSum, ColorType::RGB, BitDepth::Sixteen, ColorType::RGB, @@ -283,7 +283,7 @@ fn filter_5_for_rgb_16() { fn filter_0_for_rgb_8() { test_it_converts( "tests/files/filter_0_for_rgb_8.png", - 0, + RowFilter::None, ColorType::RGB, BitDepth::Eight, ColorType::RGB, @@ -295,7 +295,7 @@ fn filter_0_for_rgb_8() { fn filter_1_for_rgb_8() { test_it_converts( "tests/files/filter_1_for_rgb_8.png", - 1, + RowFilter::Sub, ColorType::RGB, BitDepth::Eight, ColorType::RGB, @@ -307,7 +307,7 @@ fn filter_1_for_rgb_8() { fn filter_2_for_rgb_8() { test_it_converts( "tests/files/filter_2_for_rgb_8.png", - 2, + RowFilter::Up, ColorType::RGB, BitDepth::Eight, ColorType::RGB, @@ -319,7 +319,7 @@ fn filter_2_for_rgb_8() { fn filter_3_for_rgb_8() { test_it_converts( "tests/files/filter_3_for_rgb_8.png", - 3, + RowFilter::Average, ColorType::RGB, BitDepth::Eight, ColorType::RGB, @@ -331,7 +331,7 @@ fn filter_3_for_rgb_8() { fn filter_4_for_rgb_8() { test_it_converts( "tests/files/filter_4_for_rgb_8.png", - 4, + RowFilter::Paeth, ColorType::RGB, BitDepth::Eight, ColorType::RGB, @@ -343,7 +343,7 @@ fn filter_4_for_rgb_8() { fn filter_5_for_rgb_8() { test_it_converts( "tests/files/filter_5_for_rgb_8.png", - 5, + RowFilter::MinSum, ColorType::RGB, BitDepth::Eight, ColorType::RGB, @@ -355,7 +355,7 @@ fn filter_5_for_rgb_8() { fn filter_0_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_0_for_grayscale_alpha_16.png", - 0, + RowFilter::None, ColorType::GrayscaleAlpha, BitDepth::Sixteen, ColorType::GrayscaleAlpha, @@ -367,7 +367,7 @@ fn filter_0_for_grayscale_alpha_16() { fn filter_1_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_1_for_grayscale_alpha_16.png", - 1, + RowFilter::Sub, ColorType::GrayscaleAlpha, BitDepth::Sixteen, ColorType::GrayscaleAlpha, @@ -379,7 +379,7 @@ fn filter_1_for_grayscale_alpha_16() { fn filter_2_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_2_for_grayscale_alpha_16.png", - 2, + RowFilter::Up, ColorType::GrayscaleAlpha, BitDepth::Sixteen, ColorType::GrayscaleAlpha, @@ -391,7 +391,7 @@ fn filter_2_for_grayscale_alpha_16() { fn filter_3_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_3_for_grayscale_alpha_16.png", - 3, + RowFilter::Average, ColorType::GrayscaleAlpha, BitDepth::Sixteen, ColorType::GrayscaleAlpha, @@ -403,7 +403,7 @@ fn filter_3_for_grayscale_alpha_16() { fn filter_4_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_4_for_grayscale_alpha_16.png", - 4, + RowFilter::Paeth, ColorType::GrayscaleAlpha, BitDepth::Sixteen, ColorType::GrayscaleAlpha, @@ -415,7 +415,7 @@ fn filter_4_for_grayscale_alpha_16() { fn filter_5_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_5_for_grayscale_alpha_16.png", - 5, + RowFilter::MinSum, ColorType::GrayscaleAlpha, BitDepth::Sixteen, ColorType::GrayscaleAlpha, @@ -427,7 +427,7 @@ fn filter_5_for_grayscale_alpha_16() { fn filter_0_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_0_for_grayscale_alpha_8.png", - 0, + RowFilter::None, ColorType::GrayscaleAlpha, BitDepth::Eight, ColorType::GrayscaleAlpha, @@ -439,7 +439,7 @@ fn filter_0_for_grayscale_alpha_8() { fn filter_1_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_1_for_grayscale_alpha_8.png", - 1, + RowFilter::Sub, ColorType::GrayscaleAlpha, BitDepth::Eight, ColorType::GrayscaleAlpha, @@ -451,7 +451,7 @@ fn filter_1_for_grayscale_alpha_8() { fn filter_2_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_2_for_grayscale_alpha_8.png", - 2, + RowFilter::Up, ColorType::GrayscaleAlpha, BitDepth::Eight, ColorType::GrayscaleAlpha, @@ -463,7 +463,7 @@ fn filter_2_for_grayscale_alpha_8() { fn filter_3_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_3_for_grayscale_alpha_8.png", - 3, + RowFilter::Average, ColorType::GrayscaleAlpha, BitDepth::Eight, ColorType::GrayscaleAlpha, @@ -475,7 +475,7 @@ fn filter_3_for_grayscale_alpha_8() { fn filter_4_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_4_for_grayscale_alpha_8.png", - 4, + RowFilter::Paeth, ColorType::GrayscaleAlpha, BitDepth::Eight, ColorType::GrayscaleAlpha, @@ -487,7 +487,7 @@ fn filter_4_for_grayscale_alpha_8() { fn filter_5_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_5_for_grayscale_alpha_8.png", - 5, + RowFilter::MinSum, ColorType::GrayscaleAlpha, BitDepth::Eight, ColorType::GrayscaleAlpha, @@ -499,7 +499,7 @@ fn filter_5_for_grayscale_alpha_8() { fn filter_0_for_grayscale_16() { test_it_converts( "tests/files/filter_0_for_grayscale_16.png", - 0, + RowFilter::None, ColorType::Grayscale, BitDepth::Sixteen, ColorType::Grayscale, @@ -511,7 +511,7 @@ fn filter_0_for_grayscale_16() { fn filter_1_for_grayscale_16() { test_it_converts( "tests/files/filter_1_for_grayscale_16.png", - 1, + RowFilter::Sub, ColorType::Grayscale, BitDepth::Sixteen, ColorType::Grayscale, @@ -523,7 +523,7 @@ fn filter_1_for_grayscale_16() { fn filter_2_for_grayscale_16() { test_it_converts( "tests/files/filter_2_for_grayscale_16.png", - 2, + RowFilter::Up, ColorType::Grayscale, BitDepth::Sixteen, ColorType::Grayscale, @@ -535,7 +535,7 @@ fn filter_2_for_grayscale_16() { fn filter_3_for_grayscale_16() { test_it_converts( "tests/files/filter_3_for_grayscale_16.png", - 3, + RowFilter::Average, ColorType::Grayscale, BitDepth::Sixteen, ColorType::Grayscale, @@ -547,7 +547,7 @@ fn filter_3_for_grayscale_16() { fn filter_4_for_grayscale_16() { test_it_converts( "tests/files/filter_4_for_grayscale_16.png", - 4, + RowFilter::Paeth, ColorType::Grayscale, BitDepth::Sixteen, ColorType::Grayscale, @@ -559,7 +559,7 @@ fn filter_4_for_grayscale_16() { fn filter_5_for_grayscale_16() { test_it_converts( "tests/files/filter_5_for_grayscale_16.png", - 5, + RowFilter::MinSum, ColorType::Grayscale, BitDepth::Sixteen, ColorType::Grayscale, @@ -571,7 +571,7 @@ fn filter_5_for_grayscale_16() { fn filter_0_for_grayscale_8() { test_it_converts( "tests/files/filter_0_for_grayscale_8.png", - 0, + RowFilter::None, ColorType::Grayscale, BitDepth::Eight, ColorType::Grayscale, @@ -583,7 +583,7 @@ fn filter_0_for_grayscale_8() { fn filter_1_for_grayscale_8() { test_it_converts( "tests/files/filter_1_for_grayscale_8.png", - 1, + RowFilter::Sub, ColorType::Grayscale, BitDepth::Eight, ColorType::Grayscale, @@ -595,7 +595,7 @@ fn filter_1_for_grayscale_8() { fn filter_2_for_grayscale_8() { test_it_converts( "tests/files/filter_2_for_grayscale_8.png", - 2, + RowFilter::Up, ColorType::Grayscale, BitDepth::Eight, ColorType::Grayscale, @@ -607,7 +607,7 @@ fn filter_2_for_grayscale_8() { fn filter_3_for_grayscale_8() { test_it_converts( "tests/files/filter_3_for_grayscale_8.png", - 3, + RowFilter::Average, ColorType::Grayscale, BitDepth::Eight, ColorType::Grayscale, @@ -619,7 +619,7 @@ fn filter_3_for_grayscale_8() { fn filter_4_for_grayscale_8() { test_it_converts( "tests/files/filter_4_for_grayscale_8.png", - 4, + RowFilter::Paeth, ColorType::Grayscale, BitDepth::Eight, ColorType::Grayscale, @@ -631,7 +631,7 @@ fn filter_4_for_grayscale_8() { fn filter_5_for_grayscale_8() { test_it_converts( "tests/files/filter_5_for_grayscale_8.png", - 5, + RowFilter::MinSum, ColorType::Grayscale, BitDepth::Eight, ColorType::Grayscale, @@ -643,7 +643,7 @@ fn filter_5_for_grayscale_8() { fn filter_0_for_palette_4() { test_it_converts( "tests/files/filter_0_for_palette_4.png", - 0, + RowFilter::None, ColorType::Indexed, BitDepth::Four, ColorType::Indexed, @@ -655,7 +655,7 @@ fn filter_0_for_palette_4() { fn filter_1_for_palette_4() { test_it_converts( "tests/files/filter_1_for_palette_4.png", - 1, + RowFilter::Sub, ColorType::Indexed, BitDepth::Four, ColorType::Indexed, @@ -667,7 +667,7 @@ fn filter_1_for_palette_4() { fn filter_2_for_palette_4() { test_it_converts( "tests/files/filter_2_for_palette_4.png", - 2, + RowFilter::Up, ColorType::Indexed, BitDepth::Four, ColorType::Indexed, @@ -679,7 +679,7 @@ fn filter_2_for_palette_4() { fn filter_3_for_palette_4() { test_it_converts( "tests/files/filter_3_for_palette_4.png", - 3, + RowFilter::Average, ColorType::Indexed, BitDepth::Four, ColorType::Indexed, @@ -691,7 +691,7 @@ fn filter_3_for_palette_4() { fn filter_4_for_palette_4() { test_it_converts( "tests/files/filter_4_for_palette_4.png", - 4, + RowFilter::Paeth, ColorType::Indexed, BitDepth::Four, ColorType::Indexed, @@ -703,7 +703,7 @@ fn filter_4_for_palette_4() { fn filter_5_for_palette_4() { test_it_converts( "tests/files/filter_5_for_palette_4.png", - 5, + RowFilter::MinSum, ColorType::Indexed, BitDepth::Four, ColorType::Indexed, @@ -715,7 +715,7 @@ fn filter_5_for_palette_4() { fn filter_0_for_palette_2() { test_it_converts( "tests/files/filter_0_for_palette_2.png", - 0, + RowFilter::None, ColorType::Indexed, BitDepth::Two, ColorType::Indexed, @@ -727,7 +727,7 @@ fn filter_0_for_palette_2() { fn filter_1_for_palette_2() { test_it_converts( "tests/files/filter_1_for_palette_2.png", - 1, + RowFilter::Sub, ColorType::Indexed, BitDepth::Two, ColorType::Indexed, @@ -739,7 +739,7 @@ fn filter_1_for_palette_2() { fn filter_2_for_palette_2() { test_it_converts( "tests/files/filter_2_for_palette_2.png", - 2, + RowFilter::Up, ColorType::Indexed, BitDepth::Two, ColorType::Indexed, @@ -751,7 +751,7 @@ fn filter_2_for_palette_2() { fn filter_3_for_palette_2() { test_it_converts( "tests/files/filter_3_for_palette_2.png", - 3, + RowFilter::Average, ColorType::Indexed, BitDepth::Two, ColorType::Indexed, @@ -763,7 +763,7 @@ fn filter_3_for_palette_2() { fn filter_4_for_palette_2() { test_it_converts( "tests/files/filter_4_for_palette_2.png", - 4, + RowFilter::Paeth, ColorType::Indexed, BitDepth::Two, ColorType::Indexed, @@ -775,7 +775,7 @@ fn filter_4_for_palette_2() { fn filter_5_for_palette_2() { test_it_converts( "tests/files/filter_5_for_palette_2.png", - 5, + RowFilter::MinSum, ColorType::Indexed, BitDepth::Two, ColorType::Indexed, @@ -787,7 +787,7 @@ fn filter_5_for_palette_2() { fn filter_0_for_palette_1() { test_it_converts( "tests/files/filter_0_for_palette_1.png", - 0, + RowFilter::None, ColorType::Indexed, BitDepth::One, ColorType::Indexed, @@ -799,7 +799,7 @@ fn filter_0_for_palette_1() { fn filter_1_for_palette_1() { test_it_converts( "tests/files/filter_1_for_palette_1.png", - 1, + RowFilter::Sub, ColorType::Indexed, BitDepth::One, ColorType::Indexed, @@ -811,7 +811,7 @@ fn filter_1_for_palette_1() { fn filter_2_for_palette_1() { test_it_converts( "tests/files/filter_2_for_palette_1.png", - 2, + RowFilter::Up, ColorType::Indexed, BitDepth::One, ColorType::Indexed, @@ -823,7 +823,7 @@ fn filter_2_for_palette_1() { fn filter_3_for_palette_1() { test_it_converts( "tests/files/filter_3_for_palette_1.png", - 3, + RowFilter::Average, ColorType::Indexed, BitDepth::One, ColorType::Indexed, @@ -835,7 +835,7 @@ fn filter_3_for_palette_1() { fn filter_4_for_palette_1() { test_it_converts( "tests/files/filter_4_for_palette_1.png", - 4, + RowFilter::Paeth, ColorType::Indexed, BitDepth::One, ColorType::Indexed, @@ -847,7 +847,7 @@ fn filter_4_for_palette_1() { fn filter_5_for_palette_1() { test_it_converts( "tests/files/filter_5_for_palette_1.png", - 5, + RowFilter::MinSum, ColorType::Indexed, BitDepth::One, ColorType::Indexed, diff --git a/tests/flags.rs b/tests/flags.rs index 9bacf8e0..f24f7227 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,5 +1,5 @@ use indexmap::IndexSet; -use oxipng::internal_tests::*; +use oxipng::{internal_tests::*, RowFilter}; use oxipng::{InFile, OutFile}; #[cfg(feature = "filetime")] use std::cell::RefCell; @@ -16,7 +16,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { ..Default::default() }; let mut filter = IndexSet::new(); - filter.insert(0); + filter.insert(RowFilter::None); options.filter = filter; ( @@ -173,7 +173,7 @@ fn verbose_mode() { assert_eq!(logs.len(), 1); logs.sort(); for (i, log) in logs.into_iter().enumerate() { - let expected_prefix = format!(" zc = 11 f = 0 "); + let expected_prefix = format!(" zc = 11 f = None "); assert!( log.starts_with(&expected_prefix), "logs[{}] = {:?} doesn't start with {:?}", @@ -454,7 +454,7 @@ fn interlaced_0_to_1_other_filter_mode() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(1); let mut filter = IndexSet::new(); - filter.insert(4); + filter.insert(RowFilter::Paeth); opts.filter = filter; let png = PngData::new(&input, opts.fix_errors).unwrap(); diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 80610c94..d6b3deb5 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -1,5 +1,5 @@ use indexmap::IndexSet; -use oxipng::internal_tests::*; +use oxipng::{internal_tests::*, RowFilter}; use oxipng::{InFile, OutFile}; use std::fs::remove_file; use std::path::Path; @@ -11,7 +11,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { ..Default::default() }; let mut filter = IndexSet::new(); - filter.insert(0); + filter.insert(RowFilter::None); options.filter = filter; ( diff --git a/tests/interlacing.rs b/tests/interlacing.rs new file mode 100644 index 00000000..b80eabc2 --- /dev/null +++ b/tests/interlacing.rs @@ -0,0 +1,202 @@ +use indexmap::IndexSet; +use oxipng::{internal_tests::*, RowFilter}; +use oxipng::{InFile, OutFile}; +use std::fs::remove_file; +use std::path::Path; +use std::path::PathBuf; + +fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { + let mut options = oxipng::Options { + force: true, + ..Default::default() + }; + let mut filter = IndexSet::new(); + filter.insert(RowFilter::None); + options.filter = filter; + + ( + OutFile::Path(Some(input.with_extension("out.png"))), + options, + ) +} + +fn test_it_converts( + input: &str, + interlace: u8, + color_type_in: ColorType, + bit_depth_in: BitDepth, + color_type_out: ColorType, + bit_depth_out: BitDepth, +) { + let input = PathBuf::from(input); + let (output, mut opts) = get_opts(&input); + let png = PngData::new(&input, opts.fix_errors).unwrap(); + opts.interlace = Some(interlace); + assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); + assert_eq!(png.raw.ihdr.interlaced, if interlace == 1 { 0 } else { 1 }); + + match oxipng::optimize(&InFile::Path(input), &output, &opts) { + Ok(_) => (), + Err(x) => panic!("{}", x), + }; + let output = output.path().unwrap(); + assert!(output.exists()); + + let png = match PngData::new(output, opts.fix_errors) { + Ok(x) => x, + Err(x) => { + remove_file(output).ok(); + panic!("{}", x) + } + }; + + assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); + + remove_file(output).ok(); +} + +#[test] +fn deinterlace_rgb_16() { + test_it_converts( + "tests/files/interlaced_rgb_16_should_be_rgb_16.png", + 0, + ColorType::RGB, + BitDepth::Sixteen, + ColorType::RGB, + BitDepth::Sixteen, + ); +} + +#[test] +fn deinterlace_rgb_8() { + test_it_converts( + "tests/files/interlaced_rgb_8_should_be_rgb_8.png", + 0, + ColorType::RGB, + BitDepth::Eight, + ColorType::RGB, + BitDepth::Eight, + ); +} + +#[test] +fn deinterlace_palette_8() { + test_it_converts( + "tests/files/interlaced_palette_8_should_be_palette_8.png", + 0, + ColorType::Indexed, + BitDepth::Eight, + ColorType::Indexed, + BitDepth::Eight, + ); +} + +#[test] +fn deinterlace_palette_4() { + test_it_converts( + "tests/files/interlaced_palette_4_should_be_palette_4.png", + 0, + ColorType::Indexed, + BitDepth::Four, + ColorType::Indexed, + BitDepth::Four, + ); +} + +#[test] +fn deinterlace_palette_2() { + test_it_converts( + "tests/files/interlaced_palette_2_should_be_palette_2.png", + 0, + ColorType::Indexed, + BitDepth::Two, + ColorType::Indexed, + BitDepth::Two, + ); +} + +#[test] +fn deinterlace_palette_1() { + test_it_converts( + "tests/files/interlaced_palette_1_should_be_palette_1.png", + 0, + ColorType::Indexed, + BitDepth::One, + ColorType::Indexed, + BitDepth::One, + ); +} + +#[test] +fn interlace_rgb_16() { + test_it_converts( + "tests/files/rgb_16_should_be_rgb_16.png", + 1, + ColorType::RGB, + BitDepth::Sixteen, + ColorType::RGB, + BitDepth::Sixteen, + ); +} + +#[test] +fn interlace_rgb_8() { + test_it_converts( + "tests/files/rgb_8_should_be_rgb_8.png", + 1, + ColorType::RGB, + BitDepth::Eight, + ColorType::RGB, + BitDepth::Eight, + ); +} + +#[test] +fn interlace_palette_8() { + test_it_converts( + "tests/files/palette_8_should_be_palette_8.png", + 1, + ColorType::Indexed, + BitDepth::Eight, + ColorType::Indexed, + BitDepth::Eight, + ); +} + +#[test] +fn interlace_palette_4() { + test_it_converts( + "tests/files/palette_4_should_be_palette_4.png", + 1, + ColorType::Indexed, + BitDepth::Four, + ColorType::Indexed, + BitDepth::Four, + ); +} + +#[test] +fn interlace_palette_2() { + test_it_converts( + "tests/files/palette_2_should_be_palette_2.png", + 1, + ColorType::Indexed, + BitDepth::Two, + ColorType::Indexed, + BitDepth::Two, + ); +} + +#[test] +fn interlace_palette_1() { + test_it_converts( + "tests/files/palette_1_should_be_palette_1.png", + 1, + ColorType::Indexed, + BitDepth::One, + ColorType::Indexed, + BitDepth::One, + ); +} diff --git a/tests/reduction.rs b/tests/reduction.rs index 0951c628..2d084ad9 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1,5 +1,5 @@ use indexmap::IndexSet; -use oxipng::internal_tests::*; +use oxipng::{internal_tests::*, RowFilter}; use oxipng::{InFile, OutFile}; use std::fs::remove_file; use std::path::Path; @@ -11,7 +11,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { ..Default::default() }; let mut filter = IndexSet::new(); - filter.insert(0); + filter.insert(RowFilter::None); options.filter = filter; ( diff --git a/tests/regression.rs b/tests/regression.rs index 539ec88f..de63039f 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -1,5 +1,5 @@ use indexmap::IndexSet; -use oxipng::internal_tests::*; +use oxipng::{internal_tests::*, RowFilter}; use oxipng::{InFile, OutFile}; use std::fs::remove_file; use std::path::Path; @@ -11,7 +11,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { ..Default::default() }; let mut filter = IndexSet::new(); - filter.insert(0); + filter.insert(RowFilter::None); options.filter = filter; ( @@ -288,7 +288,7 @@ fn issue_92_filter_0() { fn issue_92_filter_5() { let input = "tests/files/issue-92.png"; let (_, mut opts) = get_opts(Path::new(input)); - opts.filter = [5].iter().cloned().collect(); + opts.filter = [RowFilter::MinSum].iter().cloned().collect(); let output = OutFile::Path(Some(Path::new(input).with_extension("-f5-out.png"))); test_it_converts( diff --git a/tests/strategies.rs b/tests/strategies.rs new file mode 100644 index 00000000..aa5d80f4 --- /dev/null +++ b/tests/strategies.rs @@ -0,0 +1,124 @@ +use indexmap::IndexSet; +use oxipng::{internal_tests::*, RowFilter}; +use oxipng::{InFile, OutFile}; +use std::fs::remove_file; +use std::path::Path; +use std::path::PathBuf; + +fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { + let mut options = oxipng::Options { + force: true, + ..Default::default() + }; + let mut filter = IndexSet::new(); + filter.insert(RowFilter::None); + options.filter = filter; + + ( + OutFile::Path(Some(input.with_extension("out.png"))), + options, + ) +} + +fn test_it_converts( + input: &str, + filter: RowFilter, + color_type_in: ColorType, + bit_depth_in: BitDepth, + color_type_out: ColorType, + bit_depth_out: BitDepth, +) { + let input = PathBuf::from(input); + + let (output, mut opts) = get_opts(&input); + let png = PngData::new(&input, opts.fix_errors).unwrap(); + opts.filter = IndexSet::new(); + opts.filter.insert(filter); + assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); + + match oxipng::optimize(&InFile::Path(input), &output, &opts) { + Ok(_) => (), + Err(x) => panic!("{}", x), + }; + let output = output.path().unwrap(); + assert!(output.exists()); + + let png = match PngData::new(output, opts.fix_errors) { + Ok(x) => x, + Err(x) => { + remove_file(output).ok(); + panic!("{}", x) + } + }; + + assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); + if let Some(palette) = png.raw.palette.as_ref() { + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth.as_u8() as usize)); + } else { + assert_ne!(png.raw.ihdr.color_type, ColorType::Indexed); + } + + remove_file(output).ok(); +} + +#[test] +fn filter_minsum() { + test_it_converts( + "tests/files/rgb_16_should_be_rgb_16.png", + RowFilter::MinSum, + ColorType::RGB, + BitDepth::Sixteen, + ColorType::RGB, + BitDepth::Sixteen, + ); +} + +#[test] +fn filter_entropy() { + test_it_converts( + "tests/files/rgb_8_should_be_rgb_8.png", + RowFilter::Entropy, + ColorType::RGB, + BitDepth::Eight, + ColorType::RGB, + BitDepth::Eight, + ); +} + +#[test] +fn filter_bigrams() { + test_it_converts( + "tests/files/rgba_8_should_be_rgba_8.png", + RowFilter::Bigrams, + ColorType::RGBA, + BitDepth::Eight, + ColorType::RGBA, + BitDepth::Eight, + ); +} + +#[test] +fn filter_bigent() { + test_it_converts( + "tests/files/grayscale_8_should_be_grayscale_8.png", + RowFilter::BigEnt, + ColorType::Grayscale, + BitDepth::Eight, + ColorType::Grayscale, + BitDepth::Eight, + ); +} + +#[test] +fn filter_brute() { + test_it_converts( + "tests/files/palette_8_should_be_palette_8.png", + RowFilter::Brute, + ColorType::Indexed, + BitDepth::Eight, + ColorType::Indexed, + BitDepth::Eight, + ); +}