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** **Version 0.8.1**
- Minor optimizations - Minor optimizations
- Fix issue where interlaced images with certain widths would fail to optimize - Fix issue where interlaced images with certain widths would fail to optimize

2
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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