Make reductions return a new uncompressed image (#158)

* Fix verbose message

* No cloning when restoring original data

* Make reductions return a new uncompressed image

Partially fixes #145

* Async reduction evaluator

* Assert

* Faster bit depth check

* Also try 4-bit depth for small-depth images

* Skip test when using miniz

* Ensure palette is trimmed after depth reduction

Fixes #159

* Fudge factor for reductions to prefer better reductions even if gzip estimation says otherwise
This commit is contained in:
Kornel 2019-01-27 08:22:49 +00:00 committed by Josh Holmer
parent 55d85df9fc
commit 9af874d21b
22 changed files with 943 additions and 787 deletions

6
Cargo.lock generated
View file

@ -85,7 +85,7 @@ dependencies = [
[[package]]
name = "cloudflare-zlib"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cloudflare-zlib-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -265,7 +265,7 @@ dependencies = [
"bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cloudflare-zlib 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cloudflare-zlib 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"image 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
@ -422,7 +422,7 @@ dependencies = [
"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16"
"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e"
"checksum cloudflare-zlib 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "398f5d6b4431f230c89858e5389803bd0135bc764fbf9b7b50d4be0ca2657a6c"
"checksum cloudflare-zlib 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cd97a72b7777ef134de513cef589e48c393dc2ea7180ff6dca87dcd3ee078dac"
"checksum cloudflare-zlib-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7e195cb274a0d6ee87e718838a09baecd7cbc9f6075dac256a84cb5842739c06"
"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
"checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"

View file

@ -49,7 +49,7 @@ optional = true
version = "2.0.0"
[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies]
cloudflare-zlib = "^0.2.0"
cloudflare-zlib = "^0.2.2"
[dependencies.image]
default-features = false

View file

@ -14,7 +14,7 @@ fn deflate_16_bits_strategy_0(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
deflate(png.raw.data.as_ref(), 9, 0, 15, &min)
});
}
@ -25,7 +25,7 @@ fn deflate_8_bits_strategy_0(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
deflate(png.raw.data.as_ref(), 9, 0, 15, &min)
});
}
@ -38,7 +38,7 @@ fn deflate_4_bits_strategy_0(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
deflate(png.raw.data.as_ref(), 9, 0, 15, &min)
});
}
@ -51,7 +51,7 @@ fn deflate_2_bits_strategy_0(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
deflate(png.raw.data.as_ref(), 9, 0, 15, &min)
});
}
@ -64,7 +64,7 @@ fn deflate_1_bits_strategy_0(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
deflate(png.raw.data.as_ref(), 9, 0, 15, &min)
});
}
@ -75,7 +75,7 @@ fn deflate_16_bits_strategy_1(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
deflate(png.raw.data.as_ref(), 9, 1, 15, &min)
});
}
@ -86,7 +86,7 @@ fn deflate_8_bits_strategy_1(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
deflate(png.raw.data.as_ref(), 9, 1, 15, &min)
});
}
@ -99,7 +99,7 @@ fn deflate_4_bits_strategy_1(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
deflate(png.raw.data.as_ref(), 9, 1, 15, &min)
});
}
@ -112,7 +112,7 @@ fn deflate_2_bits_strategy_1(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
deflate(png.raw.data.as_ref(), 9, 1, 15, &min)
});
}
@ -125,7 +125,7 @@ fn deflate_1_bits_strategy_1(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
deflate(png.raw.data.as_ref(), 9, 1, 15, &min)
});
}
@ -136,7 +136,7 @@ fn deflate_16_bits_strategy_2(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
deflate(png.raw.data.as_ref(), 9, 2, 15, &min)
});
}
@ -147,7 +147,7 @@ fn deflate_8_bits_strategy_2(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
deflate(png.raw.data.as_ref(), 9, 2, 15, &min)
});
}
@ -160,7 +160,7 @@ fn deflate_4_bits_strategy_2(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
deflate(png.raw.data.as_ref(), 9, 2, 15, &min)
});
}
@ -173,7 +173,7 @@ fn deflate_2_bits_strategy_2(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
deflate(png.raw.data.as_ref(), 9, 2, 15, &min)
});
}
@ -186,7 +186,7 @@ fn deflate_1_bits_strategy_2(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
deflate(png.raw.data.as_ref(), 9, 2, 15, &min)
});
}
@ -197,7 +197,7 @@ fn deflate_16_bits_strategy_3(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
deflate(png.raw.data.as_ref(), 9, 3, 15, &min)
});
}
@ -208,7 +208,7 @@ fn deflate_8_bits_strategy_3(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
deflate(png.raw.data.as_ref(), 9, 3, 15, &min)
});
}
@ -221,7 +221,7 @@ fn deflate_4_bits_strategy_3(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
deflate(png.raw.data.as_ref(), 9, 3, 15, &min)
});
}
@ -234,7 +234,7 @@ fn deflate_2_bits_strategy_3(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
deflate(png.raw.data.as_ref(), 9, 3, 15, &min)
});
}
@ -247,7 +247,7 @@ fn deflate_1_bits_strategy_3(b: &mut Bencher) {
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
deflate(png.raw.data.as_ref(), 9, 3, 15, &min)
});
}

View file

@ -14,7 +14,7 @@ fn reductions_16_to_8_bits(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
safe_png.reduce_bit_depth()
});
}
@ -27,7 +27,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
safe_png.reduce_bit_depth()
});
}
@ -40,7 +40,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
safe_png.reduce_bit_depth()
});
}
@ -53,7 +53,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
safe_png.reduce_bit_depth()
});
}
@ -66,7 +66,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
safe_png.reduce_bit_depth()
});
}
@ -79,7 +79,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
safe_png.reduce_bit_depth()
});
}
@ -92,7 +92,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
safe_png.reduce_bit_depth()
});
}
@ -103,7 +103,7 @@ fn reductions_rgba_to_rgb_16(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -114,7 +114,7 @@ fn reductions_rgba_to_rgb_8(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -127,7 +127,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -140,7 +140,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -153,7 +153,7 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -166,7 +166,7 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -179,7 +179,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -190,7 +190,7 @@ fn reductions_rgb_to_grayscale_8(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -201,7 +201,7 @@ fn reductions_rgba_to_palette_8(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -212,7 +212,7 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
safe_png.reduce_color_type()
});
}
@ -225,7 +225,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_palette();
safe_png.reduce_palette()
});
}
@ -238,7 +238,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_palette();
safe_png.reduce_palette()
});
}
@ -251,7 +251,7 @@ fn reductions_palette_full_reduction(b: &mut Bencher) {
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_palette();
safe_png.reduce_palette()
});
}

View file

@ -13,7 +13,7 @@ fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw_data.as_ref()).ok();
zopfli_deflate(png.raw.data.as_ref()).ok();
});
}
@ -23,7 +23,7 @@ fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw_data.as_ref()).ok();
zopfli_deflate(png.raw.data.as_ref()).ok();
});
}
@ -35,7 +35,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw_data.as_ref()).ok();
zopfli_deflate(png.raw.data.as_ref()).ok();
});
}
@ -47,7 +47,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw_data.as_ref()).ok();
zopfli_deflate(png.raw.data.as_ref()).ok();
});
}
@ -59,6 +59,6 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw_data.as_ref()).ok();
zopfli_deflate(png.raw.data.as_ref()).ok();
});
}

View file

@ -56,7 +56,7 @@ impl ColorType {
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
/// The number of bits to be used per channel per pixel
pub enum BitDepth {
/// One bit per channel per pixel

View file

@ -9,7 +9,14 @@ use PngResult;
pub mod miniz_stream;
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
mod cfzlib;
pub mod cfzlib;
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
pub mod cfzlib {
pub fn is_supported() -> bool {
return false;
}
}
/// Decompress a data stream using the DEFLATE algorithm
pub fn inflate(data: &[u8]) -> PngResult<Vec<u8>> {

112
src/evaluate.rs Normal file
View file

@ -0,0 +1,112 @@
//! Check if a reduction makes file smaller, and keep best reductions.
//! Works asynchronously when possible
use atomicmin::AtomicMin;
use deflate;
use png::PngData;
use png::PngImage;
use png::STD_COMPRESSION;
use png::STD_FILTERS;
use png::STD_STRATEGY;
use png::STD_WINDOW;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use std::sync::mpsc::*;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
/// Collect image versions and pick one that compresses best
pub struct Evaluator {
/// images are sent to the thread for evaluation
eval_send: Option<SyncSender<(Arc<PngImage>, f32, bool)>>,
// the thread helps evaluate images asynchronously
eval_thread: thread::JoinHandle<Option<PngData>>,
}
impl Evaluator {
pub fn new() -> Self {
// queue size ensures we're not using too much memory for pending reductions
let (tx, rx) = sync_channel(4);
Self {
eval_send: Some(tx),
eval_thread: thread::spawn(move || Self::evaluate_images(rx)),
}
}
/// Wait for all evaluations to finish and return smallest reduction
/// Or `None` if all reductions were worse than baseline.
pub fn get_result(mut self) -> Option<PngData> {
let _ = self.eval_send.take(); // disconnect the sender, breaking the loop in the thread
self.eval_thread.join().expect("eval thread")
}
/// Set baseline image. It will be used only to measure minimum compression level required
pub fn set_baseline(&self, image: Arc<PngImage>) {
self.try_image_inner(image, 1.0, false)
}
/// Check if the image is smaller than others
/// Bias is a value in 0..=1 range. Compressed size is multiplied by
/// this fraction when comparing to the best, so 0.95 allows 5% larger size.
pub fn try_image(&self, image: Arc<PngImage>, bias: f32) {
self.try_image_inner(image, bias, true)
}
fn try_image_inner(&self, image: Arc<PngImage>, bias: f32, is_reduction: bool) {
self.eval_send.as_ref().expect("not finished yet").send((image, bias, is_reduction)).expect("send")
}
/// Main loop of evaluation thread
fn evaluate_images(from_channel: Receiver<(Arc<PngImage>, f32, bool)>) -> Option<PngData> {
let best_candidate_size = AtomicMin::new(None);
let best_result: Mutex<Option<(PngData, _, _)>> = Mutex::new(None);
// ends when sender is dropped
for (nth, (image, bias, is_reduction)) in from_channel.iter().enumerate() {
#[cfg(feature = "parallel")]
let filters_iter = STD_FILTERS.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))]
let filters_iter = STD_FILTERS.iter();
filters_iter.for_each(|&f| {
if let Ok(idat_data) = deflate::deflate(
&image.filter_image(f),
STD_COMPRESSION,
STD_STRATEGY,
STD_WINDOW,
&best_candidate_size,
) {
let mut res = best_result.lock().unwrap();
if best_candidate_size.get().map_or(true, |old_best_len| {
let new_len = (idat_data.len() as f64 * bias as f64) as usize;
// a tie-breaker is required to make evaluation deterministic
if let Some(res) = res.as_ref() {
// choose smallest compressed, or if compresses the same, smallest uncompressed, or cheaper filter
let old_img = &res.0.raw;
let new = (new_len, image.data.len(), image.ihdr.bit_depth, f, nth);
let old = (old_best_len, old_img.data.len(), old_img.ihdr.bit_depth, res.1, res.2);
new < old
} else if new_len < old_best_len {
true
} else {
false
}
}) {
best_candidate_size.set_min(idat_data.len());
*res = if is_reduction {
Some((PngData {
idat_data,
raw: Arc::clone(&image),
}, f, nth))
} else {
None
};
}
}
});
}
best_result.into_inner().expect("filters should be done")
.map(|(img, _, _)| img)
}
}

View file

@ -1,38 +1,38 @@
use reduction::ReducedPng;
use headers::IhdrData;
use bit_vec::BitVec;
use png::PngData;
use png::PngImage;
#[must_use]
pub fn interlace_image(png: &PngData) -> ReducedPng {
pub fn interlace_image(png: &PngImage) -> PngImage {
let mut passes: Vec<BitVec> = vec![BitVec::new(); 7];
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
let bits_per_pixel = png.ihdr.bit_depth.as_u8() * png.channels_per_pixel();
for (index, line) in png.scan_lines().enumerate() {
match index % 8 {
// Add filter bytes to passes that will be in the output image
0 => {
passes[0].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 5 {
if png.ihdr.width >= 5 {
passes[1].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 3 {
if png.ihdr.width >= 3 {
passes[3].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 2 {
if png.ihdr.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
4 => {
passes[2].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 3 {
if png.ihdr.width >= 3 {
passes[3].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 2 {
if png.ihdr.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
2 | 6 => {
passes[4].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 2 {
if png.ihdr.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
@ -43,7 +43,7 @@ pub fn interlace_image(png: &PngData) -> ReducedPng {
let bit_vec = BitVec::from_bytes(&line.data);
for (i, bit) in bit_vec.iter().enumerate() {
// Avoid moving padded 0's into new image
if i >= (png.ihdr_data.width * u32::from(bits_per_pixel)) as usize {
if i >= (png.ihdr.width * u32::from(bits_per_pixel)) as usize {
break;
}
// Copy pixels into interlaced passes
@ -77,35 +77,36 @@ pub fn interlace_image(png: &PngData) -> ReducedPng {
}
}
let mut output = Vec::with_capacity(png.raw_data.len());
let mut output = Vec::with_capacity(png.data.len());
for pass in &passes {
output.extend(pass.to_bytes());
}
ReducedPng {
raw_data: output,
interlaced: 1,
color_type: png.ihdr_data.color_type,
bit_depth: png.ihdr_data.bit_depth,
aux_headers: Default::default(),
palette: None,
transparency_pixel: None,
PngImage {
data: output,
ihdr: IhdrData {
interlaced: 1,
..png.ihdr
},
aux_headers: png.aux_headers.clone(),
palette: png.palette.clone(),
transparency_pixel: png.transparency_pixel.clone(),
}
}
pub fn deinterlace_image(png: &PngData) -> ReducedPng {
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr_data.width as usize;
pub fn deinterlace_image(png: &PngImage) -> PngImage {
let bits_per_pixel = png.ihdr.bit_depth.as_u8() * png.channels_per_pixel();
let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr.width as usize;
// Initialize each output line with a starting filter byte of 0
// as well as some blank data
let mut lines: Vec<BitVec> =
vec![BitVec::from_elem(bits_per_line, false); png.ihdr_data.height as usize];
vec![BitVec::from_elem(bits_per_line, false); png.ihdr.height as usize];
let mut current_pass = 1;
let mut pass_constants = interlaced_constants(current_pass);
let mut current_y: usize = pass_constants.y_shift as usize;
for line in png.scan_lines() {
let bit_vec = BitVec::from_bytes(&line.data);
let bits_in_line = ((png.ihdr_data.width - u32::from(pass_constants.x_shift)
let bits_in_line = ((png.ihdr.width - u32::from(pass_constants.x_shift)
+ u32::from(pass_constants.x_step)
- 1)
/ u32::from(pass_constants.x_step)) as usize
@ -123,36 +124,37 @@ pub fn deinterlace_image(png: &PngData) -> ReducedPng {
}
// Calculate the next line and move to next pass if necessary
current_y += pass_constants.y_step as usize;
if current_y >= png.ihdr_data.height as usize {
if current_y >= png.ihdr.height as usize {
if current_pass == 7 {
break;
}
current_pass += 1;
if current_pass == 2 && png.ihdr_data.width <= 4 {
if current_pass == 2 && png.ihdr.width <= 4 {
current_pass += 1;
}
if current_pass == 3 && png.ihdr_data.height <= 4 {
if current_pass == 3 && png.ihdr.height <= 4 {
current_pass += 1;
}
pass_constants = interlaced_constants(current_pass);
current_y = pass_constants.y_shift as usize;
}
}
let mut output = Vec::with_capacity(png.raw_data.len());
let mut output = Vec::with_capacity(png.data.len());
for line in &mut lines {
while line.len() % 8 != 0 {
line.push(false);
}
output.extend(line.to_bytes());
}
ReducedPng {
raw_data: output,
interlaced: 0,
color_type: png.ihdr_data.color_type,
bit_depth: png.ihdr_data.bit_depth,
aux_headers: Default::default(),
palette: None,
transparency_pixel: None,
PngImage {
data: output,
ihdr: IhdrData {
interlaced: 0,
..png.ihdr
},
aux_headers: png.aux_headers.clone(),
palette: png.palette.clone(),
transparency_pixel: png.transparency_pixel.clone(),
}
}

View file

@ -16,8 +16,11 @@ use reduction::*;
use atomicmin::AtomicMin;
use crc::crc32;
use deflate::inflate;
use evaluate::Evaluator;
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
use png::PngImage;
use png::PngData;
use colors::BitDepth;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
@ -27,6 +30,7 @@ use std::io::{stdin, stdout, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use std::sync::Arc;
pub use colors::AlphaOptim;
pub use deflate::Deflaters;
@ -37,6 +41,7 @@ mod atomicmin;
mod colors;
mod deflate;
mod error;
mod evaluate;
mod filters;
mod headers;
mod interlace;
@ -357,7 +362,9 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
let mut optimized_output = optimize_png(&mut png, &in_data, opts)?;
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) {
eprintln!("File already optimized");
if opts.verbosity.is_some() {
eprintln!("File already optimized");
}
match (output, input) {
// if p is None, it also means same as the input path
(&OutFile::Path(ref p), &InFile::Path(ref input_path))
@ -473,20 +480,20 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
if opts.verbosity.is_some() {
eprintln!(
" {}x{} pixels, PNG format",
png.ihdr_data.width, png.ihdr_data.height
png.raw.ihdr.width, png.raw.ihdr.height
);
if let Some(ref palette) = png.palette {
if let Some(ref palette) = png.raw.palette {
eprintln!(
" {} bits/pixel, {} colors in palette",
png.ihdr_data.bit_depth,
png.raw.ihdr.bit_depth,
palette.len() / 3
);
} else {
eprintln!(
" {}x{} bits/pixel, {:?}",
png.channels_per_pixel(),
png.ihdr_data.bit_depth,
png.ihdr_data.color_type
png.raw.channels_per_pixel(),
png.raw.ihdr.bit_depth,
png.raw.ihdr.color_type
);
}
eprintln!(" IDAT size = {} bytes", idat_original_size);
@ -499,8 +506,8 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
if opts.use_heuristics {
// Heuristically determine which set of options to use
if png.ihdr_data.bit_depth.as_u8() >= 8
&& png.ihdr_data.color_type != colors::ColorType::Indexed
if png.raw.ihdr.bit_depth.as_u8() >= 8
&& png.raw.ihdr.color_type != colors::ColorType::Indexed
{
if filter.is_empty() {
filter.push(5);
@ -518,7 +525,20 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
}
}
let reduction_occurred = perform_reductions(png, opts, &deadline);
// This will collect all versions of images and pick one that compresses best
let eval = Evaluator::new();
// Usually we want transformations that are smaller than the unmodified original,
// but if we're interlacing, we have to accept a possible file size increase.
if opts.interlace.is_none() {
eval.set_baseline(png.raw.clone());
}
perform_reductions(png.raw.clone(), opts, &deadline, &eval);
let reduction_occurred = if let Some(result) = eval.get_result() {
*png = result;
true
} else {
false
};
if opts.idat_recoding || reduction_occurred {
// Go through selected permutations and determine the best
@ -564,11 +584,11 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
let filters: HashMap<u8, Vec<u8>> = filter_iter
.map(|f| {
let png = png.clone();
(*f, png.filter_image(*f))
(*f, png.raw.filter_image(*f))
}).collect();
let original_len = original_png.idat_data.len();
let added_interlacing = opts.interlace == Some(1) && original_png.ihdr_data.interlaced == 0;
let added_interlacing = opts.interlace == Some(1) && original_png.raw.ihdr.interlaced == 0;
let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) });
#[cfg(feature = "parallel")]
@ -653,7 +673,7 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
);
}
} else if reduction_occurred {
png.reset_from_original(&original_png);
*png = original_png;
}
}
@ -721,66 +741,67 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
Err(PngError::new("The resulting image is corrupted"))
}
/// Attempt all reduction operations requested by the given `Options` struct
/// and apply them directly to the `PngData` passed in
fn perform_reductions(png: &mut PngData, opts: &Options, deadline: &Deadline) -> bool {
let mut reduction_occurred = false;
fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadline, eval: &Evaluator) {
if opts.palette_reduction {
if let Some(reduced) = reduced_palette(png) {
png.apply_reduction(reduced);
reduction_occurred = true;
if opts.verbosity == Some(1) {
report_reduction(png);
}
// must be done first to evaluate rest with the correct interlacing
if let Some(interlacing) = opts.interlace {
if let Some(reduced) = png.change_interlacing(interlacing) {
png = Arc::new(reduced);
eval.try_image(png.clone(), 0.);
}
if deadline.passed() {
return;
}
}
if deadline.passed() {
return reduction_occurred;
if opts.palette_reduction {
if let Some(reduced) = reduced_palette(&png) {
png = Arc::new(reduced);
eval.try_image(png.clone(), 0.95);
if opts.verbosity == Some(1) {
report_reduction(&png);
}
}
if deadline.passed() {
return;
}
}
if opts.bit_depth_reduction {
if let Some(reduced) = png.reduce_bit_depth() {
png.apply_reduction(reduced);
reduction_occurred = true;
if let Some(reduced) = reduce_bit_depth(&png, 1) {
let previous = png.clone();
let bits = reduced.ihdr.bit_depth;
png = Arc::new(reduced);
eval.try_image(png.clone(), 1.0);
if (bits == BitDepth::One || bits == BitDepth::Two) && previous.ihdr.bit_depth != BitDepth::Four {
// Also try 16-color mode for all lower bits images, since that may compress better
if let Some(reduced) = reduce_bit_depth(&previous, 4) {
eval.try_image(Arc::new(reduced), 0.98);
}
}
if opts.verbosity == Some(1) {
report_reduction(png);
report_reduction(&png);
}
}
}
if deadline.passed() {
return reduction_occurred;
}
if opts.color_type_reduction && png.reduce_color_type() {
reduction_occurred = true;
if opts.verbosity == Some(1) {
report_reduction(png);
if deadline.passed() {
return;
}
}
if reduction_occurred && opts.verbosity.is_some() {
report_reduction(png);
}
if let Some(interlacing) = opts.interlace {
if let Some(reduced) = png.change_interlacing(interlacing) {
png.apply_reduction(reduced);
reduction_occurred = true;
if opts.color_type_reduction {
if let Some(reduced) = reduce_color_type(&png) {
png = Arc::new(reduced);
eval.try_image(png.clone(), 0.96);
if opts.verbosity == Some(1) {
report_reduction(&png);
}
}
if deadline.passed() {
return;
}
}
if deadline.passed() {
return reduction_occurred;
}
if png.try_alpha_reduction(&opts.alphas) {
reduction_occurred = true;
}
reduction_occurred
try_alpha_reductions(png, &opts.alphas, eval);
}
/// Keep track of processing timeout
@ -817,48 +838,49 @@ impl Deadline {
}
/// Display the status of the image data after a reduction has taken place
fn report_reduction(png: &PngData) {
fn report_reduction(png: &PngImage) {
if let Some(ref palette) = png.palette {
eprintln!(
"Reducing image to {} bits/pixel, {} colors in palette",
png.ihdr_data.bit_depth,
png.ihdr.bit_depth,
palette.len() / 3
);
} else {
eprintln!(
"Reducing image to {}x{} bits/pixel, {}",
png.channels_per_pixel(),
png.ihdr_data.bit_depth,
png.ihdr_data.color_type
png.ihdr.bit_depth,
png.ihdr.color_type
);
}
}
/// 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) => {
png.aux_headers.retain(|chunk, _| {
raw.aux_headers.retain(|chunk, _| {
std::str::from_utf8(chunk)
.ok()
.map_or(false, |name| hdrs.contains(name))
});
}
Headers::Strip(ref hdrs) => for hdr in hdrs {
png.aux_headers.remove(hdr.as_bytes());
raw.aux_headers.remove(hdr.as_bytes());
},
Headers::Safe => {
const PRESERVED_HEADERS: [[u8; 4]; 9] = [
*b"cHRM", *b"gAMA", *b"iCCP", *b"sBIT", *b"sRGB", *b"bKGD", *b"hIST", *b"pHYs",
*b"sPLT",
];
png.aux_headers
raw.aux_headers
.retain(|hdr, _| PRESERVED_HEADERS.contains(hdr));
}
Headers::All => {
png.aux_headers = HashMap::new();
raw.aux_headers = HashMap::new();
}
}
@ -871,18 +893,18 @@ fn perform_strip(png: &mut PngData, opts: &Options) {
};
if may_replace_iccp {
if png.aux_headers.get(b"sRGB").is_some() {
if raw.aux_headers.get(b"sRGB").is_some() {
// Files aren't supposed to have both chunks, so we chose to honor sRGB
png.aux_headers.remove(b"iCCP");
} else if let Some(intent) = png
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
png.aux_headers.remove(b"iCCP");
png.aux_headers.insert(*b"sRGB", vec![intent]);
raw.aux_headers.remove(b"iCCP");
raw.aux_headers.insert(*b"sRGB", vec![intent]);
}
}
}

View file

@ -1,46 +1,37 @@
use atomicmin::AtomicMin;
use byteorder::{BigEndian, WriteBytesExt};
use colors::{AlphaOptim, BitDepth, ColorType};
use colors::ColorType;
use crc::crc32;
use deflate;
use error::PngError;
use reduction::*;
use filters::*;
use headers::*;
use interlace::{deinterlace_image, interlace_image};
use itertools::flatten;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use reduction::bit_depth::*;
use reduction::color::*;
use reduction::alpha::*;
use rgb::ComponentSlice;
use rgb::RGBA8;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::iter::Iterator;
use std::path::Path;
use std::sync::Arc;
const STD_COMPRESSION: u8 = 8;
pub(crate) const STD_COMPRESSION: u8 = 6;
/// Must use normal compression, as faster ones (Huffman/RLE-only) are not representative
const STD_STRATEGY: u8 = 0;
const STD_WINDOW: u8 = 15;
const STD_FILTERS: [u8; 2] = [0, 5];
pub(crate) const STD_STRATEGY: u8 = 0;
/// OK to use a bit smalller window for evaluation
pub(crate) const STD_WINDOW: u8 = 13;
pub(crate) const STD_FILTERS: [u8; 2] = [0, 5];
mod scan_lines;
pub(crate) mod scan_lines;
use self::scan_lines::{ScanLine, ScanLines, ScanLinesMut};
use self::scan_lines::{ScanLines, ScanLinesMut};
#[derive(Debug, Clone)]
/// Contains all data relevant to a PNG image
pub struct PngData {
/// The filtered and compressed data of the IDAT chunk
pub idat_data: Vec<u8>,
pub struct PngImage {
/// The headers stored in the IHDR chunk
pub ihdr_data: IhdrData,
pub ihdr: IhdrData,
/// The uncompressed, optionally filtered data from the IDAT chunk
pub raw_data: Vec<u8>,
pub data: Vec<u8>,
/// The palette containing colors used in an Indexed image
/// Contains 3 bytes per color (R+G+B), up to 768
pub palette: Option<Vec<RGBA8>>,
@ -50,6 +41,15 @@ pub struct PngData {
pub aux_headers: HashMap<[u8; 4], Vec<u8>>,
}
/// Contains all data relevant to a PNG image
#[derive(Debug, Clone)]
pub struct PngData {
/// Uncompressed image data
pub raw: Arc<PngImage>,
/// The filtered and compressed data of the IDAT chunk
pub idat_data: Vec<u8>,
}
type PaletteWithTrns = (Option<Vec<RGBA8>>, Option<Vec<u8>>);
impl PngData {
@ -125,17 +125,19 @@ impl PngData {
aux_headers.remove(b"tRNS"),
)?;
let mut png_data = Self {
idat_data: idat_headers,
ihdr_data: ihdr_header,
raw_data,
let mut raw = PngImage {
ihdr: ihdr_header,
data: raw_data,
palette,
transparency_pixel,
aux_headers,
};
png_data.raw_data = png_data.unfilter_image();
raw.data = raw.unfilter_image();
// Return the PngData
Ok(png_data)
Ok(Self {
idat_data: idat_headers,
raw: Arc::new(raw),
})
}
/// Handle transparency header
@ -163,37 +165,22 @@ impl PngData {
}
}
pub(crate) fn reset_from_original(&mut self, original: &Self) {
self.idat_data = original.idat_data.clone();
self.ihdr_data = original.ihdr_data;
self.raw_data = original.raw_data.clone();
self.palette = original.palette.clone();
self.transparency_pixel = original.transparency_pixel.clone();
self.aux_headers = original.aux_headers.clone();
}
/// Return the number of channels in the image, based on color type
#[inline]
pub fn channels_per_pixel(&self) -> u8 {
self.ihdr_data.color_type.channels_per_pixel()
}
/// Format the `PngData` struct into a valid PNG bytestream
pub fn output(&self) -> Vec<u8> {
// PNG header
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
// IHDR
let mut ihdr_data = Vec::with_capacity(13);
let _ = ihdr_data.write_u32::<BigEndian>(self.ihdr_data.width);
let _ = ihdr_data.write_u32::<BigEndian>(self.ihdr_data.height);
let _ = ihdr_data.write_u8(self.ihdr_data.bit_depth.as_u8());
let _ = ihdr_data.write_u8(self.ihdr_data.color_type.png_header_code());
let _ = ihdr_data.write_u32::<BigEndian>(self.raw.ihdr.width);
let _ = ihdr_data.write_u32::<BigEndian>(self.raw.ihdr.height);
let _ = ihdr_data.write_u8(self.raw.ihdr.bit_depth.as_u8());
let _ = ihdr_data.write_u8(self.raw.ihdr.color_type.png_header_code());
let _ = ihdr_data.write_u8(0); // Compression -- deflate
let _ = ihdr_data.write_u8(0); // Filter method -- 5-way adaptive filtering
let _ = ihdr_data.write_u8(self.ihdr_data.interlaced);
let _ = ihdr_data.write_u8(self.raw.ihdr.interlaced);
write_png_block(b"IHDR", &ihdr_data, &mut output);
// Ancillary headers
for (key, header) in self
for (key, header) in self.raw
.aux_headers
.iter()
.filter(|&(key, _)| !(key == b"bKGD" || key == b"hIST" || key == b"tRNS"))
@ -201,14 +188,15 @@ impl PngData {
write_png_block(key, header, &mut output);
}
// Palette
if let Some(ref palette) = self.palette {
if let Some(ref palette) = self.raw.palette {
let mut palette_data = Vec::with_capacity(palette.len() * 3);
for px in palette {
let max_palette_size = 1 << (self.raw.ihdr.bit_depth.as_u8() as usize);
for px in palette.iter().take(max_palette_size) {
palette_data.extend_from_slice(px.rgb().as_slice());
}
write_png_block(b"PLTE", &palette_data, &mut output);
let num_transparent =
palette.iter().enumerate().fold(
palette.iter().take(max_palette_size).enumerate().fold(
0,
|prev, (index, px)| {
if px.a != 255 {
@ -222,12 +210,12 @@ impl PngData {
let trns_data: Vec<_> = palette[0..num_transparent].iter().map(|px| px.a).collect();
write_png_block(b"tRNS", &trns_data, &mut output);
}
} else if let Some(ref transparency_pixel) = self.transparency_pixel {
} else if let Some(ref transparency_pixel) = self.raw.transparency_pixel {
// Transparency pixel
write_png_block(b"tRNS", transparency_pixel, &mut output);
}
// Special ancillary headers that need to come after PLTE but before IDAT
for (key, header) in self
for (key, header) in self.raw
.aux_headers
.iter()
.filter(|&(key, _)| key == b"bKGD" || key == b"hIST" || key == b"tRNS")
@ -241,6 +229,35 @@ impl PngData {
output
}
}
impl PngImage {
/// Convert the image to the specified interlacing type
/// Returns true if the interlacing was changed, false otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
/// Assumes that the data has already been de-filtered
#[inline]
#[must_use]
pub fn change_interlacing(&self, interlace: u8) -> Option<PngImage> {
if interlace == self.ihdr.interlaced {
return None;
}
Some(if interlace == 1 {
// Convert progressive to interlaced data
interlace_image(self)
} else {
// Convert interlaced to progressive data
deinterlace_image(self)
})
}
/// Return the number of channels in the image, based on color type
#[inline]
pub fn channels_per_pixel(&self) -> u8 {
self.ihdr.color_type.channels_per_pixel()
}
/// Return an iterator over the scanlines of the image
#[inline]
@ -255,9 +272,9 @@ impl PngData {
}
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
pub fn unfilter_image(&self) -> Vec<u8> {
let mut unfiltered = Vec::with_capacity(self.raw_data.len());
let bpp = ((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
fn unfilter_image(&self) -> Vec<u8> {
let mut unfiltered = Vec::with_capacity(self.data.len());
let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
let mut last_line: Vec<u8> = Vec::new();
let mut last_pass = 1;
for line in self.scan_lines() {
@ -283,8 +300,8 @@ impl PngData {
/// 4: Paeth
/// 5: All (heuristically pick the best filter for each line)
pub fn filter_image(&self, filter: u8) -> Vec<u8> {
let mut filtered = Vec::with_capacity(self.raw_data.len());
let bpp = ((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
let mut filtered = Vec::with_capacity(self.data.len());
let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
let mut last_line: &[u8] = &[];
let mut last_pass: Option<u8> = None;
for line in self.scan_lines() {
@ -325,351 +342,7 @@ impl PngData {
}
filtered
}
/// Attempt to reduce the bit depth of the image
/// Returns true if the bit depth was reduced, false otherwise
#[must_use]
pub fn reduce_bit_depth(&self) -> Option<ReducedPng> {
if self.ihdr_data.bit_depth != BitDepth::Sixteen {
if self.ihdr_data.color_type == ColorType::Indexed
|| self.ihdr_data.color_type == ColorType::Grayscale
{
return reduce_bit_depth_8_or_less(self);
}
return None;
}
// Reduce from 16 to 8 bits per channel per pixel
let mut reduced = Vec::with_capacity(
(self.ihdr_data.width * self.ihdr_data.height * u32::from(self.channels_per_pixel())
+ self.ihdr_data.height) as usize,
);
let mut high_byte = 0;
for line in self.scan_lines() {
reduced.push(line.filter);
for (i, &byte) in line.data.iter().enumerate() {
if i % 2 == 0 {
// High byte
high_byte = byte;
} else {
// Low byte
if high_byte != byte {
// Can't reduce, exit early
return None;
}
reduced.push(byte);
}
}
}
Some(ReducedPng {
color_type: self.ihdr_data.color_type,
interlaced: self.ihdr_data.interlaced,
bit_depth: BitDepth::Eight,
raw_data: reduced,
palette: self.palette.clone(),
transparency_pixel: self.transparency_pixel.clone(),
aux_headers: Default::default(),
})
}
/// Attempt to reduce the color type of the image
/// Returns true if the color type was reduced, false otherwise
pub fn reduce_color_type(&mut self) -> bool {
let mut changed = false;
let mut should_reduce_bit_depth = false;
// Go down one step at a time
// Maybe not the most efficient, but it's safe
if self.ihdr_data.color_type == ColorType::RGBA {
if let Some(reduced) = reduce_rgba_to_grayscale_alpha(self).or_else(|| reduced_alpha_channel(self)) {
self.apply_reduction(reduced);
changed = true;
} else if let Some(reduced) = reduced_color_to_palette(self) {
self.apply_reduction(reduced);
changed = true;
should_reduce_bit_depth = true;
}
}
if self.ihdr_data.color_type == ColorType::GrayscaleAlpha {
if let Some(reduced) = reduced_alpha_channel(self) {
self.apply_reduction(reduced);
changed = true;
should_reduce_bit_depth = true;
}
}
if self.ihdr_data.color_type == ColorType::RGB {
if let Some(reduced) = reduce_rgb_to_grayscale(self).or_else(|| reduced_color_to_palette(self)) {
self.apply_reduction(reduced);
changed = true;
should_reduce_bit_depth = true;
}
}
if should_reduce_bit_depth {
// Some conversions will allow us to perform bit depth reduction that
// wasn't possible before
if let Some(reduced) = reduce_bit_depth_8_or_less(self) {
self.apply_reduction(reduced);
}
}
changed
}
pub(crate) fn apply_reduction(&mut self, ReducedPng {color_type, bit_depth, raw_data, interlaced, palette, transparency_pixel, aux_headers}: ReducedPng) {
self.ihdr_data.color_type = color_type;
self.ihdr_data.bit_depth = bit_depth;
self.ihdr_data.interlaced = interlaced;
self.raw_data = raw_data;
if palette.is_some() {
self.transparency_pixel = None;
self.palette = palette;
}
if transparency_pixel.is_some() {
self.transparency_pixel = transparency_pixel;
}
self.idat_data.clear(); // this field is out of date and needs to be replaced
for (header, val) in aux_headers {
match val {
Some(val) => self.aux_headers.insert(header, val),
None => self.aux_headers.remove(&header),
};
}
}
pub fn try_alpha_reduction(&mut self, alphas: &HashSet<AlphaOptim>) -> bool {
assert!(!alphas.is_empty());
let alphas = alphas.iter().collect::<Vec<_>>();
let best_size = AtomicMin::new(None);
#[cfg(feature = "parallel")]
let alphas_iter = alphas.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))]
let alphas_iter = alphas.iter();
let best = alphas_iter
.filter_map(|&alpha| {
let image = match self.reduced_alpha_channel(*alpha) {
Some(image) => image,
None => return None,
};
#[cfg(feature = "parallel")]
let filters_iter = STD_FILTERS.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))]
let filters_iter = STD_FILTERS.iter();
filters_iter
.filter_map(|f| {
deflate::deflate(
&image.filter_image(*f),
STD_COMPRESSION,
STD_STRATEGY,
STD_WINDOW,
&best_size,
).ok()
.as_ref()
.map(|l| {
best_size.set_min(l.len());
l.len()
})
}).min()
.map(|size| (size, image))
}).min_by_key(|&(size, _)| size);
if let Some(best) = best {
self.raw_data = best.1.raw_data;
return true;
}
false
}
/// It doesn't recompress `idat_data`, so this field is out of date
/// after the reduction.
pub fn reduced_alpha_channel(&self, optim: AlphaOptim) -> Option<Self> {
let (bpc, bpp) = match self.ihdr_data.color_type {
ColorType::RGBA | ColorType::GrayscaleAlpha => {
let cpp = self.channels_per_pixel();
let bpc = self.ihdr_data.bit_depth.as_u8() / 8;
(bpc as usize, (bpc * cpp) as usize)
}
_ => {
return None;
}
};
let raw_data = match optim {
AlphaOptim::NoOp => return None,
AlphaOptim::Black => self.reduced_alpha_to_black(bpc, bpp),
AlphaOptim::White => self.reduced_alpha_to_white(bpc, bpp),
AlphaOptim::Up => self.reduced_alpha_to_up(bpc, bpp),
AlphaOptim::Down => self.reduced_alpha_to_down(bpc, bpp),
AlphaOptim::Left => self.reduced_alpha_to_left(bpc, bpp),
AlphaOptim::Right => self.reduced_alpha_to_right(bpc, bpp),
};
Some(Self {
raw_data,
idat_data: vec![],
ihdr_data: self.ihdr_data,
palette: self.palette.clone(),
transparency_pixel: self.transparency_pixel.clone(),
aux_headers: self.aux_headers.clone(),
})
}
fn reduced_alpha_to_black(&self, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(self.raw_data.len());
for line in self.scan_lines() {
reduced.push(line.filter);
for pixel in line.data.chunks(bpp) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
for _ in 0..bpp {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
}
}
reduced
}
fn reduced_alpha_to_white(&self, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(self.raw_data.len());
for line in self.scan_lines() {
reduced.push(line.filter);
for pixel in line.data.chunks(bpp) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
for _ in 0..(bpp - bpc) {
reduced.push(255);
}
for _ in 0..bpc {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
}
}
reduced
}
fn reduced_alpha_to_up(&self, bpc: usize, bpp: usize) -> Vec<u8> {
let mut lines = Vec::new();
let mut scan_lines = self.scan_lines().collect::<Vec<ScanLine>>();
scan_lines.reverse();
let mut last_line = Vec::new();
let mut current_line = Vec::with_capacity(last_line.len());
for line in scan_lines {
if line.data.len() != last_line.len() {
last_line = vec![0; line.data.len()];
}
current_line.push(line.filter);
for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
current_line.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
current_line.push(0);
}
} else {
current_line.extend_from_slice(pixel);
}
}
last_line = current_line.clone();
lines.push(current_line.clone());
current_line.clear();
}
flatten(lines.into_iter().rev()).collect()
}
fn reduced_alpha_to_down(&self, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(self.raw_data.len());
let mut last_line = Vec::new();
for line in self.scan_lines() {
if line.data.len() != last_line.len() {
last_line = vec![0; line.data.len()];
}
reduced.push(line.filter);
for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
}
last_line = reduced.clone();
}
reduced
}
fn reduced_alpha_to_left(&self, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(self.raw_data.len());
for line in self.scan_lines() {
let mut line_bytes = Vec::with_capacity(line.data.len());
let mut last_pixel = vec![0; bpp];
for pixel in line.data.chunks(bpp).rev() {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
line_bytes.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
line_bytes.push(0);
}
} else {
line_bytes.extend_from_slice(pixel);
}
last_pixel = pixel.to_owned();
}
reduced.push(line.filter);
reduced.extend(flatten(line_bytes.chunks(bpp).rev()));
}
reduced
}
fn reduced_alpha_to_right(&self, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(self.raw_data.len());
for line in self.scan_lines() {
reduced.push(line.filter);
let mut last_pixel = vec![0; bpp];
for pixel in line.data.chunks(bpp) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
last_pixel = pixel.to_owned();
}
}
reduced
}
/// Convert the image to the specified interlacing type
/// Returns true if the interlacing was changed, false otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
/// Assumes that the data has already been de-filtered
#[inline]
#[must_use]
pub fn change_interlacing(&mut self, interlace: u8) -> Option<ReducedPng> {
if interlace == self.ihdr_data.interlaced {
return None;
}
Some(if interlace == 1 {
// Convert progressive to interlaced data
interlace_image(self)
} else {
// Convert interlaced to progressive data
deinterlace_image(self)
})
}
}
fn write_png_block(key: &[u8], header: &[u8], output: &mut Vec<u8>) {
let mut header_data = Vec::with_capacity(header.len() + 4);
header_data.extend_from_slice(key);

View file

@ -1,4 +1,4 @@
use super::PngData;
use png::PngImage;
#[derive(Debug, Clone)]
/// An iterator over the scan lines of a PNG image
@ -9,10 +9,10 @@ pub struct ScanLines<'a> {
}
impl<'a> ScanLines<'a> {
pub fn new(png: &'a PngData) -> Self {
pub fn new(png: &'a PngImage) -> Self {
Self {
iter: ScanLineRanges::new(png),
raw_data: &png.raw_data,
raw_data: &png.data,
}
}
}
@ -39,10 +39,10 @@ pub struct ScanLinesMut<'a> {
}
impl<'a> ScanLinesMut<'a> {
pub fn new(png: &'a mut PngData) -> Self {
pub fn new(png: &'a mut PngImage) -> Self {
Self {
iter: ScanLineRanges::new(png),
raw_data: Some(&mut png.raw_data),
raw_data: Some(&mut png.data),
}
}
}
@ -73,13 +73,13 @@ struct ScanLineRanges {
}
impl ScanLineRanges {
pub fn new(png: &PngData) -> Self {
pub fn new(png: &PngImage) -> Self {
Self {
bits_per_pixel: png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel(),
width: png.ihdr_data.width,
height: png.ihdr_data.height,
left: png.raw_data.len(),
pass: if png.ihdr_data.interlaced == 1 {
bits_per_pixel: png.ihdr.bit_depth.as_u8() * png.channels_per_pixel(),
width: png.ihdr.width,
height: png.ihdr.height,
left: png.data.len(),
pass: if png.ihdr.interlaced == 1 {
Some((1, 0))
} else {
None
@ -153,6 +153,7 @@ impl Iterator for ScanLineRanges {
let bits_per_line = pixels_per_line * u32::from(self.bits_per_pixel);
let bytes_per_line = ((bits_per_line + 7) / 8) as usize;
let len = bytes_per_line + 1;
assert!(self.left >= len);
self.left -= len;
Some((len, current_pass))
}

View file

@ -1,16 +1,196 @@
use reduction::ReducedPng;
use png::PngData;
use evaluate::Evaluator;
use itertools::flatten;
use png::scan_lines::ScanLine;
use std::collections::HashSet;
use std::sync::Arc;
use colors::AlphaOptim;
use headers::IhdrData;
use png::PngImage;
use colors::ColorType;
use std::collections::HashMap;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
pub fn try_alpha_reductions(png: Arc<PngImage>, alphas: &HashSet<AlphaOptim>, eval: &Evaluator) {
assert!(!alphas.is_empty());
let alphas = alphas.iter().collect::<Vec<_>>();
#[cfg(feature = "parallel")]
let alphas_iter = alphas.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))]
let alphas_iter = alphas.iter();
alphas_iter
.filter_map(|&alpha| filtered_alpha_channel(&png, *alpha))
.for_each(|image| eval.try_image(Arc::new(image), 0.99));
}
pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option<PngImage> {
let (bpc, bpp) = match png.ihdr.color_type {
ColorType::RGBA | ColorType::GrayscaleAlpha => {
let cpp = png.channels_per_pixel();
let bpc = png.ihdr.bit_depth.as_u8() / 8;
(bpc as usize, (bpc * cpp) as usize)
}
_ => {
return None;
}
};
let raw_data = match optim {
AlphaOptim::NoOp => return None,
AlphaOptim::Black => reduced_alpha_to_black(png, bpc, bpp),
AlphaOptim::White => reduced_alpha_to_white(png, bpc, bpp),
AlphaOptim::Up => reduced_alpha_to_up(png, bpc, bpp),
AlphaOptim::Down => reduced_alpha_to_down(png, bpc, bpp),
AlphaOptim::Left => reduced_alpha_to_left(png, bpc, bpp),
AlphaOptim::Right => reduced_alpha_to_right(png, bpc, bpp),
};
Some(PngImage {
data: raw_data,
ihdr: png.ihdr,
palette: png.palette.clone(),
transparency_pixel: png.transparency_pixel.clone(),
aux_headers: png.aux_headers.clone(),
})
}
fn reduced_alpha_to_black(png: &PngImage, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(png.data.len());
for line in png.scan_lines() {
reduced.push(line.filter);
for pixel in line.data.chunks(bpp) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
for _ in 0..bpp {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
}
}
reduced
}
fn reduced_alpha_to_white(png: &PngImage, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(png.data.len());
for line in png.scan_lines() {
reduced.push(line.filter);
for pixel in line.data.chunks(bpp) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
for _ in 0..(bpp - bpc) {
reduced.push(255);
}
for _ in 0..bpc {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
}
}
reduced
}
fn reduced_alpha_to_up(png: &PngImage, bpc: usize, bpp: usize) -> Vec<u8> {
let mut lines = Vec::new();
let mut scan_lines = png.scan_lines().collect::<Vec<ScanLine>>();
scan_lines.reverse();
let mut last_line = Vec::new();
let mut current_line = Vec::with_capacity(last_line.len());
for line in scan_lines {
if line.data.len() != last_line.len() {
last_line = vec![0; line.data.len()];
}
current_line.push(line.filter);
for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
current_line.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
current_line.push(0);
}
} else {
current_line.extend_from_slice(pixel);
}
}
last_line = current_line.clone();
lines.push(current_line.clone());
current_line.clear();
}
flatten(lines.into_iter().rev()).collect()
}
fn reduced_alpha_to_down(png: &PngImage, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(png.data.len());
let mut last_line = Vec::new();
for line in png.scan_lines() {
if line.data.len() != last_line.len() {
last_line = vec![0; line.data.len()];
}
reduced.push(line.filter);
for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
}
last_line = reduced.clone();
}
reduced
}
fn reduced_alpha_to_left(png: &PngImage, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(png.data.len());
for line in png.scan_lines() {
let mut line_bytes = Vec::with_capacity(line.data.len());
let mut last_pixel = vec![0; bpp];
for pixel in line.data.chunks(bpp).rev() {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
line_bytes.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
line_bytes.push(0);
}
} else {
line_bytes.extend_from_slice(pixel);
}
last_pixel = pixel.to_owned();
}
reduced.push(line.filter);
reduced.extend(flatten(line_bytes.chunks(bpp).rev()));
}
reduced
}
fn reduced_alpha_to_right(png: &PngImage, bpc: usize, bpp: usize) -> Vec<u8> {
let mut reduced = Vec::with_capacity(png.data.len());
for line in png.scan_lines() {
reduced.push(line.filter);
let mut last_pixel = vec![0; bpp];
for pixel in line.data.chunks(bpp) {
if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 {
reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]);
for _ in 0..bpc {
reduced.push(0);
}
} else {
reduced.extend_from_slice(pixel);
}
last_pixel = pixel.to_owned();
}
}
reduced
}
#[must_use]
pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> {
let target_color_type = match png.ihdr_data.color_type {
pub fn reduced_alpha_channel(png: &PngImage) -> Option<PngImage> {
let target_color_type = match png.ihdr.color_type {
ColorType::GrayscaleAlpha => ColorType::Grayscale,
ColorType::RGBA => ColorType::RGB,
_ => return None,
};
let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3;
let byte_depth = png.ihdr.bit_depth.as_u8() >> 3;
let channels = png.channels_per_pixel();
let bpp = channels * byte_depth;
let bpp_mask = bpp - 1;
@ -24,7 +204,7 @@ pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> {
}
}
let mut raw_data = Vec::with_capacity(png.raw_data.len());
let mut raw_data = Vec::with_capacity(png.data.len());
for line in png.scan_lines() {
raw_data.push(line.filter);
for (i, &byte) in line.data.iter().enumerate() {
@ -36,19 +216,20 @@ pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> {
}
}
let mut aux_headers = HashMap::new();
let mut aux_headers = png.aux_headers.clone();
// sBIT contains information about alpha channel's original depth,
// and alpha has just been removed
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", Some(sbit_header.iter().cloned().take(3).collect()));
aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect());
}
Some(ReducedPng {
raw_data,
bit_depth: png.ihdr_data.bit_depth,
interlaced: png.ihdr_data.interlaced,
color_type: target_color_type,
Some(PngImage {
data: raw_data,
ihdr: IhdrData {
color_type: target_color_type,
..png.ihdr
},
aux_headers,
transparency_pixel: None,
palette: None,

View file

@ -1,7 +1,7 @@
use reduction::ReducedPng;
use headers::IhdrData;
use bit_vec::BitVec;
use colors::{BitDepth, ColorType};
use png::PngData;
use png::PngImage;
const ONE_BIT_PERMUTATIONS: [u8; 2] = [0b0000_0000, 0b1111_1111];
const TWO_BIT_PERMUTATIONS: [u8; 5] = [
@ -25,32 +25,92 @@ const FOUR_BIT_PERMUTATIONS: [u8; 11] = [
0b1111_1111,
];
/// Attempt to reduce the bit depth of the image
/// Returns true if the bit depth was reduced, false otherwise
#[must_use]
pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option<ReducedPng> {
let mut reduced = BitVec::with_capacity(png.raw_data.len() * 8);
let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize;
let mut allowed_bits = 1;
pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Sixteen {
if png.ihdr.color_type == ColorType::Indexed
|| png.ihdr.color_type == ColorType::Grayscale
{
return reduce_bit_depth_8_or_less(png, minimum_bits);
}
return None;
}
// Reduce from 16 to 8 bits per channel per pixel
let mut reduced = Vec::with_capacity(
(png.ihdr.width * png.ihdr.height * u32::from(png.channels_per_pixel())
+ png.ihdr.height) as usize,
);
let mut high_byte = 0;
for line in png.scan_lines() {
let bit_vec = BitVec::from_bytes(&line.data);
if png.ihdr_data.color_type == ColorType::Indexed {
for (i, bit) in bit_vec.iter().enumerate() {
let bit_index = bit_depth - (i % bit_depth);
if bit && bit_index > allowed_bits {
allowed_bits = bit_index.next_power_of_two();
if allowed_bits == bit_depth {
// Not reducable
return None;
}
reduced.push(line.filter);
for (i, &byte) in line.data.iter().enumerate() {
if i % 2 == 0 {
// High byte
high_byte = byte;
} else {
// Low byte
if high_byte != byte {
// Can't reduce, exit early
return None;
}
reduced.push(byte);
}
}
}
Some(PngImage {
data: reduced,
ihdr: IhdrData {
bit_depth: BitDepth::Eight,
..png.ihdr
},
palette: None,
transparency_pixel: png.transparency_pixel.clone(),
aux_headers: png.aux_headers.clone(),
})
}
#[must_use]
pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> {
assert!(minimum_bits >= 1 && minimum_bits < 8);
let mut reduced = BitVec::with_capacity(png.data.len() * 8);
let bit_depth: usize = png.ihdr.bit_depth.as_u8() as usize;
if minimum_bits >= bit_depth {
return None;
}
for line in png.scan_lines() {
if png.ihdr.color_type == ColorType::Indexed {
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;
}
}
} else {
let bit_vec = BitVec::from_bytes(&line.data);
for byte in bit_vec.to_bytes() {
while allowed_bits < bit_depth {
let permutations: &[u8] = if allowed_bits == 1 {
while minimum_bits < bit_depth {
let permutations: &[u8] = if minimum_bits == 1 {
&ONE_BIT_PERMUTATIONS
} else if allowed_bits == 2 {
} else if minimum_bits == 2 {
&TWO_BIT_PERMUTATIONS
} else if allowed_bits == 4 {
} else if minimum_bits == 4 {
&FOUR_BIT_PERMUTATIONS
} else {
return None;
@ -58,7 +118,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option<ReducedPng> {
if permutations.iter().any(|perm| *perm == byte) {
break;
} else {
allowed_bits <<= 1;
minimum_bits <<= 1;
}
}
}
@ -70,7 +130,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option<ReducedPng> {
let bit_vec = BitVec::from_bytes(&line.data);
for (i, bit) in bit_vec.iter().enumerate() {
let bit_index = bit_depth - (i % bit_depth);
if bit_index <= allowed_bits {
if bit_index <= minimum_bits {
reduced.push(bit);
}
}
@ -80,12 +140,13 @@ pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option<ReducedPng> {
}
}
Some(ReducedPng {
color_type: png.ihdr_data.color_type,
interlaced: png.ihdr_data.interlaced,
raw_data: reduced.to_bytes(),
bit_depth: BitDepth::from_u8(allowed_bits as u8),
aux_headers: Default::default(),
Some(PngImage {
data: reduced.to_bytes(),
ihdr: IhdrData {
bit_depth: BitDepth::from_u8(minimum_bits as u8),
..png.ihdr
},
aux_headers: png.aux_headers.clone(),
palette: png.palette.clone(),
transparency_pixel: png.transparency_pixel.clone(),
})

View file

@ -1,15 +1,15 @@
use reduction::ReducedPng;
use headers::IhdrData;
use colors::{BitDepth, ColorType};
use itertools::Itertools;
use png::PngData;
use png::PngImage;
use rgb::{FromSlice, RGB8, RGBA8};
use std::collections::HashMap;
use std::hash::Hash;
#[must_use]
pub fn reduce_rgba_to_grayscale_alpha(png: &PngData) -> Option<ReducedPng> {
let mut reduced = Vec::with_capacity(png.raw_data.len());
let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3;
pub fn reduce_rgba_to_grayscale_alpha(png: &PngImage) -> Option<PngImage> {
let mut reduced = Vec::with_capacity(png.data.len());
let byte_depth = png.ihdr.bit_depth.as_u8() >> 3;
let bpp = 4 * byte_depth;
let bpp_mask = bpp - 1;
assert_eq!(0, bpp & bpp_mask);
@ -49,20 +49,25 @@ pub fn reduce_rgba_to_grayscale_alpha(png: &PngData) -> Option<ReducedPng> {
}
}
let mut aux_headers = HashMap::new();
let mut aux_headers = png.aux_headers.clone();
if let Some(sbit_header) = png.aux_headers.get(b"sBIT") {
aux_headers.insert(*b"sBIT", sbit_header.get(0).map(|&s| vec![s]));
if let Some(&s) = sbit_header.get(0) {
aux_headers.insert(*b"sBIT", vec![s]);
}
}
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned()));
if let Some(b) = bkgd_header.get(0..2) {
aux_headers.insert(*b"bKGD", b.to_owned());
}
}
Some(ReducedPng {
raw_data: reduced,
bit_depth: png.ihdr_data.bit_depth,
interlaced: png.ihdr_data.interlaced,
color_type: ColorType::GrayscaleAlpha,
Some(PngImage {
data: reduced,
ihdr: IhdrData {
color_type: ColorType::GrayscaleAlpha,
..png.ihdr
},
palette: None,
transparency_pixel: None,
aux_headers,
@ -95,11 +100,11 @@ where
}
#[must_use]
pub fn reduced_color_to_palette(png: &PngData) -> Option<ReducedPng> {
if png.ihdr_data.bit_depth != BitDepth::Eight {
pub fn reduced_color_to_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let mut raw_data = Vec::with_capacity(png.raw_data.len());
let mut raw_data = Vec::with_capacity(png.data.len());
let mut palette = HashMap::with_capacity(257);
let transparency_pixel = png
.transparency_pixel
@ -107,7 +112,7 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option<ReducedPng> {
.map(|t| RGB8::new(t[1], t[3], t[5]));
for line in png.scan_lines() {
raw_data.push(line.filter);
let ok = if png.ihdr_data.color_type == ColorType::RGB {
let ok = if png.ihdr.color_type == ColorType::RGB {
reduce_scanline_to_palette(
line.data.as_rgb().iter().cloned().map(|px| {
px.alpha(if Some(px) != transparency_pixel {
@ -120,7 +125,7 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option<ReducedPng> {
&mut raw_data,
)
} else {
debug_assert_eq!(png.ihdr_data.color_type, ColorType::RGBA);
debug_assert_eq!(png.ihdr.color_type, ColorType::RGBA);
reduce_scanline_to_palette(
line.data.as_rgba().iter().cloned(),
&mut palette,
@ -144,12 +149,12 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option<ReducedPng> {
let trns_size = num_transparent.map(|n| n + 8).unwrap_or(0);
let headers_size = palette.len() * 3 + 8 + trns_size;
if raw_data.len() + headers_size > png.raw_data.len() {
if raw_data.len() + headers_size > png.data.len() {
// Reduction would result in a larger image
return None;
}
let mut aux_headers = HashMap::new();
let mut aux_headers = png.aux_headers.clone();
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
assert_eq!(bkgd_header.len(), 6);
// In bKGD 16-bit values are used even for 8-bit images
@ -163,12 +168,12 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option<ReducedPng> {
} else {
return None; // No space in palette to store the bg as an index
};
aux_headers.insert(*b"bKGD", Some(vec![entry]));
aux_headers.insert(*b"bKGD", vec![entry]);
}
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", Some(sbit_header.iter().cloned().take(3).collect()));
aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect());
}
let mut palette_vec = vec![RGBA8::new(0, 0, 0, 0); palette.len()];
@ -176,21 +181,22 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option<ReducedPng> {
palette_vec[idx as usize] = color;
}
Some(ReducedPng {
color_type: ColorType::Indexed,
bit_depth: png.ihdr_data.bit_depth,
interlaced: png.ihdr_data.interlaced,
Some(PngImage {
data: raw_data,
ihdr: IhdrData {
color_type: ColorType::Indexed,
..png.ihdr
},
aux_headers,
raw_data,
transparency_pixel: None,
palette: Some(palette_vec),
})
}
#[must_use]
pub fn reduce_rgb_to_grayscale(png: &PngData) -> Option<ReducedPng> {
let mut reduced = Vec::with_capacity(png.raw_data.len());
let byte_depth: u8 = png.ihdr_data.bit_depth.as_u8() >> 3;
pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
let mut reduced = Vec::with_capacity(png.data.len());
let byte_depth: u8 = png.ihdr.bit_depth.as_u8() >> 3;
let bpp: usize = 3 * byte_depth as usize;
let mut cur_pixel = Vec::with_capacity(bpp);
for line in png.scan_lines() {
@ -232,21 +238,26 @@ pub fn reduce_rgb_to_grayscale(png: &PngData) -> Option<ReducedPng> {
png.transparency_pixel.clone()
};
let mut aux_headers = HashMap::new();
let mut aux_headers = png.aux_headers.clone();
if let Some(sbit_header) = png.aux_headers.get(b"sBIT") {
aux_headers.insert(*b"sBIT", sbit_header.get(0).map(|&byte| vec![byte]));
if let Some(&byte) = sbit_header.get(0) {
aux_headers.insert(*b"sBIT", vec![byte]);
}
}
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned()));
if let Some(b) = bkgd_header.get(0..2) {
aux_headers.insert(*b"bKGD", b.to_owned());
}
}
Some(ReducedPng {
raw_data: reduced,
color_type: ColorType::Grayscale,
bit_depth: png.ihdr_data.bit_depth,
interlaced: png.ihdr_data.interlaced,
Some(PngImage {
data: reduced,
ihdr: IhdrData {
color_type: ColorType::Grayscale,
.. png.ihdr
},
aux_headers,
palette: None,
transparency_pixel,
aux_headers,
})
}

View file

@ -1,36 +1,30 @@
use headers::IhdrData;
use std::collections::HashMap;
use colors::{BitDepth, ColorType};
use std::collections::hash_map::Entry::*;
use png::PngData;
use png::PngImage;
use rgb::RGBA8;
use std::borrow::Cow;
pub mod alpha;
use alpha::*;
pub mod bit_depth;
use bit_depth::*;
pub mod color;
use color::*;
/// Fields to replace in PngData to apply the reduction
pub struct ReducedPng {
pub color_type: ColorType,
pub raw_data: Vec<u8>,
pub bit_depth: BitDepth,
/// replace if Some
pub palette: Option<Vec<RGBA8>>,
/// replace if Some
pub transparency_pixel: Option<Vec<u8>>,
/// replace if Some, delete if None
pub aux_headers: HashMap<[u8; 4], Option<Vec<u8>>>,
pub interlaced: u8,
}
pub use bit_depth::reduce_bit_depth;
pub use alpha::try_alpha_reductions;
/// Attempt to reduce the number of colors in the palette
/// Returns `None` if palette hasn't changed
#[must_use]
pub fn reduced_palette(png: &PngData) -> Option<ReducedPng> {
if png.ihdr_data.color_type != ColorType::Indexed {
pub fn reduced_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.color_type != ColorType::Indexed {
// Can't reduce if there is no palette
return None;
}
if png.ihdr_data.bit_depth == BitDepth::One {
if png.ihdr.bit_depth == BitDepth::One {
// Gains from 1-bit images will be at most 1 byte
// Not worth the CPU time
return None;
@ -43,7 +37,7 @@ pub fn reduced_palette(png: &PngData) -> Option<ReducedPng> {
// Find palette entries that are never used
for line in png.scan_lines() {
match png.ihdr_data.bit_depth {
match png.ihdr.bit_depth {
BitDepth::Eight => for &byte in line.data {
used[byte as usize] = true;
},
@ -88,9 +82,9 @@ pub fn reduced_palette(png: &PngData) -> Option<ReducedPng> {
}
#[must_use]
fn do_palette_reduction(png: &PngData, palette_map: &[Option<u8>; 256]) -> Option<ReducedPng> {
fn do_palette_reduction(png: &PngImage, palette_map: &[Option<u8>; 256]) -> Option<PngImage> {
let byte_map = palette_map_to_byte_map(png, palette_map)?;
let mut raw_data = Vec::with_capacity(png.raw_data.len());
let mut raw_data = Vec::with_capacity(png.data.len());
// Reassign data bytes to new indices
for line in png.scan_lines() {
@ -100,25 +94,26 @@ fn do_palette_reduction(png: &PngData, palette_map: &[Option<u8>; 256]) -> Optio
}
}
let mut aux_headers = HashMap::new();
let mut aux_headers = png.aux_headers.clone();
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
if let Some(Some(map_to)) = bkgd_header.get(0).and_then(|&idx| palette_map.get(idx as usize)) {
aux_headers.insert(*b"bKGD", Some(vec![*map_to]));
aux_headers.insert(*b"bKGD", vec![*map_to]);
}
}
Some(ReducedPng {
color_type: ColorType::Indexed,
bit_depth: png.ihdr_data.bit_depth,
interlaced: png.ihdr_data.interlaced,
raw_data,
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed,
..png.ihdr
},
data: raw_data,
transparency_pixel: None,
palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)),
aux_headers,
})
}
fn palette_map_to_byte_map(png: &PngData, palette_map: &[Option<u8>; 256]) -> Option<[u8; 256]> {
fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option<u8>; 256]) -> Option<[u8; 256]> {
let len = png.palette.as_ref().map(|p| p.len()).unwrap_or(0);
if (0..len).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) {
// No reduction necessary
@ -128,7 +123,7 @@ fn palette_map_to_byte_map(png: &PngData, palette_map: &[Option<u8>; 256]) -> Op
let mut byte_map = [0u8; 256];
// low bit-depths can be pre-computed for every byte value
match png.ihdr_data.bit_depth {
match png.ihdr.bit_depth {
BitDepth::Eight => {
for byte in 0..=255 {
byte_map[byte as usize] = palette_map[byte as usize].unwrap_or(0)
@ -167,3 +162,49 @@ fn reordered_palette(palette: &[RGBA8], palette_map: &[Option<u8>; 256]) -> Vec<
}
new_palette
}
/// Attempt to reduce the color type of the image
/// Returns true if the color type was reduced, false otherwise
pub fn reduce_color_type(png: &PngImage) -> Option<PngImage> {
let mut should_reduce_bit_depth = false;
let mut reduced = Cow::Borrowed(png);
// Go down one step at a time
// Maybe not the most efficient, but it's safe
if reduced.ihdr.color_type == ColorType::RGBA {
if let Some(r) = reduce_rgba_to_grayscale_alpha(&reduced).or_else(|| reduced_alpha_channel(&reduced)) {
reduced = Cow::Owned(r);
} else if let Some(r) = reduced_color_to_palette(&reduced) {
reduced = Cow::Owned(r);
should_reduce_bit_depth = true;
}
}
if reduced.ihdr.color_type == ColorType::GrayscaleAlpha {
if let Some(r) = reduced_alpha_channel(&reduced) {
reduced = Cow::Owned(r);
should_reduce_bit_depth = true;
}
}
if reduced.ihdr.color_type == ColorType::RGB {
if let Some(r) = reduce_rgb_to_grayscale(&reduced).or_else(|| reduced_color_to_palette(&reduced)) {
reduced = Cow::Owned(r);
should_reduce_bit_depth = true;
}
}
if should_reduce_bit_depth {
// Some conversions will allow us to perform bit depth reduction that
// wasn't possible before
if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) {
reduced = Cow::Owned(r);
}
}
match reduced {
Cow::Owned(r) => Some(r),
_ => None,
}
}

BIN
tests/files/issue-159.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -35,8 +35,8 @@ fn test_it_converts(
let png = PngData::new(&input, opts.fix_errors).unwrap();
opts.filter = HashSet::new();
opts.filter.insert(filter);
assert_eq!(png.ihdr_data.color_type, color_type_in);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_in);
assert_eq!(png.raw.ihdr.color_type, color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -53,8 +53,13 @@ fn test_it_converts(
}
};
assert_eq!(png.ihdr_data.color_type, color_type_out);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_out);
assert_eq!(png.raw.ihdr.color_type, color_type_out);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out);
if let Some(palette) = png.raw.palette.as_ref() {
assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth.as_u8() as usize));
} else {
assert!(png.raw.ihdr.color_type != ColorType::Indexed);
}
remove_file(output).ok();
}

View file

@ -32,8 +32,8 @@ fn test_it_converts(
) {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, color_type_in);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_in);
assert_eq!(png.raw.ihdr.color_type, color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -50,8 +50,8 @@ fn test_it_converts(
}
};
assert_eq!(png.ihdr_data.color_type, color_type_out);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_out);
assert_eq!(png.raw.ihdr.color_type, color_type_out);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out);
remove_file(output).ok();
}
@ -81,9 +81,9 @@ fn strip_headers_list() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert!(png.aux_headers.contains_key(b"tEXt"));
assert!(png.aux_headers.contains_key(b"iTXt"));
assert!(png.aux_headers.contains_key(b"iCCP"));
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"));
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -100,9 +100,9 @@ fn strip_headers_list() {
}
};
assert!(!png.aux_headers.contains_key(b"tEXt"));
assert!(png.aux_headers.contains_key(b"iTXt"));
assert!(!png.aux_headers.contains_key(b"iCCP"));
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"));
remove_file(output).ok();
}
@ -115,9 +115,9 @@ fn strip_headers_safe() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert!(png.aux_headers.contains_key(b"tEXt"));
assert!(png.aux_headers.contains_key(b"iTXt"));
assert!(png.aux_headers.contains_key(b"iCCP"));
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"));
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -134,9 +134,9 @@ fn strip_headers_safe() {
}
};
assert!(!png.aux_headers.contains_key(b"tEXt"));
assert!(!png.aux_headers.contains_key(b"iTXt"));
assert!(png.aux_headers.contains_key(b"sRGB"));
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"));
remove_file(output).ok();
}
@ -149,9 +149,9 @@ fn strip_headers_all() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert!(png.aux_headers.contains_key(b"tEXt"));
assert!(png.aux_headers.contains_key(b"iTXt"));
assert!(png.aux_headers.contains_key(b"iCCP"));
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"));
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -168,9 +168,9 @@ fn strip_headers_all() {
}
};
assert!(!png.aux_headers.contains_key(b"tEXt"));
assert!(!png.aux_headers.contains_key(b"iTXt"));
assert!(!png.aux_headers.contains_key(b"iCCP"));
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"));
remove_file(output).ok();
}
@ -183,9 +183,9 @@ fn strip_headers_none() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert!(png.aux_headers.contains_key(b"tEXt"));
assert!(png.aux_headers.contains_key(b"iTXt"));
assert!(png.aux_headers.contains_key(b"iCCP"));
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"));
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -202,9 +202,9 @@ fn strip_headers_none() {
}
};
assert!(png.aux_headers.contains_key(b"tEXt"));
assert!(png.aux_headers.contains_key(b"iTXt"));
assert!(png.aux_headers.contains_key(b"iCCP"));
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"));
remove_file(output).ok();
}
@ -217,7 +217,7 @@ fn interlacing_0_to_1() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.raw.ihdr.interlaced, 0);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -234,7 +234,7 @@ fn interlacing_0_to_1() {
}
};
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.raw.ihdr.interlaced, 1);
remove_file(output).ok();
}
@ -247,7 +247,7 @@ fn interlacing_1_to_0() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.raw.ihdr.interlaced, 1);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -264,7 +264,7 @@ fn interlacing_1_to_0() {
}
};
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.raw.ihdr.interlaced, 0);
remove_file(output).ok();
}
@ -277,9 +277,9 @@ fn interlacing_0_to_1_small_files() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.ihdr.interlaced, 0);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -296,9 +296,9 @@ fn interlacing_0_to_1_small_files() {
}
};
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::One);
assert_eq!(png.raw.ihdr.interlaced, 1);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::One);
remove_file(output).ok();
}
@ -311,9 +311,9 @@ fn interlacing_1_to_0_small_files() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.ihdr.interlaced, 1);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -330,9 +330,9 @@ fn interlacing_1_to_0_small_files() {
}
};
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::One);
assert_eq!(png.raw.ihdr.interlaced, 0);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
// the depth can't be asserted reliably, because on such small file different zlib implementaitons pick diferent depth as the best
remove_file(output).ok();
}
@ -348,7 +348,7 @@ fn interlaced_0_to_1_other_filter_mode() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.raw.ihdr.interlaced, 0);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -365,7 +365,7 @@ fn interlaced_0_to_1_other_filter_mode() {
}
};
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.raw.ihdr.interlaced, 1);
remove_file(output).ok();
}
@ -397,8 +397,8 @@ fn fix_errors() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, ColorType::RGBA);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.ihdr.color_type, ColorType::RGBA);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -415,8 +415,8 @@ fn fix_errors() {
}
};
assert_eq!(png.ihdr_data.color_type, ColorType::Grayscale);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.ihdr.color_type, ColorType::Grayscale);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
// Cannot check if pixels are equal because image crate cannot read corrupt (input) PNGs
remove_file(output).ok();

View file

@ -32,9 +32,9 @@ fn test_it_converts(
let (output, opts) = get_opts(&input);
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, color_type_in);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_in);
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.raw.ihdr.color_type, color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
assert_eq!(png.raw.ihdr.interlaced, 1);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -51,8 +51,8 @@ fn test_it_converts(
}
};
assert_eq!(png.ihdr_data.color_type, color_type_out);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_out);
assert_eq!(png.raw.ihdr.color_type, color_type_out);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out);
remove_file(output).ok();
}
@ -444,6 +444,10 @@ fn interlaced_palette_8_should_be_palette_8() {
#[test]
fn interlaced_palette_8_should_be_palette_4() {
// miniz doesn't estimate compression that well
if !oxipng::internal_tests::cfzlib::is_supported() {
return;
}
test_it_converts(
"tests/files/interlaced_palette_8_should_be_palette_4.png",
ColorType::Indexed,

View file

@ -36,9 +36,9 @@ fn test_it_converts(
}
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, color_type_in);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_in);
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.raw.ihdr.color_type, color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken");
assert_eq!(png.raw.ihdr.interlaced, 0);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -55,8 +55,8 @@ fn test_it_converts(
}
};
assert_eq!(png.ihdr_data.color_type, color_type_out);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_out);
assert_eq!(png.raw.ihdr.color_type, color_type_out);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out);
remove_file(output).ok();
}
@ -699,26 +699,13 @@ fn grayscale_8_should_be_grayscale_8() {
#[test]
fn small_files() {
test_it_converts(
"tests/files/small_files.png",
None,
ColorType::Indexed,
BitDepth::Eight,
ColorType::Indexed,
BitDepth::One,
);
}
#[test]
fn palette_should_be_reduced_with_dupes() {
let input = PathBuf::from("tests/files/palette_should_be_reduced_with_dupes.png");
let input = PathBuf::from("tests/files/small_files.png");
let (output, opts) = get_opts(&input);
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.palette.unwrap().len(), 43);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -735,9 +722,41 @@ fn palette_should_be_reduced_with_dupes() {
}
};
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.palette.unwrap().len(), 35);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
// depth varies depending on zlib implementation used
remove_file(output).ok();
}
#[test]
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();
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
};
let output = output.path().unwrap();
assert!(output.exists());
let png = match PngData::new(&output, opts.fix_errors) {
Ok(x) => x,
Err(x) => {
remove_file(&output).ok();
panic!("{}", x)
}
};
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35);
remove_file(output).ok();
}
@ -749,9 +768,9 @@ fn palette_should_be_reduced_with_unused() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.palette.unwrap().len(), 35);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -768,9 +787,9 @@ fn palette_should_be_reduced_with_unused() {
}
};
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.palette.unwrap().len(), 33);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33);
remove_file(output).ok();
}
@ -782,9 +801,9 @@ fn palette_should_be_reduced_with_both() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.palette.unwrap().len(), 43);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -801,9 +820,9 @@ fn palette_should_be_reduced_with_both() {
}
};
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.palette.unwrap().len(), 33);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33);
remove_file(output).ok();
}

View file

@ -33,8 +33,8 @@ fn test_it_converts(
let (output, opts) = custom.unwrap_or_else(|| get_opts(&input));
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.color_type, color_type_in);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_in);
assert_eq!(png.raw.ihdr.color_type, color_type_in, "test file is broken");
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken");
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -51,8 +51,13 @@ fn test_it_converts(
}
};
assert_eq!(png.ihdr_data.color_type, color_type_out);
assert_eq!(png.ihdr_data.bit_depth, bit_depth_out);
assert_eq!(png.raw.ihdr.color_type, color_type_out, "optimized to wrong color type");
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out, "optimized to wrong bit depth");
if let Some(palette) = png.raw.palette.as_ref() {
assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth.as_u8() as usize));
} else {
assert!(png.raw.ihdr.color_type != ColorType::Indexed);
}
remove_file(output).ok();
}
@ -77,9 +82,9 @@ fn issue_42() {
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.ihdr_data.color_type, ColorType::GrayscaleAlpha);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.ihdr.interlaced, 0);
assert_eq!(png.raw.ihdr.color_type, ColorType::GrayscaleAlpha);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -96,9 +101,9 @@ fn issue_42() {
}
};
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.ihdr_data.color_type, ColorType::GrayscaleAlpha);
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.ihdr.interlaced, 1);
assert_eq!(png.raw.ihdr.color_type, ColorType::GrayscaleAlpha);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
remove_file(output).ok();
}
@ -572,3 +577,15 @@ fn issue_153() {
BitDepth::Eight,
);
}
#[test]
fn issue_159() {
test_it_converts(
"tests/files/issue-159.png",
None,
ColorType::Indexed,
BitDepth::One,
ColorType::Indexed,
BitDepth::One,
);
}