use anyhow::{Context, Result};
use image::{DynamicImage, GenericImageView, imageops};
use rayon::prelude::*;
use std::fs;
use std::path::Path;
use webp::{Encoder, WebPMemory};
pub struct ImageConfig {
pub quality: f32,
pub scale_factor: f64,
}
impl Default for ImageConfig {
fn default() -> Self {
Self {
quality: 85.0,
scale_factor: 1.0,
}
}
}
pub fn optimize_images<P: AsRef<Path>>(
input_dir: P,
output_dir: P,
config: &ImageConfig,
) -> Result<()> {
let input_dir = input_dir.as_ref();
let output_dir = output_dir.as_ref();
if !input_dir.exists() {
return Ok(());
}
fs::create_dir_all(output_dir)?;
let image_files: Vec<_> = fs::read_dir(input_dir)?
.filter_map(|entry| entry.ok())
.filter(|entry| {
let path = entry.path();
path.extension().is_some_and(|ext| {
let ext = ext.to_string_lossy().to_lowercase();
ext == "jpg" || ext == "jpeg" || ext == "png"
})
})
.collect();
image_files.par_iter().try_for_each(|entry| {
let path = entry.path();
optimize_image_to_dir(&path, output_dir, config)
})?;
Ok(())
}
fn optimize_image_to_dir(input_path: &Path, output_dir: &Path, config: &ImageConfig) -> Result<()> {
let img = image::open(input_path)
.with_context(|| format!("Failed to open image: {:?}", input_path))?;
let (w, h) = img.dimensions();
let img = if config.scale_factor < 1.0 {
let new_w = (w as f64 * config.scale_factor) as u32;
let new_h = (h as f64 * config.scale_factor) as u32;
DynamicImage::ImageRgba8(imageops::resize(
&img,
new_w,
new_h,
imageops::FilterType::Triangle,
))
} else {
img
};
let stem = input_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("image");
let encoder = Encoder::from_image(&img)
.map_err(|e| anyhow::anyhow!("Failed to create WebP encoder: {}", e))?;
let webp: WebPMemory = encoder.encode(config.quality);
let webp_path = output_dir.join(format!("{}.webp", stem));
fs::write(&webp_path, &*webp)
.with_context(|| format!("Failed to write WebP: {:?}", webp_path))?;
let file_name = input_path
.file_name()
.ok_or_else(|| anyhow::anyhow!("Invalid file path: {:?}", input_path))?;
let original_path = output_dir.join(file_name);
fs::copy(input_path, &original_path)
.with_context(|| format!("Failed to copy original: {:?}", original_path))?;
Ok(())
}
pub fn optimize_single_image(
input_path: &Path,
output_path: &Path,
config: &ImageConfig,
) -> Result<()> {
let img = image::open(input_path)
.with_context(|| format!("Failed to open image: {:?}", input_path))?;
let (w, h) = img.dimensions();
let img = if config.scale_factor < 1.0 {
let new_w = (w as f64 * config.scale_factor) as u32;
let new_h = (h as f64 * config.scale_factor) as u32;
DynamicImage::ImageRgba8(imageops::resize(
&img,
new_w,
new_h,
imageops::FilterType::Triangle,
))
} else {
img
};
let output_dir = output_path
.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid output path: {:?}", output_path))?;
let stem = output_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("image");
let encoder = Encoder::from_image(&img)
.map_err(|e| anyhow::anyhow!("Failed to create WebP encoder: {}", e))?;
let webp: WebPMemory = encoder.encode(config.quality);
let webp_path = output_dir.join(format!("{}.webp", stem));
fs::write(&webp_path, &*webp)
.with_context(|| format!("Failed to write WebP: {:?}", webp_path))?;
fs::copy(input_path, output_path)
.with_context(|| format!("Failed to copy original: {:?}", output_path))?;
Ok(())
}