Skip to content

Commit 08ea215

Browse files
committed
Separate cli command construction into new file
1 parent a426fab commit 08ea215

File tree

3 files changed

+360
-353
lines changed

3 files changed

+360
-353
lines changed

src/cli.rs

+349
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
use clap::{value_parser, Arg, ArgAction, Command};
2+
use std::path::PathBuf;
3+
4+
pub fn build_command() -> Command {
5+
// Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width.
6+
// To keep things tidy though, short help descriptions should be no more than 54 characters,
7+
// so that they can fit on a single line in an 80 character terminal.
8+
// Long help descriptions are soft wrapped here at 90 characters (column 91) but this does not
9+
// affect output, it simply matches what is rendered when help is output to a file.
10+
Command::new("oxipng")
11+
.version(env!("CARGO_PKG_VERSION"))
12+
.author("Joshua Holmer <[email protected]>")
13+
.about("Losslessly improve compression of PNG files")
14+
.arg(
15+
Arg::new("files")
16+
.help("File(s) to compress (use '-' for stdin)")
17+
.index(1)
18+
.num_args(1..)
19+
.use_value_delimiter(false)
20+
.required(true)
21+
.value_parser(value_parser!(PathBuf)),
22+
)
23+
.arg(
24+
Arg::new("optimization")
25+
.help("Optimization level (0-6, or max)")
26+
.long_help("\
27+
Set the optimization level preset. The default level 2 is quite fast and provides good \
28+
compression. Lower levels are faster, higher levels provide better compression, though \
29+
with increasingly diminishing returns.
30+
31+
0 => --zc 5 --fast (1 trial, determined heuristically)
32+
1 => --zc 10 --fast (1 trial, determined heuristically)
33+
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial)
34+
3 => --zc 11 -f 0,7,8,9 (4 trials)
35+
4 => --zc 12 -f 0,7,8,9 (4 trials)
36+
5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials)
37+
6 => --zc 12 -f 0-9 (10 trials)
38+
max => (stable alias for the max level)
39+
40+
Manually specifying a compression option (zc, f, etc.) will override the optimization \
41+
preset, regardless of the order you write the arguments.")
42+
.short('o')
43+
.long("opt")
44+
.value_name("level")
45+
.default_value("2")
46+
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"])
47+
.hide_possible_values(true),
48+
)
49+
.arg(
50+
Arg::new("backup")
51+
.help("Back up modified files")
52+
.short('b')
53+
.long("backup")
54+
.hide(true)
55+
.action(ArgAction::SetTrue),
56+
)
57+
.arg(
58+
Arg::new("recursive")
59+
.help("Recurse input directories, optimizing all PNG files")
60+
.long_help("\
61+
When directories are given as input, traverse the directory trees and optimize all PNG \
62+
files found (files with “.png” or “.apng” extension).")
63+
.short('r')
64+
.long("recursive")
65+
.action(ArgAction::SetTrue),
66+
)
67+
.arg(
68+
Arg::new("output_dir")
69+
.help("Write output file(s) to <directory>")
70+
.long_help("\
71+
Write output file(s) to <directory>. If the directory does not exist, it will be created. \
72+
Note that this will not preserve the directory structure of the input files when used with \
73+
'--recursive'.")
74+
.long("dir")
75+
.value_name("directory")
76+
.value_parser(value_parser!(PathBuf))
77+
.conflicts_with("output_file")
78+
.conflicts_with("stdout"),
79+
)
80+
.arg(
81+
Arg::new("output_file")
82+
.help("Write output file to <file>")
83+
.long("out")
84+
.value_name("file")
85+
.value_parser(value_parser!(PathBuf))
86+
.conflicts_with("output_dir")
87+
.conflicts_with("stdout"),
88+
)
89+
.arg(
90+
Arg::new("stdout")
91+
.help("Write output to stdout")
92+
.long("stdout")
93+
.action(ArgAction::SetTrue)
94+
.conflicts_with("output_dir")
95+
.conflicts_with("output_file"),
96+
)
97+
.arg(
98+
Arg::new("preserve")
99+
.help("Preserve file permissions and timestamps if possible")
100+
.short('p')
101+
.long("preserve")
102+
.action(ArgAction::SetTrue),
103+
)
104+
.arg(
105+
Arg::new("pretend")
106+
.help("Do not write any files, only show compression results")
107+
.short('P')
108+
.long("pretend")
109+
.action(ArgAction::SetTrue),
110+
)
111+
.arg(
112+
Arg::new("strip-safe")
113+
.help("Strip safely-removable chunks, same as '--strip safe'")
114+
.short('s')
115+
.action(ArgAction::SetTrue)
116+
.conflicts_with("strip"),
117+
)
118+
.arg(
119+
Arg::new("strip")
120+
.help("Strip metadata (safe, all, or comma-separated list)\nCAUTION: 'all' will convert APNGs to standard PNGs")
121+
.long_help("\
122+
Strip metadata chunks, where <mode> is one of:
123+
124+
safe => Strip all non-critical chunks, except for the following:
125+
cICP, iCCP, sRGB, pHYs, acTL, fcTL, fdAT
126+
all => Strip all non-critical chunks
127+
<list> => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM'
128+
129+
CAUTION: 'all' will convert APNGs to standard PNGs.
130+
131+
Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit \
132+
depth is changed, regardless of any options set.")
133+
.long("strip")
134+
.value_name("mode")
135+
.conflicts_with("strip-safe"),
136+
)
137+
.arg(
138+
Arg::new("keep")
139+
.help("Strip all metadata except in the comma-separated list")
140+
.long_help("\
141+
Strip all metadata chunks except those in the comma-separated list. The special value \
142+
'display' includes chunks that affect the image appearance, equivalent to '--strip safe'.
143+
144+
E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the \
145+
image appearance.")
146+
.long("keep")
147+
.value_name("list")
148+
.conflicts_with("strip")
149+
.conflicts_with("strip-safe"),
150+
)
151+
.arg(
152+
Arg::new("alpha")
153+
.help("Perform additional alpha channel optimization")
154+
.long_help("\
155+
Perform additional optimization on images with an alpha channel, by altering the color \
156+
values of fully transparent pixels. This is generally recommended for better compression, \
157+
but take care as while this is “visually lossless”, it is technically a lossy \
158+
transformation and may be unsuitable for some applications.")
159+
.short('a')
160+
.long("alpha")
161+
.action(ArgAction::SetTrue),
162+
)
163+
.arg(
164+
Arg::new("interlace")
165+
.help("Set PNG interlacing type (0, 1, keep)")
166+
.long_help("\
167+
Set the PNG interlacing type, where <type> is one of:
168+
169+
0 => Remove interlacing from all images that are processed
170+
1 => Apply Adam7 interlacing on all images that are processed
171+
keep => Keep the existing interlacing type of each image
172+
173+
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
174+
believe the benefits outweigh the costs for your use case.")
175+
.short('i')
176+
.long("interlace")
177+
.value_name("type")
178+
.default_value("0")
179+
.value_parser(["0", "1", "keep"])
180+
.hide_possible_values(true),
181+
)
182+
.arg(
183+
Arg::new("scale16")
184+
.help("Forcibly reduce 16-bit images to 8-bit (lossy)")
185+
.long_help("\
186+
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy \
187+
operation but can provide significant savings when you have no need for higher depth. \
188+
Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01 \
189+
rather than 0x00.
190+
191+
Without this flag, 16-bit images will only be reduced in depth if it can be done \
192+
losslessly.")
193+
.long("scale16")
194+
.action(ArgAction::SetTrue),
195+
)
196+
.arg(
197+
Arg::new("verbose")
198+
.help("Run in verbose mode (use twice to increase verbosity)")
199+
.short('v')
200+
.long("verbose")
201+
.action(ArgAction::Count)
202+
.conflicts_with("quiet"),
203+
)
204+
.arg(
205+
Arg::new("quiet")
206+
.help("Run in quiet mode")
207+
.short('q')
208+
.long("quiet")
209+
.action(ArgAction::SetTrue)
210+
.conflicts_with("verbose"),
211+
)
212+
.arg(
213+
Arg::new("filters")
214+
.help("Filters to try (0-9; see '--help' for details)")
215+
.long_help("\
216+
Perform compression trials with each of the given filter types. You can specify a \
217+
comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'.
218+
219+
PNG delta filters (apply the same filter to every line)
220+
0 => None (recommended to always include this filter)
221+
1 => Sub
222+
2 => Up
223+
3 => Average
224+
4 => Paeth
225+
226+
Heuristic strategies (try to find the best delta filter for each line)
227+
5 => MinSum Minimum sum of absolute differences
228+
6 => Entropy Highest Shannon entropy
229+
7 => Bigrams Lowest count of distinct bigrams
230+
8 => BigEnt Highest Shannon entropy of bigrams
231+
9 => Brute Smallest compressed size (slow)
232+
233+
The default value depends on the optimization level preset.")
234+
.short('f')
235+
.long("filters")
236+
.value_name("list"),
237+
)
238+
.arg(
239+
Arg::new("fast")
240+
.help("Use fast filter evaluation")
241+
.long_help("\
242+
Perform a fast compression evaluation of each enabled filter, followed by a single main \
243+
compression trial of the best result. Recommended if you have more filters enabled than \
244+
CPU cores.")
245+
.long("fast")
246+
.action(ArgAction::SetTrue),
247+
)
248+
.arg(
249+
Arg::new("compression")
250+
.help("Deflate compression level (1-12)")
251+
.long_help("\
252+
Deflate compression level (1-12) for main compression trials. The levels here are defined \
253+
by the libdeflate compression library.
254+
255+
The default value depends on the optimization level preset.")
256+
.long("zc")
257+
.value_name("level")
258+
.value_parser(1..=12)
259+
.conflicts_with("zopfli"),
260+
)
261+
.arg(
262+
Arg::new("no-bit-reduction")
263+
.help("Do not change bit depth")
264+
.long("nb")
265+
.action(ArgAction::SetTrue),
266+
)
267+
.arg(
268+
Arg::new("no-color-reduction")
269+
.help("Do not change color type")
270+
.long("nc")
271+
.action(ArgAction::SetTrue),
272+
)
273+
.arg(
274+
Arg::new("no-palette-reduction")
275+
.help("Do not change color palette")
276+
.long("np")
277+
.action(ArgAction::SetTrue),
278+
)
279+
.arg(
280+
Arg::new("no-grayscale-reduction")
281+
.help("Do not change to or from grayscale")
282+
.long("ng")
283+
.action(ArgAction::SetTrue),
284+
)
285+
.arg(
286+
Arg::new("no-reductions")
287+
.help("Do not perform any transformations")
288+
.long_help("\
289+
Do not perform any transformations and do not deinterlace by default.")
290+
.long("nx")
291+
.action(ArgAction::SetTrue),
292+
)
293+
.arg(
294+
Arg::new("no-recoding")
295+
.help("Do not recompress unless transformations occur")
296+
.long_help("\
297+
Do not recompress IDAT unless required due to transformations. Recompression of other \
298+
compressed chunks (such as iCCP) will also be disabled. Note that the combination of \
299+
'--nx' and '--nz' will fully disable all optimization.")
300+
.long("nz")
301+
.action(ArgAction::SetTrue),
302+
)
303+
.arg(
304+
Arg::new("fix")
305+
.help("Disable checksum validation")
306+
.long_help("\
307+
Do not perform checksum validation of PNG chunks. This may allow some files with errors to \
308+
be processed successfully.")
309+
.long("fix")
310+
.action(ArgAction::SetTrue),
311+
)
312+
.arg(
313+
Arg::new("force")
314+
.help("Write the output even if it is larger than the input")
315+
.long("force")
316+
.action(ArgAction::SetTrue),
317+
)
318+
.arg(
319+
Arg::new("zopfli")
320+
.help("Use the much slower but stronger Zopfli compressor")
321+
.long_help("\
322+
Use the much slower but stronger Zopfli compressor for main compression trials. \
323+
Recommended use is with '-o max' and '--fast'.")
324+
.short('Z')
325+
.long("zopfli")
326+
.action(ArgAction::SetTrue),
327+
)
328+
.arg(
329+
Arg::new("timeout")
330+
.help("Maximum amount of time to spend on optimizations")
331+
.long_help("\
332+
Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the \
333+
timeout before each transformation or compression trial, and will stop trying to optimize \
334+
the file if the timeout is exceeded. Note that this does not cut short any operations that \
335+
are already in progress, so it is currently of limited effectiveness for large files with \
336+
high compression levels.")
337+
.value_name("secs")
338+
.long("timeout")
339+
.value_parser(value_parser!(u64)),
340+
)
341+
.arg(
342+
Arg::new("threads")
343+
.help("Set number of threads to use [default: num CPU cores]")
344+
.long("threads")
345+
.short('t')
346+
.value_name("num")
347+
.value_parser(value_parser!(usize)),
348+
)
349+
}

src/headers.rs

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ pub enum StripChunks {
8787

8888
impl StripChunks {
8989
/// List of chunks that affect image display and will be kept when using the `Safe` option
90+
// NOTE: If this list is updated, the documentation in `cli` must also be updated
9091
pub const DISPLAY: [[u8; 4]; 7] = [
9192
*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT",
9293
];

0 commit comments

Comments
 (0)