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:
parent
55d85df9fc
commit
9af874d21b
22 changed files with 943 additions and 787 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
112
src/evaluate.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
162
src/lib.rs
162
src/lib.rs
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
479
src/png/mod.rs
479
src/png/mod.rs
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
BIN
tests/files/issue-159.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
100
tests/flags.rs
100
tests/flags.rs
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue