use crate::config::Config;
use anyhow::Result;
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView, ImageFormat};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
const SUPPORTED_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "gif", "webp", "bmp", "ico"];
pub fn process_images(config: &Config) -> Result<()> {
let images_dir = config.source_dir.join("images");
let output_dir = config.output_dir.join("images");
if !images_dir.exists() {
tracing::info!("No images directory found, skipping image processing");
return Ok(());
}
std::fs::create_dir_all(&output_dir)?;
let image_files: Vec<PathBuf> = WalkDir::new(&images_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter(|e| {
e.path()
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| SUPPORTED_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
.unwrap_or(false)
})
.map(|e| e.path().to_path_buf())
.collect();
tracing::info!("Processing {} images", image_files.len());
let results: Vec<Result<(), anyhow::Error>> = image_files
.par_iter()
.map(|path| {
let relative = path.strip_prefix(&images_dir).unwrap_or(path);
let output_path = output_dir.join(relative);
if !config.force_image_rebuild && output_path.exists() {
if let (Ok(src_meta), Ok(dst_meta)) =
(std::fs::metadata(path), std::fs::metadata(&output_path))
{
if let (Ok(src_time), Ok(dst_time)) = (src_meta.modified(), dst_meta.modified())
{
if dst_time >= src_time {
tracing::debug!("Skipping unchanged image: {:?}", path);
return Ok(());
}
}
}
}
process_single_image(path, &output_path, config)
})
.collect();
for result in results {
if let Err(e) = result {
tracing::warn!("Image processing error: {}", e);
}
}
Ok(())
}
fn process_single_image(input: &Path, output: &Path, config: &Config) -> Result<()> {
tracing::debug!("Processing image: {:?}", input);
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)?;
}
let img = image::open(input)?;
let img = strip_exif(img);
let img = resize_image(img, config.max_image_width, config.max_image_height);
let format = get_output_format(input);
save_image(&img, output, format, config.image_quality)?;
Ok(())
}
fn strip_exif(img: DynamicImage) -> DynamicImage {
img
}
fn resize_image(img: DynamicImage, max_width: u32, max_height: u32) -> DynamicImage {
let (width, height) = img.dimensions();
if width <= max_width && height <= max_height {
return img;
}
let ratio_w = max_width as f64 / width as f64;
let ratio_h = max_height as f64 / height as f64;
let ratio = ratio_w.min(ratio_h);
let new_width = (width as f64 * ratio) as u32;
let new_height = (height as f64 * ratio) as u32;
img.resize(new_width, new_height, FilterType::Lanczos3)
}
fn get_output_format(path: &Path) -> ImageFormat {
path.extension()
.and_then(|ext| ext.to_str())
.and_then(|ext| match ext.to_lowercase().as_str() {
"jpg" | "jpeg" => Some(ImageFormat::Jpeg),
"png" => Some(ImageFormat::Png),
"gif" => Some(ImageFormat::Gif),
"webp" => Some(ImageFormat::WebP),
"bmp" => Some(ImageFormat::Bmp),
"ico" => Some(ImageFormat::Ico),
_ => None,
})
.unwrap_or(ImageFormat::Jpeg)
}
fn save_image(img: &DynamicImage, path: &Path, format: ImageFormat, quality: u8) -> Result<()> {
match format {
ImageFormat::Jpeg => {
let mut file = std::fs::File::create(path)?;
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut file, quality);
img.write_with_encoder(encoder)?;
}
ImageFormat::Png => {
img.save_with_format(path, ImageFormat::Png)?;
}
ImageFormat::WebP => {
img.save_with_format(path, ImageFormat::WebP)?;
}
_ => {
img.save(path)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_output_format() {
assert_eq!(get_output_format(Path::new("test.jpg")), ImageFormat::Jpeg);
assert_eq!(get_output_format(Path::new("test.png")), ImageFormat::Png);
assert_eq!(get_output_format(Path::new("test.webp")), ImageFormat::WebP);
}
#[test]
fn test_resize_no_change() {
let img = DynamicImage::new_rgb8(100, 100);
let resized = resize_image(img, 200, 200);
assert_eq!(resized.dimensions(), (100, 100));
}
#[test]
fn test_resize_needed() {
let img = DynamicImage::new_rgb8(2000, 1000);
let resized = resize_image(img, 1000, 500);
let (w, h) = resized.dimensions();
assert!(w <= 1000);
assert!(h <= 500);
}
}