Overhaul help (#563)

This PR brings a big overhaul to oxipng's help, with new long form
descriptions of many options.

The full output (--help) is added as a text file MANUAL.txt. Critiques
welcome.

The short output (-h) is simplified and appears as follows:
```
Losslessly improve compression of PNG files

Usage: oxipng [OPTIONS] <files>...

Arguments:
  <files>...  File(s) to compress (use '-' for stdin)

Options:
  -o, --opt <level>       Optimization level (0-6, or max) [default: 2]
  -r, --recursive         Recurse input directories, optimizing all PNG files
      --dir <directory>   Write output file(s) to <directory>
      --out <file>        Write output file to <file>
      --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 <mode>      Strip metadata (safe, all, or comma-separated list)
                          CAUTION: 'all' will convert APNGs to standard PNGs
      --keep <list>       Strip all metadata except in the comma-separated list
  -a, --alpha             Perform additional alpha channel optimization
  -i, --interlace <type>  Set PNG interlacing type (0, 1, keep) [default: 0]
      --scale16           Forcibly reduce 16-bit images to 8-bit (lossy)
  -v, --verbose...        Run in verbose mode (use twice to increase verbosity)
  -q, --quiet             Run in quiet mode
  -f, --filters <list>    Filters to try (0-9; see '--help' for details)
      --fast              Use fast filter evaluation
      --zc <level>        Deflate compression level (1-12)
      --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
      --nz                Do not recompress unless transformations occur
      --fix               Disable checksum validation
      --force             Write the output even if it is larger than the input
  -Z, --zopfli            Use the much slower but stronger Zopfli compressor
      --timeout <secs>    Maximum amount of time to spend on optimizations
  -t, --threads <num>     Set number of threads to use [default: num CPU cores]
  -h, --help              Print help (see more with '--help')
  -V, --version           Print version

Run `oxipng --help` to see full details of all options
```

---------

Co-authored-by: Alejandro González <me@alegon.dev>
This commit is contained in:
andrews05 2023-10-10 11:39:37 +13:00 committed by GitHub
parent 0b3302316b
commit 93c3e7dfae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 346 additions and 56 deletions

View file

@ -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-*)

11
Cargo.lock generated
View file

@ -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"

View file

@ -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

178
MANUAL.txt Normal file
View file

@ -0,0 +1,178 @@
oxipng 9.0.0
Losslessly improve compression of PNG files
Usage: oxipng [OPTIONS] <files>...
Arguments:
<files>...
File(s) to compress (use '-' for stdin)
Options:
-o, --opt <level>
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 <directory>
Write output file(s) to <directory>. 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 <file>
Write output file to <file>
--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 <mode>
Strip metadata chunks, where <mode> 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
<list> => 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 <list>
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 <type>
Set the PNG interlacing type, where <type> 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 <list>
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 <level>
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 <secs>
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 <num>
Set number of threads to use [default: num CPU cores]
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version

5
scripts/manual.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cargo build
./target/debug/oxipng -V > MANUAL.txt
./target/debug/oxipng --help >> MANUAL.txt

View file

@ -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 <jholmer.in@gmail.com>")
.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 <directory>")
.long_help("\
Write output file(s) to <directory>. 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 <mode> is one of:
safe => Strip all non-critical chunks, except for the following:
{}
all => Strip all non-critical chunks
<list> => 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::<Vec<_>>()
.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 <type> 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") {