parent
aff99f2f7c
commit
2659776bd5
8 changed files with 307 additions and 258 deletions
|
|
@ -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
2
Cargo.lock
generated
|
|
@ -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)",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
19
src/lib.rs
19
src/lib.rs
|
|
@ -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(),
|
||||||
|
|
|
||||||
373
src/main.rs
373
src/main.rs
|
|
@ -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) {
|
||||||
|
|
|
||||||
125
src/png.rs
125
src/png.rs
|
|
@ -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
BIN
tests/files/issue_42.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue