perf: improve compression reliability and deployment safety
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
use crate::error::{AppError, ErrorCode};
|
||||
use crate::state::AppState;
|
||||
|
||||
use img_parts::{Bytes as ImgBytes, DynImage, ImageEXIF, ImageICC};
|
||||
use image::codecs::bmp::BmpEncoder;
|
||||
use image::codecs::gif::{GifDecoder, GifEncoder};
|
||||
use image::codecs::ico::IcoEncoder;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::codecs::png::PngEncoder;
|
||||
use image::codecs::tiff::TiffEncoder;
|
||||
use image::{DynamicImage, ExtendedColorType, ImageEncoder};
|
||||
use image::{AnimationDecoder, GenericImageView};
|
||||
use image::{DynamicImage, ExtendedColorType, ImageEncoder};
|
||||
use img_parts::{Bytes as ImgBytes, DynImage, ImageEXIF, ImageICC};
|
||||
use oxipng::StripChunks;
|
||||
use rgb::FromSlice;
|
||||
use std::io::Cursor;
|
||||
@@ -105,15 +105,12 @@ pub fn parse_level(value: &str) -> Result<CompressionLevel, AppError> {
|
||||
}
|
||||
|
||||
pub fn parse_compression_rate(value: &str) -> Result<u8, AppError> {
|
||||
let rate: u8 = value
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| {
|
||||
AppError::new(
|
||||
ErrorCode::InvalidRequest,
|
||||
"compression_rate 需为 1-100 的整数(压缩后体积占比)",
|
||||
)
|
||||
})?;
|
||||
let rate: u8 = value.trim().parse().map_err(|_| {
|
||||
AppError::new(
|
||||
ErrorCode::InvalidRequest,
|
||||
"compression_rate 需为 1-100 的整数(压缩后体积占比)",
|
||||
)
|
||||
})?;
|
||||
if !(1..=100).contains(&rate) {
|
||||
return Err(AppError::new(
|
||||
ErrorCode::InvalidRequest,
|
||||
@@ -199,8 +196,7 @@ pub fn detect_format(bytes: &[u8]) -> Result<ImageFmt, AppError> {
|
||||
if has_brand(
|
||||
&brands,
|
||||
&[
|
||||
*b"heic", *b"heix", *b"hevc", *b"hevx", *b"heis", *b"heim", *b"mif1",
|
||||
*b"msf1",
|
||||
*b"heic", *b"heix", *b"hevc", *b"hevx", *b"heis", *b"heim", *b"mif1", *b"msf1",
|
||||
],
|
||||
) {
|
||||
return Err(AppError::new(
|
||||
@@ -233,27 +229,69 @@ pub fn detect_format(bytes: &[u8]) -> Result<ImageFmt, AppError> {
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn compress_image_bytes(
|
||||
state: &AppState,
|
||||
input: &[u8],
|
||||
input: Vec<u8>,
|
||||
format_in: ImageFmt,
|
||||
format_out: ImageFmt,
|
||||
level: CompressionLevel,
|
||||
compression_rate: Option<u8>,
|
||||
target_size_bytes: Option<u64>, // 新增:直接指定目标大小(字节)
|
||||
target_size_bytes: Option<u64>, // 新增:直接指定目标大小(字节)
|
||||
max_width: Option<u32>,
|
||||
max_height: Option<u32>,
|
||||
preserve_metadata: bool,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
let max_image_pixels = state.config.max_image_pixels;
|
||||
let permit = state
|
||||
.image_processing_semaphore
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::Internal, "图片处理并发控制器已关闭").with_source(err)
|
||||
})?;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let _permit = permit;
|
||||
compress_image_bytes_sync(
|
||||
input,
|
||||
format_in,
|
||||
format_out,
|
||||
level,
|
||||
compression_rate,
|
||||
target_size_bytes,
|
||||
max_width,
|
||||
max_height,
|
||||
preserve_metadata,
|
||||
max_image_pixels,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "图片处理任务异常退出").with_source(err)
|
||||
})?
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn compress_image_bytes_sync(
|
||||
input: Vec<u8>,
|
||||
format_in: ImageFmt,
|
||||
format_out: ImageFmt,
|
||||
level: CompressionLevel,
|
||||
compression_rate: Option<u8>,
|
||||
target_size_bytes: Option<u64>,
|
||||
max_width: Option<u32>,
|
||||
max_height: Option<u32>,
|
||||
preserve_metadata: bool,
|
||||
max_image_pixels: u64,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
let original_size = input.len() as u64;
|
||||
if format_in == ImageFmt::Gif {
|
||||
if is_animated_gif(input)? {
|
||||
return Err(AppError::new(
|
||||
ErrorCode::UnsupportedFormat,
|
||||
"暂不支持动图 GIF",
|
||||
));
|
||||
}
|
||||
if format_in == ImageFmt::Gif && is_animated_gif(&input)? {
|
||||
return Err(AppError::new(
|
||||
ErrorCode::UnsupportedFormat,
|
||||
"暂不支持动图 GIF",
|
||||
));
|
||||
}
|
||||
|
||||
let retention_rate = effective_rate(compression_rate, level);
|
||||
@@ -269,14 +307,14 @@ pub async fn compress_image_bytes(
|
||||
&& max_height.is_none()
|
||||
{
|
||||
if preserve_metadata {
|
||||
return Ok(input.to_vec());
|
||||
return Ok(input);
|
||||
}
|
||||
let stripped = strip_metadata(input).unwrap_or_else(|_| input.to_vec());
|
||||
let stripped = strip_metadata(&input).unwrap_or_else(|_| input.clone());
|
||||
return Ok(stripped);
|
||||
}
|
||||
|
||||
let (icc_profile, exif) = if preserve_metadata {
|
||||
extract_metadata(input)
|
||||
extract_metadata(&input)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
@@ -294,13 +332,23 @@ pub async fn compress_image_bytes(
|
||||
if !preserve_metadata {
|
||||
opts.strip = StripChunks::Safe;
|
||||
}
|
||||
oxipng::optimize_from_memory(input, &opts)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "PNG 压缩失败").with_source(err))?
|
||||
oxipng::optimize_from_memory(&input, &opts).map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "PNG 压缩失败").with_source(err)
|
||||
})?
|
||||
} else {
|
||||
let image = image::load_from_memory(input)
|
||||
.map_err(|err| AppError::new(ErrorCode::InvalidImage, "图片解码失败").with_source(err))?;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if format_in == ImageFmt::Avif {
|
||||
return Err(AppError::new(
|
||||
ErrorCode::UnsupportedFormat,
|
||||
"当前平台构建不支持 AVIF 解码,请转换为 PNG/JPEG/WebP 后重试",
|
||||
));
|
||||
}
|
||||
|
||||
enforce_pixel_limit(state, &image)?;
|
||||
let image = image::load_from_memory(&input).map_err(|err| {
|
||||
AppError::new(ErrorCode::InvalidImage, "图片解码失败").with_source(err)
|
||||
})?;
|
||||
|
||||
enforce_pixel_limit(max_image_pixels, &image)?;
|
||||
|
||||
let (image, did_resize) = resize_if_needed(image, max_width, max_height);
|
||||
resized = did_resize;
|
||||
@@ -332,23 +380,23 @@ pub async fn compress_image_bytes(
|
||||
|
||||
if !resized && output.len() >= input.len() {
|
||||
if preserve_metadata {
|
||||
return Ok(input.to_vec());
|
||||
return Ok(input);
|
||||
}
|
||||
let stripped = strip_metadata(input).unwrap_or_else(|_| input.to_vec());
|
||||
let stripped = strip_metadata(&input).unwrap_or_else(|_| input.clone());
|
||||
return Ok(if stripped.len() <= input.len() {
|
||||
stripped
|
||||
} else {
|
||||
input.to_vec()
|
||||
input
|
||||
});
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn enforce_pixel_limit(state: &AppState, image: &DynamicImage) -> Result<(), AppError> {
|
||||
fn enforce_pixel_limit(max_image_pixels: u64, image: &DynamicImage) -> Result<(), AppError> {
|
||||
let (w, h) = image.dimensions();
|
||||
let pixels = (w as u64).saturating_mul(h as u64);
|
||||
if pixels > state.config.max_image_pixels {
|
||||
if pixels > max_image_pixels {
|
||||
return Err(AppError::new(
|
||||
ErrorCode::TooManyPixels,
|
||||
format!("图片像素过大({}x{})", w, h),
|
||||
@@ -394,11 +442,7 @@ fn fit_within(w: u32, h: u32, max_width: Option<u32>, max_height: Option<u32>) -
|
||||
(nw, nh)
|
||||
}
|
||||
|
||||
fn encode_png(
|
||||
image: DynamicImage,
|
||||
rate: u8,
|
||||
preserve_metadata: bool,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
fn encode_png(image: DynamicImage, rate: u8, preserve_metadata: bool) -> Result<Vec<u8>, AppError> {
|
||||
let rgba = image.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let mut out = Vec::new();
|
||||
@@ -406,7 +450,9 @@ fn encode_png(
|
||||
let encoder = PngEncoder::new(&mut out);
|
||||
encoder
|
||||
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "PNG 编码失败").with_source(err))?;
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "PNG 编码失败").with_source(err)
|
||||
})?;
|
||||
|
||||
let preset = png_preset_from_rate(rate);
|
||||
let mut opts = oxipng::Options::from_preset(preset);
|
||||
@@ -433,7 +479,9 @@ fn encode_jpeg_raw(raw: &[u8], w: u32, h: u32, quality: u8) -> Result<Vec<u8>, A
|
||||
let mut encoder = JpegEncoder::new_with_quality(&mut out, quality);
|
||||
encoder
|
||||
.encode(raw, w, h, ExtendedColorType::Rgb8)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "JPEG 编码失败").with_source(err))?;
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "JPEG 编码失败").with_source(err)
|
||||
})?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -451,12 +499,6 @@ fn encode_webp(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
fn encode_webp_with_quality(image: DynamicImage, quality: u8) -> Result<Vec<u8>, AppError> {
|
||||
let rgba = image.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
encode_webp_raw(rgba.as_raw(), w, h, quality)
|
||||
}
|
||||
|
||||
fn encode_webp_raw(raw: &[u8], w: u32, h: u32, quality: u8) -> Result<Vec<u8>, AppError> {
|
||||
let encoder = webp::Encoder::from_rgba(raw, w, h);
|
||||
Ok(encoder.encode(quality as f32).to_vec())
|
||||
@@ -473,26 +515,20 @@ fn encode_avif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
|
||||
let img = ravif::Img::new(pixels, w as usize, h as usize);
|
||||
|
||||
let encoder = ravif::Encoder::new().with_quality(quality);
|
||||
let encoded = encoder
|
||||
.encode_rgba(img)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "AVIF 编码失败").with_source(err))?;
|
||||
let encoded = encoder.encode_rgba(img).map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "AVIF 编码失败").with_source(err)
|
||||
})?;
|
||||
|
||||
Ok(encoded.avif_file)
|
||||
}
|
||||
|
||||
fn encode_avif_with_quality(image: DynamicImage, quality: u8) -> Result<Vec<u8>, AppError> {
|
||||
let rgba = image.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
encode_avif_raw(rgba.as_raw(), w, h, quality)
|
||||
}
|
||||
|
||||
fn encode_avif_raw(raw: &[u8], w: u32, h: u32, quality: u8) -> Result<Vec<u8>, AppError> {
|
||||
let pixels = raw.as_rgba();
|
||||
let img = ravif::Img::new(pixels, w as usize, h as usize);
|
||||
let encoder = ravif::Encoder::new().with_quality(quality as f32);
|
||||
let encoded = encoder
|
||||
.encode_rgba(img)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "AVIF 编码失败").with_source(err))?;
|
||||
let encoded = encoder.encode_rgba(img).map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "AVIF 编码失败").with_source(err)
|
||||
})?;
|
||||
Ok(encoded.avif_file)
|
||||
}
|
||||
|
||||
@@ -579,7 +615,8 @@ where
|
||||
Some((_bytes, best_w, best_h, best_size)) => {
|
||||
let new_pixels = (new_w as u64).saturating_mul(new_h as u64);
|
||||
let best_pixels = (*best_w as u64).saturating_mul(*best_h as u64);
|
||||
new_pixels > best_pixels || (new_pixels == best_pixels && result_size > *best_size)
|
||||
new_pixels > best_pixels
|
||||
|| (new_pixels == best_pixels && result_size > *best_size)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -587,9 +624,7 @@ where
|
||||
best_under = Some((result, new_w, new_h, result_size));
|
||||
}
|
||||
|
||||
if new_w == orig_w
|
||||
&& new_h == orig_h
|
||||
&& target_size.saturating_sub(result_size) <= 1024
|
||||
if new_w == orig_w && new_h == orig_h && target_size.saturating_sub(result_size) <= 1024
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -645,11 +680,7 @@ where
|
||||
let mut consider = |bytes: Vec<u8>| {
|
||||
let size = bytes.len() as u64;
|
||||
let is_under = size <= target_size;
|
||||
let diff = if size > target_size {
|
||||
size - target_size
|
||||
} else {
|
||||
target_size - size
|
||||
};
|
||||
let diff = size.abs_diff(target_size);
|
||||
|
||||
let should_update = match (best_is_under, is_under) {
|
||||
(false, true) => true,
|
||||
@@ -689,87 +720,6 @@ where
|
||||
best.ok_or_else(|| AppError::new(ErrorCode::CompressionFailed, "压缩失败"))
|
||||
}
|
||||
|
||||
fn encode_target_quality<F>(
|
||||
min_q: u8,
|
||||
max_q: u8,
|
||||
target_size: u64,
|
||||
mut encode: F,
|
||||
) -> Result<Vec<u8>, AppError>
|
||||
where
|
||||
F: FnMut(u8) -> Result<Vec<u8>, AppError>,
|
||||
{
|
||||
let mut best: Option<Vec<u8>> = None;
|
||||
let mut best_diff = u64::MAX;
|
||||
let mut best_is_under = false; // 记录最佳结果是否小于目标
|
||||
let mut best_size = 0u64;
|
||||
|
||||
// 考虑一个候选结果
|
||||
let consider = |bytes: Vec<u8>, best: &mut Option<Vec<u8>>, best_diff: &mut u64, best_is_under: &mut bool, best_size: &mut u64| {
|
||||
let size = bytes.len() as u64;
|
||||
let is_under = size <= target_size;
|
||||
let diff = if size > target_size {
|
||||
size - target_size
|
||||
} else {
|
||||
target_size - size
|
||||
};
|
||||
|
||||
// 优先选择不超过目标大小的结果
|
||||
let should_update = match (*best_is_under, is_under) {
|
||||
(false, true) => true, // 当前小于目标,之前大于目标 -> 更新
|
||||
(true, false) => false, // 当前大于目标,之前小于目标 -> 不更新
|
||||
_ => diff < *best_diff, // 同类情况,选择更接近的
|
||||
};
|
||||
|
||||
if should_update {
|
||||
*best_diff = diff;
|
||||
*best_is_under = is_under;
|
||||
*best_size = size;
|
||||
*best = Some(bytes);
|
||||
}
|
||||
};
|
||||
|
||||
// 先尝试两端
|
||||
let bytes = encode(min_q)?;
|
||||
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||
if min_q != max_q {
|
||||
let bytes = encode(max_q)?;
|
||||
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||
}
|
||||
|
||||
// 二分查找,增加迭代次数到 12 次以提高精度
|
||||
let mut low = min_q;
|
||||
let mut high = max_q;
|
||||
for _ in 0..12 {
|
||||
if low > high {
|
||||
break;
|
||||
}
|
||||
let mid = (low + high) / 2;
|
||||
let bytes = encode(mid)?;
|
||||
let size = bytes.len() as u64;
|
||||
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||
if size > target_size {
|
||||
high = mid.saturating_sub(1);
|
||||
} else {
|
||||
low = mid.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 精细调整:如果当前结果超出目标太多,尝试更低质量
|
||||
if best_size > target_size {
|
||||
let mut q = min_q;
|
||||
while q <= min_q.saturating_add(5) && q <= max_q {
|
||||
let bytes = encode(q)?;
|
||||
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||
if best_size <= target_size {
|
||||
break; // 已找到满足条件的结果
|
||||
}
|
||||
q = q.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
best.ok_or_else(|| AppError::new(ErrorCode::CompressionFailed, "压缩失败"))
|
||||
}
|
||||
|
||||
fn encode_gif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
|
||||
let rgba = image.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
@@ -780,7 +730,9 @@ fn encode_gif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
|
||||
let mut encoder = GifEncoder::new_with_speed(&mut out, speed);
|
||||
encoder
|
||||
.encode(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "GIF 编码失败").with_source(err))?;
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "GIF 编码失败").with_source(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
@@ -793,7 +745,9 @@ fn encode_bmp(image: DynamicImage) -> Result<Vec<u8>, AppError> {
|
||||
let encoder = BmpEncoder::new(&mut out);
|
||||
encoder
|
||||
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "BMP 编码失败").with_source(err))?;
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "BMP 编码失败").with_source(err)
|
||||
})?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -804,7 +758,9 @@ fn encode_tiff(image: DynamicImage) -> Result<Vec<u8>, AppError> {
|
||||
let encoder = TiffEncoder::new(&mut out);
|
||||
encoder
|
||||
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "TIFF 编码失败").with_source(err))?;
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "TIFF 编码失败").with_source(err)
|
||||
})?;
|
||||
Ok(out.into_inner())
|
||||
}
|
||||
|
||||
@@ -815,7 +771,9 @@ fn encode_ico(image: DynamicImage) -> Result<Vec<u8>, AppError> {
|
||||
let encoder = IcoEncoder::new(&mut out);
|
||||
encoder
|
||||
.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "ICO 编码失败").with_source(err))?;
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "ICO 编码失败").with_source(err)
|
||||
})?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -837,8 +795,9 @@ fn apply_metadata(
|
||||
}
|
||||
|
||||
let out_bytes = ImgBytes::from(output);
|
||||
let dyn_img = DynImage::from_bytes(out_bytes.clone())
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "解析输出图片元数据失败").with_source(err))?;
|
||||
let dyn_img = DynImage::from_bytes(out_bytes.clone()).map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "解析输出图片元数据失败").with_source(err)
|
||||
})?;
|
||||
|
||||
let Some(mut img) = dyn_img else {
|
||||
return Ok(out_bytes.to_vec());
|
||||
@@ -848,16 +807,17 @@ fn apply_metadata(
|
||||
img.set_exif(exif);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
img.encoder()
|
||||
.write_to(&mut buf)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "写入图片元数据失败").with_source(err))?;
|
||||
img.encoder().write_to(&mut buf).map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "写入图片元数据失败").with_source(err)
|
||||
})?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn strip_metadata(input: &[u8]) -> Result<Vec<u8>, AppError> {
|
||||
let bytes = ImgBytes::copy_from_slice(input);
|
||||
let dyn_img = DynImage::from_bytes(bytes.clone())
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "解析图片元数据失败").with_source(err))?;
|
||||
let dyn_img = DynImage::from_bytes(bytes.clone()).map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "解析图片元数据失败").with_source(err)
|
||||
})?;
|
||||
let Some(mut img) = dyn_img else {
|
||||
return Ok(bytes.to_vec());
|
||||
};
|
||||
@@ -866,9 +826,9 @@ fn strip_metadata(input: &[u8]) -> Result<Vec<u8>, AppError> {
|
||||
img.set_exif(None);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
img.encoder()
|
||||
.write_to(&mut buf)
|
||||
.map_err(|err| AppError::new(ErrorCode::CompressionFailed, "写入图片元数据失败").with_source(err))?;
|
||||
img.encoder().write_to(&mut buf).map_err(|err| {
|
||||
AppError::new(ErrorCode::CompressionFailed, "写入图片元数据失败").with_source(err)
|
||||
})?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
@@ -927,13 +887,58 @@ fn strength_from_rate(rate: u8) -> u8 {
|
||||
fn is_animated_gif(input: &[u8]) -> Result<bool, AppError> {
|
||||
let decoder = GifDecoder::new(Cursor::new(input))
|
||||
.map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?;
|
||||
let mut frames = decoder.into_frames().into_iter();
|
||||
let mut frames = decoder.into_frames();
|
||||
if let Some(frame) = frames.next() {
|
||||
frame.map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?;
|
||||
frame.map_err(|err| {
|
||||
AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err)
|
||||
})?;
|
||||
}
|
||||
if let Some(frame) = frames.next() {
|
||||
frame.map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?;
|
||||
frame.map_err(|err| {
|
||||
AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err)
|
||||
})?;
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn detects_supported_formats_from_signatures() {
|
||||
assert_eq!(detect_format(b"\x89PNG\r\n\x1a\n").unwrap(), ImageFmt::Png);
|
||||
assert_eq!(detect_format(b"\xff\xd8").unwrap(), ImageFmt::Jpeg);
|
||||
assert_eq!(
|
||||
detect_format(b"RIFF\x00\x00\x00\x00WEBP").unwrap(),
|
||||
ImageFmt::Webp
|
||||
);
|
||||
assert_eq!(detect_format(b"GIF89a").unwrap(), ImageFmt::Gif);
|
||||
assert_eq!(detect_format(b"BM").unwrap(), ImageFmt::Bmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_avif_and_rejects_heic() {
|
||||
let avif = b"\x00\x00\x00\x18ftypavif\x00\x00\x00\x00avif";
|
||||
assert_eq!(detect_format(avif).unwrap(), ImageFmt::Avif);
|
||||
|
||||
let heic = b"\x00\x00\x00\x18ftypheic\x00\x00\x00\x00mif1";
|
||||
let error = detect_format(heic).unwrap_err();
|
||||
assert_eq!(error.code, ErrorCode::UnsupportedFormat);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_within_preserves_aspect_ratio_and_never_upscales() {
|
||||
assert_eq!(fit_within(4000, 2000, Some(1000), None), (1000, 500));
|
||||
assert_eq!(fit_within(4000, 2000, None, Some(250)), (500, 250));
|
||||
assert_eq!(fit_within(400, 200, Some(800), Some(800)), (400, 200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compression_rate_maps_to_expected_target_size() {
|
||||
assert_eq!(target_size_from_rate(10_000, 1), 100);
|
||||
assert_eq!(target_size_from_rate(10_000, 55), 5_500);
|
||||
assert_eq!(target_size_from_rate(10_000, 100), 10_000);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user