feat: 支持按目标大小压缩图片
- 新增 target_size_bytes 参数,支持直接指定压缩后的目标大小(字节) - 实现自动缩放算法:当仅调整质量无法达到目标时,自动缩小图片尺寸 - 前端新增压缩模式切换:百分比模式 / 目标大小模式 - 支持 KB/MB 单位选择 - 优化二分搜索算法,提高目标大小的精准度 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -188,6 +188,7 @@ pub async fn compress_image_bytes(
|
||||
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,
|
||||
@@ -203,7 +204,11 @@ pub async fn compress_image_bytes(
|
||||
}
|
||||
|
||||
let retention_rate = effective_rate(compression_rate, level);
|
||||
let target_size = compression_rate.map(|value| target_size_from_rate(original_size, value));
|
||||
// 优先使用直接指定的目标大小,其次根据百分比计算
|
||||
let target_size = match target_size_bytes {
|
||||
Some(bytes) => Some(bytes),
|
||||
None => compression_rate.map(|value| target_size_from_rate(original_size, value)),
|
||||
};
|
||||
|
||||
if compression_rate == Some(100)
|
||||
&& format_in == format_out
|
||||
@@ -439,24 +444,157 @@ fn encode_avif_raw(raw: &[u8], w: u32, h: u32, quality: u8) -> Result<Vec<u8>, A
|
||||
}
|
||||
|
||||
fn encode_jpeg_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
||||
let rgb = image.to_rgb8();
|
||||
let (w, h) = rgb.dimensions();
|
||||
let raw = rgb.into_raw();
|
||||
encode_target_quality(35, 95, target_size, |q| encode_jpeg_raw(&raw, w, h, q))
|
||||
encode_with_auto_resize(image, target_size, 1, 95, |img, q| {
|
||||
let rgb = img.to_rgb8();
|
||||
let (w, h) = rgb.dimensions();
|
||||
encode_jpeg_raw(rgb.as_raw(), w, h, q)
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_webp_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
||||
let rgba = image.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let raw = rgba.into_raw();
|
||||
encode_target_quality(30, 95, target_size, |q| encode_webp_raw(&raw, w, h, q))
|
||||
encode_with_auto_resize(image, target_size, 1, 95, |img, q| {
|
||||
let rgba = img.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
encode_webp_raw(rgba.as_raw(), w, h, q)
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_avif_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
||||
let rgba = image.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let raw = rgba.into_raw();
|
||||
encode_target_quality(35, 90, target_size, |q| encode_avif_raw(&raw, w, h, q))
|
||||
encode_with_auto_resize(image, target_size, 1, 95, |img, q| {
|
||||
let rgba = img.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
encode_avif_raw(rgba.as_raw(), w, h, q)
|
||||
})
|
||||
}
|
||||
|
||||
/// 支持自动缩放尺寸的目标大小压缩
|
||||
/// 当仅调整质量无法达到目标大小时,自动缩小图片尺寸
|
||||
fn encode_with_auto_resize<F>(
|
||||
image: DynamicImage,
|
||||
target_size: u64,
|
||||
min_q: u8,
|
||||
max_q: u8,
|
||||
mut encode_fn: F,
|
||||
) -> Result<Vec<u8>, AppError>
|
||||
where
|
||||
F: FnMut(&DynamicImage, u8) -> Result<Vec<u8>, AppError>,
|
||||
{
|
||||
let (orig_w, orig_h) = image.dimensions();
|
||||
let min_dimension = 16u32; // 最小尺寸限制
|
||||
|
||||
// 首先尝试用最低质量压缩原始尺寸
|
||||
let min_q_result = encode_fn(&image, min_q)?;
|
||||
if min_q_result.len() as u64 <= target_size {
|
||||
// 最低质量已满足,用二分法找最佳质量
|
||||
return encode_target_quality_with_image(&image, min_q, max_q, target_size, &mut encode_fn);
|
||||
}
|
||||
|
||||
// 需要缩放:根据当前大小和目标大小计算缩放比例
|
||||
let current_size = min_q_result.len() as u64;
|
||||
// 文件大小大致与像素数成正比,所以尺寸缩放系数 = sqrt(目标大小/当前大小)
|
||||
let scale = ((target_size as f64 / current_size as f64).sqrt() * 0.9).min(1.0); // 0.9 为安全系数
|
||||
|
||||
let mut best_result = min_q_result;
|
||||
let mut best_is_under = false;
|
||||
|
||||
// 尝试多个缩放级别
|
||||
let scales = [scale, scale * 0.8, scale * 0.6, scale * 0.4, 0.3, 0.2, 0.1];
|
||||
|
||||
for &s in &scales {
|
||||
let new_w = ((orig_w as f64 * s).round() as u32).max(min_dimension);
|
||||
let new_h = ((orig_h as f64 * s).round() as u32).max(min_dimension);
|
||||
|
||||
if new_w < min_dimension && new_h < min_dimension {
|
||||
break; // 达到最小尺寸
|
||||
}
|
||||
|
||||
let resized = image.resize(new_w, new_h, image::imageops::FilterType::Lanczos3);
|
||||
|
||||
// 对缩放后的图片进行二分质量搜索
|
||||
let result = encode_target_quality_with_image(&resized, min_q, max_q, target_size, &mut encode_fn)?;
|
||||
let result_size = result.len() as u64;
|
||||
|
||||
if result_size <= target_size {
|
||||
// 找到满足条件的结果
|
||||
if !best_is_under || result_size > best_result.len() as u64 {
|
||||
// 优先选择更大的(更接近目标且不超过)
|
||||
best_result = result;
|
||||
best_is_under = true;
|
||||
}
|
||||
break; // 已找到满足条件的最大尺寸
|
||||
} else if !best_is_under && result_size < best_result.len() as u64 {
|
||||
// 还没找到满足条件的,保存最接近的
|
||||
best_result = result;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(best_result)
|
||||
}
|
||||
|
||||
/// 对给定图片进行二分质量搜索
|
||||
fn encode_target_quality_with_image<F>(
|
||||
image: &DynamicImage,
|
||||
min_q: u8,
|
||||
max_q: u8,
|
||||
target_size: u64,
|
||||
encode_fn: &mut F,
|
||||
) -> Result<Vec<u8>, AppError>
|
||||
where
|
||||
F: FnMut(&DynamicImage, 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 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 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);
|
||||
}
|
||||
};
|
||||
|
||||
// 先尝试两端
|
||||
consider(encode_fn(image, min_q)?);
|
||||
if min_q != max_q {
|
||||
consider(encode_fn(image, max_q)?);
|
||||
}
|
||||
|
||||
// 二分查找
|
||||
let mut low = min_q;
|
||||
let mut high = max_q;
|
||||
for _ in 0..10 {
|
||||
if low > high {
|
||||
break;
|
||||
}
|
||||
let mid = (low + high) / 2;
|
||||
let bytes = encode_fn(image, mid)?;
|
||||
let size = bytes.len() as u64;
|
||||
consider(bytes);
|
||||
if size > target_size {
|
||||
high = mid.saturating_sub(1);
|
||||
} else {
|
||||
low = mid.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
best.ok_or_else(|| AppError::new(ErrorCode::CompressionFailed, "压缩失败"))
|
||||
}
|
||||
|
||||
fn encode_target_quality<F>(
|
||||
@@ -470,35 +608,53 @@ where
|
||||
{
|
||||
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 mut consider = |bytes: Vec<u8>| {
|
||||
// 考虑一个候选结果
|
||||
let mut 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
|
||||
};
|
||||
if diff < best_diff {
|
||||
best_diff = diff;
|
||||
best = Some(bytes);
|
||||
|
||||
// 优先选择不超过目标大小的结果
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
consider(encode(min_q)?);
|
||||
// 先尝试两端
|
||||
let bytes = encode(min_q)?;
|
||||
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||
if min_q != max_q {
|
||||
consider(encode(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..7 {
|
||||
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);
|
||||
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||
if size > target_size {
|
||||
high = mid.saturating_sub(1);
|
||||
} else {
|
||||
@@ -506,6 +662,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// 精细调整:如果当前结果超出目标太多,尝试更低质量
|
||||
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, "压缩失败"))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user