Skip to content

第一步:创建项目

bash
cargo new image-processor
cd image-processor

Cargo.toml

toml
[package]
name = "image-processor"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
image = "0.24"
tokio = { version = "1", features = ["full"] }
rayon = "1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
walkdir = "2"
indicatif = "0.17"  # 进度条

第二步:图片格式

src/utils/format.rs

rust
use std::path::Path;

/// 支持的图片格式
#[derive(Debug, Clone, Copy)]
pub enum ImageFormat {
    Jpeg,
    Png,
    Gif,
    WebP,
    Bmp,
    Tiff,
}

impl ImageFormat {
    /// 从扩展名解析
    pub fn from_extension(ext: &str) -> Option<Self> {
        match ext.to_lowercase().as_str() {
            "jpg" | "jpeg" => Some(Self::Jpeg),
            "png" => Some(Self::Png),
            "gif" => Some(Self::Gif),
            "webp" => Some(Self::WebP),
            "bmp" => Some(Self::Bmp),
            "tiff" | "tif" => Some(Self::Tiff),
            _ => None,
        }
    }
    
    /// 从路径解析
    pub fn from_path(path: &Path) -> Option<Self> {
        path.extension()
            .and_then(|e| e.to_str())
            .and_then(Self::from_extension)
    }
    
    /// 获取扩展名
    pub fn extension(&self) -> &'static str {
        match self {
            Self::Jpeg => "jpg",
            Self::Png => "png",
            Self::Gif => "gif",
            Self::WebP => "webp",
            Self::Bmp => "bmp",
            Self::Tiff => "tiff",
        }
    }
    
    /// 转换为 image crate 格式
    pub fn to_image_format(&self) -> image::ImageFormat {
        match self {
            Self::Jpeg => image::ImageFormat::Jpeg,
            Self::Png => image::ImageFormat::Png,
            Self::Gif => image::ImageFormat::Gif,
            Self::WebP => image::ImageFormat::WebP,
            Self::Bmp => image::ImageFormat::Bmp,
            Self::Tiff => image::ImageFormat::Tiff,
        }
    }
}
▶ Run

第三步:调整大小

src/processor/resize.rs

rust
use image::{DynamicImage, GenericImageView, imageops::FilterType};

/// 调整大小选项
pub struct ResizeOptions {
    /// 宽度
    pub width: Option<u32>,
    /// 高度
    pub height: Option<u32>,
    /// 是否保持宽高比
    pub maintain_aspect: bool,
    /// 滤镜类型
    pub filter: FilterType,
}

impl Default for ResizeOptions {
    fn default() -> Self {
        Self {
            width: None,
            height: None,
            maintain_aspect: true,
            filter: FilterType::Lanczos3,
        }
    }
}

impl ResizeOptions {
    /// 创建新选项
    pub fn new() -> Self {
        Self::default()
    }
    
    /// 设置宽度
    pub fn width(mut self, width: u32) -> Self {
        self.width = Some(width);
        self
    }
    
    /// 设置高度
    pub fn height(mut self, height: u32) -> Self {
        self.height = Some(height);
        self
    }
    
    /// 设置是否保持宽高比
    pub fn maintain_aspect(mut self, maintain: bool) -> Self {
        self.maintain_aspect = maintain;
        self
    }
}

/// 调整图片大小
pub fn resize(image: &DynamicImage, options: &ResizeOptions) -> DynamicImage {
    let (orig_width, orig_height) = image.dimensions();
    
    let (new_width, new_height) = match (options.width, options.height) {
        (Some(w), Some(h)) => {
            if options.maintain_aspect {
                // 保持宽高比
                let aspect = orig_width as f32 / orig_height as f32;
                let target_aspect = w as f32 / h as f32;
                
                if aspect > target_aspect {
                    (w, (w as f32 / aspect) as u32)
                } else {
                    ((h as f32 * aspect) as u32, h)
                }
            } else {
                (w, h)
            }
        }
        (Some(w), None) => {
            let aspect = orig_height as f32 / orig_width as f32;
            (w, (w as f32 * aspect) as u32)
        }
        (None, Some(h)) => {
            let aspect = orig_width as f32 / orig_height as f32;
            ((h as f32 * aspect) as u32, h)
        }
        (None, None) => return image.clone(),
    };
    
    image.resize(new_width, new_height, options.filter)
}

/// 裁剪图片
pub fn crop(image: &DynamicImage, x: u32, y: u32, width: u32, height: u32) -> DynamicImage {
    image.crop_imm(x, y, width, height)
}

/// 缩放到适合的尺寸(不超过指定大小)
pub fn fit(image: &DynamicImage, max_width: u32, max_height: u32) -> DynamicImage {
    let (orig_width, orig_height) = image.dimensions();
    
    if orig_width <= max_width && orig_height <= max_height {
        return image.clone();
    }
    
    let width_ratio = max_width as f32 / orig_width as f32;
    let height_ratio = max_height as f32 / orig_height as f32;
    let ratio = width_ratio.min(height_ratio);
    
    let new_width = (orig_width as f32 * ratio) as u32;
    let new_height = (orig_height as f32 * ratio) as u32;
    
    image.resize(new_width, new_height, FilterType::Lanczos3)
}
▶ Run

第四步:格式转换

src/processor/convert.rs

rust
use image::{DynamicImage, ImageFormat};
use std::path::Path;
use crate::utils::format::ImageFormat;

/// 格式转换选项
pub struct ConvertOptions {
    /// 输出格式
    pub format: ImageFormat,
    /// JPEG 质量 (1-100)
    pub quality: u8,
}

impl ConvertOptions {
    /// 创建新选项
    pub fn new(format: ImageFormat) -> Self {
        Self {
            format,
            quality: 85,
        }
    }
    
    /// 设置质量
    pub fn quality(mut self, quality: u8) -> Self {
        self.quality = quality.min(100).max(1);
        self
    }
}

/// 转换图片格式
pub fn convert(image: &DynamicImage, options: &ConvertOptions) -> Vec<u8> {
    let mut buffer = std::io::Cursor::new(Vec::new());
    
    match options.format {
        ImageFormat::Jpeg => {
            let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
                &mut buffer,
                options.quality,
            );
            encoder.encode(
                image.to_rgb8().as_raw(),
                image.width(),
                image.height(),
                image::ExtendedColorType::Rgb8,
            ).ok();
        }
        _ => {
            image.write_to(&mut buffer, options.format.to_image_format()).ok();
        }
    }
    
    buffer.into_inner()
}

/// 保存图片
pub fn save(image: &DynamicImage, path: &Path, format: ImageFormat) -> anyhow::Result<()> {
    image.save_with_format(path, format.to_image_format())?;
    Ok(())
}
▶ Run

第五步:压缩优化

src/processor/compress.rs

rust
use image::{DynamicImage, ImageBuffer, Rgb};

/// 压缩选项
pub struct CompressOptions {
    /// 质量 (1-100)
    pub quality: u8,
    /// 是否移除元数据
    pub strip_metadata: bool,
}

impl Default for CompressOptions {
    fn default() -> Self {
        Self {
            quality: 80,
            strip_metadata: true,
        }
    }
}

impl CompressOptions {
    pub fn new() -> Self {
        Self::default()
    }
    
    pub fn quality(mut self, quality: u8) -> Self {
        self.quality = quality.min(100).max(1);
        self
    }
}

/// 压缩图片
pub fn compress(image: &DynamicImage, options: &CompressOptions) -> DynamicImage {
    // 简单的颜色量化压缩
    let quality_factor = options.quality as f32 / 100.0;
    
    let rgb_image = image.to_rgb8();
    let (width, height) = rgb_image.dimensions();
    
    let compressed: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_fn(
        width, height,
        |x, y| {
            let pixel = rgb_image.get_pixel(x, y);
            
            // 减少颜色深度
            let reduce = |v: u8| -> u8 {
                let factor = if quality_factor < 0.5 {
                    32.0  // 更激进的量化
                } else {
                    16.0
                };
                ((v as f32 / factor).round() * factor) as u8
            };
            
            Rgb([reduce(pixel[0]), reduce(pixel[1]), reduce(pixel[2])])
        }
    );
    
    DynamicImage::ImageRgb8(compressed)
}

/// 计算图片大小(字节)
pub fn calculate_size(image: &DynamicImage, format: ImageFormat) -> usize {
    let mut buffer = std::io::Cursor::new(Vec::new());
    image.write_to(&mut buffer, format.to_image_format()).ok();
    buffer.into_inner().len()
}
▶ Run