Files
rust-imager/src/image_processor.rs
lychang b915e69222 update
2026-07-19 15:03:13 +08:00

174 lines
5.1 KiB
Rust

use crate::config::StorageConfig;
use image::GenericImageView;
use std::fs;
use std::path::Path;
use thiserror::Error;
use uuid::Uuid;
#[derive(Error, Debug)]
pub enum ImageProcessorError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Image processing error: {0}")]
Image(#[from] image::ImageError),
#[error("Invalid image data")]
InvalidData,
}
// 手动实现 From<reqwest::Error> 以支持 ? 操作符
impl From<reqwest::Error> for ImageProcessorError {
fn from(err: reqwest::Error) -> Self {
ImageProcessorError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
err.to_string(),
))
}
}
impl ImageProcessorError {
pub fn is_send(&self) -> bool {
true
}
}
pub struct ImageProcessor {
pub config: StorageConfig,
}
impl ImageProcessor {
pub fn new(config: StorageConfig) -> Self {
// 确保目录存在
fs::create_dir_all(&config.images_dir).unwrap_or(());
Self { config }
}
pub async fn save_image(
&self,
image_data: &[u8],
filename: &str,
) -> Result<ImageInfo, ImageProcessorError> {
let image_id = Uuid::new_v4();
let file_extension = Path::new(filename)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("jpg");
let new_filename = format!("{}.{}", image_id, file_extension);
let image_path = self.config.images_dir.join(&new_filename);
// 保存原始图片
fs::write(&image_path, image_data)?;
// 获取图片信息
let img = image::load_from_memory(image_data)?;
let (width, height) = img.dimensions();
let file_size = image_data.len() as i64;
// 不再生成缩略图,直接返回原图信息
Ok(ImageInfo {
id: image_id,
filename: new_filename,
width: width as i32,
height: height as i32,
file_size,
mime_type: self.detect_mime_type(file_extension),
})
}
/// 生成缩略图并保存到指定路径(使用统一尺寸)
/// 此功能已被移除,直接返回成功
pub fn generate_thumbnail(
&self,
_img: &image::DynamicImage,
_output_path: &Path,
_max_width: u32,
_max_height: u32,
) -> Result<(), ImageProcessorError> {
// 缩略图功能已移除,此函数不再执行任何操作
Ok(())
}
pub fn detect_mime_type(&self, extension: &str) -> String {
match extension.to_lowercase().as_str() {
"jpg" | "jpeg" => "image/jpeg".to_string(),
"png" => "image/png".to_string(),
"gif" => "image/gif".to_string(),
"webp" => "image/webp".to_string(),
_ => "application/octet-stream".to_string(),
}
}
/// 快速调整图片尺寸(最大宽度)
pub async fn resize_image_to_max(&self, image_data: &[u8], max_width: u32) -> Vec<u8> {
match image::load_from_memory(image_data) {
Ok(img) => {
let (width, height) = img.dimensions();
if width <= max_width {
return image_data.to_vec(); // 无需调整
}
let new_height = (height as f32 * max_width as f32 / width as f32) as u32;
let resized = img.thumbnail(max_width, new_height);
let mut output = Vec::new();
if resized
.write_to(
&mut std::io::Cursor::new(&mut output),
image::ImageOutputFormat::Jpeg(75),
)
.is_ok()
{
output
} else {
image_data.to_vec() // 失败时返回原图
}
}
Err(_) => image_data.to_vec(), // 解码失败时返回原图
}
}
pub fn get_image_url(&self, filename: &str) -> String {
format!("/images/{}", filename)
}
pub async fn download_and_save_image(
&self,
url: &str,
filename: &str,
) -> Result<ImageInfo, ImageProcessorError> {
let response = reqwest::get(url).await?;
let image_data = response.bytes().await?;
self.save_image(&image_data, filename).await
}
/// 检测图片格式
fn detect_image_format(&self, image_data: &[u8]) -> Option<String> {
if image_data.len() < 12 {
return None;
}
match &image_data[0..12] {
b"\xff\xd8\xff" => Some("JPEG".to_string()),
b"\x89PNG\r\n\x1a\n" => Some("PNG".to_string()),
b"GIF87a" => Some("GIF87a".to_string()),
b"GIF89a" => Some("GIF89a".to_string()),
b"BM" => Some("BMP".to_string()),
b"RIFF" if image_data.len() > 12 && &image_data[8..12] == b"WEBP" => {
Some("WebP".to_string())
}
_ => None,
}
}
}
#[derive(Debug)]
pub struct ImageInfo {
pub id: Uuid,
pub filename: String,
pub width: i32,
pub height: i32,
pub file_size: i64,
pub mime_type: String,
}