From fab3a437e861fa30062c18cb4f666b68f094185e Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 5 Mar 2024 20:19:33 +1300 Subject: [PATCH 1/6] Separate cli command construction into new file --- src/cli.rs | 349 +++++++++++++++++++++++++++++++++++++++++++++++ src/headers.rs | 1 + src/main.rs | 363 ++----------------------------------------------- 3 files changed, 360 insertions(+), 353 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..5b965f3a --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,349 @@ +use clap::{value_parser, Arg, ArgAction, Command}; +use std::path::PathBuf; + +pub fn build_command() -> Command { + // Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width. + // To keep things tidy though, short help descriptions should be no more than 54 characters, + // so that they can fit on a single line in an 80 character terminal. + // Long help descriptions are soft wrapped here at 90 characters (column 91) but this does not + // affect output, it simply matches what is rendered when help is output to a file. + Command::new("oxipng") + .version(env!("CARGO_PKG_VERSION")) + .author("Joshua Holmer ") + .about("Losslessly improve compression of PNG files") + .arg( + Arg::new("files") + .help("File(s) to compress (use '-' for stdin)") + .index(1) + .num_args(1..) + .use_value_delimiter(false) + .required(true) + .value_parser(value_parser!(PathBuf)), + ) + .arg( + Arg::new("optimization") + .help("Optimization level (0-6, or max)") + .long_help("\ +Set the optimization level preset. The default level 2 is quite fast and provides good \ +compression. Lower levels are faster, higher levels provide better compression, though \ +with increasingly diminishing returns. + + 0 => --zc 5 --fast (1 trial, determined heuristically) + 1 => --zc 10 --fast (1 trial, determined heuristically) + 2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial) + 3 => --zc 11 -f 0,7,8,9 (4 trials) + 4 => --zc 12 -f 0,7,8,9 (4 trials) + 5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials) + 6 => --zc 12 -f 0-9 (10 trials) + max => (stable alias for the max level) + +Manually specifying a compression option (zc, f, etc.) will override the optimization \ +preset, regardless of the order you write the arguments.") + .short('o') + .long("opt") + .value_name("level") + .default_value("2") + .value_parser(["0", "1", "2", "3", "4", "5", "6", "max"]) + .hide_possible_values(true), + ) + .arg( + Arg::new("backup") + .help("Back up modified files") + .short('b') + .long("backup") + .hide(true) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("recursive") + .help("Recurse input directories, optimizing all PNG files") + .long_help("\ +When directories are given as input, traverse the directory trees and optimize all PNG \ +files found (files with “.png” or “.apng” extension).") + .short('r') + .long("recursive") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("output_dir") + .help("Write output file(s) to ") + .long_help("\ +Write output file(s) to . If the directory does not exist, it will be created. \ +Note that this will not preserve the directory structure of the input files when used with \ +'--recursive'.") + .long("dir") + .value_name("directory") + .value_parser(value_parser!(PathBuf)) + .conflicts_with("output_file") + .conflicts_with("stdout"), + ) + .arg( + Arg::new("output_file") + .help("Write output file to ") + .long("out") + .value_name("file") + .value_parser(value_parser!(PathBuf)) + .conflicts_with("output_dir") + .conflicts_with("stdout"), + ) + .arg( + Arg::new("stdout") + .help("Write output to stdout") + .long("stdout") + .action(ArgAction::SetTrue) + .conflicts_with("output_dir") + .conflicts_with("output_file"), + ) + .arg( + Arg::new("preserve") + .help("Preserve file permissions and timestamps if possible") + .short('p') + .long("preserve") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("pretend") + .help("Do not write any files, only show compression results") + .short('P') + .long("pretend") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("strip-safe") + .help("Strip safely-removable chunks, same as '--strip safe'") + .short('s') + .action(ArgAction::SetTrue) + .conflicts_with("strip"), + ) + .arg( + Arg::new("strip") + .help("Strip metadata (safe, all, or comma-separated list)\nCAUTION: 'all' will convert APNGs to standard PNGs") + .long_help("\ +Strip metadata chunks, where is one of: + + safe => Strip all non-critical chunks, except for the following: + cICP, iCCP, sRGB, pHYs, acTL, fcTL, fdAT + all => Strip all non-critical chunks + => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM' + +CAUTION: 'all' will convert APNGs to standard PNGs. + +Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit \ +depth is changed, regardless of any options set.") + .long("strip") + .value_name("mode") + .conflicts_with("strip-safe"), + ) + .arg( + Arg::new("keep") + .help("Strip all metadata except in the comma-separated list") + .long_help("\ +Strip all metadata chunks except those in the comma-separated list. The special value \ +'display' includes chunks that affect the image appearance, equivalent to '--strip safe'. + +E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the \ +image appearance.") + .long("keep") + .value_name("list") + .conflicts_with("strip") + .conflicts_with("strip-safe"), + ) + .arg( + Arg::new("alpha") + .help("Perform additional alpha channel optimization") + .long_help("\ +Perform additional optimization on images with an alpha channel, by altering the color \ +values of fully transparent pixels. This is generally recommended for better compression, \ +but take care as while this is “visually lossless”, it is technically a lossy \ +transformation and may be unsuitable for some applications.") + .short('a') + .long("alpha") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("interlace") + .help("Set PNG interlacing type (0, 1, keep)") + .long_help("\ +Set the PNG interlacing type, where is one of: + + 0 => Remove interlacing from all images that are processed + 1 => Apply Adam7 interlacing on all images that are processed + keep => Keep the existing interlacing type of each image + +Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \ +believe the benefits outweigh the costs for your use case.") + .short('i') + .long("interlace") + .value_name("type") + .default_value("0") + .value_parser(["0", "1", "keep"]) + .hide_possible_values(true), + ) + .arg( + Arg::new("scale16") + .help("Forcibly reduce 16-bit images to 8-bit (lossy)") + .long_help("\ +Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy \ +operation but can provide significant savings when you have no need for higher depth. \ +Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01 \ +rather than 0x00. + +Without this flag, 16-bit images will only be reduced in depth if it can be done \ +losslessly.") + .long("scale16") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("verbose") + .help("Run in verbose mode (use twice to increase verbosity)") + .short('v') + .long("verbose") + .action(ArgAction::Count) + .conflicts_with("quiet"), + ) + .arg( + Arg::new("quiet") + .help("Run in quiet mode") + .short('q') + .long("quiet") + .action(ArgAction::SetTrue) + .conflicts_with("verbose"), + ) + .arg( + Arg::new("filters") + .help("Filters to try (0-9; see '--help' for details)") + .long_help("\ +Perform compression trials with each of the given filter types. You can specify a \ +comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'. + +PNG delta filters (apply the same filter to every line) + 0 => None (recommended to always include this filter) + 1 => Sub + 2 => Up + 3 => Average + 4 => Paeth + +Heuristic strategies (try to find the best delta filter for each line) + 5 => MinSum Minimum sum of absolute differences + 6 => Entropy Highest Shannon entropy + 7 => Bigrams Lowest count of distinct bigrams + 8 => BigEnt Highest Shannon entropy of bigrams + 9 => Brute Smallest compressed size (slow) + +The default value depends on the optimization level preset.") + .short('f') + .long("filters") + .value_name("list"), + ) + .arg( + Arg::new("fast") + .help("Use fast filter evaluation") + .long_help("\ +Perform a fast compression evaluation of each enabled filter, followed by a single main \ +compression trial of the best result. Recommended if you have more filters enabled than \ +CPU cores.") + .long("fast") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("compression") + .help("Deflate compression level (1-12)") + .long_help("\ +Deflate compression level (1-12) for main compression trials. The levels here are defined \ +by the libdeflate compression library. + +The default value depends on the optimization level preset.") + .long("zc") + .value_name("level") + .value_parser(1..=12) + .conflicts_with("zopfli"), + ) + .arg( + Arg::new("no-bit-reduction") + .help("Do not change bit depth") + .long("nb") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no-color-reduction") + .help("Do not change color type") + .long("nc") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no-palette-reduction") + .help("Do not change color palette") + .long("np") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no-grayscale-reduction") + .help("Do not change to or from grayscale") + .long("ng") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no-reductions") + .help("Do not perform any transformations") + .long_help("\ +Do not perform any transformations and do not deinterlace by default.") + .long("nx") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("no-recoding") + .help("Do not recompress unless transformations occur") + .long_help("\ +Do not recompress IDAT unless required due to transformations. Recompression of other \ +compressed chunks (such as iCCP) will also be disabled. Note that the combination of \ +'--nx' and '--nz' will fully disable all optimization.") + .long("nz") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("fix") + .help("Disable checksum validation") + .long_help("\ +Do not perform checksum validation of PNG chunks. This may allow some files with errors to \ +be processed successfully.") + .long("fix") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("force") + .help("Write the output even if it is larger than the input") + .long("force") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("zopfli") + .help("Use the much slower but stronger Zopfli compressor") + .long_help("\ +Use the much slower but stronger Zopfli compressor for main compression trials. \ +Recommended use is with '-o max' and '--fast'.") + .short('Z') + .long("zopfli") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("timeout") + .help("Maximum amount of time to spend on optimizations") + .long_help("\ +Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the \ +timeout before each transformation or compression trial, and will stop trying to optimize \ +the file if the timeout is exceeded. Note that this does not cut short any operations that \ +are already in progress, so it is currently of limited effectiveness for large files with \ +high compression levels.") + .value_name("secs") + .long("timeout") + .value_parser(value_parser!(u64)), + ) + .arg( + Arg::new("threads") + .help("Set number of threads to use [default: num CPU cores]") + .long("threads") + .short('t') + .value_name("num") + .value_parser(value_parser!(usize)), + ) +} diff --git a/src/headers.rs b/src/headers.rs index 58dd56d2..1035045b 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -87,6 +87,7 @@ pub enum StripChunks { impl StripChunks { /// List of chunks that affect image display and will be kept when using the `Safe` option + // NOTE: If this list is updated, the documentation in `cli` must also be updated pub const DISPLAY: [[u8; 4]; 7] = [ *b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT", ]; diff --git a/src/main.rs b/src/main.rs index 150d3247..42c707e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,365 +20,22 @@ mod rayon; use std::num::NonZeroU8; use std::{ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::exit, time::Duration}; -use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; +use clap::ArgMatches; +mod cli; use indexmap::IndexSet; use log::{error, warn, Level, LevelFilter}; use oxipng::{Deflaters, InFile, Options, OutFile, RowFilter, StripChunks}; use rayon::prelude::*; fn main() { - // Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width. - // To keep things tidy though, short help descriptions should be no more than 54 characters, - // so that they can fit on a single line in an 80 character terminal. - // Long help descriptions are soft wrapped here at 90 characters (column 91) but this does not - // affect output, it simply matches what is rendered when help is output to a file. - let matches = Command::new("oxipng") - .version(env!("CARGO_PKG_VERSION")) - .author("Joshua Holmer ") - .about("Losslessly improve compression of PNG files") - .arg( - Arg::new("files") - .help("File(s) to compress (use '-' for stdin)") - .index(1) - .num_args(1..) - .use_value_delimiter(false) - .required(true) - .value_parser(value_parser!(PathBuf)), - ) - .arg( - Arg::new("optimization") - .help("Optimization level (0-6, or max)") - .long_help("\ -Set the optimization level preset. The default level 2 is quite fast and provides good \ -compression. Lower levels are faster, higher levels provide better compression, though \ -with increasingly diminishing returns. - -0 => --zc 5 --fast (1 trial, determined heuristically) -1 => --zc 10 --fast (1 trial, determined heuristically) -2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial) -3 => --zc 11 -f 0,7,8,9 (4 trials) -4 => --zc 12 -f 0,7,8,9 (4 trials) -5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials) -6 => --zc 12 -f 0-9 (10 trials) -max => (stable alias for the max level) - -Manually specifying a compression option (zc, f, etc.) will override the optimization \ -preset, regardless of the order you write the arguments.") - .short('o') - .long("opt") - .value_name("level") - .default_value("2") - .value_parser(["0", "1", "2", "3", "4", "5", "6", "max"]) - .hide_possible_values(true), - ) - .arg( - Arg::new("backup") - .help("Back up modified files") - .short('b') - .long("backup") - .hide(true) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("recursive") - .help("Recurse input directories, optimizing all PNG files") - .long_help("\ -When directories are given as input, traverse the directory trees and optimize all PNG \ -files found (files with “.png” or “.apng” extension).") - .short('r') - .long("recursive") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("output_dir") - .help("Write output file(s) to ") - .long_help("\ -Write output file(s) to . If the directory does not exist, it will be created. \ -Note that this will not preserve the directory structure of the input files when used with \ -'--recursive'.") - .long("dir") - .value_name("directory") - .value_parser(value_parser!(PathBuf)) - .conflicts_with("output_file") - .conflicts_with("stdout"), - ) - .arg( - Arg::new("output_file") - .help("Write output file to ") - .long("out") - .value_name("file") - .value_parser(value_parser!(PathBuf)) - .conflicts_with("output_dir") - .conflicts_with("stdout"), - ) - .arg( - Arg::new("stdout") - .help("Write output to stdout") - .long("stdout") - .action(ArgAction::SetTrue) - .conflicts_with("output_dir") - .conflicts_with("output_file"), - ) - .arg( - Arg::new("preserve") - .help("Preserve file permissions and timestamps if possible") - .short('p') - .long("preserve") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("pretend") - .help("Do not write any files, only show compression results") - .short('P') - .long("pretend") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("strip-safe") - .help("Strip safely-removable chunks, same as '--strip safe'") - .short('s') - .action(ArgAction::SetTrue) - .conflicts_with("strip"), - ) - .arg( - Arg::new("strip") - .help("Strip metadata (safe, all, or comma-separated list)\nCAUTION: 'all' will convert APNGs to standard PNGs") - .long_help(format!("\ -Strip metadata chunks, where is one of: - -safe => Strip all non-critical chunks, except for the following: - {} -all => Strip all non-critical chunks - => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM' - -CAUTION: 'all' will convert APNGs to standard PNGs. - -Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit \ -depth is changed, regardless of any options set.", - StripChunks::DISPLAY - .iter() - .map(|c| String::from_utf8_lossy(c)) - .collect::>() - .join(", "))) - .long("strip") - .value_name("mode") - .conflicts_with("strip-safe"), - ) - .arg( - Arg::new("keep") - .help("Strip all metadata except in the comma-separated list") - .long_help("\ -Strip all metadata chunks except those in the comma-separated list. The special value \ -'display' includes chunks that affect the image appearance, equivalent to '--strip safe'. - -E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the \ -image appearance.") - .long("keep") - .value_name("list") - .conflicts_with("strip") - .conflicts_with("strip-safe"), - ) - .arg( - Arg::new("alpha") - .help("Perform additional alpha channel optimization") - .long_help("\ -Perform additional optimization on images with an alpha channel, by altering the color \ -values of fully transparent pixels. This is generally recommended for better compression, \ -but take care as while this is “visually lossless”, it is technically a lossy \ -transformation and may be unsuitable for some applications.") - .short('a') - .long("alpha") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("interlace") - .help("Set PNG interlacing type (0, 1, keep)") - .long_help("\ -Set the PNG interlacing type, where is one of: - -0 => Remove interlacing from all images that are processed -1 => Apply Adam7 interlacing on all images that are processed -keep => Keep the existing interlacing type of each image - -Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \ -believe the benefits outweigh the costs for your use case.") - .short('i') - .long("interlace") - .value_name("type") - .default_value("0") - .value_parser(["0", "1", "keep"]) - .hide_possible_values(true), - ) - .arg( - Arg::new("scale16") - .help("Forcibly reduce 16-bit images to 8-bit (lossy)") - .long_help("\ -Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy \ -operation but can provide significant savings when you have no need for higher depth. \ -Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01 \ -rather than 0x00. - -Without this flag, 16-bit images will only be reduced in depth if it can be done \ -losslessly.") - .long("scale16") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("verbose") - .help("Run in verbose mode (use twice to increase verbosity)") - .short('v') - .long("verbose") - .action(ArgAction::Count) - .conflicts_with("quiet"), - ) - .arg( - Arg::new("quiet") - .help("Run in quiet mode") - .short('q') - .long("quiet") - .action(ArgAction::SetTrue) - .conflicts_with("verbose"), - ) - .arg( - Arg::new("filters") - .help(format!("Filters to try (0-{}; see '--help' for details)", RowFilter::LAST)) - .long_help("\ -Perform compression trials with each of the given filter types. You can specify a \ -comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'. - -PNG delta filters (apply the same filter to every line) - 0 => None (recommended to always include this filter) - 1 => Sub - 2 => Up - 3 => Average - 4 => Paeth -Heuristic strategies (try to find the best delta filter for each line) - 5 => MinSum Minimum sum of absolute differences - 6 => Entropy Highest Shannon entropy - 7 => Bigrams Lowest count of distinct bigrams - 8 => BigEnt Highest Shannon entropy of bigrams - 9 => Brute Smallest compressed size (slow) - -The default value depends on the optimization level preset.") - .short('f') - .long("filters") - .value_name("list") - .value_parser(|x: &str| { - parse_numeric_range_opts(x, 0, RowFilter::LAST) - .map_err(|_| "Invalid option for filters") - }), - ) - .arg( - Arg::new("fast") - .help("Use fast filter evaluation") - .long_help("\ -Perform a fast compression evaluation of each enabled filter, followed by a single main \ -compression trial of the best result. Recommended if you have more filters enabled than \ -CPU cores.") - .long("fast") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("compression") - .help("Deflate compression level (1-12)") - .long_help("\ -Deflate compression level (1-12) for main compression trials. The levels here are defined \ -by the libdeflate compression library. - -The default value depends on the optimization level preset.") - .long("zc") - .value_name("level") - .value_parser(1..=12) - .conflicts_with("zopfli"), - ) - .arg( - Arg::new("no-bit-reduction") - .help("Do not change bit depth") - .long("nb") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("no-color-reduction") - .help("Do not change color type") - .long("nc") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("no-palette-reduction") - .help("Do not change color palette") - .long("np") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("no-grayscale-reduction") - .help("Do not change to or from grayscale") - .long("ng") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("no-reductions") - .help("Do not perform any transformations") - .long_help("\ -Do not perform any transformations and do not deinterlace by default.") - .long("nx") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("no-recoding") - .help("Do not recompress unless transformations occur") - .long_help("\ -Do not recompress IDAT unless required due to transformations. Recompression of other \ -compressed chunks (such as iCCP) will also be disabled. Note that the combination of \ -'--nx' and '--nz' will fully disable all optimization.") - .long("nz") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("fix") - .help("Disable checksum validation") - .long_help("\ -Do not perform checksum validation of PNG chunks. This may allow some files with errors to \ -be processed successfully.") - .long("fix") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("force") - .help("Write the output even if it is larger than the input") - .long("force") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("zopfli") - .help("Use the much slower but stronger Zopfli compressor") - .long_help("\ -Use the much slower but stronger Zopfli compressor for main compression trials. \ -Recommended use is with '-o max' and '--fast'.") - .short('Z') - .long("zopfli") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("timeout") - .help("Maximum amount of time to spend on optimizations") - .long_help("\ -Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the \ -timeout before each transformation or compression trial, and will stop trying to optimize \ -the file if the timeout is exceeded. Note that this does not cut short any operations that \ -are already in progress, so it is currently of limited effectiveness for large files with \ -high compression levels.") - .value_name("secs") - .long("timeout") - .value_parser(value_parser!(u64)), - ) - .arg( - Arg::new("threads") - .help("Set number of threads to use [default: num CPU cores]") - .long("threads") - .short('t') - .value_name("num") - .value_parser(value_parser!(usize)), - ) + let matches = cli::build_command() + // Set the value parser for filters which isn't appropriate to do in the build_command function + .mut_arg("filters", |arg| { + arg.value_parser(|x: &str| { + parse_numeric_range_opts(x, 0, RowFilter::LAST) + .map_err(|_| "Invalid option for filters") + }) + }) .after_help("Run `oxipng --help` to see full details of all options") .after_long_help("") .get_matches_from(std::env::args()); From 58b37840ffd19014b479932d00a383f2106ea216 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 5 Mar 2024 20:20:45 +1300 Subject: [PATCH 2/6] Add build script for man generation --- Cargo.lock | 17 +++++++++++++++++ Cargo.toml | 2 ++ build.rs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 build.rs diff --git a/Cargo.lock b/Cargo.lock index 4b80a65b..bb3deb30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "clap_mangen" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -455,6 +465,7 @@ version = "9.0.0" dependencies = [ "bitvec", "clap", + "clap_mangen", "crossbeam-channel", "env_logger", "filetime", @@ -529,6 +540,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + [[package]] name = "rustc-hash" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 1808a1f1..5357b8f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,8 @@ features = ["png"] version = "0.24.6" [build-dependencies] +clap = "4.3.8" +clap_mangen = "0.2.20" rustc_version = "0.4.0" [features] diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..4063d01d --- /dev/null +++ b/build.rs @@ -0,0 +1,35 @@ +use clap_mangen::Man; +use std::{env, fs::File, io::Error, path::Path}; + +include!("src/cli.rs"); + +fn build_manpages(outdir: &Path) -> Result<(), Error> { + let app = build_command(); + + let file = Path::new(&outdir).join("oxipng.1"); + let mut file = File::create(file)?; + + Man::new(app).render(&mut file)?; + + Ok(()) +} + +fn main() -> Result<(), Error> { + println!("cargo:rerun-if-changed=src/cli.rs"); + println!("cargo:rerun-if-changed=man"); + + let outdir = match env::var_os("OUT_DIR") { + None => return Ok(()), + Some(outdir) => outdir, + }; + + // Create `target/assets/` folder. + let out_path = PathBuf::from(outdir); + let mut path = out_path.ancestors().nth(4).unwrap().to_owned(); + path.push("assets"); + std::fs::create_dir_all(&path).unwrap(); + + build_manpages(&path)?; + + Ok(()) +} From 4109dd5743d59050f1f0c6c468d153cb81a86489 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 5 Mar 2024 20:42:02 +1300 Subject: [PATCH 3/6] Downgrade clap_mangen --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb3deb30..24a07745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clap_mangen" -version = "0.2.20" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" +checksum = "8f2e32b579dae093c2424a8b7e2bea09c89da01e1ce5065eb2f0a6f1cc15cc1f" dependencies = [ "clap", "roff", diff --git a/Cargo.toml b/Cargo.toml index 5357b8f1..cbd4dfd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ version = "0.24.6" [build-dependencies] clap = "4.3.8" -clap_mangen = "0.2.20" +clap_mangen = "0.2.12" rustc_version = "0.4.0" [features] From 64e1a04c406e530749e8cca37fd5102c7ac3ca49 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 17 Mar 2024 14:08:29 +1300 Subject: [PATCH 4/6] Specify assets for cargo-deb --- Cargo.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index cbd4dfd4..8172c66c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,3 +100,11 @@ opt-level = 2 lto = "fat" strip = "symbols" panic = "abort" + +[package.metadata.deb] +assets = [ + ["target/release/oxipng", "usr/bin/", "755"], + ["target/assets/oxipng.1", "usr/share/man/man1/", "644"], + ["README.md", "usr/share/doc/oxipng/", "644"], + ["CHANGELOG.md", "usr/share/doc/oxipng/", "644"], +] From d7f6757a7845809af14524dafdcddfe9d2929208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 11:57:53 +0100 Subject: [PATCH 5/6] Generate man pages to non-Cargo directory This is much less fragile than messing with `OUT_DIR`, whose location relative to the `target` directory is purposefully underspecified. --- .gitignore | 1 + Cargo.toml | 2 +- build.rs | 23 +++++++++++------------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index c03f402b..868a47a4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target *.out.png /.idea /node_modules +/generated diff --git a/Cargo.toml b/Cargo.toml index 8172c66c..8d9a825e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ panic = "abort" [package.metadata.deb] assets = [ ["target/release/oxipng", "usr/bin/", "755"], - ["target/assets/oxipng.1", "usr/share/man/man1/", "644"], + ["generated/assets/oxipng.1", "usr/share/man/man1/", "644"], ["README.md", "usr/share/doc/oxipng/", "644"], ["CHANGELOG.md", "usr/share/doc/oxipng/", "644"], ] diff --git a/build.rs b/build.rs index 4063d01d..91cd1b6d 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,11 @@ +use std::{ + env, + fs::File, + io::{BufWriter, Error}, + path::Path, +}; + use clap_mangen::Man; -use std::{env, fs::File, io::Error, path::Path}; include!("src/cli.rs"); @@ -7,7 +13,7 @@ fn build_manpages(outdir: &Path) -> Result<(), Error> { let app = build_command(); let file = Path::new(&outdir).join("oxipng.1"); - let mut file = File::create(file)?; + let mut file = BufWriter::new(File::create(file)?); Man::new(app).render(&mut file)?; @@ -16,17 +22,10 @@ fn build_manpages(outdir: &Path) -> Result<(), Error> { fn main() -> Result<(), Error> { println!("cargo:rerun-if-changed=src/cli.rs"); - println!("cargo:rerun-if-changed=man"); - - let outdir = match env::var_os("OUT_DIR") { - None => return Ok(()), - Some(outdir) => outdir, - }; + println!("cargo:rerun-if-changed=src/display_chunks.rs"); - // Create `target/assets/` folder. - let out_path = PathBuf::from(outdir); - let mut path = out_path.ancestors().nth(4).unwrap().to_owned(); - path.push("assets"); + // Create `generated/assets/` folder. + let path = env::current_dir()?.join("generated").join("assets"); std::fs::create_dir_all(&path).unwrap(); build_manpages(&path)?; From 3ce88ebbcedce0fbdd6e13dc441fe77763da54e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 12:02:39 +0100 Subject: [PATCH 6/6] Bring back generation of help strings from `DISPLAY` chunks constant This is done by extracting such a constant to a separate module file, outside of `StripChunks`. --- src/cli.rs | 16 ++++++++++++---- src/display_chunks.rs | 4 ++++ src/headers.rs | 9 ++------- src/lib.rs | 1 + src/main.rs | 4 +++- 5 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 src/display_chunks.rs diff --git a/src/cli.rs b/src/cli.rs index 5b965f3a..5b0bfee6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,9 @@ -use clap::{value_parser, Arg, ArgAction, Command}; use std::path::PathBuf; +use clap::{value_parser, Arg, ArgAction, Command}; + +include!("display_chunks.rs"); + pub fn build_command() -> Command { // Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width. // To keep things tidy though, short help descriptions should be no more than 54 characters, @@ -118,18 +121,23 @@ Note that this will not preserve the directory structure of the input files when .arg( Arg::new("strip") .help("Strip metadata (safe, all, or comma-separated list)\nCAUTION: 'all' will convert APNGs to standard PNGs") - .long_help("\ + .long_help(format!("\ Strip metadata chunks, where is one of: safe => Strip all non-critical chunks, except for the following: - cICP, iCCP, sRGB, pHYs, acTL, fcTL, fdAT + {} all => Strip all non-critical chunks => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM' CAUTION: 'all' will convert APNGs to standard PNGs. Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit \ -depth is changed, regardless of any options set.") +depth is changed, regardless of any options set.", + DISPLAY_CHUNKS + .iter() + .map(|c| String::from_utf8_lossy(c)) + .collect::>() + .join(", "))) .long("strip") .value_name("mode") .conflicts_with("strip-safe"), diff --git a/src/display_chunks.rs b/src/display_chunks.rs new file mode 100644 index 00000000..7a5e5187 --- /dev/null +++ b/src/display_chunks.rs @@ -0,0 +1,4 @@ +/// List of chunks that affect image display and will be kept when using the `Safe` chunk strip option +pub const DISPLAY_CHUNKS: [[u8; 4]; 7] = [ + *b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT", +]; diff --git a/src/headers.rs b/src/headers.rs index 1035045b..c85257ae 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -5,6 +5,7 @@ use rgb::{RGB16, RGBA8}; use crate::{ colors::{BitDepth, ColorType}, deflate::{crc32, inflate}, + display_chunks::DISPLAY_CHUNKS, error::PngError, interlace::Interlacing, AtomicMin, Deflaters, PngResult, @@ -86,18 +87,12 @@ pub enum StripChunks { } impl StripChunks { - /// List of chunks that affect image display and will be kept when using the `Safe` option - // NOTE: If this list is updated, the documentation in `cli` must also be updated - pub const DISPLAY: [[u8; 4]; 7] = [ - *b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT", - ]; - pub(crate) fn keep(&self, name: &[u8; 4]) -> bool { match &self { StripChunks::None => true, StripChunks::Keep(names) => names.contains(name), StripChunks::Strip(names) => !names.contains(name), - StripChunks::Safe => Self::DISPLAY.contains(name), + StripChunks::Safe => DISPLAY_CHUNKS.contains(name), StripChunks::All => false, } } diff --git a/src/lib.rs b/src/lib.rs index 8f9122b0..ae3d522b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,7 @@ pub use crate::{ mod atomicmin; mod colors; mod deflate; +mod display_chunks; mod error; mod evaluate; mod filters; diff --git a/src/main.rs b/src/main.rs index 42c707e9..e1a24435 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,8 @@ use log::{error, warn, Level, LevelFilter}; use oxipng::{Deflaters, InFile, Options, OutFile, RowFilter, StripChunks}; use rayon::prelude::*; +use crate::cli::DISPLAY_CHUNKS; + fn main() { let matches = cli::build_command() // Set the value parser for filters which isn't appropriate to do in the build_command function @@ -281,7 +283,7 @@ fn parse_opts_into_struct( }) .collect::, _>>()?; if keep_display { - names.extend(StripChunks::DISPLAY.iter().cloned()); + names.extend(DISPLAY_CHUNKS.iter().cloned()); } opts.strip = StripChunks::Keep(names) }