From 04483c6ef17106b435ee65b7b2c65751ff833fc2 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Sat, 20 Dec 2025 16:28:14 +0800 Subject: [PATCH] Adjust compression_rate semantics and no-charge at 100% --- docs/api.md | 4 +-- docs/prd.md | 2 +- docs/ui.md | 2 +- frontend/src/pages/HomePage.vue | 2 +- src/api/compress.rs | 6 ++-- src/services/compress.rs | 54 ++++++++++++++++++++++++--------- src/worker/mod.rs | 3 +- 7 files changed, 50 insertions(+), 23 deletions(-) diff --git a/docs/api.md b/docs/api.md index 822b083..da333ea 100644 --- a/docs/api.md +++ b/docs/api.md @@ -272,7 +272,7 @@ Idempotency-Key: # 建议 | 字段 | 类型 | 必填 | 说明 | |---|---|---:|---| | `file` | File | 是 | 图片文件 | -| `compression_rate` | Integer | 否 | 压缩率 1-100(数值越大压缩越强),优先级高于 `level` | +| `compression_rate` | Integer | 否 | 压缩率 1-100(压缩后体积占原图比例,数值越小压缩越强,100 表示不压缩),优先级高于 `level` | | `level` | String | 否 | `high` / `medium` / `low`(兼容参数,默认 `medium`) | | `output_format` | String | 否 | 已停用,仅支持保持原格式 | | `max_width` | Integer | 否 | 最大宽度(等比缩放) | @@ -337,7 +337,7 @@ Idempotency-Key: # 建议 | 字段 | 类型 | 必填 | 说明 | |---|---|---:|---| | `files[]` | File[] | 是 | 图片文件数组(上限由套餐决定) | -| `compression_rate` | Integer | 否 | 压缩率 1-100(数值越大压缩越强),优先级高于 `level` | +| `compression_rate` | Integer | 否 | 压缩率 1-100(压缩后体积占原图比例,数值越小压缩越强,100 表示不压缩),优先级高于 `level` | | `level` | String | 否 | `high` / `medium` / `low`(兼容参数) | | `output_format` | String | 否 | 已停用,仅支持保持原格式 | | `preserve_metadata` | Boolean | 否 | 是否保留元数据(默认 `false`) | diff --git a/docs/prd.md b/docs/prd.md index 8300ee8..df422a8 100644 --- a/docs/prd.md +++ b/docs/prd.md @@ -44,7 +44,7 @@ ### 3.1 网站压缩(Web) - 拖拽/选择图片(单次多文件)。 - 压缩参数: - - 压缩率:1-100(数值越大压缩越强)。 + - 压缩率:1-100(压缩后体积占原图比例,数值越小压缩越强,100 表示不压缩)。 - 输出格式:保持原格式。 - 可选:限制宽高(等比缩放)。 - 可选:是否保留元数据(默认不保留)。 diff --git a/docs/ui.md b/docs/ui.md index 0952305..96c3ad7 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -44,7 +44,7 @@ 核心组件: - 顶栏:Logo + `/pricing` `/docs` + 登录/注册(登录后显示头像菜单) - 上传区:拖拽 + 点击选择 + 限制提示(格式/大小/数量) -- 参数区:压缩率、尺寸限制、元数据开关(输出格式保持原图) +- 参数区:压缩率(压缩后体积占比)、尺寸限制、元数据开关(输出格式保持原图) - 结果区:文件列表(缩略图、大小、节省%、状态、下载/重试) - 汇总区:总节省、下载 ZIP、清空 - 信任区:隐私说明(默认去 EXIF)、保留期说明、状态页/联系入口 diff --git a/frontend/src/pages/HomePage.vue b/frontend/src/pages/HomePage.vue index 9dda2c4..01d1487 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" /> -
数值越大压缩越强,系统尽量接近目标节省比例。
+
数值越小压缩越强,目标为压缩后体积占原图比例(100% 为不压缩)。
diff --git a/src/api/compress.rs b/src/api/compress.rs index fdaddc0..67c1466 100644 --- a/src/api/compress.rs +++ b/src/api/compress.rs @@ -260,7 +260,8 @@ async fn compress_json( } else { (saved_bytes as f64) * 100.0 / (original_size as f64) }; - let charge_units = compressed_size < original_size; + let skip_charge = req.compression_rate == Some(100); + let charge_units = !skip_charge && compressed_size < original_size; if charge_units { if let QuotaContext::Anonymous { session_id, ip } = "a_ctx { @@ -616,7 +617,8 @@ async fn compress_direct( } else { (saved_bytes as f64) * 100.0 / (original_size as f64) }; - let charge_units = compressed_size < original_size; + let skip_charge = req.compression_rate == Some(100); + let charge_units = !skip_charge && compressed_size < original_size; if state.config.storage_type.to_ascii_lowercase() != "local" { return Err(AppError::new( diff --git a/src/services/compress.rs b/src/services/compress.rs index e0314a1..acd319c 100644 --- a/src/services/compress.rs +++ b/src/services/compress.rs @@ -100,11 +100,16 @@ pub fn parse_compression_rate(value: &str) -> Result { let rate: u8 = value .trim() .parse() - .map_err(|_| AppError::new(ErrorCode::InvalidRequest, "compression_rate 需为 1-100 的整数"))?; + .map_err(|_| { + AppError::new( + ErrorCode::InvalidRequest, + "compression_rate 需为 1-100 的整数(压缩后体积占比)", + ) + })?; if !(1..=100).contains(&rate) { return Err(AppError::new( ErrorCode::InvalidRequest, - "compression_rate 需在 1-100 之间", + "compression_rate 需在 1-100 之间(压缩后体积占比)", )); } Ok(rate) @@ -112,9 +117,9 @@ pub fn parse_compression_rate(value: &str) -> Result { pub fn rate_to_level(rate: u8) -> CompressionLevel { match rate { - 1..=33 => CompressionLevel::Low, + 1..=33 => CompressionLevel::High, 34..=66 => CompressionLevel::Medium, - _ => CompressionLevel::High, + _ => CompressionLevel::Low, } } @@ -197,21 +202,36 @@ pub async fn compress_image_bytes( } } - let rate = effective_rate(compression_rate, level); + let retention_rate = effective_rate(compression_rate, level); let target_size = compression_rate.map(|value| target_size_from_rate(original_size, value)); + + if compression_rate == Some(100) + && format_in == format_out + && max_width.is_none() + && max_height.is_none() + { + if preserve_metadata { + return Ok(input.to_vec()); + } + let stripped = strip_metadata(input).unwrap_or_else(|_| input.to_vec()); + return Ok(stripped); + } + let (icc_profile, exif) = if preserve_metadata { extract_metadata(input) } else { (None, None) }; + let strength_rate = strength_from_rate(retention_rate); + let mut resized = false; let mut output = if format_in == ImageFmt::Png && format_out == ImageFmt::Png && max_width.is_none() && max_height.is_none() { - let preset = png_preset_from_rate(rate); + let preset = png_preset_from_rate(strength_rate); let mut opts = oxipng::Options::from_preset(preset); if !preserve_metadata { opts.strip = StripChunks::Safe; @@ -228,20 +248,20 @@ pub async fn compress_image_bytes( resized = did_resize; match format_out { - ImageFmt::Png => encode_png(image, rate, preserve_metadata)?, + ImageFmt::Png => encode_png(image, strength_rate, preserve_metadata)?, ImageFmt::Jpeg => match target_size { Some(target) => encode_jpeg_target(image, target)?, - None => encode_jpeg(image, rate)?, + None => encode_jpeg(image, strength_rate)?, }, ImageFmt::Webp => match target_size { Some(target) => encode_webp_target(image, target)?, - None => encode_webp(image, rate)?, + None => encode_webp(image, strength_rate)?, }, ImageFmt::Avif => match target_size { Some(target) => encode_avif_target(image, target)?, - None => encode_avif(image, rate)?, + None => encode_avif(image, strength_rate)?, }, - ImageFmt::Gif => encode_gif(image, rate)?, + ImageFmt::Gif => encode_gif(image, strength_rate)?, ImageFmt::Bmp => encode_bmp(image)?, ImageFmt::Tiff => encode_tiff(image)?, ImageFmt::Ico => encode_ico(image)?, @@ -595,17 +615,16 @@ fn effective_rate(rate: Option, level: CompressionLevel) -> u8 { match rate { Some(value) => value.clamp(1, 100), None => match level { - CompressionLevel::Low => 25, + CompressionLevel::Low => 80, CompressionLevel::Medium => 55, - CompressionLevel::High => 80, + CompressionLevel::High => 30, }, } } 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; + let target = original_size.saturating_mul(rate) / 100; target.max(1) } @@ -639,6 +658,11 @@ fn gif_speed_from_rate(rate: u8) -> i32 { 1 + ((rate - 1) * 29 / 99) } +fn strength_from_rate(rate: u8) -> u8 { + let rate = rate.clamp(1, 100); + 101_u8.saturating_sub(rate) +} + fn is_animated_gif(input: &[u8]) -> Result { let decoder = GifDecoder::new(Cursor::new(input)) .map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?; diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 4cbcf30..794db02 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -310,7 +310,8 @@ async fn process_task(state: &AppState, task_id: Uuid) -> Result<(), AppError> { } else { (original_size.saturating_sub(compressed_size) as f64) * 100.0 / (original_size as f64) }; - let charge_units = compressed_size < original_size; + let skip_charge = compression_rate == Some(100); + let charge_units = !skip_charge && compressed_size < original_size; // Anonymous quota enforcement requires session_id + client_ip. if task.user_id.is_none() && charge_units {