Add json output option (#761)
This commit is contained in:
parent
a8edb2871c
commit
ff168f5d2d
8 changed files with 274 additions and 78 deletions
96
Cargo.lock
generated
96
Cargo.lock
generated
|
|
@ -309,6 +309,12 @@ version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.175"
|
version = "0.2.175"
|
||||||
|
|
@ -346,6 +352,12 @@ version = "0.4.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -398,6 +410,7 @@ dependencies = [
|
||||||
"rayon",
|
"rayon",
|
||||||
"rgb",
|
"rgb",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"serde_json",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -426,6 +439,15 @@ dependencies = [
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pxfm"
|
name = "pxfm"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
|
|
@ -435,6 +457,15 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radium"
|
name = "radium"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -489,6 +520,48 @@ dependencies = [
|
||||||
"windows-sys 0.61.0",
|
"windows-sys 0.61.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.147"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
@ -507,6 +580,17 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.111"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -523,6 +607,12 @@ dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -633,6 +723,12 @@ dependencies = [
|
||||||
"tap",
|
"tap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4af59da1029247450b54ba43e0b62c8e376582464bbe5504dd525fe521e7e8fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
|
||||||
47
Cargo.toml
47
Cargo.toml
|
|
@ -38,46 +38,25 @@ name = "zopfli"
|
||||||
required-features = ["zopfli"]
|
required-features = ["zopfli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zopfli = { version = "0.8.3", optional = true, default-features = false, features = ["std", "zlib"] }
|
bitvec = "1.0.1"
|
||||||
rgb = "0.8.52"
|
clap = { version = "4.5.53", optional = true, features = ["wrap_help"] }
|
||||||
|
crossbeam-channel = { version = "0.5.15", optional = true }
|
||||||
|
env_logger = { version = "0.11.8", optional = true, default-features = false, features = ["auto-color"] }
|
||||||
|
image = { version = "0.25.9", optional = true, default-features = false, features = ["png"] }
|
||||||
indexmap = "2.13.0"
|
indexmap = "2.13.0"
|
||||||
libdeflater = "1.25.0"
|
libdeflater = "1.25.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
bitvec = "1.0.1"
|
parse-size = { version = "1.1.0", optional = true }
|
||||||
|
rayon = { version = "1.11.0", optional = true }
|
||||||
|
rgb = "0.8.52"
|
||||||
rustc-hash = "2.1.1"
|
rustc-hash = "2.1.1"
|
||||||
|
zopfli = { version = "0.8.3", optional = true, default-features = false, features = ["std", "zlib"] }
|
||||||
|
|
||||||
[dependencies.env_logger]
|
[target.'cfg(windows)'.dependencies]
|
||||||
optional = true
|
glob = { version = "0.3.3", optional = true }
|
||||||
default-features = false
|
|
||||||
features = ["auto-color"]
|
|
||||||
version = "0.11.8"
|
|
||||||
|
|
||||||
[dependencies.crossbeam-channel]
|
[dev-dependencies]
|
||||||
optional = true
|
serde_json = "1.0.147"
|
||||||
version = "0.5.15"
|
|
||||||
|
|
||||||
[dependencies.rayon]
|
|
||||||
optional = true
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[dependencies.clap]
|
|
||||||
optional = true
|
|
||||||
version = "4.5.54"
|
|
||||||
features = ["wrap_help"]
|
|
||||||
|
|
||||||
[dependencies.parse-size]
|
|
||||||
optional = true
|
|
||||||
version = "1.1.0"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies.glob]
|
|
||||||
optional = true
|
|
||||||
version = "0.3.3"
|
|
||||||
|
|
||||||
[dependencies.image]
|
|
||||||
optional = true
|
|
||||||
default-features = false
|
|
||||||
features = ["png"]
|
|
||||||
version = "0.25.9"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
|
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,14 @@ losslessly.")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.conflicts_with("verbose"),
|
.conflicts_with("verbose"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("json")
|
||||||
|
.help("Print results as JSON")
|
||||||
|
.short('j')
|
||||||
|
.long("json")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.conflicts_with("stdout"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("filters")
|
Arg::new("filters")
|
||||||
.help("Filters to try (0-9; see '--help' for details)")
|
.help("Filters to try (0-9; see '--help' for details)")
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ impl Evaluator {
|
||||||
|
|
||||||
/// Check if the image is smaller than others
|
/// Check if the image is smaller than others
|
||||||
pub fn try_image(&self, image: Arc<PngImage>) {
|
pub fn try_image(&self, image: Arc<PngImage>) {
|
||||||
let description = format!("{}", image.ihdr.color_type);
|
let description = image.ihdr.color_type.to_string();
|
||||||
self.try_image_with_description(image, &description);
|
self.try_image_with_description(image, &description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ pub mod internal_tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PngResult<T> = Result<T, PngError>;
|
pub type PngResult<T> = Result<T, PngError>;
|
||||||
|
pub type OptimizationResult = PngResult<(usize, usize)>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// A raw image definition which can be used to create an optimized png
|
/// A raw image definition which can be used to create an optimized png
|
||||||
|
|
@ -165,7 +166,7 @@ impl RawImage {
|
||||||
/// Perform optimization on the input file using the options provided
|
/// Perform optimization on the input file using the options provided
|
||||||
///
|
///
|
||||||
/// Returns the original and optimized file sizes
|
/// Returns the original and optimized file sizes
|
||||||
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(usize, usize)> {
|
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
|
||||||
// Read in the file and try to decode as PNG.
|
// Read in the file and try to decode as PNG.
|
||||||
info!("Processing: {input}");
|
info!("Processing: {input}");
|
||||||
|
|
||||||
|
|
|
||||||
158
src/main.rs
158
src/main.rs
|
|
@ -19,7 +19,9 @@ use indexmap::IndexSet;
|
||||||
use log::{Level, LevelFilter, error, warn};
|
use log::{Level, LevelFilter, error, warn};
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
use oxipng::ZopfliOptions;
|
use oxipng::ZopfliOptions;
|
||||||
use oxipng::{Deflater, FilterStrategy, InFile, Options, OutFile, PngError, StripChunks};
|
use oxipng::{
|
||||||
|
Deflater, FilterStrategy, InFile, OptimizationResult, Options, OutFile, PngError, StripChunks,
|
||||||
|
};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use crate::cli::DISPLAY_CHUNKS;
|
use crate::cli::DISPLAY_CHUNKS;
|
||||||
|
|
@ -51,15 +53,22 @@ fn main() -> ExitCode {
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let inputs: Vec<_> = file_args.collect();
|
let inputs: Vec<_> = file_args.collect();
|
||||||
let using_stdin = inputs.len() == 1 && inputs[0].to_str() == Some("-");
|
let using_stdin = inputs.len() == 1 && inputs[0].to_str() == Some("-");
|
||||||
|
if using_stdin && out_dir.is_some() {
|
||||||
|
error!("Cannot use --dir when reading from stdin.");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
if using_stdin && matches!(out_file, OutFile::Path { path: None, .. }) {
|
||||||
|
out_file = OutFile::StdOut;
|
||||||
|
}
|
||||||
|
let using_stdout = matches!(out_file, OutFile::StdOut);
|
||||||
|
let json = matches.get_flag("json");
|
||||||
|
if using_stdout && json {
|
||||||
|
error!("Cannot use --json when writing to stdout.");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
let files = if using_stdin {
|
let files = if using_stdin {
|
||||||
if out_dir.is_some() {
|
vec![(InFile::StdIn, out_file)]
|
||||||
error!("Cannot use --dir when reading from stdin.");
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
}
|
|
||||||
if matches!(out_file, OutFile::Path { path: None, .. }) {
|
|
||||||
out_file = OutFile::StdOut;
|
|
||||||
}
|
|
||||||
vec![(InFile::StdIn, out_file.clone())]
|
|
||||||
} else {
|
} else {
|
||||||
collect_files(
|
collect_files(
|
||||||
inputs,
|
inputs,
|
||||||
|
|
@ -71,7 +80,7 @@ fn main() -> ExitCode {
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_verbose = matches.get_count("verbose") > 0;
|
let is_verbose = matches.get_count("verbose") > 0;
|
||||||
let print_summary = !matches.get_flag("quiet") && !matches!(out_file, OutFile::StdOut);
|
let print_summary = !matches.get_flag("quiet") && !using_stdout;
|
||||||
let print_progress = print_summary && !is_verbose && stdout().is_terminal();
|
let print_progress = print_summary && !is_verbose && stdout().is_terminal();
|
||||||
let total_files = files.len();
|
let total_files = files.len();
|
||||||
let num_processed = AtomicUsize::new(0);
|
let num_processed = AtomicUsize::new(0);
|
||||||
|
|
@ -79,9 +88,9 @@ fn main() -> ExitCode {
|
||||||
print!("Files processed: 0/{}...", total_files);
|
print!("Files processed: 0/{}...", total_files);
|
||||||
stdout().flush().ok();
|
stdout().flush().ok();
|
||||||
}
|
}
|
||||||
let process = |(input, output): (InFile, OutFile)| {
|
let process = |(input, output): &(InFile, OutFile)| {
|
||||||
let result = process_file(&input, &output, &opts);
|
let result = process_file(input, output, &opts);
|
||||||
if print_progress && matches!(result, OptimizationResult::Ok(_, _)) {
|
if print_progress && matches!(result, OptimizationResult::Ok(_)) {
|
||||||
let value = num_processed.fetch_add(1, AcqRel) + 1;
|
let value = num_processed.fetch_add(1, AcqRel) + 1;
|
||||||
print!("\rFiles processed: {}/{}...", value, total_files);
|
print!("\rFiles processed: {}/{}...", value, total_files);
|
||||||
stdout().flush().ok();
|
stdout().flush().ok();
|
||||||
|
|
@ -89,9 +98,9 @@ fn main() -> ExitCode {
|
||||||
result
|
result
|
||||||
};
|
};
|
||||||
let results: Vec<OptimizationResult> = if matches.get_flag("parallel-files") {
|
let results: Vec<OptimizationResult> = if matches.get_flag("parallel-files") {
|
||||||
files.into_par_iter().map(process).collect()
|
files.par_iter().map(process).collect()
|
||||||
} else {
|
} else {
|
||||||
files.into_iter().map(process).collect()
|
files.iter().map(process).collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Collect stats
|
// Collect stats
|
||||||
|
|
@ -100,23 +109,25 @@ fn main() -> ExitCode {
|
||||||
let mut num_failed = 0;
|
let mut num_failed = 0;
|
||||||
let mut total_in: i64 = 0;
|
let mut total_in: i64 = 0;
|
||||||
let mut total_out: i64 = 0;
|
let mut total_out: i64 = 0;
|
||||||
for result in results {
|
for result in &results {
|
||||||
match result {
|
match result {
|
||||||
OptimizationResult::Ok(insize, outsize) => {
|
Ok((insize, outsize)) => {
|
||||||
num_succeeded += 1;
|
num_succeeded += 1;
|
||||||
total_in += insize as i64;
|
total_in += *insize as i64;
|
||||||
total_out += outsize as i64;
|
total_out += *outsize as i64;
|
||||||
if !opts.force && insize == outsize {
|
if !opts.force && insize == outsize {
|
||||||
num_not_optimized += 1;
|
num_not_optimized += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OptimizationResult::Failed => num_failed += 1,
|
Err(PngError::C2PAMetadataPreventsChanges | PngError::InflatedDataTooLong(_)) => {}
|
||||||
OptimizationResult::Skipped => {}
|
Err(_) => num_failed += 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print summary
|
// Print results
|
||||||
if print_summary {
|
if json {
|
||||||
|
json_output(&files, &results);
|
||||||
|
} else if print_summary {
|
||||||
let in_bytes = format_bytes(total_in, true);
|
let in_bytes = format_bytes(total_in, true);
|
||||||
let out_bytes = format_bytes(total_out, true);
|
let out_bytes = format_bytes(total_out, true);
|
||||||
let saved = total_in - total_out;
|
let saved = total_in - total_out;
|
||||||
|
|
@ -143,6 +154,13 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For optimizing single files, this will return the correct exit code always.
|
||||||
|
// For recursive optimization, the correct choice is a bit subjective.
|
||||||
|
// We're choosing to return a 0 exit code if ANY file in the set
|
||||||
|
// runs correctly.
|
||||||
|
// The reason for this is that recursion may pick up files that are not
|
||||||
|
// PNG files, and return an error for them.
|
||||||
|
// We don't really want to return an error code for those files.
|
||||||
if num_succeeded > 0 {
|
if num_succeeded > 0 {
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
} else if num_failed > 0 {
|
} else if num_failed > 0 {
|
||||||
|
|
@ -152,13 +170,6 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Ord, PartialOrd)]
|
|
||||||
enum OptimizationResult {
|
|
||||||
Ok(usize, usize),
|
|
||||||
Failed,
|
|
||||||
Skipped,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_files(
|
fn collect_files(
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
out_dir: &Option<PathBuf>,
|
out_dir: &Option<PathBuf>,
|
||||||
|
|
@ -506,28 +517,91 @@ fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
|
||||||
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
|
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
|
||||||
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
|
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
|
||||||
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
|
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
|
||||||
return OptimizationResult::Skipped;
|
return Err(PngError::InflatedDataTooLong(max_size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match oxipng::optimize(input, output, opts) {
|
let result = oxipng::optimize(input, output, opts);
|
||||||
// For optimizing single files, this will return the correct exit code always.
|
match &result {
|
||||||
// For recursive optimization, the correct choice is a bit subjective.
|
Ok(_) => {}
|
||||||
// We're choosing to return a 0 exit code if ANY file in the set
|
|
||||||
// runs correctly.
|
|
||||||
// The reason for this is that recursion may pick up files that are not
|
|
||||||
// PNG files, and return an error for them.
|
|
||||||
// We don't really want to return an error code for those files.
|
|
||||||
Ok((insize, outsize)) => OptimizationResult::Ok(insize, outsize),
|
|
||||||
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
|
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
|
||||||
warn!("{input}: Skipped: {e}");
|
warn!("{input}: Skipped: {e}");
|
||||||
OptimizationResult::Skipped
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{input}: {e}");
|
error!("{input}: {e}");
|
||||||
OptimizationResult::Failed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write optimization results as json.
|
||||||
|
/// ```
|
||||||
|
/// {
|
||||||
|
/// "results": [
|
||||||
|
/// {
|
||||||
|
/// "input": string,
|
||||||
|
/// "status": "success",
|
||||||
|
/// "output": string|null,
|
||||||
|
/// "insize": number,
|
||||||
|
/// "outsize": number
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// "input": string,
|
||||||
|
/// "status": "error",
|
||||||
|
/// "error": string
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn json_output(files: &[(InFile, OutFile)], results: &[OptimizationResult]) {
|
||||||
|
print!(r#"{{"results":["#);
|
||||||
|
let mut first = true;
|
||||||
|
results
|
||||||
|
.iter()
|
||||||
|
.zip(files)
|
||||||
|
.for_each(|(result, (input, output))| {
|
||||||
|
if !first {
|
||||||
|
print!(",");
|
||||||
|
}
|
||||||
|
print!(r#"{{"input":"{}","#, json_escape(&input.to_string()));
|
||||||
|
match result {
|
||||||
|
Ok((insize, outsize)) => {
|
||||||
|
let outpath = match output {
|
||||||
|
OutFile::None => "null".to_owned(),
|
||||||
|
OutFile::Path { path: None, .. } => {
|
||||||
|
format!(r#""{}""#, json_escape(&input.to_string()))
|
||||||
|
}
|
||||||
|
OutFile::Path { path: Some(p), .. } => {
|
||||||
|
format!(r#""{}""#, json_escape(&p.display().to_string()))
|
||||||
|
}
|
||||||
|
OutFile::StdOut => unreachable!(),
|
||||||
|
};
|
||||||
|
print!(
|
||||||
|
r#""status":"success","output":{},"insize":{},"outsize":{}}}"#,
|
||||||
|
outpath, insize, outsize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print!(
|
||||||
|
r#""status":"error","error":"{}"}}"#,
|
||||||
|
json_escape(&e.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
});
|
||||||
|
print!("]}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_escape(string: &str) -> String {
|
||||||
|
string
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\x08", "\\b")
|
||||||
|
.replace("\x0c", "\\f")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format byte counts as IEC units to 3 significant figures.
|
/// Format byte counts as IEC units to 3 significant figures.
|
||||||
|
|
|
||||||
BIN
tests/files/json.png
Normal file
BIN
tests/files/json.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
|
|
@ -1,10 +1,12 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs::remove_file,
|
fs::remove_file,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use indexmap::indexset;
|
use indexmap::indexset;
|
||||||
use oxipng::{internal_tests::*, *};
|
use oxipng::{internal_tests::*, *};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
const GRAY: u8 = 0;
|
const GRAY: u8 = 0;
|
||||||
const RGB: u8 = 2;
|
const RGB: u8 = 2;
|
||||||
|
|
@ -662,3 +664,39 @@ fn zopfli_mode() {
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn json_success() {
|
||||||
|
let path = "tests/files/json.png";
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
|
||||||
|
.args(["--json", "--nx", "--nz", path])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to run executable");
|
||||||
|
assert!(output.status.success());
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
|
||||||
|
let result = &data["results"][0];
|
||||||
|
assert_eq!(result["input"], path);
|
||||||
|
assert_eq!(result["status"], "success");
|
||||||
|
assert_eq!(result["output"], path);
|
||||||
|
assert_eq!(result["insize"], result["outsize"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_dry_run() {
|
||||||
|
let path = "tests/files/json.png";
|
||||||
|
let path2 = "tests/files/escape chars \\ \" \n \t \r \x08 \x0c.png";
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
|
||||||
|
.args(["--json", "--dry-run", path, path2])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to run executable");
|
||||||
|
assert!(output.status.success());
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
|
||||||
|
let result = &data["results"][0];
|
||||||
|
assert_eq!(result["input"], path);
|
||||||
|
assert_eq!(result["status"], "success");
|
||||||
|
assert_eq!(result["output"], Value::Null);
|
||||||
|
let result2 = &data["results"][1];
|
||||||
|
assert_eq!(result2["input"], path2);
|
||||||
|
assert_eq!(result2["status"], "error");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue