Improve compression targeting and UI hint

This commit is contained in:
2025-12-20 16:04:07 +08:00
parent 11f48fd3dd
commit 0d4e2b9ab1
3 changed files with 131 additions and 9 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@
/uploads /uploads
/static /static
/logs
/frontend/node_modules /frontend/node_modules
/frontend/dist /frontend/dist

View File

@@ -382,7 +382,7 @@ async function resendVerification() {
step="1" step="1"
class="w-full" class="w-full"
/> />
<div class="text-xs text-slate-500">数值越大压缩越强输出保持原格式</div> <div class="text-xs text-slate-500">数值越大压缩越强系统尽量接近目标节省比例</div>
</label> </label>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">

View File

@@ -187,6 +187,7 @@ pub async fn compress_image_bytes(
max_height: Option<u32>, max_height: Option<u32>,
preserve_metadata: bool, preserve_metadata: bool,
) -> Result<Vec<u8>, AppError> { ) -> Result<Vec<u8>, AppError> {
let original_size = input.len() as u64;
if format_in == ImageFmt::Gif { if format_in == ImageFmt::Gif {
if is_animated_gif(input)? { if is_animated_gif(input)? {
return Err(AppError::new( return Err(AppError::new(
@@ -197,6 +198,7 @@ pub async fn compress_image_bytes(
} }
let rate = effective_rate(compression_rate, level); let rate = effective_rate(compression_rate, level);
let target_size = compression_rate.map(|value| target_size_from_rate(original_size, value));
let (icc_profile, exif) = if preserve_metadata { let (icc_profile, exif) = if preserve_metadata {
extract_metadata(input) extract_metadata(input)
} else { } else {
@@ -227,9 +229,18 @@ pub async fn compress_image_bytes(
match format_out { match format_out {
ImageFmt::Png => encode_png(image, rate, preserve_metadata)?, ImageFmt::Png => encode_png(image, rate, preserve_metadata)?,
ImageFmt::Jpeg => encode_jpeg(image, rate)?, ImageFmt::Jpeg => match target_size {
ImageFmt::Webp => encode_webp(image, rate)?, Some(target) => encode_jpeg_target(image, target)?,
ImageFmt::Avif => encode_avif(image, rate)?, None => encode_jpeg(image, rate)?,
},
ImageFmt::Webp => match target_size {
Some(target) => encode_webp_target(image, target)?,
None => encode_webp(image, rate)?,
},
ImageFmt::Avif => match target_size {
Some(target) => encode_avif_target(image, target)?,
None => encode_avif(image, rate)?,
},
ImageFmt::Gif => encode_gif(image, rate)?, ImageFmt::Gif => encode_gif(image, rate)?,
ImageFmt::Bmp => encode_bmp(image)?, ImageFmt::Bmp => encode_bmp(image)?,
ImageFmt::Tiff => encode_tiff(image)?, ImageFmt::Tiff => encode_tiff(image)?,
@@ -329,17 +340,22 @@ fn encode_png(
} }
fn encode_jpeg(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> { fn encode_jpeg(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
let quality = jpeg_quality_from_rate(rate);
encode_jpeg_with_quality(image, quality)
}
fn encode_jpeg_with_quality(image: DynamicImage, quality: u8) -> Result<Vec<u8>, AppError> {
let rgb = image.to_rgb8(); let rgb = image.to_rgb8();
let (w, h) = rgb.dimensions(); let (w, h) = rgb.dimensions();
encode_jpeg_raw(rgb.as_raw(), w, h, quality)
}
fn encode_jpeg_raw(raw: &[u8], w: u32, h: u32, quality: u8) -> Result<Vec<u8>, AppError> {
let mut out = Vec::new(); let mut out = Vec::new();
let quality = jpeg_quality_from_rate(rate);
let mut encoder = JpegEncoder::new_with_quality(&mut out, quality); let mut encoder = JpegEncoder::new_with_quality(&mut out, quality);
encoder encoder
.encode(rgb.as_raw(), w, h, ExtendedColorType::Rgb8) .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) Ok(out)
} }
@@ -357,6 +373,17 @@ fn encode_webp(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
Ok(bytes.to_vec()) 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())
}
fn encode_avif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> { fn encode_avif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8(); let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions(); let (w, h) = rgba.dimensions();
@@ -375,6 +402,93 @@ fn encode_avif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
Ok(encoded.avif_file) 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))?;
Ok(encoded.avif_file)
}
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))
}
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))
}
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))
}
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 consider = |bytes: Vec<u8>| {
let size = bytes.len() as u64;
let diff = if size > target_size {
size - target_size
} else {
target_size - size
};
if diff < best_diff {
best_diff = diff;
best = Some(bytes);
}
};
consider(encode(min_q)?);
if min_q != max_q {
consider(encode(max_q)?);
}
let mut low = min_q;
let mut high = max_q;
for _ in 0..7 {
if low > high {
break;
}
let mid = (low + high) / 2;
let bytes = encode(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_gif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> { fn encode_gif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
let rgba = image.to_rgba8(); let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions(); let (w, h) = rgba.dimensions();
@@ -488,6 +602,13 @@ fn effective_rate(rate: Option<u8>, level: CompressionLevel) -> u8 {
} }
} }
fn target_size_from_rate(original_size: u64, rate: u8) -> u64 {
let rate = rate.clamp(1, 100) as u64;
let remaining = 100_u64.saturating_sub(rate);
let target = original_size.saturating_mul(remaining) / 100;
target.max(1)
}
fn png_preset_from_rate(rate: u8) -> u8 { fn png_preset_from_rate(rate: u8) -> u8 {
(((rate.saturating_sub(1)) as f32 / 99.0) * 6.0).round() as u8 (((rate.saturating_sub(1)) as f32 / 99.0) * 6.0).round() as u8
} }