1use std::fmt;
7use std::ops::BitOr;
8use std::str::FromStr;
9
10fn parse_set<T: Copy + BitOr<Output = T>>(
13 s: &str,
14 empty: T,
15 names: &[(T, &str)],
16) -> Result<T, String> {
17 let mut out = empty;
18 for item in s.split(',').map(str::trim).filter(|i| !i.is_empty()) {
19 if item == "none" {
20 continue;
21 }
22 let Some((value, _)) = names.iter().find(|(_, n)| *n == item) else {
23 let known: Vec<&str> = names.iter().map(|(_, n)| *n).collect();
24 return Err(format!(
25 "unknown name `{item}`; expected one of {}",
26 known.join(", ")
27 ));
28 };
29 out = out | *value;
30 }
31 Ok(out)
32}
33
34#[derive(Clone, Copy, PartialEq, Eq, Hash)]
39pub struct Codecs(u16);
40
41impl Codecs {
42 pub const NONE: Codecs = Codecs(0);
43 pub const JPEG: Codecs = Codecs(1 << 0);
45 pub const FLATE: Codecs = Codecs(1 << 1);
47 pub const G4: Codecs = Codecs(1 << 2);
49 pub const JBIG2: Codecs = Codecs(1 << 3);
51 pub const SOURCE: Codecs = Codecs(1 << 4);
53
54 pub const fn contains(self, other: Codecs) -> bool {
55 self.0 & other.0 == other.0
56 }
57
58 pub const fn is_empty(self) -> bool {
59 self.0 == 0
60 }
61
62 const NAMES: [(Codecs, &'static str); 5] = [
63 (Codecs::JPEG, "jpeg"),
64 (Codecs::FLATE, "flate"),
65 (Codecs::G4, "g4"),
66 (Codecs::JBIG2, "jbig2"),
67 (Codecs::SOURCE, "source"),
68 ];
69}
70
71impl BitOr for Codecs {
72 type Output = Codecs;
73 fn bitor(self, rhs: Codecs) -> Codecs {
74 Codecs(self.0 | rhs.0)
75 }
76}
77
78impl FromStr for Codecs {
79 type Err = String;
80 fn from_str(s: &str) -> Result<Codecs, String> {
82 parse_set(s, Codecs::NONE, &Codecs::NAMES)
83 }
84}
85
86impl fmt::Debug for Codecs {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 if self.is_empty() {
89 return write!(f, "none");
90 }
91 let names: Vec<&str> = Codecs::NAMES
92 .iter()
93 .filter(|(c, _)| self.contains(*c))
94 .map(|(_, n)| *n)
95 .collect();
96 write!(f, "{}", names.join("|"))
97 }
98}
99
100#[derive(Clone, Copy, PartialEq, Eq, Hash)]
102pub struct Strip(u16);
103
104impl Strip {
105 pub const NONE: Strip = Strip(0);
106 pub const THREADS: Strip = Strip(1 << 0);
107 pub const METADATA: Strip = Strip(1 << 1);
108 pub const PIECE_INFO: Strip = Strip(1 << 2);
109 pub const STRUCT_TREE: Strip = Strip(1 << 3);
110 pub const THUMBNAILS: Strip = Strip(1 << 4);
111 pub const SPIDER: Strip = Strip(1 << 5);
112 pub const ALTERNATES: Strip = Strip(1 << 6);
113 pub const OUTPUT_INTENTS: Strip = Strip(1 << 7);
114
115 pub const fn contains(self, other: Strip) -> bool {
116 self.0 & other.0 == other.0
117 }
118
119 const NAMES: [(Strip, &'static str); 8] = [
120 (Strip::THREADS, "threads"),
121 (Strip::METADATA, "metadata"),
122 (Strip::PIECE_INFO, "piece-info"),
123 (Strip::STRUCT_TREE, "struct-tree"),
124 (Strip::THUMBNAILS, "thumbnails"),
125 (Strip::SPIDER, "spider"),
126 (Strip::ALTERNATES, "alternates"),
127 (Strip::OUTPUT_INTENTS, "output-intents"),
128 ];
129}
130
131impl BitOr for Strip {
132 type Output = Strip;
133 fn bitor(self, rhs: Strip) -> Strip {
134 Strip(self.0 | rhs.0)
135 }
136}
137
138impl FromStr for Strip {
139 type Err = String;
140 fn from_str(s: &str) -> Result<Strip, String> {
142 parse_set(s, Strip::NONE, &Strip::NAMES)
143 }
144}
145
146impl fmt::Debug for Strip {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 if self.0 == 0 {
149 return write!(f, "none");
150 }
151 let names: Vec<&str> = Strip::NAMES
152 .iter()
153 .filter(|(s, _)| self.contains(*s))
154 .map(|(_, n)| *n)
155 .collect();
156 write!(f, "{}", names.join("|"))
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162#[non_exhaustive]
163pub enum ColorConversion {
164 None,
165 Rgb,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq)]
171pub struct Dpi {
172 pub target: f32,
174 pub threshold: f32,
177}
178
179impl Dpi {
180 pub const fn new(target: f32, threshold: f32) -> Dpi {
181 Dpi { target, threshold }
182 }
183
184 pub const fn disabled(target: f32) -> Dpi {
185 Dpi {
186 target,
187 threshold: -1.0,
188 }
189 }
190
191 pub fn enabled(&self) -> bool {
192 self.threshold >= 0.0
193 }
194
195 pub fn is_sane(&self) -> bool {
196 self.target > 0.0 && (!self.enabled() || self.threshold >= self.target)
197 }
198}
199
200#[derive(Debug, Clone, PartialEq)]
201#[non_exhaustive]
202pub struct Config {
203 pub bitonal: Codecs,
205 pub continuous: Codecs,
206 pub indexed: Codecs,
207
208 pub bitonal_dpi: Dpi,
209 pub gray_dpi: Dpi,
210 pub color_dpi: Dpi,
211
212 pub jpeg_quality: u8,
214 pub color_conversion: ColorConversion,
215 pub clip_images: bool,
217 pub reduce_color_complexity: bool,
220
221 pub subset_fonts: bool,
223 pub merge_fonts: bool,
224 pub remove_standard_fonts: bool,
225 pub convert_to_cff: bool,
226
227 pub optimize_resources: bool,
229 pub remove_redundant_objects: bool,
230 pub rebuild_content_streams: bool,
232 pub strip: Strip,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236#[non_exhaustive]
237pub enum Preset {
238 Less,
239 Standard,
240 More,
241}
242
243impl Config {
244 fn baseline() -> Config {
246 Config {
247 bitonal: Codecs::NONE,
248 continuous: Codecs::NONE,
249 indexed: Codecs::NONE,
250 bitonal_dpi: Dpi::disabled(200.0),
251 gray_dpi: Dpi::disabled(150.0),
252 color_dpi: Dpi::disabled(150.0),
253 jpeg_quality: 75,
254 color_conversion: ColorConversion::None,
255 clip_images: false,
256 reduce_color_complexity: false,
257 subset_fonts: false,
258 merge_fonts: false,
259 remove_standard_fonts: false,
260 convert_to_cff: false,
261 optimize_resources: false,
262 remove_redundant_objects: false,
263 rebuild_content_streams: false,
264 strip: Strip::NONE,
265 }
266 }
267
268 fn heavy_base() -> Config {
271 Config {
272 bitonal: Codecs::G4 | Codecs::SOURCE,
273 continuous: Codecs::JPEG | Codecs::FLATE | Codecs::SOURCE,
274 indexed: Codecs::FLATE | Codecs::SOURCE,
275 jpeg_quality: 80,
276 clip_images: true,
277 reduce_color_complexity: true,
278 subset_fonts: true,
279 merge_fonts: true,
280 convert_to_cff: true,
281 optimize_resources: true,
282 remove_redundant_objects: true,
283 rebuild_content_streams: true,
284 strip: Strip::THREADS
285 | Strip::PIECE_INFO
286 | Strip::STRUCT_TREE
287 | Strip::THUMBNAILS
288 | Strip::SPIDER,
289 ..Config::baseline()
290 }
291 }
292
293 pub fn preset(p: Preset) -> Config {
294 match p {
295 Preset::Less => Config {
296 bitonal: Codecs::JBIG2 | Codecs::SOURCE,
297 continuous: Codecs::JPEG | Codecs::SOURCE,
298 indexed: Codecs::NONE,
299 bitonal_dpi: Dpi::new(200.0, 400.0),
300 gray_dpi: Dpi::new(200.0, 400.0),
301 color_dpi: Dpi::new(200.0, 400.0),
302 jpeg_quality: 75,
303 optimize_resources: true,
304 remove_redundant_objects: true,
305 rebuild_content_streams: true,
306 remove_standard_fonts: true,
307 subset_fonts: true,
308 ..Config::baseline()
309 },
310 Preset::Standard => Config {
311 bitonal_dpi: Dpi::new(150.0, 150.0),
312 gray_dpi: Dpi::new(150.0, 150.0),
313 color_dpi: Dpi::new(150.0, 150.0),
314 jpeg_quality: 60,
315 strip: Strip::THREADS
316 | Strip::METADATA
317 | Strip::PIECE_INFO
318 | Strip::THUMBNAILS
319 | Strip::SPIDER
320 | Strip::ALTERNATES
321 | Strip::OUTPUT_INTENTS,
322 ..Config::heavy_base()
323 },
324 Preset::More => Config {
325 bitonal: Codecs::JBIG2 | Codecs::SOURCE,
326 continuous: Codecs::JPEG | Codecs::SOURCE,
327 bitonal_dpi: Dpi::new(72.0, 110.0),
328 gray_dpi: Dpi::new(72.0, 110.0),
329 color_dpi: Dpi::new(72.0, 110.0),
330 jpeg_quality: 60,
331 color_conversion: ColorConversion::Rgb,
332 remove_standard_fonts: true,
333 strip: Strip::THREADS
334 | Strip::METADATA
335 | Strip::PIECE_INFO
336 | Strip::STRUCT_TREE
337 | Strip::THUMBNAILS
338 | Strip::SPIDER
339 | Strip::ALTERNATES,
340 ..Config::heavy_base()
341 },
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn presets_are_sane() {
352 for p in [Preset::Less, Preset::Standard, Preset::More] {
353 let c = Config::preset(p);
354 assert!(c.bitonal_dpi.is_sane(), "{p:?} bitonal dpi");
355 assert!(c.gray_dpi.is_sane(), "{p:?} gray dpi");
356 assert!(c.color_dpi.is_sane(), "{p:?} color dpi");
357 assert!((1..=100).contains(&c.jpeg_quality));
358 assert!(c.bitonal.is_empty() || c.bitonal.contains(Codecs::SOURCE));
360 assert!(c.continuous.is_empty() || c.continuous.contains(Codecs::SOURCE));
361 assert!(c.indexed.is_empty() || c.indexed.contains(Codecs::SOURCE));
362 }
363 }
364
365 #[test]
366 fn standard_does_not_convert_color() {
367 assert_eq!(
368 Config::preset(Preset::Standard).color_conversion,
369 ColorConversion::None
370 );
371 }
372
373 #[test]
374 fn flag_debug_is_readable() {
375 assert_eq!(
376 format!("{:?}", Codecs::JPEG | Codecs::SOURCE),
377 "jpeg|source"
378 );
379 assert_eq!(format!("{:?}", Strip::NONE), "none");
380 }
381}