Fix issue for interlacing images smaller than 4px

Fixes #42
This commit is contained in:
Joshua Holmer 2016-05-25 23:10:44 -04:00
parent aff99f2f7c
commit 2659776bd5
8 changed files with 307 additions and 258 deletions

View file

@ -1,3 +1,6 @@
**Version 0.8.2**
- Fix issue where images smaller than 4px width would crash on interlacing ([#42](https://github.com/shssoichiro/oxipng/issues/42))
**Version 0.8.1**
- Minor optimizations
- Fix issue where interlaced images with certain widths would fail to optimize

2
Cargo.lock generated
View file

@ -1,6 +1,6 @@
[root]
name = "oxipng"
version = "0.8.0"
version = "0.8.2"
dependencies = [
"bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -1,6 +1,6 @@
[package]
name = "oxipng"
version = "0.8.1"
version = "0.8.2"
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
description = "A lossless PNG compression optimizer"
license = "MIT"

View file

@ -185,9 +185,9 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
match copy(in_file,
in_file.with_extension(format!("bak.{}",
in_file.extension()
.unwrap()
.to_str()
.unwrap()))) {
.unwrap()
.to_str()
.unwrap()))) {
Ok(x) => x,
Err(_) => {
return Err(format!("Unable to write to backup file at {}",
@ -222,9 +222,9 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
match out_file.metadata() {
Ok(out_meta) => {
let readonly = metadata.permissions()
.readonly();
.readonly();
out_meta.permissions()
.set_readonly(readonly);
.set_readonly(readonly);
}
Err(_) => {
if opts.verbosity.is_some() {
@ -388,12 +388,9 @@ fn optimize_png(mut png: &mut png::PngData, file_original_size: usize, opts: &Op
let filtered = filters.get(&trial.0).unwrap();
let best = best.clone();
scope.execute(move || {
let new_idat = deflate::deflate::deflate(filtered,
trial.1,
trial.2,
trial.3,
opts.window)
.unwrap();
let new_idat =
deflate::deflate::deflate(filtered, trial.1, trial.2, trial.3, opts.window)
.unwrap();
if opts.verbosity == Some(1) {
writeln!(&mut stderr(),

View file

@ -10,186 +10,185 @@ use std::fs::DirBuilder;
use std::io::{Write, stderr};
use std::path::PathBuf;
const VERSION_STRING: &'static str = "0.8.1";
const VERSION_STRING: &'static str = "0.8.2";
fn main() {
let matches =
App::new("oxipng")
.version(VERSION_STRING)
.author("Joshua Holmer <jholmer.in@gmail.com>")
.about("Losslessly improves compression of PNG files")
.arg(Arg::with_name("files")
.help("File(s) to compress")
.index(1)
.multiple(true)
.required(true))
.arg(Arg::with_name("optimization")
.help("Optimization level - Default: 2")
.short("o")
.long("opt")
.takes_value(true)
.possible_value("0")
.possible_value("1")
.possible_value("2")
.possible_value("3")
.possible_value("4")
.possible_value("5")
.possible_value("6"))
.arg(Arg::with_name("backup")
.help("Back up modified files")
.short("b")
.long("backup"))
.arg(Arg::with_name("force")
.help("Write output even if larger than the original")
.short("F")
.long("force"))
.arg(Arg::with_name("recursive")
.help("Recurse into subdirectories")
.short("r")
.long("recursive"))
.arg(Arg::with_name("output_dir")
.help("Write output file(s) to <directory>")
.long("dir")
.takes_value(true)
.conflicts_with("output_file")
.conflicts_with("stdout"))
.arg(Arg::with_name("output_file")
.help("Write output file to <file>")
.long("out")
.takes_value(true)
.conflicts_with("output_dir")
.conflicts_with("stdout"))
.arg(Arg::with_name("stdout")
.help("Write output to stdout")
.long("stdout")
.conflicts_with("output_dir")
.conflicts_with("output_file"))
.arg(Arg::with_name("fix")
.help("Enable error recovery")
.long("fix"))
.arg(Arg::with_name("no-clobber")
.help("Do not overwrite existing files")
.long("no-clobber"))
.arg(Arg::with_name("pretend")
.help("Do not write any files, only calculate compression gains")
.short("P")
.long("pretend"))
.arg(Arg::with_name("preserve")
.help("Preserve file attributes if possible")
.short("p")
.long("preserve"))
.arg(Arg::with_name("quiet")
.help("Run in quiet mode")
.short("q")
.long("quiet")
.conflicts_with("verbose"))
.arg(Arg::with_name("verbose")
.help("Run in verbose mode")
.short("v")
.long("verbose")
.conflicts_with("quiet"))
.arg(Arg::with_name("filters")
.help("PNG delta filters (0-5) - Default: 0,5")
.short("f")
.long("filters")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 0, 5) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for filters".to_owned()),
}
}))
.arg(Arg::with_name("interlace")
.help("PNG interlace type")
.short("i")
.long("interlace")
.takes_value(true)
.possible_value("0")
.possible_value("1"))
.arg(Arg::with_name("compression")
.help("zlib compression levels (1-9) - Default: 9")
.long("zc")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 1, 9) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for compression".to_owned()),
}
}))
.arg(Arg::with_name("memory")
.help("zlib memory levels (1-9) - Default: 9")
.long("zm")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 1, 9) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for memory".to_owned()),
}
}))
.arg(Arg::with_name("strategies")
.help("zlib compression strategies (0-3) - Default: 0-3")
.long("zs")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 0, 3) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for strategies".to_owned()),
}
}))
.arg(Arg::with_name("window")
.help("zlib window size - Default: 32k")
.long("zw")
.takes_value(true)
.possible_value("256")
.possible_value("512")
.possible_value("1k")
.possible_value("2k")
.possible_value("4k")
.possible_value("8k")
.possible_value("16k")
.possible_value("32k"))
.arg(Arg::with_name("no-bit-reduction")
.help("No bit depth reduction")
.long("nb"))
.arg(Arg::with_name("no-color-reduction")
.help("No color type reduction")
.long("nc"))
.arg(Arg::with_name("no-palette-reduction")
.help("No palette reduction")
.long("np"))
.arg(Arg::with_name("no-reductions")
.help("No reductions")
.long("nx"))
.arg(Arg::with_name("no-recoding")
.help("No IDAT recoding unless necessary")
.long("nz"))
.arg(Arg::with_name("strip")
.help("Strip metadata objects ['safe', 'all', or comma-separated list]")
.long("strip")
.takes_value(true)
.conflicts_with("strip-safe"))
.arg(Arg::with_name("strip-safe")
.help("Strip safely-removable metadata objects")
.short("s")
.conflicts_with("strip"))
.arg(Arg::with_name("threads")
.help("Set number of threads to use - default 1.5x CPU cores")
.long("threads")
.short("t")
.takes_value(true)
.validator(|x| {
match x.parse::<usize>() {
Ok(val) => {
if val > 0 {
Ok(())
} else {
Err("Thread count must be >= 1".to_owned())
}
}
Err(_) => Err("Thread count must be >= 1".to_owned()),
}
}))
.after_help("Optimization levels:
let matches = App::new("oxipng")
.version(VERSION_STRING)
.author("Joshua Holmer <jholmer.in@gmail.com>")
.about("Losslessly improves compression of PNG files")
.arg(Arg::with_name("files")
.help("File(s) to compress")
.index(1)
.multiple(true)
.required(true))
.arg(Arg::with_name("optimization")
.help("Optimization level - Default: 2")
.short("o")
.long("opt")
.takes_value(true)
.possible_value("0")
.possible_value("1")
.possible_value("2")
.possible_value("3")
.possible_value("4")
.possible_value("5")
.possible_value("6"))
.arg(Arg::with_name("backup")
.help("Back up modified files")
.short("b")
.long("backup"))
.arg(Arg::with_name("force")
.help("Write output even if larger than the original")
.short("F")
.long("force"))
.arg(Arg::with_name("recursive")
.help("Recurse into subdirectories")
.short("r")
.long("recursive"))
.arg(Arg::with_name("output_dir")
.help("Write output file(s) to <directory>")
.long("dir")
.takes_value(true)
.conflicts_with("output_file")
.conflicts_with("stdout"))
.arg(Arg::with_name("output_file")
.help("Write output file to <file>")
.long("out")
.takes_value(true)
.conflicts_with("output_dir")
.conflicts_with("stdout"))
.arg(Arg::with_name("stdout")
.help("Write output to stdout")
.long("stdout")
.conflicts_with("output_dir")
.conflicts_with("output_file"))
.arg(Arg::with_name("fix")
.help("Enable error recovery")
.long("fix"))
.arg(Arg::with_name("no-clobber")
.help("Do not overwrite existing files")
.long("no-clobber"))
.arg(Arg::with_name("pretend")
.help("Do not write any files, only calculate compression gains")
.short("P")
.long("pretend"))
.arg(Arg::with_name("preserve")
.help("Preserve file attributes if possible")
.short("p")
.long("preserve"))
.arg(Arg::with_name("quiet")
.help("Run in quiet mode")
.short("q")
.long("quiet")
.conflicts_with("verbose"))
.arg(Arg::with_name("verbose")
.help("Run in verbose mode")
.short("v")
.long("verbose")
.conflicts_with("quiet"))
.arg(Arg::with_name("filters")
.help("PNG delta filters (0-5) - Default: 0,5")
.short("f")
.long("filters")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 0, 5) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for filters".to_owned()),
}
}))
.arg(Arg::with_name("interlace")
.help("PNG interlace type")
.short("i")
.long("interlace")
.takes_value(true)
.possible_value("0")
.possible_value("1"))
.arg(Arg::with_name("compression")
.help("zlib compression levels (1-9) - Default: 9")
.long("zc")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 1, 9) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for compression".to_owned()),
}
}))
.arg(Arg::with_name("memory")
.help("zlib memory levels (1-9) - Default: 9")
.long("zm")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 1, 9) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for memory".to_owned()),
}
}))
.arg(Arg::with_name("strategies")
.help("zlib compression strategies (0-3) - Default: 0-3")
.long("zs")
.takes_value(true)
.validator(|x| {
match parse_numeric_range_opts(&x, 0, 3) {
Ok(_) => Ok(()),
Err(_) => Err("Invalid option for strategies".to_owned()),
}
}))
.arg(Arg::with_name("window")
.help("zlib window size - Default: 32k")
.long("zw")
.takes_value(true)
.possible_value("256")
.possible_value("512")
.possible_value("1k")
.possible_value("2k")
.possible_value("4k")
.possible_value("8k")
.possible_value("16k")
.possible_value("32k"))
.arg(Arg::with_name("no-bit-reduction")
.help("No bit depth reduction")
.long("nb"))
.arg(Arg::with_name("no-color-reduction")
.help("No color type reduction")
.long("nc"))
.arg(Arg::with_name("no-palette-reduction")
.help("No palette reduction")
.long("np"))
.arg(Arg::with_name("no-reductions")
.help("No reductions")
.long("nx"))
.arg(Arg::with_name("no-recoding")
.help("No IDAT recoding unless necessary")
.long("nz"))
.arg(Arg::with_name("strip")
.help("Strip metadata objects ['safe', 'all', or comma-separated list]")
.long("strip")
.takes_value(true)
.conflicts_with("strip-safe"))
.arg(Arg::with_name("strip-safe")
.help("Strip safely-removable metadata objects")
.short("s")
.conflicts_with("strip"))
.arg(Arg::with_name("threads")
.help("Set number of threads to use - default 1.5x CPU cores")
.long("threads")
.short("t")
.takes_value(true)
.validator(|x| {
match x.parse::<usize>() {
Ok(val) => {
if val > 0 {
Ok(())
} else {
Err("Thread count must be >= 1".to_owned())
}
}
Err(_) => Err("Thread count must be >= 1".to_owned()),
}
}))
.after_help("Optimization levels:
-o 0 => --zc 3 --nz (0 or 1 trials)
-o 1 => --zc 9 (1 trial, determined heuristically)
-o 2 => --zc 9 --zs 0-3 --f 0,5 (8 trials)
@ -204,7 +203,7 @@ fn main() {
Manually specifying a compression option (zc, zm, etc.) will override the optimization preset,
regardless of the order you write the arguments.")
.get_matches();
.get_matches();
let mut opts = oxipng::Options::default();
@ -217,9 +216,9 @@ fn main() {
}
handle_optimization(matches.values_of("files")
.unwrap()
.map(PathBuf::from)
.collect(),
.unwrap()
.map(PathBuf::from)
.collect(),
opts);
}
@ -390,8 +389,8 @@ fn parse_opts_into_struct(matches: &ArgMatches, opts: &mut oxipng::Options) -> R
let path = PathBuf::from(x);
if !path.exists() {
match DirBuilder::new()
.recursive(true)
.create(&path) {
.recursive(true)
.create(&path) {
Ok(_) => (),
Err(x) => return Err(format!("Could not create output directory {}", x)),
};
@ -473,7 +472,7 @@ fn parse_opts_into_struct(matches: &ArgMatches, opts: &mut oxipng::Options) -> R
if hdrs.contains(&"safe".to_owned()) || hdrs.contains(&"all".to_owned()) {
if hdrs.len() > 1 {
return Err("'safe' or 'all' presets for --strip should be used by themselves"
.to_owned());
.to_owned());
}
if hdrs[0] == "safe" {
opts.strip = png::Headers::Safe;
@ -512,8 +511,8 @@ fn parse_numeric_range_opts(input: &str,
max_value,
min_value,
max_value)
.as_ref())
.unwrap();
.as_ref())
.unwrap();
let mut items = HashSet::new();
if one_item.is_match(input) {

View file

@ -416,9 +416,9 @@ impl PngData {
ihdr_data.write_u8(self.ihdr_data.interlaced).ok();
write_png_block(b"IHDR", &ihdr_data, &mut output);
// Ancillary headers
for (key, header) in self.aux_headers.iter().filter(|&(ref key, _)| {
!(**key == "bKGD" || **key == "hIST" || **key == "tRNS")
}) {
for (key, header) in self.aux_headers
.iter()
.filter(|&(ref key, _)| !(**key == "bKGD" || **key == "hIST" || **key == "tRNS")) {
write_png_block(key.as_bytes(), header, &mut output);
}
// Palette
@ -433,9 +433,9 @@ impl PngData {
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.aux_headers.iter().filter(|&(ref key, _)| {
**key == "bKGD" || **key == "hIST" || **key == "tRNS"
}) {
for (key, header) in self.aux_headers
.iter()
.filter(|&(ref key, _)| **key == "bKGD" || **key == "hIST" || **key == "tRNS") {
write_png_block(key.as_bytes(), header, &mut output);
}
// IDAT data
@ -459,7 +459,7 @@ impl PngData {
let mut unfiltered = Vec::with_capacity(self.raw_data.len());
let bpp = (((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel()) as f32) /
8f32)
.ceil() as usize;
.ceil() as usize;
let mut last_line: Vec<u8> = Vec::new();
for line in self.scan_lines() {
let unfiltered_line = unfilter_line(line.filter, bpp, &line.data, &last_line);
@ -480,7 +480,7 @@ impl PngData {
let mut filtered = Vec::with_capacity(self.raw_data.len());
let bpp = (((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel()) as f32) /
8f32)
.ceil() as usize;
.ceil() as usize;
let mut last_line: Vec<u8> = Vec::new();
let mut last_pass: Option<u8> = None;
for line in self.scan_lines() {
@ -512,13 +512,13 @@ impl PngData {
trials.insert(filter, filter_line(filter, bpp, &line.data, &last_line));
}
let (best_filter, best_line) = trials.iter()
.min_by_key(|x| {
x.1.iter().fold(0u64, |acc, &x| {
let signed = x as i8;
acc + (signed as i16).abs() as u64
})
})
.unwrap();
.min_by_key(|x| {
x.1.iter().fold(0u64, |acc, &x| {
let signed = x as i8;
acc + (signed as i16).abs() as u64
})
})
.unwrap();
filtered.push(*best_filter);
filtered.extend_from_slice(best_line);
}
@ -548,10 +548,10 @@ impl PngData {
}
// Reduce from 16 to 8 bits per channel per pixel
let mut reduced =
Vec::with_capacity((self.ihdr_data.width * self.ihdr_data.height *
self.channels_per_pixel() as u32 +
self.ihdr_data.height) as usize);
let mut reduced = Vec::with_capacity((self.ihdr_data.width * self.ihdr_data.height *
self.channels_per_pixel() as u32 +
self.ihdr_data
.height) as usize);
let mut high_byte = 0;
for line in self.scan_lines() {
@ -888,23 +888,33 @@ fn interlace_image(png: &mut PngData) {
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
for (index, line) in png.scan_lines().enumerate() {
match index % 8 {
// Add filter bytes to appropriate lines
// Add filter bytes to passes that will be in the output image
0 => {
passes[0].extend(BitVec::from_elem(8, false));
passes[3].extend(BitVec::from_elem(8, false));
passes[5].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width > 4 {
if png.ihdr_data.width >= 5 {
passes[1].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 3 {
passes[3].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
4 => {
passes[3].extend(BitVec::from_elem(8, false));
passes[5].extend(BitVec::from_elem(8, false));
passes[2].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 3 {
passes[3].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
2 | 6 => {
passes[4].extend(BitVec::from_elem(8, false));
passes[5].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
_ => {
passes[6].extend(BitVec::from_elem(8, false));
@ -975,8 +985,7 @@ fn deinterlace_image(png: &mut PngData) {
let bit_vec = BitVec::from_bytes(&line.data);
let bits_in_line = ((png.ihdr_data.width - pass_constants.x_shift as u32) as f32 /
pass_constants.x_step as f32)
.ceil() as usize *
bits_per_pixel as usize;
.ceil() as usize * bits_per_pixel as usize;
for (i, bit) in bit_vec.iter().enumerate() {
// Avoid moving padded 0's into new image
if i >= bits_in_line {
@ -1093,19 +1102,19 @@ fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8>
1 => {
filtered.extend_from_slice(&data[0..bpp]);
filtered.extend_from_slice(&data.iter()
.skip(bpp)
.zip(data.iter())
.map(|(cur, last)| cur.wrapping_sub(*last))
.collect::<Vec<u8>>());
.skip(bpp)
.zip(data.iter())
.map(|(cur, last)| cur.wrapping_sub(*last))
.collect::<Vec<u8>>());
}
2 => {
if last_line.is_empty() {
filtered.extend_from_slice(data);
} else {
filtered.extend_from_slice(&data.iter()
.zip(last_line.iter())
.map(|(cur, last)| cur.wrapping_sub(*last))
.collect::<Vec<u8>>());
.zip(last_line.iter())
.map(|(cur, last)| cur.wrapping_sub(*last))
.collect::<Vec<u8>>());
};
}
3 => {
@ -1171,9 +1180,9 @@ fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u
unfiltered.extend_from_slice(data);
} else {
unfiltered.extend_from_slice(&data.iter()
.zip(last_line.iter())
.map(|(cur, last)| cur.wrapping_add(*last))
.collect::<Vec<u8>>());
.zip(last_line.iter())
.map(|(cur, last)| cur.wrapping_add(*last))
.collect::<Vec<u8>>());
};
}
3 => {
@ -1535,14 +1544,14 @@ fn reduce_rgb_to_grayscale(png: &PngData) -> Option<Vec<u8>> {
reduced.push(cur_pixel[0]);
} else {
let mut pixel_zip = cur_pixel.iter()
.enumerate()
.filter(|&(i, _)| i % 2 == 0)
.map(|(_, x)| *x)
.zip(cur_pixel.iter()
.enumerate()
.filter(|&(i, _)| i % 2 == 1)
.map(|(_, x)| *x))
.collect::<Vec<(u8, u8)>>();
.enumerate()
.filter(|&(i, _)| i % 2 == 0)
.map(|(_, x)| *x)
.zip(cur_pixel.iter()
.enumerate()
.filter(|&(i, _)| i % 2 == 1)
.map(|(_, x)| *x))
.collect::<Vec<(u8, u8)>>();
pixel_zip.sort();
pixel_zip.dedup();
if pixel_zip.len() > 1 {
@ -1605,10 +1614,10 @@ fn parse_next_header(byte_data: &[u8],
fix_errors: bool)
-> Result<Option<(String, Vec<u8>)>, String> {
let mut rdr = Cursor::new(byte_data.iter()
.skip(*byte_offset)
.take(4)
.cloned()
.collect::<Vec<u8>>());
.skip(*byte_offset)
.take(4)
.cloned()
.collect::<Vec<u8>>());
let length: u32 = match rdr.read_u32::<BigEndian>() {
Ok(x) => x,
Err(_) => return Err("Invalid data found--unable to read PNG file".to_owned()),
@ -1627,16 +1636,16 @@ fn parse_next_header(byte_data: &[u8],
*byte_offset += 4;
let data: Vec<u8> = byte_data.iter()
.skip(*byte_offset)
.take(length as usize)
.cloned()
.collect();
.skip(*byte_offset)
.take(length as usize)
.cloned()
.collect();
*byte_offset += length as usize;
let mut rdr = Cursor::new(byte_data.iter()
.skip(*byte_offset)
.take(4)
.cloned()
.collect::<Vec<u8>>());
.skip(*byte_offset)
.take(4)
.cloned()
.collect::<Vec<u8>>());
let crc: u32 = match rdr.read_u32::<BigEndian>() {
Ok(x) => x,
Err(_) => return Err("Invalid data found--unable to read PNG file".to_owned()),

BIN
tests/files/issue_42.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

@ -486,3 +486,44 @@ fn fix_errors() {
// Cannot check if pixels are equal because image crate cannot read corrupt (input) PNGs
remove_file(output).ok();
}
#[test]
fn issue_42() {
let input = PathBuf::from("tests/files/issue_42.png");
let mut opts = get_opts(&input);
opts.interlace = Some(1);
let output = opts.out_file.clone();
let png = png::PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.ihdr_data.interlaced, 0);
assert_eq!(png.ihdr_data.color_type, png::ColorType::GrayscaleAlpha);
assert_eq!(png.ihdr_data.bit_depth, png::BitDepth::Eight);
match oxipng::optimize(&input, &opts) {
Ok(_) => (),
Err(x) => panic!(x.to_owned()),
};
assert!(output.exists());
let png = match png::PngData::new(&output, opts.fix_errors) {
Ok(x) => x,
Err(x) => {
remove_file(output).ok();
panic!(x.to_owned())
}
};
assert_eq!(png.ihdr_data.interlaced, 1);
assert_eq!(png.ihdr_data.color_type, png::ColorType::GrayscaleAlpha);
assert_eq!(png.ihdr_data.bit_depth, png::BitDepth::Eight);
let old_png = image::open(&input).unwrap();
let new_png = image::open(&output).unwrap();
// Conversion should be lossless
assert_eq!(old_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>(),
new_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>());
remove_file(output).ok();
}