diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 28c2d5c0..8207ef60 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,13 +48,16 @@ jobs: name: Oxipng binary (${{ matrix.target }}) path: target + - name: Generate up to date manual + run: scripts/manual.sh + - name: Build archives working-directory: target run: | ARCHIVE_NAME="oxipng-${{ steps.oxipngMeta.outputs.version }}-${{ matrix.target }}" mkdir "$ARCHIVE_NAME" - cp ../CHANGELOG.md ../README.md "$ARCHIVE_NAME" + cp ../CHANGELOG.md ../README.md ../MANUAL.txt "$ARCHIVE_NAME" case '${{ matrix.target }}' in *-windows-*) diff --git a/Cargo.lock b/Cargo.lock index 5466053c..00df5fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,7 @@ dependencies = [ "bitflags", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -596,6 +597,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix", + "windows-sys", +] + [[package]] name = "typed-arena" version = "2.0.2" diff --git a/Cargo.toml b/Cargo.toml index 0120f831..147a80d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ version = "1.7.0" [dependencies.clap] optional = true version = "4.3.8" +features = ["wrap_help"] [target.'cfg(windows)'.dependencies.glob] optional = true diff --git a/MANUAL.txt b/MANUAL.txt new file mode 100644 index 00000000..cfd711fb --- /dev/null +++ b/MANUAL.txt @@ -0,0 +1,178 @@ +oxipng 9.0.0 +Losslessly improve compression of PNG files + +Usage: oxipng [OPTIONS] ... + +Arguments: + ... + File(s) to compress (use '-' for stdin) + +Options: + -o, --opt + 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. + + [default: 2] + + -r, --recursive + When directories are given as input, traverse the directory trees and optimize all PNG + files found (files with “.png” or “.apng” extension). + + --dir + 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'. + + --out + Write output file to + + --stdout + Write output to stdout + + -p, --preserve + Preserve file permissions and timestamps if possible + + -P, --pretend + Do not write any files, only show compression results + + -s + Strip safely-removable chunks, same as '--strip safe' + + --strip + 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. + + --keep + Strip all metadata except in the comma-separated list + + -a, --alpha + 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. + + -i, --interlace + 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. + + [default: 0] + + --scale16 + 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. + + -v, --verbose... + Run in verbose mode (use twice to increase verbosity) + + -q, --quiet + Run in quiet mode + + -f, --filters + 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. + + --fast + 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. + + --zc + 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. + + --nb + Do not change bit depth + + --nc + Do not change color type + + --np + Do not change color palette + + --ng + Do not change to or from grayscale + + --nx + Do not perform any transformations and do not deinterlace by default. + + --nz + 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. + + --fix + Do not perform checksum validation of PNG chunks. This may allow some files with errors to + be processed successfully. + + --force + Write the output even if it is larger than the input + + -Z, --zopfli + Use the much slower but stronger Zopfli compressor for main compression trials. + Recommended use is with '-o max' and '--fast'. + + --timeout + 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. + + -t, --threads + Set number of threads to use [default: num CPU cores] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version diff --git a/scripts/manual.sh b/scripts/manual.sh new file mode 100755 index 00000000..56e9b320 --- /dev/null +++ b/scripts/manual.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cargo build + +./target/debug/oxipng -V > MANUAL.txt +./target/debug/oxipng --help >> MANUAL.txt diff --git a/src/main.rs b/src/main.rs index 94c0cfc6..94e2f9b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,13 +35,18 @@ use std::process::exit; use std::time::Duration; 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 improves compression of PNG files") + .about("Losslessly improve compression of PNG files") .arg( Arg::new("files") - .help("File(s) to compress (use \"-\" for stdin)") + .help("File(s) to compress (use '-' for stdin)") .index(1) .num_args(1..) .use_value_delimiter(false) @@ -50,11 +55,29 @@ fn main() { ) .arg( Arg::new("optimization") - .help("Optimization level - Default: 2") + .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") - .value_parser(["0", "1", "2", "3", "4", "5", "6", "max"]), + .default_value("2") + .value_parser(["0", "1", "2", "3", "4", "5", "6", "max"]) + .hide_possible_values(true), ) .arg( Arg::new("backup") @@ -66,7 +89,10 @@ fn main() { ) .arg( Arg::new("recursive") - .help("Recurse into subdirectories and optimize all *.png/*.apng files") + .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), @@ -74,6 +100,10 @@ fn main() { .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)) @@ -99,35 +129,52 @@ fn main() { ) .arg( Arg::new("preserve") - .help("Preserve file attributes if possible") + .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 calculate compression gains") + .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 metadata objects") + .help("Strip safely-removable chunks, same as '--strip safe'") .short('s') .action(ArgAction::SetTrue) .conflicts_with("strip"), ) .arg( Arg::new("strip") - .help("Strip metadata objects ['safe', 'all', or comma-separated list]\nCAUTION: stripping 'all' will convert APNGs to standard PNGs") + .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::KEEP_SAFE + .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 optional metadata except objects in the comma-separated list") + .help("Strip all metadata except in the comma-separated list") .long("keep") .value_name("list") .conflicts_with("strip") @@ -135,28 +182,52 @@ fn main() { ) .arg( Arg::new("alpha") - .help("Perform additional alpha optimizations") + .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("PNG interlace type - Default: 0") + .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") - .value_parser(["0", "1", "keep"]), + .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") + .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 multiple times to increase verbosity)") + .help("Run in verbose mode (use twice to increase verbosity)") .short('v') .long("verbose") .action(ArgAction::Count) @@ -172,9 +243,28 @@ fn main() { ) .arg( Arg::new("filters") - .help(format!("PNG delta filters (0-{})", RowFilter::LAST)) + .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") @@ -182,13 +272,22 @@ fn main() { ) .arg( Arg::new("fast") - .help("Use fast filter evaluation (helpful when you have more filters enabled than CPU cores)") + .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("zlib compression level (1-12)") + .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) @@ -196,43 +295,52 @@ fn main() { ) .arg( Arg::new("no-bit-reduction") - .help("No bit depth reduction") + .help("Do not change bit depth") .long("nb") .action(ArgAction::SetTrue), ) .arg( Arg::new("no-color-reduction") - .help("No color type reduction") + .help("Do not change color type") .long("nc") .action(ArgAction::SetTrue), ) .arg( Arg::new("no-palette-reduction") - .help("No palette reduction") + .help("Do not change color palette") .long("np") .action(ArgAction::SetTrue), ) .arg( Arg::new("no-grayscale-reduction") - .help("No grayscale reduction") + .help("Do not change to or from grayscale") .long("ng") .action(ArgAction::SetTrue), ) .arg( Arg::new("no-reductions") - .help("No reductions or deinterlacing") + .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("No recoding of IDAT or other compressed chunks unless necessary") + .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("Enable error recovery") + .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), ) @@ -244,53 +352,37 @@ fn main() { ) .arg( Arg::new("zopfli") - .help("Use the slow but stronger Zopfli compressor (recommended use is with all filters and `--fast` enabled)") + .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, in seconds, to spend on optimizations (currently of limited use due to the shift away from zlib)") + .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") + .help("Set number of threads to use [default: num CPU cores]") .long("threads") .short('t') .value_name("num") .value_parser(value_parser!(usize)), ) - .after_help( - "Optimization levels: - -o 0 => --zc 5 --fast (1 trial, determined heuristically) - -o 1 => --zc 10 --fast (1 trial, determined heuristically) - -o 2 => --zc 11 -f 0,1,6,7 --fast (1 trial, determined by fast evaluation) - -o 3 => --zc 11 -f 0,7,8,9 (4 trials) - -o 4 => --zc 12 -f 0,7,8,9 (4 trials; same as `-o 3` for zopfli) - -o 5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials) - -o 6 => --zc 12 -f 0-9 (10 trials) - -o max => (stable alias for the max compression) - - Manually specifying a compression option (zc, f, etc.) will override the optimization preset, - regardless of the order you write the arguments. - -PNG delta filters: - 0 => None - 1 => Sub - 2 => Up - 3 => Average - 4 => Paeth -Heuristic filter selection strategies: - 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)", - ) + .after_help("Run `oxipng --help` to see full details of all options") + .after_long_help("") .get_matches_from(std::env::args()); if matches.get_flag("backup") {