combostew/processor/
encoding_format.rs

1use std::path::Path;
2
3use crate::config::Config;
4use crate::processor::ProcessWithConfig;
5
6const DEFAULT_PIPED_OUTPUT_FORMAT: image::ImageOutputFormat = image::ImageOutputFormat::BMP;
7
8#[derive(Debug, Default)]
9pub struct EncodingFormatDecider;
10
11impl EncodingFormatDecider {
12    // return: Ok: valid extension, err: invalid i.e. no extension or no valid output path
13    fn get_output_extension(config: &Config) -> Result<String, String> {
14        match &config.output {
15            Some(v) => {
16                let path = &Path::new(v);
17                let extension = path.extension();
18
19                extension
20                    .and_then(std::ffi::OsStr::to_str)
21                    .ok_or_else(|| "No extension was found".into())
22                    .map(str::to_lowercase)
23            }
24            None => Err("No valid output path found (type: efd/ext)".into()),
25        }
26    }
27
28    fn sample_encoding(config: &Config) -> image::pnm::SampleEncoding {
29        if config.encoding_settings.pnm_settings.ascii {
30            image::pnm::SampleEncoding::Ascii
31        } else {
32            image::pnm::SampleEncoding::Binary
33        }
34    }
35
36    // <output format type as String, error message as String>
37    fn determine_format_string(config: &Config) -> Result<String, String> {
38        if let Some(v) = &config.forced_output_format {
39            Ok(v.to_lowercase())
40        } else {
41            EncodingFormatDecider::get_output_extension(config)
42        }
43    }
44
45    fn determine_format_from_str(
46        config: &Config,
47        identifier: &str,
48    ) -> Result<image::ImageOutputFormat, String> {
49        match identifier {
50            "bmp" => Ok(image::ImageOutputFormat::BMP),
51            "gif" => Ok(image::ImageOutputFormat::GIF),
52            "ico" => Ok(image::ImageOutputFormat::ICO),
53            "jpeg" | "jpg" => Ok(image::ImageOutputFormat::JPEG(
54                config.encoding_settings.jpeg_settings.quality,
55            )),
56            "png" => Ok(image::ImageOutputFormat::PNG),
57            "pbm" => {
58                let sample_encoding = EncodingFormatDecider::sample_encoding(&config);
59
60                Ok(image::ImageOutputFormat::PNM(
61                    image::pnm::PNMSubtype::Bitmap(sample_encoding),
62                ))
63            }
64            "pgm" => {
65                let sample_encoding = EncodingFormatDecider::sample_encoding(&config);
66
67                Ok(image::ImageOutputFormat::PNM(
68                    image::pnm::PNMSubtype::Graymap(sample_encoding),
69                ))
70            }
71            "ppm" => {
72                let sample_encoding = EncodingFormatDecider::sample_encoding(&config);
73
74                Ok(image::ImageOutputFormat::PNM(
75                    image::pnm::PNMSubtype::Pixmap(sample_encoding),
76                ))
77            }
78            "pam" => Ok(image::ImageOutputFormat::PNM(
79                image::pnm::PNMSubtype::ArbitraryMap,
80            )),
81            _ => Err(format!(
82                "No supported image output format was found, input: {}.",
83                identifier
84            )),
85        }
86    }
87
88    fn compute_format(config: &Config) -> Result<image::ImageOutputFormat, String> {
89        if config.output.is_none() && config.forced_output_format.is_none() {
90            return Ok(DEFAULT_PIPED_OUTPUT_FORMAT);
91        }
92
93        // 1. get the format type
94        //   a. if not -f or and we have an extension
95        //      use the extension to determine the type
96        //   b. if  -f v
97        //      use v to determine the type
98        //   c. else
99        //      unable to determine format error
100        let format = EncodingFormatDecider::determine_format_string(&config);
101
102        // 2. match on additional options such as PNM's subtype or JPEG's quality
103        //    ensure that user set cases are above default cases
104        EncodingFormatDecider::determine_format_from_str(&config, &format?)
105    }
106}
107
108impl ProcessWithConfig<Result<image::ImageOutputFormat, String>> for EncodingFormatDecider {
109    fn process(&self, config: &Config) -> Result<image::ImageOutputFormat, String> {
110        EncodingFormatDecider::compute_format(&config)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use crate::config::{
117        Config, ConfigItem, FormatEncodingSettings, JPEGEncodingSettings, PNMEncodingSettings,
118    };
119    use crate::processor::mod_test_includes::*;
120
121    use super::*;
122
123    const OUTPUT_NO_EXT: &str = "dont_care";
124    const INPUT_FORMATS: &[&str] = &[
125        "bmp", "gif", "ico", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", "pam",
126    ];
127    const EXPECTED_VALUES: &[image::ImageOutputFormat] = &[
128        image::ImageOutputFormat::BMP,
129        image::ImageOutputFormat::GIF,
130        image::ImageOutputFormat::ICO,
131        image::ImageOutputFormat::JPEG(80),
132        image::ImageOutputFormat::JPEG(80),
133        image::ImageOutputFormat::PNG,
134        image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::Bitmap(
135            image::pnm::SampleEncoding::Binary,
136        )),
137        image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::Graymap(
138            image::pnm::SampleEncoding::Binary,
139        )),
140        image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::Pixmap(
141            image::pnm::SampleEncoding::Binary,
142        )),
143        image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::ArbitraryMap),
144    ];
145
146    fn setup_dummy_config(
147        output: &str,
148        ext: &str,
149        force_format: Option<String>,
150        pnm_ascii: bool,
151    ) -> Config {
152        Config {
153            tool_name: env!("CARGO_PKG_NAME"),
154            licenses: vec![],
155            forced_output_format: force_format,
156            disable_automatic_color_type_adjustment: false,
157
158            encoding_settings: FormatEncodingSettings {
159                jpeg_settings: JPEGEncodingSettings::new_result((false, None))
160                    .expect("Invalid jpeg settings"),
161                pnm_settings: PNMEncodingSettings::new(pnm_ascii),
162            },
163
164            output: setup_output_path(&format!("{}.{}", output, ext))
165                .to_str()
166                .map(|v| v.into()),
167
168            application_specific: vec![
169                ConfigItem::OptionStringItem(None),
170                ConfigItem::OptionStringItem(None),
171            ],
172        }
173    }
174
175    fn test_with_extension(ext: &str, expected: &image::ImageOutputFormat) {
176        let output_name = &format!("encoding_processing_w_ext_{}", OUTPUT_NO_EXT);
177
178        let settings = setup_dummy_config(output_name, ext, None, false);
179
180        let conversion_processor = EncodingFormatDecider::default();
181        let result = conversion_processor
182            .process(&settings)
183            .expect("Failed to compute image format.");
184
185        assert_eq!(*expected, result);
186    }
187
188    fn test_with_force_format(format: &str, expected: &image::ImageOutputFormat) {
189        let output_name = &format!("encoding_processing_w_ff_{}", OUTPUT_NO_EXT);
190
191        let settings = setup_dummy_config(output_name, "", Some(String::from(format)), false);
192
193        let conversion_processor = EncodingFormatDecider::default();
194        let result = conversion_processor
195            .process(&settings)
196            .expect("Failed to compute image format.");
197
198        assert_eq!(*expected, result);
199    }
200
201    #[test]
202    fn test_with_extensions_with_defaults() {
203        let zipped = INPUT_FORMATS.iter().zip(EXPECTED_VALUES.iter());
204
205        for (ext, exp) in zipped {
206            println!("testing `test_with_extension`: {}", ext);
207            test_with_extension(ext, exp);
208        }
209    }
210
211    #[test]
212    fn test_with_force_formats_with_defaults() {
213        let zipped = INPUT_FORMATS.iter().zip(EXPECTED_VALUES.iter());
214
215        for (format, exp) in zipped {
216            println!("testing `test_with_force_format`: {}", format);
217            test_with_force_format(format, exp);
218        }
219    }
220
221    #[test]
222    fn test_with_extension_jpg_and_force_format_png() {
223        let output_name = &format!("encoding_processing_w_ext_and_ff_{}", OUTPUT_NO_EXT);
224
225        let settings = setup_dummy_config(output_name, "jpg", Some(String::from("png")), false);
226
227        let conversion_processor = EncodingFormatDecider::default();
228        let result = conversion_processor
229            .process(&settings)
230            .expect("Failed to compute image format.");
231
232        assert_eq!(image::ImageOutputFormat::PNG, result);
233    }
234
235    #[test]
236    fn test_with_extension_and_ascii_pbm() {
237        let output_name = &format!("encoding_processing_ascii_pbm_{}", OUTPUT_NO_EXT);
238
239        let settings = setup_dummy_config(output_name, "pbm", None, true);
240
241        let conversion_processor = EncodingFormatDecider::default();
242        let result = conversion_processor
243            .process(&settings)
244            .expect("Failed to compute image format.");
245
246        assert_eq!(
247            image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::Bitmap(
248                image::pnm::SampleEncoding::Ascii,
249            )),
250            result
251        );
252    }
253
254    #[test]
255    fn test_with_extension_and_ascii_pgm() {
256        let output_name = &format!("encoding_processing_ascii_pgm_{}", OUTPUT_NO_EXT);
257
258        let settings = setup_dummy_config(output_name, "pgm", None, true);
259
260        let conversion_processor = EncodingFormatDecider::default();
261        let result = conversion_processor
262            .process(&settings)
263            .expect("Failed to compute image format.");
264
265        assert_eq!(
266            image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::Graymap(
267                image::pnm::SampleEncoding::Ascii,
268            )),
269            result
270        );
271    }
272
273    #[test]
274    fn test_with_extension_and_ascii_ppm() {
275        let output_name = &format!("encoding_processing_ascii_ppm_{}", OUTPUT_NO_EXT);
276
277        let settings = setup_dummy_config(output_name, "ppm", None, true);
278
279        let conversion_processor = EncodingFormatDecider::default();
280        let result = conversion_processor
281            .process(&settings)
282            .expect("Failed to compute image format.");
283
284        assert_eq!(
285            image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::Pixmap(
286                image::pnm::SampleEncoding::Ascii,
287            )),
288            result
289        );
290    }
291
292    #[test]
293    fn test_with_extension_and_ascii_pam_doesnt_care() {
294        // PAM is not influenced by the PNM ascii setting
295        let output_name = &format!(
296            "encoding_processing_ascii_pam_doesnt_care_{}",
297            OUTPUT_NO_EXT
298        );
299
300        let settings = setup_dummy_config(output_name, "pam", None, true);
301
302        let conversion_processor = EncodingFormatDecider::default();
303        let result = conversion_processor
304            .process(&settings)
305            .expect("Failed to compute image format.");
306
307        assert_eq!(
308            image::ImageOutputFormat::PNM(image::pnm::PNMSubtype::ArbitraryMap),
309            result
310        );
311    }
312
313    #[test]
314    fn test_jpeg_custom_quality() {
315        let jpeg_conf = Config {
316            tool_name: env!("CARGO_PKG_NAME"),
317            licenses: vec![],
318            forced_output_format: None,
319            disable_automatic_color_type_adjustment: false,
320
321            encoding_settings: FormatEncodingSettings {
322                jpeg_settings: JPEGEncodingSettings::new_result((true, Some("40")))
323                    .expect("Invalid jpeg settings"),
324                pnm_settings: PNMEncodingSettings::new(false),
325            },
326
327            output: setup_output_path("encoding_processing_jpeg_quality_valid.jpg")
328                .to_str()
329                .map(|v| v.into()),
330
331            application_specific: vec![
332                ConfigItem::OptionStringItem(None),
333                ConfigItem::OptionStringItem(None),
334            ],
335        };
336
337        let conversion_processor = EncodingFormatDecider::default();
338        let result = conversion_processor
339            .process(&jpeg_conf)
340            .expect("Failed to compute image format.");
341
342        assert_eq!(image::ImageOutputFormat::JPEG(40), result);
343    }
344
345    #[should_panic]
346    #[test]
347    fn test_output_unsupported_extension() {
348        let jpeg_conf = Config {
349            tool_name: env!("CARGO_PKG_NAME"),
350            licenses: vec![],
351            forced_output_format: None,
352            disable_automatic_color_type_adjustment: false,
353
354            encoding_settings: FormatEncodingSettings {
355                jpeg_settings: JPEGEncodingSettings { quality: 90 },
356                pnm_settings: PNMEncodingSettings::new(false),
357            },
358
359            output: setup_output_path("encoding_processing_invalid.😉")
360                .to_str()
361                .map(|v| v.into()),
362
363            application_specific: vec![
364                ConfigItem::OptionStringItem(None),
365                ConfigItem::OptionStringItem(None),
366            ],
367        };
368
369        let conversion_processor = EncodingFormatDecider::default();
370        let _ = conversion_processor
371            .process(&jpeg_conf)
372            .expect("Failed to compute image format.");
373    }
374
375    #[should_panic]
376    #[test]
377    fn test_output_no_ext_or_ff() {
378        let jpeg_conf = Config {
379            tool_name: env!("CARGO_PKG_NAME"),
380            licenses: vec![],
381            forced_output_format: None,
382            disable_automatic_color_type_adjustment: false,
383
384            encoding_settings: FormatEncodingSettings {
385                jpeg_settings: JPEGEncodingSettings { quality: 90 },
386                pnm_settings: PNMEncodingSettings::new(false),
387            },
388
389            output: setup_output_path("encoding_processing_invalid.")
390                .to_str()
391                .map(|v| v.into()),
392
393            application_specific: vec![
394                ConfigItem::OptionStringItem(None),
395                ConfigItem::OptionStringItem(None),
396            ],
397        };
398
399        let conversion_processor = EncodingFormatDecider::default();
400        let _ = conversion_processor
401            .process(&jpeg_conf)
402            .expect("Failed to compute image format.");
403    }
404
405    #[should_panic]
406    #[test]
407    fn test_output_unsupported_ff_with_ext() {
408        let jpeg_conf = Config {
409            tool_name: env!("CARGO_PKG_NAME"),
410            licenses: vec![],
411            forced_output_format: Some("OiOi".into()), // unsupported format
412            disable_automatic_color_type_adjustment: false,
413
414            encoding_settings: FormatEncodingSettings {
415                jpeg_settings: JPEGEncodingSettings { quality: 90 },
416                pnm_settings: PNMEncodingSettings::new(false),
417            },
418
419            output: setup_output_path("encoding_processing_invalid.jpg")
420                .to_str()
421                .map(|v| v.into()),
422
423            application_specific: vec![
424                ConfigItem::OptionStringItem(None),
425                ConfigItem::OptionStringItem(None),
426            ],
427        };
428
429        let conversion_processor = EncodingFormatDecider::default();
430        let _ = conversion_processor
431            .process(&jpeg_conf)
432            .expect("Unable to save file to the test computer");
433    }
434
435    #[should_panic]
436    #[test]
437    fn test_output_unsupported_ff_without_ext() {
438        let jpeg_conf = Config {
439            tool_name: env!("CARGO_PKG_NAME"),
440            licenses: vec![],
441            forced_output_format: Some("OiOi".into()), // unsupported format
442            disable_automatic_color_type_adjustment: false,
443
444            encoding_settings: FormatEncodingSettings {
445                jpeg_settings: JPEGEncodingSettings { quality: 90 },
446                pnm_settings: PNMEncodingSettings::new(false),
447            },
448
449            output: setup_output_path("encoding_processing_invalid")
450                .to_str()
451                .map(|v| v.into()),
452
453            application_specific: vec![
454                ConfigItem::OptionStringItem(None),
455                ConfigItem::OptionStringItem(None),
456            ],
457        };
458
459        let conversion_processor = EncodingFormatDecider::default();
460        let _ = conversion_processor
461            .process(&jpeg_conf)
462            .expect("Unable to save file to the test computer");
463    }
464
465    // TODO{}: test bad cases, edges
466}