From ea5f1884be78f694f72a14f619c384df77b0e220 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Mon, 22 May 2023 07:34:23 +1200 Subject: [PATCH] Refactor aux chunk handling (#505) --- benches/deflate.rs | 16 +- benches/filters.rs | 63 +++--- benches/interlacing.rs | 23 +-- benches/reductions.rs | 61 +++--- benches/strategies.rs | 13 +- benches/zopfli.rs | 11 +- src/colors.rs | 8 - src/deflate/deflater.rs | 7 +- src/deflate/mod.rs | 30 +++ src/evaluate.rs | 31 ++- src/headers.rs | 150 ++++++++++---- src/interlace.rs | 2 - src/lib.rs | 308 +++++++++++------------------ src/main.rs | 63 +++--- src/png/mod.rs | 143 +++++++------- src/reduction/alpha.rs | 10 - src/reduction/bit_depth.rs | 45 ++--- src/reduction/color.rs | 70 +------ src/reduction/palette.rs | 29 --- tests/files/strip_headers_all.png | Bin 117347 -> 117445 bytes tests/files/strip_headers_list.png | Bin 117347 -> 117445 bytes tests/files/strip_headers_none.png | Bin 117347 -> 117445 bytes tests/files/strip_headers_safe.png | Bin 117347 -> 117445 bytes tests/filters.rs | 8 +- tests/flags.rs | 113 ++++++----- tests/interlaced.rs | 8 +- tests/interlacing.rs | 8 +- tests/lib.rs | 32 +-- tests/raw.rs | 16 +- tests/reduction.rs | 20 +- tests/regression.rs | 22 +-- tests/strategies.rs | 8 +- 32 files changed, 619 insertions(+), 699 deletions(-) diff --git a/benches/deflate.rs b/benches/deflate.rs index de9e0318a..d8cc984b3 100644 --- a/benches/deflate.rs +++ b/benches/deflate.rs @@ -3,15 +3,15 @@ extern crate oxipng; extern crate test; +use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; -use oxipng::internal_tests::*; - #[bench] fn deflate_16_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -22,7 +22,7 @@ fn deflate_16_bits(b: &mut Bencher) { #[bench] fn deflate_8_bits(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -35,7 +35,7 @@ fn deflate_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -48,7 +48,7 @@ fn deflate_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -61,7 +61,7 @@ fn deflate_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -72,7 +72,7 @@ fn deflate_1_bits(b: &mut Bencher) { #[bench] fn inflate_generic(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| inflate(png.idat_data.as_ref(), png.raw.ihdr.raw_data_size())); } diff --git a/benches/filters.rs b/benches/filters.rs index b1ce453a8..67c701a6e 100644 --- a/benches/filters.rs +++ b/benches/filters.rs @@ -3,14 +3,15 @@ extern crate oxipng; extern crate test; -use oxipng::{internal_tests::*, RowFilter}; +use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; #[bench] fn filters_16_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -20,7 +21,7 @@ fn filters_16_bits_filter_0(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_0(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -32,7 +33,7 @@ fn filters_4_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -44,7 +45,7 @@ fn filters_2_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -56,7 +57,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -66,7 +67,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -76,7 +77,7 @@ fn filters_16_bits_filter_1(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_1(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -88,7 +89,7 @@ fn filters_4_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -100,7 +101,7 @@ fn filters_2_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -112,7 +113,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -122,7 +123,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -132,7 +133,7 @@ fn filters_16_bits_filter_2(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_2(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -144,7 +145,7 @@ fn filters_4_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -156,7 +157,7 @@ fn filters_2_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -168,7 +169,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -178,7 +179,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -188,7 +189,7 @@ fn filters_16_bits_filter_3(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_3(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -200,7 +201,7 @@ fn filters_4_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -212,7 +213,7 @@ fn filters_2_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -224,7 +225,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -234,7 +235,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -244,7 +245,7 @@ fn filters_16_bits_filter_4(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_4(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -256,7 +257,7 @@ fn filters_4_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -268,7 +269,7 @@ fn filters_2_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -280,7 +281,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -290,7 +291,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -300,7 +301,7 @@ fn filters_16_bits_filter_5(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_5(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -312,7 +313,7 @@ fn filters_4_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -324,7 +325,7 @@ fn filters_2_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -336,7 +337,7 @@ fn filters_1_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); diff --git a/benches/interlacing.rs b/benches/interlacing.rs index def3d5a49..3c17eadc0 100644 --- a/benches/interlacing.rs +++ b/benches/interlacing.rs @@ -3,14 +3,15 @@ extern crate oxipng; extern crate test; -use oxipng::{internal_tests::*, Interlacing}; +use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; #[bench] fn interlacing_16_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -18,7 +19,7 @@ fn interlacing_16_bits(b: &mut Bencher) { #[bench] fn interlacing_8_bits(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -28,7 +29,7 @@ fn interlacing_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -38,7 +39,7 @@ fn interlacing_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -48,7 +49,7 @@ fn interlacing_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -58,7 +59,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_rgb_16_should_be_rgb_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -68,7 +69,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_rgb_8_should_be_rgb_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -78,7 +79,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -88,7 +89,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -98,7 +99,7 @@ fn deinterlacing_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } diff --git a/benches/reductions.rs b/benches/reductions.rs index 28803a38b..16aae41fa 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -4,13 +4,14 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; #[bench] fn reductions_16_to_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw)); } @@ -20,7 +21,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -30,7 +31,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -40,7 +41,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -50,7 +51,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -60,7 +61,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -70,7 +71,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -80,7 +81,7 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_8_should_be_grayscale_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -90,7 +91,7 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_8_should_be_grayscale_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -100,7 +101,7 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_8_should_be_grayscale_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -110,7 +111,7 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_4_should_be_grayscale_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -120,7 +121,7 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_4_should_be_grayscale_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -130,7 +131,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_2_should_be_grayscale_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -138,7 +139,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) { #[bench] fn reductions_rgba_to_rgb_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } @@ -146,7 +147,7 @@ fn reductions_rgba_to_rgb_16(b: &mut Bencher) { #[bench] fn reductions_rgba_to_rgb_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } @@ -156,7 +157,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_16_should_be_grayscale_alpha_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -166,7 +167,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_8_should_be_grayscale_alpha_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -176,7 +177,7 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_16_should_be_grayscale_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { color::reduced_rgb_to_grayscale(&png.raw) @@ -189,7 +190,7 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_8_should_be_grayscale_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { color::reduced_rgb_to_grayscale(&png.raw) @@ -202,7 +203,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgb_16_should_be_grayscale_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -210,7 +211,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { #[bench] fn reductions_rgb_to_grayscale_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -218,7 +219,7 @@ fn reductions_rgb_to_grayscale_8(b: &mut Bencher) { #[bench] fn reductions_rgba_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_to_indexed(&png.raw)); } @@ -226,7 +227,7 @@ fn reductions_rgba_to_palette_8(b: &mut Bencher) { #[bench] fn reductions_rgb_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_to_indexed(&png.raw)); } @@ -236,7 +237,7 @@ fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_8_should_be_palette_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_to_indexed(&png.raw)); } @@ -246,7 +247,7 @@ fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_grayscale_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::indexed_to_channels(&png.raw)); } @@ -256,7 +257,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_should_be_reduced_with_dupes.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| palette::reduced_palette(&png.raw, false)); } @@ -266,7 +267,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_should_be_reduced_with_unused.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| palette::reduced_palette(&png.raw, false)); } @@ -276,7 +277,7 @@ fn reductions_palette_full_reduction(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_should_be_reduced_with_both.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| palette::reduced_palette(&png.raw, false)); } @@ -286,7 +287,7 @@ fn reductions_palette_sort(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_palette_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| palette::sorted_palette(&png.raw)); } @@ -294,7 +295,7 @@ fn reductions_palette_sort(b: &mut Bencher) { #[bench] fn reductions_alpha(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| alpha::cleaned_alpha_channel(&png.raw)); } diff --git a/benches/strategies.rs b/benches/strategies.rs index de5a009a3..c3aab816c 100644 --- a/benches/strategies.rs +++ b/benches/strategies.rs @@ -3,14 +3,15 @@ extern crate oxipng; extern crate test; -use oxipng::{internal_tests::*, RowFilter}; +use oxipng::internal_tests::*; +use oxipng::*; 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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -20,7 +21,7 @@ fn filters_minsum(b: &mut Bencher) { #[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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Entropy, false); @@ -30,7 +31,7 @@ fn filters_entropy(b: &mut Bencher) { #[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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Bigrams, false); @@ -40,7 +41,7 @@ fn filters_bigrams(b: &mut Bencher) { #[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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::BigEnt, false); @@ -50,7 +51,7 @@ fn filters_bigent(b: &mut Bencher) { #[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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Brute, false); diff --git a/benches/zopfli.rs b/benches/zopfli.rs index a9b91e081..03f83f651 100644 --- a/benches/zopfli.rs +++ b/benches/zopfli.rs @@ -4,6 +4,7 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; +use oxipng::*; use std::num::NonZeroU8; use std::path::PathBuf; use test::Bencher; @@ -14,7 +15,7 @@ const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1 #[bench] fn zopfli_16_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -24,7 +25,7 @@ fn zopfli_16_bits_strategy_0(b: &mut Bencher) { #[bench] fn zopfli_8_bits_strategy_0(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(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -36,7 +37,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -48,7 +49,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -60,7 +61,7 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); diff --git a/src/colors.rs b/src/colors.rs index 59cd589e7..744ec7204 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -70,14 +70,6 @@ impl ColorType { matches!(self, ColorType::RGB { .. } | ColorType::RGBA) } - #[inline] - pub(crate) fn is_grayscale(&self) -> bool { - matches!( - self, - ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha - ) - } - #[inline] pub(crate) fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) diff --git a/src/deflate/deflater.rs b/src/deflate/deflater.rs index a2f9dacb0..efffe123e 100644 --- a/src/deflate/deflater.rs +++ b/src/deflate/deflater.rs @@ -13,13 +13,8 @@ pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult PngError::DeflatedDataTooLong(capacity), + CompressionError::InsufficientSpace => PngError::DeflatedDataTooLong(capacity - 9), })?; - if let Some(max) = max_size.get() { - if len > max { - return Err(PngError::DeflatedDataTooLong(max)); - } - } dest.truncate(len); Ok(dest) } diff --git a/src/deflate/mod.rs b/src/deflate/mod.rs index 9606a8526..0e8a65f32 100644 --- a/src/deflate/mod.rs +++ b/src/deflate/mod.rs @@ -1,7 +1,10 @@ mod deflater; +use crate::AtomicMin; +use crate::{PngError, PngResult}; pub use deflater::crc32; pub use deflater::deflate; pub use deflater::inflate; +use std::{fmt, fmt::Display}; #[cfg(feature = "zopfli")] use std::num::NonZeroU8; @@ -27,3 +30,30 @@ pub enum Deflaters { iterations: NonZeroU8, }, } + +impl Deflaters { + pub(crate) fn deflate(self, data: &[u8], max_size: &AtomicMin) -> PngResult> { + let compressed = match self { + Self::Libdeflater { compression } => deflate(data, compression, max_size)?, + #[cfg(feature = "zopfli")] + Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?, + }; + if let Some(max) = max_size.get() { + if compressed.len() > max { + return Err(PngError::DeflatedDataTooLong(max)); + } + } + Ok(compressed) + } +} + +impl Display for Deflaters { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Libdeflater { compression } => Display::fmt(compression, f), + #[cfg(feature = "zopfli")] + Self::Zopfli { .. } => Display::fmt("zopfli", f), + } + } +} diff --git a/src/evaluate.rs b/src/evaluate.rs index 15763a2cd..8c05fde77 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -4,7 +4,6 @@ use crate::atomicmin::AtomicMin; use crate::deflate; use crate::filters::RowFilter; -use crate::png::PngData; use crate::png::PngImage; #[cfg(not(feature = "parallel"))] use crate::rayon; @@ -22,7 +21,9 @@ use std::sync::atomic::Ordering::SeqCst; use std::sync::Arc; pub struct Candidate { - pub image: PngData, + pub image: Arc, + pub idat_data: Vec, + pub filtered: Vec, pub filter: RowFilter, pub is_reduction: bool, // first wins tie-breaker @@ -32,9 +33,9 @@ pub struct Candidate { impl Candidate { fn cmp_key(&self) -> impl Ord { ( - self.image.estimated_output_size(), - self.image.raw.data.len(), - self.image.raw.ihdr.bit_depth, + self.idat_data.len() + self.image.key_chunks_size(), + self.image.data.len(), + self.image.ihdr.bit_depth, self.filter, self.nth, ) @@ -135,17 +136,7 @@ impl Evaluator { let filtered = image.filter_image(filter, optimize_alpha); let idat_data = deflate::deflate(&filtered, compression, &best_candidate_size); if let Ok(idat_data) = idat_data { - let new = Candidate { - image: PngData { - idat_data, - filtered, - raw: Arc::clone(&image), - }, - filter, - is_reduction, - nth, - }; - let size = new.image.estimated_output_size(); + let size = idat_data.len() + image.key_chunks_size(); best_candidate_size.set_min(size); trace!( "Eval: {}-bit {:20} {:8} {} bytes", @@ -154,6 +145,14 @@ impl Evaluator { filter, size ); + let new = Candidate { + image: image.clone(), + idat_data, + filtered, + filter, + is_reduction, + nth, + }; #[cfg(feature = "parallel")] { diff --git a/src/headers.rs b/src/headers.rs index 6511f604f..274173d74 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -1,12 +1,13 @@ use crate::colors::{BitDepth, ColorType}; -use crate::deflate::crc32; +use crate::deflate::{crc32, inflate}; use crate::error::PngError; use crate::interlace::Interlacing; +use crate::AtomicMin; +use crate::Deflaters; use crate::PngResult; use indexmap::IndexSet; +use log::warn; use rgb::{RGB16, RGBA8}; -use std::io; -use std::io::{Cursor, Read}; #[derive(Debug, Clone)] /// Headers from the IHDR chunk of the image @@ -62,21 +63,42 @@ impl IhdrData { } } +#[derive(Debug, Clone)] +pub struct Chunk { + pub name: [u8; 4], + pub data: Vec, +} + #[derive(Debug, PartialEq, Eq, Clone)] -/// Options to use for performing operations on headers (such as stripping) -pub enum Headers { +/// Options to use when stripping chunks +pub enum StripChunks { /// None None, /// Remove specific chunks - Strip(Vec), - /// Headers that won't affect rendering (all but cICP, iCCP, sBIT, sRGB, pHYs) + Strip(IndexSet<[u8; 4]>), + /// Remove all chunks that won't affect rendering Safe, /// Remove all non-critical chunks except these - Keep(IndexSet), - /// All non-critical headers + Keep(IndexSet<[u8; 4]>), + /// All non-critical chunks All, } +impl StripChunks { + /// List of chunks that will be kept when using the `Safe` option + pub const KEEP_SAFE: [[u8; 4]; 4] = [*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs"]; + + pub(crate) fn keep(&self, name: &[u8; 4]) -> bool { + match &self { + StripChunks::None => true, + StripChunks::Keep(names) => names.contains(name), + StripChunks::Strip(names) => !names.contains(name), + StripChunks::Safe => Self::KEEP_SAFE.contains(name), + StripChunks::All => false, + } + } +} + #[inline] pub fn file_header_is_valid(bytes: &[u8]) -> bool { let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; @@ -85,27 +107,26 @@ pub fn file_header_is_valid(bytes: &[u8]) -> bool { } #[derive(Debug, Clone, Copy)] -pub struct RawHeader<'a> { +pub struct RawChunk<'a> { pub name: [u8; 4], pub data: &'a [u8], } -pub fn parse_next_header<'a>( +pub fn parse_next_chunk<'a>( byte_data: &'a [u8], byte_offset: &mut usize, fix_errors: bool, -) -> PngResult>> { - let mut rdr = Cursor::new( +) -> PngResult>> { + let length = read_be_u32( byte_data .get(*byte_offset..*byte_offset + 4) .ok_or(PngError::TruncatedData)?, ); - let length = read_be_u32(&mut rdr).unwrap(); *byte_offset += 4; - let header_start = *byte_offset; + let chunk_start = *byte_offset; let chunk_name = byte_data - .get(header_start..header_start + 4) + .get(chunk_start..chunk_start + 4) .ok_or(PngError::TruncatedData)?; if chunk_name == b"IEND" { // End of data @@ -117,37 +138,34 @@ pub fn parse_next_header<'a>( .get(*byte_offset..*byte_offset + length as usize) .ok_or(PngError::TruncatedData)?; *byte_offset += length as usize; - let mut rdr = Cursor::new( + let crc = read_be_u32( byte_data .get(*byte_offset..*byte_offset + 4) .ok_or(PngError::TruncatedData)?, ); - let crc = read_be_u32(&mut rdr).unwrap(); *byte_offset += 4; - let header_bytes = byte_data - .get(header_start..header_start + 4 + length as usize) + let chunk_bytes = byte_data + .get(chunk_start..chunk_start + 4 + length as usize) .ok_or(PngError::TruncatedData)?; - if !fix_errors && crc32(header_bytes) != crc { + if !fix_errors && crc32(chunk_bytes) != crc { return Err(PngError::new(&format!( - "CRC Mismatch in {} header; May be recoverable by using --fix", + "CRC Mismatch in {} chunk; May be recoverable by using --fix", String::from_utf8_lossy(chunk_name) ))); } - let mut name = [0_u8; 4]; - name.copy_from_slice(chunk_name); - Ok(Some(RawHeader { name, data })) + let name: [u8; 4] = chunk_name.try_into().unwrap(); + Ok(Some(RawChunk { name, data })) } -pub fn parse_ihdr_header( +pub fn parse_ihdr_chunk( byte_data: &[u8], palette_data: Option>, trns_data: Option>, ) -> PngResult { // This eliminates bounds checks for the rest of the function let interlaced = byte_data.get(12).copied().ok_or(PngError::TruncatedData)?; - let mut rdr = Cursor::new(&byte_data[0..8]); Ok(IhdrData { color_type: match byte_data[9] { 0 => ColorType::Grayscale { @@ -170,8 +188,8 @@ pub fn parse_ihdr_header( _ => return Err(PngError::new("Unexpected color type in header")), }, bit_depth: byte_data[8].try_into()?, - width: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, - height: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, + width: read_be_u32(&byte_data[0..4]), + height: read_be_u32(&byte_data[4..8]), interlaced: interlaced.try_into()?, }) } @@ -196,8 +214,74 @@ fn palette_to_rgba( } #[inline] -fn read_be_u32>(rdr: &mut Cursor) -> Result { - let mut int_buf = [0; 4]; - rdr.read_exact(&mut int_buf)?; - Ok(u32::from_be_bytes(int_buf)) +fn read_be_u32(bytes: &[u8]) -> u32 { + u32::from_be_bytes(bytes.try_into().unwrap()) +} + +/// Extract and decompress the ICC profile from an iCCP chunk +pub fn extract_icc(iccp: &Chunk) -> Option> { + // Skip (useless) profile name + let mut data = iccp.data.as_slice(); + loop { + let (&n, rest) = data.split_first()?; + data = rest; + if n == 0 { + break; + } + } + + let (&compression_method, compressed_data) = data.split_first()?; + if compression_method != 0 { + return None; // The profile is supposed to be compressed (method 0) + } + // The decompressed size is unknown so we have to guess the required buffer size + let max_size = compressed_data.len() * 2 + 1000; + match inflate(compressed_data, max_size) { + Ok(icc) => Some(icc), + Err(e) => { + // Log the error so we can know if the buffer size needs to be adjusted + warn!("Failed to decompress icc: {}", e); + None + } + } +} + +/// Construct an iCCP chunk by compressing the ICC profile +pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult { + let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?; + let mut data = Vec::with_capacity(compressed.len() + 5); + data.extend(b"icc"); // Profile name - generally unused, can be anything + data.extend([0, 0]); // Null separator, zlib compression method + data.append(&mut compressed); + Ok(Chunk { + name: *b"iCCP", + data, + }) +} + +/// If the profile is sRGB, extracts the rendering intent value from it +pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option { + let rendering_intent = *icc_data.get(67)?; + + // The known profiles are the same as in libpng's `png_sRGB_checks`. + // The Profile ID header of ICC has a fixed layout, + // and is supposed to contain MD5 of profile data at this offset + match icc_data.get(84..100)? { + b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d" + | b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89" + | b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8" + | b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => { + Some(rendering_intent) + } + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { + // Known-bad profiles are identified by their CRC + match (crc32(icc_data), icc_data.len()) { + (0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => { + Some(rendering_intent) + } + _ => None, + } + } + _ => None, + } } diff --git a/src/interlace.rs b/src/interlace.rs index 0fd63f21a..0293dce1d 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -90,7 +90,6 @@ pub fn interlace_image(png: &PngImage) -> PngImage { interlaced: Interlacing::Adam7, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), } } @@ -105,7 +104,6 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage { interlaced: Interlacing::None, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), } } diff --git a/src/lib.rs b/src/lib.rs index 4c43e287c..46886f334 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,9 +25,8 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; -use crate::headers::IhdrData; +use crate::headers::*; use crate::png::PngData; use crate::png::PngImage; use crate::reduction::*; @@ -45,9 +44,9 @@ pub use crate::colors::{BitDepth, ColorType}; pub use crate::deflate::Deflaters; pub use crate::error::PngError; pub use crate::filters::RowFilter; -pub use crate::headers::Headers; +pub use crate::headers::StripChunks; pub use crate::interlace::Interlacing; -pub use indexmap::{indexset, IndexMap, IndexSet}; +pub use indexmap::{indexset, IndexSet}; pub use rgb::{RGB16, RGBA8}; mod atomicmin; @@ -67,9 +66,7 @@ mod sanity_checks; #[doc(hidden)] pub mod internal_tests { pub use crate::atomicmin::*; - pub use crate::colors::*; pub use crate::deflate::*; - pub use crate::headers::*; pub use crate::png::*; pub use crate::reduction::*; #[cfg(feature = "sanity-checks")] @@ -189,10 +186,10 @@ pub struct Options { /// /// Default: `true` pub idat_recoding: bool, - /// Which headers to strip from the PNG file, if any + /// Which chunks to strip from the PNG file, if any /// /// Default: `None` - pub strip: Headers, + pub strip: StripChunks, /// Which DEFLATE algorithm to use /// /// Default: `Libdeflater` @@ -306,7 +303,7 @@ impl Default for Options { palette_reduction: true, grayscale_reduction: true, idat_recoding: true, - strip: Headers::None, + strip: StripChunks::None, deflate: Deflaters::Libdeflater { compression: 11 }, fast_evaluation: true, timeout: None, @@ -318,6 +315,7 @@ impl Default for Options { /// A raw image definition which can be used to create an optimized png pub struct RawImage { png: Arc, + aux_chunks: Vec, } impl RawImage { @@ -363,35 +361,40 @@ impl RawImage { interlaced: Interlacing::None, }, data, - aux_headers: IndexMap::new(), }), + aux_chunks: Vec::new(), }) } /// Add a png chunk, such as "iTXt", to be included in the output - pub fn add_png_chunk(&mut self, chunk_type: [u8; 4], data: Vec) { - // We can guarantee this will succeed - failure indicates a bug - let png = Arc::get_mut(&mut self.png).unwrap(); - png.aux_headers.insert(chunk_type, data); + pub fn add_png_chunk(&mut self, name: [u8; 4], data: Vec) { + self.aux_chunks.push(Chunk { name, data }); } /// Add an ICC profile for the image pub fn add_icc_profile(&mut self, data: &[u8]) { - // Compress with default compression level - if let Ok(mut compressed) = deflate::deflate(data, 11, &AtomicMin::new(None)) { - let mut iccp = Vec::with_capacity(compressed.len() + 13); - iccp.extend(b"icc"); // Profile name - generally unused, can be anything - iccp.extend([0, 0]); // Null separator, zlib compression method - iccp.append(&mut compressed); - self.add_png_chunk(*b"iCCP", iccp); + // Compress with fastest compression level - will be recompressed during optimization + let deflater = Deflaters::Libdeflater { compression: 1 }; + if let Ok(iccp) = construct_iccp(data, deflater) { + self.aux_chunks.push(iccp); } } /// Create an optimized png from the raw image data using the options provided pub fn create_optimized_png(&self, opts: &Options) -> PngResult> { let deadline = Arc::new(Deadline::new(opts.timeout)); - let png = optimize_raw(Arc::clone(&self.png), opts, deadline, None) + let mut png = optimize_raw(self.png.clone(), opts, deadline, None) .ok_or_else(|| PngError::new("Failed to optimize input data"))?; + + // Process aux chunks + png.aux_chunks = self + .aux_chunks + .iter() + .filter(|c| opts.strip.keep(&c.name)) + .cloned() + .collect(); + postprocess_chunks(&mut png, opts, &self.png.ihdr); + Ok(png.output()) } } @@ -434,7 +437,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<( } }; - let mut png = PngData::from_slice(&in_data, opts.fix_errors)?; + let mut png = PngData::from_slice(&in_data, opts)?; if opts.check { info!("Running in check mode, not optimizing"); @@ -522,7 +525,7 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult> { let deadline = Arc::new(Deadline::new(opts.timeout)); let original_size = data.len(); - let mut png = PngData::from_slice(data, opts.fix_errors)?; + let mut png = PngData::from_slice(data, opts)?; // Run the optimizer on the decoded PNG. let optimized_output = optimize_png(&mut png, data, opts, deadline)?; @@ -535,13 +538,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: RowFilter, - pub compression: u8, -} -type TrialWithData = (TrialOptions, Vec); +type TrialResult = (RowFilter, Vec); /// Perform optimization on the input PNG object using the options provided fn optimize_png( @@ -553,27 +550,27 @@ fn optimize_png( // Print png info let file_original_size = original_data.len(); let idat_original_size = png.idat_data.len(); + let raw = png.raw.clone(); debug!( " {}x{} pixels, PNG format", - png.raw.ihdr.width, png.raw.ihdr.height + raw.ihdr.width, raw.ihdr.height ); - report_format(" ", &png.raw); + report_format(" ", &raw); debug!(" IDAT size = {} bytes", idat_original_size); debug!(" File size = {} bytes", file_original_size); - // Do this first so that reductions can ignore certain chunks such as bKGD - perform_strip(png, opts); - let max_size = if opts.force { None } else { Some(png.estimated_output_size()) }; - if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, max_size) { + if let Some(new_png) = optimize_raw(raw.clone(), opts, deadline, max_size) { png.raw = new_png.raw; png.idat_data = new_png.idat_data; } + postprocess_chunks(png, opts, &raw.ihdr); + let output = png.output(); if idat_original_size >= png.idat_data.len() { @@ -635,7 +632,7 @@ fn optimize_raw( let mut eval_result = eval.get_best_candidate(); if let Some(ref result) = eval_result { if result.is_reduction { - png = Arc::clone(&result.image.raw); + png = result.image.clone(); reduction_occurred = true; } } @@ -647,7 +644,7 @@ fn optimize_raw( if opts.idat_recoding || reduction_occurred { let mut filters = opts.filter.clone(); let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some()); - let best: Option = if fast_eval { + let best: Option = if fast_eval { // Perform a fast evaluation of selected filters followed by a single main compression trial if eval_result.is_some() { @@ -659,7 +656,7 @@ fn optimize_raw( trace!("Evaluating: {} filters", filters.len()); let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha); if let Some(ref result) = eval_result { - eval.set_best_size(result.image.idat_data.len()); + eval.set_best_size(result.idat_data.len()); } eval.try_image(png.clone()); if let Some(result) = eval.get_best_candidate() { @@ -667,22 +664,18 @@ fn optimize_raw( } } // We should have a result here - fail if not (e.g. deadline passed) - let eval_result = eval_result?; + let result = eval_result?; - let trial = TrialOptions { - filter: eval_result.filter, - compression: match opts.deflate { - Deflaters::Libdeflater { compression } => compression, - _ => 0, - }, - }; - if trial.compression > 0 && trial.compression <= eval_compression { - // No further compression required - Some((trial, eval_result.image.idat_data)) - } else { - debug!("Trying: {}", trial.filter); - let best_size = AtomicMin::new(max_size); - perform_trial(&eval_result.image.filtered, opts, trial, &best_size) + match opts.deflate { + Deflaters::Libdeflater { compression } if compression <= eval_compression => { + // No further compression required + Some((result.filter, result.idat_data)) + } + _ => { + debug!("Trying: {}", result.filter); + let best_size = AtomicMin::new(max_size); + perform_trial(&result.filtered, opts, result.filter, &best_size) + } } } else { // Perform full compression trials of selected filters and determine the best @@ -698,28 +691,16 @@ fn optimize_raw( } } - let mut results: Vec = Vec::with_capacity(filters.len()); - - for f in &filters { - results.push(TrialOptions { - filter: *f, - compression: match opts.deflate { - Deflaters::Libdeflater { compression } => compression, - _ => 0, - }, - }); - } - - debug!("Trying: {} filters", results.len()); + debug!("Trying: {} filters", filters.len()); let best_size = AtomicMin::new(max_size); - let results_iter = results.into_par_iter().with_max_len(1); - let best = results_iter.filter_map(|trial| { + let results_iter = filters.into_par_iter().with_max_len(1); + let best = results_iter.filter_map(|filter| { if deadline.passed() { return None; } - let filtered = &png.filter_image(trial.filter, opts.optimize_alpha); - perform_trial(filtered, opts, trial, &best_size) + let filtered = &png.filter_image(filter, opts.optimize_alpha); + perform_trial(filtered, opts, filter, &best_size) }); best.reduce_with(|i, j| { if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) { @@ -730,19 +711,18 @@ fn optimize_raw( }) }; - if let Some((trial, idat_data)) = best { + if let Some((filter, idat_data)) = best { let image = PngData { raw: png, - // The filtered data has not been retained here, but we don't need to return it - filtered: vec![], idat_data, + aux_chunks: Vec::new(), }; if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) { debug!("Found better combination:"); debug!( " zc = {} f = {:8} {} bytes", - trial.compression, - trial.filter, + opts.deflate, + filter, image.idat_data.len() ); return Some(image); @@ -751,7 +731,11 @@ fn optimize_raw( } else if let Some(result) = eval_result { // If idat_recoding is off and reductions were attempted but ended up choosing the baseline, // we should still check if the evaluator compressed the baseline smaller than the original. - let image = result.image; + let image = PngData { + raw: result.image, + idat_data: result.idat_data, + aux_chunks: Vec::new(), + }; if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) { debug!("Found better combination:"); debug!( @@ -771,37 +755,26 @@ fn optimize_raw( fn perform_trial( filtered: &[u8], opts: &Options, - trial: TrialOptions, + filter: RowFilter, best_size: &AtomicMin, -) -> Option { - let new_idat = match opts.deflate { - Deflaters::Libdeflater { .. } => deflate::deflate(filtered, trial.compression, best_size), - #[cfg(feature = "zopfli")] - Deflaters::Zopfli { iterations } => deflate::zopfli_deflate(filtered, iterations), - }; - - // update best size or convert to error if not smaller - let new_idat = match new_idat { - Ok(n) if !best_size.set_min(n.len()) => Err(PngError::DeflatedDataTooLong(n.len())), - _ => new_idat, - }; - - match new_idat { - Ok(n) => { - let bytes = n.len(); +) -> Option { + match opts.deflate.deflate(filtered, best_size) { + Ok(new_idat) => { + let bytes = new_idat.len(); + best_size.set_min(bytes); trace!( " zc = {} f = {:8} {} bytes", - trial.compression, - trial.filter, + opts.deflate, + filter, bytes ); - Some((trial, n)) + Some((filter, new_idat)) } Err(PngError::DeflatedDataTooLong(bytes)) => { trace!( " zc = {} f = {:8} >{} bytes", - trial.compression, - trial.filter, + opts.deflate, + filter, bytes, ); None @@ -867,100 +840,59 @@ fn report_format(prefix: &str, png: &PngImage) { ); } -/// Strip headers from the `PngData` object, as requested by the passed `Options` -fn perform_strip(png: &mut PngData, opts: &Options) { - let raw = Arc::make_mut(&mut png.raw); - match opts.strip { - // Strip headers - Headers::None => (), - Headers::Keep(ref hdrs) => raw - .aux_headers - .retain(|hdr, _| std::str::from_utf8(hdr).map_or(false, |name| hdrs.contains(name))), - Headers::Strip(ref hdrs) => { - for hdr in hdrs { - raw.aux_headers.remove(hdr.as_bytes()); - } - } - Headers::Safe => { - const PRESERVED_HEADERS: [[u8; 4]; 5] = - [*b"cICP", *b"iCCP", *b"sBIT", *b"sRGB", *b"pHYs"]; - let keys: Vec<[u8; 4]> = raw.aux_headers.keys().cloned().collect(); - for hdr in &keys { - if !PRESERVED_HEADERS.contains(hdr) { - raw.aux_headers.remove(hdr); +/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed +fn postprocess_chunks(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData) { + if let Some(iccp_idx) = png.aux_chunks.iter().position(|c| &c.name == b"iCCP") { + // See if we can replace an iCCP chunk with an sRGB chunk + let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB"); + if may_replace_iccp && png.aux_chunks.iter().any(|c| &c.name == b"sRGB") { + // Files aren't supposed to have both chunks, so we chose to honor sRGB + trace!("Removing iCCP chunk due to conflict with sRGB chunk"); + png.aux_chunks.remove(iccp_idx); + } else if let Some(icc) = extract_icc(&png.aux_chunks[iccp_idx]) { + let intent = if may_replace_iccp { + srgb_rendering_intent(&icc) + } else { + None + }; + // sRGB-like profile can be replaced with an sRGB chunk with the same rendering intent + // Otherwise try recompressing the profile + if let Some(intent) = intent { + trace!("Replacing iCCP chunk with equivalent sRGB chunk"); + png.aux_chunks[iccp_idx] = Chunk { + name: *b"sRGB", + data: vec![intent], + }; + } else if let Ok(iccp) = construct_iccp(&icc, opts.deflate) { + let cur_len = png.aux_chunks[iccp_idx].data.len(); + let new_len = iccp.data.len(); + if new_len < cur_len { + debug!( + "Recompressed iCCP chunk: {} ({} bytes decrease)", + new_len, + cur_len - new_len + ); + png.aux_chunks[iccp_idx] = iccp; } } } - Headers::All => { - raw.aux_headers = IndexMap::new(); - } } - let may_replace_iccp = match opts.strip { - Headers::Keep(ref hdrs) => hdrs.contains("sRGB"), - Headers::Strip(ref hdrs) => !hdrs.iter().any(|v| v == "sRGB"), - Headers::Safe => true, - Headers::None | Headers::All => false, - }; - - if may_replace_iccp { - if raw.aux_headers.get(b"sRGB").is_some() { - // Files aren't supposed to have both chunks, so we chose to honor sRGB - raw.aux_headers.remove(b"iCCP"); - } else if let Some(intent) = raw - .aux_headers - .get(b"iCCP") - .and_then(|iccp| srgb_rendering_intent(iccp)) - { - // sRGB-like profile can be safely replaced with - // an sRGB chunk with the same rendering intent - raw.aux_headers.remove(b"iCCP"); - raw.aux_headers.insert(*b"sRGB", vec![intent]); - } - } -} - -/// If the profile is sRGB, extracts the rendering intent value from it -fn srgb_rendering_intent(mut iccp: &[u8]) -> Option { - // Skip (useless) profile name - loop { - let (&n, rest) = iccp.split_first()?; - iccp = rest; - if n == 0 { - break; - } - } - - let (&compression_method, compressed_data) = iccp.split_first()?; - if compression_method != 0 { - return None; // The profile is supposed to be compressed (method 0) - } - // The decompressed size is unknown so we have to guess the required buffer size - let max_size = (compressed_data.len() * 2).max(1000); - let icc_data = inflate(compressed_data, max_size).ok()?; - - let rendering_intent = *icc_data.get(67)?; - - // The known profiles are the same as in libpng's `png_sRGB_checks`. - // The Profile ID header of ICC has a fixed layout, - // and is supposed to contain MD5 of profile data at this offset - match icc_data.get(84..100)? { - b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d" - | b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89" - | b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8" - | b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => { - Some(rendering_intent) - } - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { - // Known-bad profiles are identified by their CRC - match (crc32(&icc_data), icc_data.len()) { - (0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => { - Some(rendering_intent) - } - _ => None, + // If the depth/color type has changed, some chunks may be invalid and should be dropped + // While these could potentially be converted, they have no known use case today and are + // generally more trouble than they're worth + let ihdr = &png.raw.ihdr; + if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type { + png.aux_chunks.retain(|c| { + let invalid = &c.name == b"bKGD" || &c.name == b"sBIT" || &c.name == b"hIST"; + if invalid { + warn!( + "Removing {} chunk as it no longer matches the image data", + std::str::from_utf8(&c.name).unwrap() + ); } - } - _ => None, + !invalid + }); } } diff --git a/src/main.rs b/src/main.rs index 3676e85e1..585f70863 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,9 +17,9 @@ use clap::{AppSettings, Arg, ArgAction, ArgMatches, Command}; use indexmap::IndexSet; use log::{error, warn}; use oxipng::Deflaters; -use oxipng::Headers; use oxipng::Options; use oxipng::RowFilter; +use oxipng::StripChunks; use oxipng::{InFile, OutFile}; use std::fs::DirBuilder; #[cfg(feature = "zopfli")] @@ -496,40 +496,44 @@ fn parse_opts_into_struct( opts.idat_recoding = false; } - if let Some(hdrs) = matches.value_of("keep") { - opts.strip = Headers::Keep(hdrs.split(',').map(|x| x.trim().to_owned()).collect()) + if let Some(keep) = matches.value_of("keep") { + let names = keep + .split(',') + .map(parse_chunk_name) + .collect::>()?; + opts.strip = StripChunks::Keep(names) } - if let Some(hdrs) = matches.value_of("strip") { - let hdrs = hdrs - .split(',') - .map(|x| x.trim().to_owned()) - .collect::>(); - if hdrs.contains(&"safe".to_owned()) || hdrs.contains(&"all".to_owned()) { - if hdrs.len() > 1 { - return Err( - "'safe' or 'all' presets for --strip should be used by themselves".to_owned(), - ); - } - if hdrs[0] == "safe" { - opts.strip = Headers::Safe; - } else { - opts.strip = Headers::All; - } + if let Some(strip) = matches.value_of("strip") { + if strip == "safe" { + opts.strip = StripChunks::Safe; + } else if strip == "all" { + opts.strip = StripChunks::All; } else { const FORBIDDEN_CHUNKS: [[u8; 4]; 5] = [*b"IHDR", *b"IDAT", *b"tRNS", *b"PLTE", *b"IEND"]; - for i in &hdrs { - if FORBIDDEN_CHUNKS.iter().any(|chunk| chunk == i.as_bytes()) { - return Err(format!("{} chunk is not allowed to be stripped", i)); - } - } - opts.strip = Headers::Strip(hdrs); + let names = strip + .split(',') + .map(|x| { + if x == "safe" || x == "all" { + return Err( + "'safe' or 'all' presets for --strip should be used by themselves" + .to_owned(), + ); + } + let name = parse_chunk_name(x)?; + if FORBIDDEN_CHUNKS.contains(&name) { + return Err(format!("{} chunk is not allowed to be stripped", x)); + } + Ok(name) + }) + .collect::>()?; + opts.strip = StripChunks::Strip(names); } } if matches.is_present("strip-safe") { - opts.strip = Headers::Safe; + opts.strip = StripChunks::Safe; } if matches.is_present("zopfli") { @@ -556,6 +560,13 @@ fn parse_opts_into_struct( Ok((out_file, out_dir, opts)) } +fn parse_chunk_name(name: &str) -> Result<[u8; 4], String> { + name.trim() + .as_bytes() + .try_into() + .map_err(|_| format!("Invalid chunk name {}", name)) +} + fn parse_numeric_range_opts( input: &str, min_value: u8, diff --git a/src/png/mod.rs b/src/png/mod.rs index c25c52a9b..49f7a89dc 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -4,8 +4,8 @@ use crate::error::PngError; use crate::filters::*; use crate::headers::*; use crate::interlace::{deinterlace_image, interlace_image, Interlacing}; +use crate::Options; use bitvec::bitarr; -use indexmap::IndexMap; use libdeflater::{CompressionLvl, Compressor}; use rgb::ComponentSlice; use rustc_hash::FxHashMap; @@ -30,8 +30,6 @@ pub struct PngImage { pub ihdr: IhdrData, /// The uncompressed, unfiltered data from the IDAT chunk pub data: Vec, - /// All non-critical headers from the PNG are stored here - pub aux_headers: IndexMap<[u8; 4], Vec>, } /// Contains all data relevant to a PNG image @@ -41,17 +39,17 @@ pub struct PngData { pub raw: Arc, /// The filtered and compressed data of the IDAT chunk pub idat_data: Vec, - /// The filtered, uncompressed data of the IDAT chunk - pub filtered: Vec, + /// All non-critical chunks from the PNG are stored here + pub aux_chunks: Vec, } impl PngData { /// Create a new `PngData` struct by opening a file #[inline] - pub fn new(filepath: &Path, fix_errors: bool) -> Result { + pub fn new(filepath: &Path, opts: &Options) -> Result { let byte_data = Self::read_file(filepath)?; - Self::from_slice(&byte_data, fix_errors) + Self::from_slice(&byte_data, opts) } pub fn read_file(filepath: &Path) -> Result, PngError> { @@ -80,7 +78,7 @@ impl PngData { } /// Create a new `PngData` struct by reading a slice - pub fn from_slice(byte_data: &[u8], fix_errors: bool) -> Result { + pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result { let mut byte_offset: usize = 0; // Test that png header is valid let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?; @@ -88,71 +86,65 @@ impl PngData { return Err(PngError::NotPNG); } byte_offset += 8; - // Read the data headers - let mut aux_headers: IndexMap<[u8; 4], Vec> = IndexMap::new(); - let mut idat_headers: Vec = Vec::new(); - while let Some(header) = parse_next_header(byte_data, &mut byte_offset, fix_errors)? { - match &header.name { - b"IDAT" => idat_headers.extend_from_slice(header.data), + + // Read the data chunks + let mut idat_data: Vec = Vec::new(); + let mut key_chunks: FxHashMap<[u8; 4], Vec> = FxHashMap::default(); + let mut aux_chunks: Vec = Vec::new(); + while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? { + match &chunk.name { + b"IDAT" => idat_data.extend_from_slice(chunk.data), b"acTL" => return Err(PngError::APNGNotSupported), + b"IHDR" | b"PLTE" | b"tRNS" => { + key_chunks.insert(chunk.name, chunk.data.to_owned()); + } _ => { - aux_headers.insert(header.name, header.data.to_owned()); + if opts.strip.keep(&chunk.name) { + aux_chunks.push(Chunk { + name: chunk.name, + data: chunk.data.to_owned(), + }) + } } } } - // Parse the headers into our PngData - if idat_headers.is_empty() { + + // Parse the chunks into our PngData + if idat_data.is_empty() { return Err(PngError::ChunkMissing("IDAT")); } - let ihdr = match aux_headers.remove(b"IHDR") { + let ihdr_chunk = match key_chunks.remove(b"IHDR") { Some(ihdr) => ihdr, None => return Err(PngError::ChunkMissing("IHDR")), }; - let ihdr_header = parse_ihdr_header( - &ihdr, - aux_headers.remove(b"PLTE"), - aux_headers.remove(b"tRNS"), + let ihdr = parse_ihdr_chunk( + &ihdr_chunk, + key_chunks.remove(b"PLTE"), + key_chunks.remove(b"tRNS"), )?; - let raw_data = deflate::inflate(idat_headers.as_ref(), ihdr_header.raw_data_size())?; + let raw_data = deflate::inflate(idat_data.as_ref(), ihdr.raw_data_size())?; // Reject files with incorrect width/height or truncated data - if raw_data.len() != ihdr_header.raw_data_size() { + if raw_data.len() != ihdr.raw_data_size() { return Err(PngError::TruncatedData); } let mut raw = PngImage { - ihdr: ihdr_header, + ihdr, data: raw_data, - aux_headers, }; - let unfiltered = raw.unfilter_image()?; + raw.data = raw.unfilter_image()?; // Return the PngData Ok(Self { - idat_data: idat_headers, - filtered: std::mem::replace(&mut raw.data, unfiltered), + idat_data, raw: Arc::new(raw), + aux_chunks, }) } - /// Return an estimate of the output size + /// Return an estimate of the output size which can help with evaluation of very small data pub fn estimated_output_size(&self) -> usize { - // Add the size of the PLTE and tRNS chunks to the compressed idat size - // This can help with evaluation of very small data - let size = self.idat_data.len(); - size + match &self.raw.ihdr.color_type { - ColorType::Indexed { palette } => { - let plte = 12 + palette.len() * 3; - let trns = palette.iter().filter(|p| p.a != 255).count(); - if trns != 0 { - plte + 12 + trns - } else { - plte - } - } - ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2, - ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6, - _ => 0, - } + self.idat_data.len() + self.raw.key_chunks_size() } /// Format the `PngData` struct into a valid PNG bytestream @@ -173,14 +165,13 @@ impl PngData { ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok(); write_png_block(b"IHDR", &ihdr_data, &mut output); - // Ancillary headers - for (key, header) in self - .raw - .aux_headers + // Ancillary chunks + for chunk in self + .aux_chunks .iter() - .filter(|&(key, _)| !(key == b"bKGD" || key == b"hIST" || key == b"tRNS")) + .filter(|c| !(&c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")) { - write_png_block(key, header, &mut output); + write_png_block(&chunk.name, &chunk.data, &mut output); } // Palette and transparency match &self.raw.ihdr.color_type { @@ -210,14 +201,13 @@ impl PngData { } _ => {} } - // Special ancillary headers that need to come after PLTE but before IDAT - for (key, header) in self - .raw - .aux_headers + // Special ancillary chunks that need to come after PLTE but before IDAT + for chunk in self + .aux_chunks .iter() - .filter(|&(key, _)| key == b"bKGD" || key == b"hIST" || key == b"tRNS") + .filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS") { - write_png_block(key, header, &mut output); + write_png_block(&chunk.name, &chunk.data, &mut output); } // IDAT data write_png_block(b"IDAT", &self.idat_data, &mut output); @@ -265,6 +255,24 @@ impl PngImage { } } + /// Calculate the size of the PLTE and tRNS chunks + pub fn key_chunks_size(&self) -> usize { + match &self.ihdr.color_type { + ColorType::Indexed { palette } => { + let plte = 12 + palette.len() * 3; + let trns = palette.iter().filter(|p| p.a != 255).count(); + if trns != 0 { + plte + 12 + trns + } else { + plte + } + } + ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2, + ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6, + _ => 0, + } + } + /// Return an iterator over the scanlines of the image #[inline] pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> { @@ -451,14 +459,15 @@ impl PngImage { filtered } } -fn write_png_block(key: &[u8], header: &[u8], output: &mut Vec) { - let mut header_data = Vec::with_capacity(header.len() + 4); - header_data.extend_from_slice(key); - header_data.extend_from_slice(header); - output.reserve(header_data.len() + 8); - output.extend_from_slice(&(header_data.len() as u32 - 4).to_be_bytes()); - let crc = deflate::crc32(&header_data); - output.append(&mut header_data); + +fn write_png_block(key: &[u8], chunk: &[u8], output: &mut Vec) { + let mut chunk_data = Vec::with_capacity(chunk.len() + 4); + chunk_data.extend_from_slice(key); + chunk_data.extend_from_slice(chunk); + output.reserve(chunk_data.len() + 8); + output.extend_from_slice(&(chunk_data.len() as u32 - 4).to_be_bytes()); + let crc = deflate::crc32(&chunk_data); + output.append(&mut chunk_data); output.extend_from_slice(&crc.to_be_bytes()); } diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index 4b9fd4394..3045ddf65 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -25,7 +25,6 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option { Some(PngImage { data: reduced, ihdr: png.ihdr.clone(), - aux_headers: png.aux_headers.clone(), }) } @@ -88,20 +87,11 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option Option { bit_depth: BitDepth::Eight, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), }) } @@ -37,37 +36,22 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O // Calculate the current number of pixels per byte let ppb = 8 / bit_depth; - if let ColorType::Indexed { .. } = png.ihdr.color_type { - for line in png.scan_lines(false) { - let line_max = line - .data - .iter() - .map(|&byte| match png.ihdr.bit_depth { - BitDepth::Two => (byte & 0x3) - .max((byte >> 2) & 0x3) - .max((byte >> 4) & 0x3) - .max(byte >> 6), - BitDepth::Four => (byte & 0xF).max(byte >> 4), - _ => byte, - }) - .max() - .unwrap_or(0); - let required_bits = match line_max { - x if x > 0x0F => 8, - x if x > 0x03 => 4, - x if x > 0x01 => 2, - _ => 1, - }; - if required_bits > minimum_bits { - minimum_bits = required_bits; - if minimum_bits >= bit_depth { - // Not reducable - return None; - } - } + if let ColorType::Indexed { palette } = &png.ihdr.color_type { + // We can easily determine minimum depth by the palette size + let required_bits = match palette.len() { + 0..=2 => 1, + 3..=4 => 2, + 5..=16 => 4, + _ => 8, + }; + if required_bits >= bit_depth { + // Not reducable + return None; + } else if required_bits > minimum_bits { + minimum_bits = required_bits; } } else { - // Checking for grayscale depth reduction is quite different than for indexed + // Finding minimum depth for grayscale is much more complicated let mut mask = (1 << minimum_bits) - 1; let mut divisions = 1..(bit_depth / minimum_bits); for &b in &png.data { @@ -155,6 +139,5 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O bit_depth: (minimum_bits as u8).try_into().unwrap(), ..png.ihdr }, - aux_headers: png.aux_headers.clone(), }) } diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 177ae1d61..630619d23 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -3,7 +3,7 @@ use crate::headers::IhdrData; use crate::png::PngImage; use indexmap::IndexSet; use rgb::alt::Gray; -use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, RGBA8}; +use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA}; use rustc_hash::FxHasher; use std::hash::{BuildHasherDefault, Hash}; @@ -41,7 +41,7 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option { } let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel()); - let mut palette: Vec<_> = match png.ihdr.color_type { + let palette: Vec<_> = match png.ihdr.color_type { ColorType::Grayscale { transparent_shade } => { let pmap = build_palette(png.data.as_gray().iter().cloned(), &mut raw_data)?; // Convert the Gray16 transparency to Gray8 @@ -81,51 +81,12 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option { _ => return None, }; - let mut aux_headers = png.aux_headers.clone(); - if let Some(bkgd_header) = aux_headers.remove(b"bKGD") { - let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 { - // In bKGD 16-bit values are used even for 8-bit images - Some(RGBA8::new( - bkgd_header[1], - bkgd_header[3], - bkgd_header[5], - 255, - )) - } else if png.ihdr.color_type.is_grayscale() && bkgd_header.len() == 2 { - Some(RGBA8::new( - bkgd_header[1], - bkgd_header[1], - bkgd_header[1], - 255, - )) - } else { - None - }; - if let Some(bg) = bg { - let idx = palette.iter().position(|&px| px == bg).or_else(|| { - if palette.len() < 256 { - palette.push(bg); - Some(palette.len() - 1) - } else { - None // No space in palette to store the bg as an index - } - })?; - aux_headers.insert(*b"bKGD", vec![idx as u8]); - } - } - - if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { - // Some programs save the sBIT header as RGB even if the image is RGBA. - aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect()); - } - Some(PngImage { data: raw_data, ihdr: IhdrData { color_type: ColorType::Indexed { palette }, ..png.ihdr }, - aux_headers, }) } @@ -150,18 +111,6 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { reduced.extend_from_slice(&pixel[last_color..]); } - let mut aux_headers = png.aux_headers.clone(); - if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { - if let Some(&byte) = sbit_header.first() { - aux_headers.insert(*b"sBIT", vec![byte]); - } - } - if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - if let Some(b) = bkgd_header.get(0..2) { - aux_headers.insert(*b"bKGD", b.to_owned()); - } - } - let color_type = match png.ihdr.color_type { ColorType::RGB { transparent_color } => ColorType::Grayscale { // Copy the transparent component if it is also gray @@ -178,7 +127,6 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { color_type, ..png.ihdr }, - aux_headers, }) } @@ -223,25 +171,11 @@ pub fn indexed_to_channels(png: &PngImage) -> Option { data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]); } - // Update bKGD if it exists - let mut aux_headers = png.aux_headers.clone(); - if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) { - if let Some(color) = palette.get(idx as usize) { - let bkgd = if is_gray { - vec![0, color.r] - } else { - vec![0, color.r, 0, color.g, 0, color.b] - }; - aux_headers.insert(*b"bKGD", bkgd); - } - } - Some(PngImage { ihdr: IhdrData { color_type, ..png.ihdr }, data, - aux_headers, }) } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index 83c14e241..8c610d039 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -30,15 +30,6 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option } } - // Update bKGD if it exists, ensuring it comes last in the palette if otherwise unused - let mut aux_headers = png.aux_headers.clone(); - if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) { - if let Some(&color) = palette.get(idx as usize) { - let idx = add_color_to_set(color, &mut condensed, optimize_alpha); - aux_headers.insert(*b"bKGD", vec![idx]); - } - } - let data = if did_change { // Reassign data bytes to new indices let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &palette_map); @@ -59,7 +50,6 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option ..png.ihdr }, data, - aux_headers, }) } @@ -145,15 +135,6 @@ pub fn sorted_palette(png: &PngImage) -> Option { let mut enumerated: Vec<_> = palette.iter().enumerate().collect(); - // If the background is the last entry in the palette we should make sure it stays last - // Otherwise an entry that's unused by the idat could prevent reduction to a lower depth - let mut aux_headers = png.aux_headers.clone(); - let bkgd_idx = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()); - let bkgd_last = match bkgd_idx { - Some(idx) if idx as usize + 1 == palette.len() => enumerated.pop(), - _ => None, - }; - // Sort the palette enumerated.sort_by(|a, b| { // Sort by ascending alpha and descending luma @@ -167,10 +148,6 @@ pub fn sorted_palette(png: &PngImage) -> Option { color_val(a.1).cmp(&color_val(b.1)) }); - if let Some(bkgd) = bkgd_last { - enumerated.push(bkgd); - } - // Extract the new palette and determine if anything changed let (old_map, palette): (Vec<_>, Vec) = enumerated.into_iter().unzip(); if old_map.iter().enumerate().all(|(a, b)| a == *b) { @@ -185,17 +162,11 @@ pub fn sorted_palette(png: &PngImage) -> Option { let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &new_map); let data = png.data.iter().map(|&b| byte_map[b as usize]).collect(); - // Update bKGD if it exists - if let Some(idx) = bkgd_idx.map(|idx| new_map[idx as usize]) { - aux_headers.insert(*b"bKGD", vec![idx]); - } - Some(PngImage { ihdr: IhdrData { color_type: ColorType::Indexed { palette }, ..png.ihdr }, data, - aux_headers, }) } diff --git a/tests/files/strip_headers_all.png b/tests/files/strip_headers_all.png index d714a05d305858ceb92cdbfd6010d39c377699ad..6742de54312ee276d18763533983a4e3c2fe998b 100644 GIT binary patch delta 115 zcmaDnh5hJM_6^gy5*SoVTq8Tv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8WTv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8WTv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8WTv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8W x, Err(x) => { remove_file(output).ok(); diff --git a/tests/flags.rs b/tests/flags.rs index ebb2108e2..f3fb17d21 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,6 +1,6 @@ -use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use indexmap::{indexset, IndexSet}; +use oxipng::internal_tests::*; +use oxipng::*; #[cfg(feature = "filetime")] use std::cell::RefCell; use std::fs::remove_file; @@ -47,7 +47,7 @@ fn test_it_converts_callbacks( CBPOST: FnMut(&Path), CBPRE: FnMut(&Path), { - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); @@ -63,7 +63,7 @@ fn test_it_converts_callbacks( callback_post(output); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -203,17 +203,24 @@ fn verbose_mode() { } } +fn count_chunk(png: &PngData, name: &[u8; 4]) -> usize { + png.aux_chunks + .iter() + .filter(|chunk| &chunk.name == name) + .count() +} + #[test] fn strip_headers_list() { let input = PathBuf::from("tests/files/strip_headers_list.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::Strip(vec!["iCCP".to_owned(), "tEXt".to_owned()]); + opts.strip = StripChunks::Strip(indexset![*b"iCCP", *b"tEXt"]); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -222,7 +229,7 @@ fn strip_headers_list() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -230,9 +237,9 @@ fn strip_headers_list() { } }; - assert!(!png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(!png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 0); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 0); remove_file(output).ok(); } @@ -241,13 +248,13 @@ fn strip_headers_list() { fn strip_headers_safe() { let input = PathBuf::from("tests/files/strip_headers_safe.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::Safe; + opts.strip = StripChunks::Safe; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -256,7 +263,7 @@ fn strip_headers_safe() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -264,9 +271,9 @@ fn strip_headers_safe() { } }; - assert!(!png.raw.aux_headers.contains_key(b"tEXt")); - assert!(!png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"sRGB")); + assert_eq!(count_chunk(&png, b"tEXt"), 0); + assert_eq!(count_chunk(&png, b"iTXt"), 0); + assert_eq!(count_chunk(&png, b"sRGB"), 1); remove_file(output).ok(); } @@ -275,13 +282,13 @@ fn strip_headers_safe() { fn strip_headers_all() { let input = PathBuf::from("tests/files/strip_headers_all.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::All; + opts.strip = StripChunks::All; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -290,7 +297,7 @@ fn strip_headers_all() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -298,9 +305,9 @@ fn strip_headers_all() { } }; - assert!(!png.raw.aux_headers.contains_key(b"tEXt")); - assert!(!png.raw.aux_headers.contains_key(b"iTXt")); - assert!(!png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 0); + assert_eq!(count_chunk(&png, b"iTXt"), 0); + assert_eq!(count_chunk(&png, b"iCCP"), 0); remove_file(output).ok(); } @@ -309,13 +316,13 @@ fn strip_headers_all() { fn strip_headers_none() { let input = PathBuf::from("tests/files/strip_headers_none.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::None; + opts.strip = StripChunks::None; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -324,7 +331,7 @@ fn strip_headers_none() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -332,9 +339,9 @@ fn strip_headers_none() { } }; - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); remove_file(output).ok(); } @@ -345,7 +352,7 @@ fn interlacing_0_to_1() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::Adam7); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); @@ -356,7 +363,7 @@ fn interlacing_0_to_1() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -375,7 +382,7 @@ fn interlacing_1_to_0() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::None); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); @@ -386,7 +393,7 @@ fn interlacing_1_to_0() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -405,7 +412,7 @@ fn interlacing_0_to_1_small_files() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::Adam7); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); @@ -418,7 +425,7 @@ fn interlacing_0_to_1_small_files() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -439,7 +446,7 @@ fn interlacing_1_to_0_small_files() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::None); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); @@ -452,7 +459,7 @@ fn interlacing_1_to_0_small_files() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -476,7 +483,7 @@ fn interlaced_0_to_1_other_filter_mode() { filter.insert(RowFilter::Paeth); opts.filter = filter; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); @@ -487,7 +494,7 @@ fn interlaced_0_to_1_other_filter_mode() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -570,7 +577,7 @@ fn fix_errors() { let (output, mut opts) = get_opts(&input); opts.fix_errors = true; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGBA); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -582,7 +589,7 @@ fn fix_errors() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, false) { + let png = match PngData::new(output, &Options::default()) { Ok(x) => x, Err(x) => { remove_file(output).ok(); diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 52540a852..573c31820 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -1,6 +1,6 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -35,7 +35,7 @@ fn test_it_converts( ) { let input = PathBuf::from(input); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); @@ -48,7 +48,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); diff --git a/tests/interlacing.rs b/tests/interlacing.rs index 10f558e66..2e9719793 100644 --- a/tests/interlacing.rs +++ b/tests/interlacing.rs @@ -1,6 +1,6 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -33,7 +33,7 @@ fn test_it_converts( ) { let input = PathBuf::from(input); let (output, mut opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); opts.interlace = Some(interlace); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); @@ -53,7 +53,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); diff --git a/tests/lib.rs b/tests/lib.rs index 8f5e28d10..a32de8b15 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,6 +1,4 @@ -use oxipng::Headers; -use oxipng::OutFile; -use std::default::Default; +use oxipng::*; use std::fs; use std::fs::File; use std::io::prelude::*; @@ -11,9 +9,7 @@ fn optimize_from_memory() { let mut in_file_buf: Vec = Vec::new(); in_file.read_to_end(&mut in_file_buf).unwrap(); - let opts: oxipng::Options = Default::default(); - - let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); assert!(result.is_ok()); } @@ -23,9 +19,7 @@ fn optimize_from_memory_corrupted() { let mut in_file_buf: Vec = Vec::new(); in_file.read_to_end(&mut in_file_buf).unwrap(); - let opts: oxipng::Options = Default::default(); - - let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); assert!(result.is_err()); } @@ -35,44 +29,36 @@ fn optimize_from_memory_apng() { let mut in_file_buf: Vec = Vec::new(); in_file.read_to_end(&mut in_file_buf).unwrap(); - let opts: oxipng::Options = Default::default(); - - let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); assert!(result.is_err()); } #[test] fn optimize() { - let opts: oxipng::Options = Default::default(); - let result = oxipng::optimize( &"tests/files/fully_optimized.png".into(), &OutFile::Path(None), - &opts, + &Options::default(), ); assert!(result.is_ok()); } #[test] fn optimize_corrupted() { - let opts: oxipng::Options = Default::default(); - let result = oxipng::optimize( &"tests/files/corrupted_header.png".into(), &OutFile::Path(None), - &opts, + &Options::default(), ); assert!(result.is_err()); } #[test] fn optimize_apng() { - let opts: oxipng::Options = Default::default(); - let result = oxipng::optimize( &"tests/files/apng_file.png".into(), &OutFile::Path(None), - &opts, + &Options::default(), ); assert!(result.is_err()); } @@ -80,12 +66,12 @@ fn optimize_apng() { #[test] fn optimize_srgb_icc() { let file = fs::read("tests/files/badsrgb.png").unwrap(); - let mut opts: oxipng::Options = Default::default(); + let mut opts = Options::default(); let result = oxipng::optimize_from_memory(&file, &opts); assert!(result.unwrap().len() > 1000); - opts.strip = Headers::Safe; + opts.strip = StripChunks::Safe; let result = oxipng::optimize_from_memory(&file, &opts); assert!(result.unwrap().len() < 1000); } diff --git a/tests/raw.rs b/tests/raw.rs index 038024445..c3e16fb2f 100644 --- a/tests/raw.rs +++ b/tests/raw.rs @@ -16,11 +16,11 @@ fn test_it_converts(input: &str) { let opts = get_opts(); let original_data = PngData::read_file(&PathBuf::from(input)).unwrap(); - let png = PngData::from_slice(&original_data, opts.fix_errors).unwrap(); - let png = Arc::try_unwrap(png.raw).unwrap(); + let image = PngData::from_slice(&original_data, &opts).unwrap(); + let png = Arc::try_unwrap(image.raw).unwrap(); - let num_headers = png.aux_headers.len(); - assert!(num_headers > 0); + let num_chunks = image.aux_chunks.len(); + assert!(num_chunks > 0); let mut raw = RawImage::new( png.ihdr.width, @@ -31,14 +31,14 @@ fn test_it_converts(input: &str) { ) .unwrap(); - for (chunk_type, data) in png.aux_headers { - raw.add_png_chunk(chunk_type, data); + for chunk in image.aux_chunks { + raw.add_png_chunk(chunk.name, chunk.data); } let output = raw.create_optimized_png(&opts).unwrap(); - let new = PngData::from_slice(&output, opts.fix_errors).unwrap(); - assert!(new.raw.aux_headers.len() == num_headers); + let new = PngData::from_slice(&output, &opts).unwrap(); + assert!(new.aux_chunks.len() == num_chunks); #[cfg(feature = "sanity-checks")] assert!(validate_output(&output, &original_data)); diff --git a/tests/reduction.rs b/tests/reduction.rs index 3d6577eb3..8a1a1e347 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1,6 +1,6 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -37,7 +37,7 @@ fn test_it_converts( let input = PathBuf::from(input); let (output, mut opts) = get_opts(&input); opts.optimize_alpha = optimize_alpha; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); @@ -50,7 +50,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -933,7 +933,7 @@ fn palette_should_be_reduced_with_dupes() { let input = PathBuf::from("tests/files/palette_should_be_reduced_with_dupes.png"); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -948,7 +948,7 @@ fn palette_should_be_reduced_with_dupes() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -970,7 +970,7 @@ fn palette_should_be_reduced_with_unused() { let input = PathBuf::from("tests/files/palette_should_be_reduced_with_unused.png"); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -985,7 +985,7 @@ fn palette_should_be_reduced_with_unused() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -1007,7 +1007,7 @@ fn palette_should_be_reduced_with_both() { let input = PathBuf::from("tests/files/palette_should_be_reduced_with_both.png"); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -1022,7 +1022,7 @@ fn palette_should_be_reduced_with_both() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); diff --git a/tests/regression.rs b/tests/regression.rs index c24c83732..c84a25ddc 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -1,6 +1,6 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -36,7 +36,7 @@ fn test_it_converts( ) { let input = PathBuf::from(input); let (output, opts) = custom.unwrap_or_else(|| get_opts(&input)); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!( png.raw.ihdr.color_type.png_header_code(), @@ -52,7 +52,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -70,13 +70,7 @@ fn test_it_converts( "optimized to wrong bit depth" ); if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { - let mut max_palette_size = 1 << (png.raw.ihdr.bit_depth as u8); - // Ensure bKGD color is valid - if let Some(&idx) = png.raw.aux_headers.get(b"bKGD").and_then(|b| b.first()) { - assert!(palette.len() > idx as usize); - max_palette_size = max_palette_size.max(idx as usize + 1); - } - assert!(palette.len() <= max_palette_size); + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8)); } remove_file(output).ok(); @@ -100,7 +94,7 @@ fn issue_42() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::Adam7); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); assert_eq!(png.raw.ihdr.color_type, ColorType::GrayscaleAlpha); @@ -113,7 +107,7 @@ fn issue_42() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -231,7 +225,7 @@ fn issue_59() { None, RGBA, BitDepth::Eight, - RGBA, + INDEXED, BitDepth::Eight, ); } diff --git a/tests/strategies.rs b/tests/strategies.rs index 415014fe4..8b4b94785 100644 --- a/tests/strategies.rs +++ b/tests/strategies.rs @@ -1,6 +1,6 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -36,7 +36,7 @@ fn test_it_converts( let input = PathBuf::from(input); let (output, mut opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); opts.filter = IndexSet::new(); opts.filter.insert(filter); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); @@ -49,7 +49,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok();