Repository: shssoichiro/oxipng (language: rust)

Issue:
Add man page for the command-line utility
I think this is useful for understanding the detailed usage of the command-line utility.


Discussion / hints:
Probably should be done concurrently w/ #472.
oxipng --help > oxipng.txt
Given so many recent updates (most of which haven't merged, much less released), I'm not aware whether even the command-line help is current.
Even like this? Sorry.
Although in version 6.0.0 they added. 
[Feature] Add --check/-c CLI option
is present in --help. They didn't add anything else.
I vaguely remember a discussion someplace in these Issues re: how out-of-date the `--help` is, & another PR recently (merged? 🤷🏾‍♂️) that is supposed to (partially? 🤔) solve that.
`--help` has been kept up-to-date with all changes, it's just the Read Me that may be out of date.

@sorairolake Not saying there shouldn't be a man page, but was there something in particular you were wanting to understand that wasn't explained sufficiently in `--help`?
@andrews05 I also think the usage in `--help` is explained sufficiently. However, I think it would be more useful to be able to see how to use `oxipng` on the web (such as <https://man.archlinux.org/>) or a console.
One easy thing we could do is append the help output to the Read Me (in some sort of automated fashion). This would at least make it easily accessible online.

Fix patch:
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.lock b/Cargo.lock
index 4b80a65b..24a07745 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.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f2e32b579dae093c2424a8b7e2bea09c89da01e1ce5065eb2f0a6f1cc15cc1f"
+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..8d9a825e 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.12"
 rustc_version = "0.4.0"
 
 [features]
@@ -98,3 +100,11 @@ opt-level = 2
 lto = "fat"
 strip = "symbols"
 panic = "abort"
+
+[package.metadata.deb]
+assets = [
+    ["target/release/oxipng", "usr/bin/", "755"],
+    ["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
new file mode 100644
index 00000000..91cd1b6d
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,34 @@
+use std::{
+    env,
+    fs::File,
+    io::{BufWriter, Error},
+    path::Path,
+};
+
+use clap_mangen::Man;
+
+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 = BufWriter::new(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=src/display_chunks.rs");
+
+    // Create `generated/assets/` folder.
+    let path = env::current_dir()?.join("generated").join("assets");
+    std::fs::create_dir_all(&path).unwrap();
+
+    build_manpages(&path)?;
+
+    Ok(())
+}
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 00000000..5b0bfee6
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,357 @@
+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,
+    // 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 <jholmer.in@gmail.com>")
+        .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 <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))
+                .conflicts_with("output_file")
+                .conflicts_with("stdout"),
+        )
+        .arg(
+            Arg::new("output_file")
+                .help("Write output file to <file>")
+                .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 <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.",
+                       DISPLAY_CHUNKS
+                           .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 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 <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")
+                .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/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 58dd56d2..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,17 +87,12 @@ pub enum StripChunks {
 }
 
 impl StripChunks {
-    /// List of chunks that affect image display and will be kept when using the `Safe` option
-    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 150d3247..e1a24435 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,365 +20,24 @@ 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::*;
 
+use crate::cli::DISPLAY_CHUNKS;
+
 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 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 <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))
-                .conflicts_with("output_file")
-                .conflicts_with("stdout"),
-        )
-        .arg(
-            Arg::new("output_file")
-                .help("Write output file to <file>")
-                .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 <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::DISPLAY
-                        .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 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 <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")
-                .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());
@@ -624,7 +283,7 @@ fn parse_opts_into_struct(
             })
             .collect::<Result<IndexSet<_>, _>>()?;
         if keep_display {
-            names.extend(StripChunks::DISPLAY.iter().cloned());
+            names.extend(DISPLAY_CHUNKS.iter().cloned());
         }
         opts.strip = StripChunks::Keep(names)
     }


Test patch: