Improve compression targeting and UI hint
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
/uploads
|
/uploads
|
||||||
/static
|
/static
|
||||||
|
/logs
|
||||||
|
|
||||||
/frontend/node_modules
|
/frontend/node_modules
|
||||||
/frontend/dist
|
/frontend/dist
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user