Adjust compression_rate semantics and no-charge at 100%

This commit is contained in:
2025-12-20 16:28:14 +08:00
parent 0d4e2b9ab1
commit 04483c6ef1
7 changed files with 50 additions and 23 deletions

View File

@@ -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 } = &quota_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(

View File

@@ -100,11 +100,16 @@ pub fn parse_compression_rate(value: &str) -> Result<u8, AppError> {
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<u8, AppError> {
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<u8>, 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<bool, AppError> {
let decoder = GifDecoder::new(Cursor::new(input))
.map_err(|err| AppError::new(ErrorCode::InvalidImage, "GIF 解码失败").with_source(err))?;

View File

@@ -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 {