Improve compression targeting and UI hint
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
|
||||
/uploads
|
||||
/static
|
||||
/logs
|
||||
|
||||
/frontend/node_modules
|
||||
/frontend/dist
|
||||
|
||||
@@ -382,7 +382,7 @@ async function resendVerification() {
|
||||
step="1"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="text-xs text-slate-500">数值越大压缩越强,输出保持原格式。</div>
|
||||
<div class="text-xs text-slate-500">数值越大压缩越强,系统尽量接近目标节省比例。</div>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
|
||||
@@ -187,6 +187,7 @@ pub async fn compress_image_bytes(
|
||||
max_height: Option<u32>,
|
||||
preserve_metadata: bool,
|
||||
) -> 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(
|
||||
@@ -197,6 +198,7 @@ pub async fn compress_image_bytes(
|
||||
}
|
||||
|
||||
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 {
|
||||
extract_metadata(input)
|
||||
} else {
|
||||
@@ -227,9 +229,18 @@ pub async fn compress_image_bytes(
|
||||
|
||||
match format_out {
|
||||
ImageFmt::Png => encode_png(image, rate, preserve_metadata)?,
|
||||
ImageFmt::Jpeg => encode_jpeg(image, rate)?,
|
||||
ImageFmt::Webp => encode_webp(image, rate)?,
|
||||
ImageFmt::Avif => encode_avif(image, rate)?,
|
||||
ImageFmt::Jpeg => match target_size {
|
||||
Some(target) => encode_jpeg_target(image, target)?,
|
||||
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::Bmp => encode_bmp(image)?,
|
||||
ImageFmt::Tiff => encode_tiff(image)?,
|
||||
@@ -329,17 +340,22 @@ fn encode_png(
|
||||
}
|
||||
|
||||
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 (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 quality = jpeg_quality_from_rate(rate);
|
||||
|
||||
let mut encoder = JpegEncoder::new_with_quality(&mut out, quality);
|
||||
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))?;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -357,6 +373,17 @@ 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())
|
||||
}
|
||||
|
||||
fn encode_avif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
|
||||
let rgba = image.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
@@ -375,6 +402,93 @@ fn encode_avif(image: DynamicImage, rate: u8) -> Result<Vec<u8>, AppError> {
|
||||
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> {
|
||||
let rgba = image.to_rgba8();
|
||||
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 {
|
||||
(((rate.saturating_sub(1)) as f32 / 99.0) * 6.0).round() as u8
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user