diff --git a/CHANGELOG.md b/CHANGELOG.md index 6256fc4d..d7981267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index e76bf41a..720501f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", diff --git a/Cargo.toml b/Cargo.toml index 52dcf318..546241d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxipng" -version = "0.8.1" +version = "0.8.2" authors = ["Joshua Holmer "] description = "A lossless PNG compression optimizer" license = "MIT" diff --git a/src/lib.rs b/src/lib.rs index ed8454ea..744cfc03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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(), diff --git a/src/main.rs b/src/main.rs index 8e70243e..097acc77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 ") - .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 ") - .long("dir") - .takes_value(true) - .conflicts_with("output_file") - .conflicts_with("stdout")) - .arg(Arg::with_name("output_file") - .help("Write output file to ") - .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::() { - 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 ") + .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 ") + .long("dir") + .takes_value(true) + .conflicts_with("output_file") + .conflicts_with("stdout")) + .arg(Arg::with_name("output_file") + .help("Write output file to ") + .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::() { + 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) { diff --git a/src/png.rs b/src/png.rs index 14689f92..1f9917ef 100644 --- a/src/png.rs +++ b/src/png.rs @@ -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 = 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 = Vec::new(); let mut last_pass: Option = 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 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::>()); + .skip(bpp) + .zip(data.iter()) + .map(|(cur, last)| cur.wrapping_sub(*last)) + .collect::>()); } 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::>()); + .zip(last_line.iter()) + .map(|(cur, last)| cur.wrapping_sub(*last)) + .collect::>()); }; } 3 => { @@ -1171,9 +1180,9 @@ fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec>()); + .zip(last_line.iter()) + .map(|(cur, last)| cur.wrapping_add(*last)) + .collect::>()); }; } 3 => { @@ -1535,14 +1544,14 @@ fn reduce_rgb_to_grayscale(png: &PngData) -> Option> { 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::>(); + .enumerate() + .filter(|&(i, _)| i % 2 == 0) + .map(|(_, x)| *x) + .zip(cur_pixel.iter() + .enumerate() + .filter(|&(i, _)| i % 2 == 1) + .map(|(_, x)| *x)) + .collect::>(); 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)>, String> { let mut rdr = Cursor::new(byte_data.iter() - .skip(*byte_offset) - .take(4) - .cloned() - .collect::>()); + .skip(*byte_offset) + .take(4) + .cloned() + .collect::>()); let length: u32 = match rdr.read_u32::() { 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 = 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::>()); + .skip(*byte_offset) + .take(4) + .cloned() + .collect::>()); let crc: u32 = match rdr.read_u32::() { Ok(x) => x, Err(_) => return Err("Invalid data found--unable to read PNG file".to_owned()), diff --git a/tests/files/issue_42.png b/tests/files/issue_42.png new file mode 100644 index 00000000..cb148d07 Binary files /dev/null and b/tests/files/issue_42.png differ diff --git a/tests/flags.rs b/tests/flags.rs index 90b10ca6..39adf274 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -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::>>(), + new_png.pixels().map(|x| x.2.channels().to_owned()).collect::>>()); + + remove_file(output).ok(); +}