diff --git a/.gitignore b/.gitignore
index 8896adc..f8834f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
/uploads
/static
+/logs
/frontend/node_modules
/frontend/dist
diff --git a/frontend/src/pages/HomePage.vue b/frontend/src/pages/HomePage.vue
index 2990ef2..9dda2c4 100644
--- a/frontend/src/pages/HomePage.vue
+++ b/frontend/src/pages/HomePage.vue
@@ -382,7 +382,7 @@ async function resendVerification() {
step="1"
class="w-full"
/>
-
数值越大压缩越强,输出保持原格式。
+ 数值越大压缩越强,系统尽量接近目标节省比例。
diff --git a/src/services/compress.rs b/src/services/compress.rs
index 457f627..e0314a1 100644
--- a/src/services/compress.rs
+++ b/src/services/compress.rs
@@ -187,6 +187,7 @@ pub async fn compress_image_bytes(
max_height: Option,
preserve_metadata: bool,
) -> Result, 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, AppError> {
+ let quality = jpeg_quality_from_rate(rate);
+ encode_jpeg_with_quality(image, quality)
+}
+
+fn encode_jpeg_with_quality(image: DynamicImage, quality: u8) -> Result, 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, 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, AppError> {
Ok(bytes.to_vec())
}
+fn encode_webp_with_quality(image: DynamicImage, quality: u8) -> Result, 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, 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, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
@@ -375,6 +402,93 @@ fn encode_avif(image: DynamicImage, rate: u8) -> Result, AppError> {
Ok(encoded.avif_file)
}
+fn encode_avif_with_quality(image: DynamicImage, quality: u8) -> Result, 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, 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, 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, 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, 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(
+ min_q: u8,
+ max_q: u8,
+ target_size: u64,
+ mut encode: F,
+) -> Result, AppError>
+where
+ F: FnMut(u8) -> Result, AppError>,
+{
+ let mut best: Option> = None;
+ let mut best_diff = u64::MAX;
+
+ let mut consider = |bytes: Vec| {
+ 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, AppError> {
let rgba = image.to_rgba8();
let (w, h) = rgba.dimensions();
@@ -488,6 +602,13 @@ fn effective_rate(rate: Option, 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
}