image_optimizer/optimization/
png_optimizer.rs

1use anyhow::{Context, Result};
2use image::{DynamicImage, ImageFormat};
3use std::fs;
4use std::path::Path;
5
6use crate::cli::Cli;
7
8/// Optimizes a PNG image using oxipng with configurable optimization levels.
9///
10/// This function uses `oxipng` with configurable optimization levels (0-6 or `"max"`).
11/// Higher levels use zopfli compression for better compression at the cost of speed.
12/// It enables alpha optimization and safe chunk stripping for the best balance
13/// between file size reduction and compatibility.
14///
15/// # Arguments
16///
17/// * `input_path` - Path to the source PNG file
18/// * `output_path` - Path where the optimized PNG will be written
19/// * `args` - CLI configuration containing oxipng optimization level
20/// * `resized_img` - Optional pre-resized image data; if None, copies from `input_path`
21///
22/// # Returns
23///
24/// Returns `Ok(())` on successful optimization.
25///
26/// # Errors
27///
28/// Returns an error if:
29/// - Invalid optimization level is provided (not 0-6 or `"max"`)
30/// - PNG optimization fails
31/// - File I/O operations fail (copying or saving)
32/// - Image format conversion fails
33pub fn optimize_png(
34    input_path: &Path,
35    output_path: &Path,
36    args: &Cli,
37    resized_img: Option<DynamicImage>,
38) -> Result<()> {
39    if let Some(img) = resized_img {
40        img.save_with_format(output_path, ImageFormat::Png)?;
41    } else {
42        fs::copy(input_path, output_path)?;
43    }
44
45    let optimization_level = if args.png_optimization_level == "max" {
46        6
47    } else {
48        match args.png_optimization_level.parse::<u8>() {
49            Ok(level) if level <= 6 => level,
50            _ => {
51                return Err(anyhow::anyhow!(
52                    "Invalid oxipng optimization level: {}. Valid values are 0-6 or 'max'",
53                    args.png_optimization_level
54                ));
55            }
56        }
57    };
58
59    let mut options = oxipng::Options::from_preset(optimization_level);
60    options.optimize_alpha = true;
61    options.fast_evaluation = true;
62    options.strip = oxipng::StripChunks::Safe;
63
64    if args.no_zopfli {
65        options.deflate = oxipng::Deflaters::Libdeflater { compression: 12 };
66    } else {
67        options.deflate = oxipng::Deflaters::Zopfli {
68            iterations: args.zopfli_iterations,
69        };
70    }
71
72    let input_file = oxipng::InFile::Path(output_path.to_path_buf());
73    let output_file = oxipng::OutFile::Path {
74        path: Some(output_path.to_path_buf()),
75        preserve_attrs: true,
76    };
77
78    oxipng::optimize(&input_file, &output_file, &options).context("Failed to optimize PNG")?;
79
80    Ok(())
81}