perf: harden quotas and reduce compression overhead
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
237899745
2026-07-25 23:10:59 +08:00
parent 49189d346b
commit 86da5cf1f5
15 changed files with 464 additions and 210 deletions

View File

@@ -874,11 +874,16 @@ fn encode_jpeg_target(
JPEG_TARGET_MIN_QUALITY,
95,
deadline,
|img, q| {
|img| {
let rgb = jpeg_rgb(img);
let (w, h) = rgb.dimensions();
encode_jpeg_raw(rgb.as_raw(), w, h, q)
TargetPixels {
bytes: rgb.into_raw(),
width: w,
height: h,
}
},
encode_jpeg_raw,
)
}
@@ -893,11 +898,16 @@ fn encode_webp_target(
WEBP_TARGET_MIN_QUALITY,
95,
deadline,
|img, q| {
|img| {
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
encode_webp_raw(rgba.as_raw(), w, h, q)
TargetPixels {
bytes: rgba.into_raw(),
width: w,
height: h,
}
},
encode_webp_raw,
)
}
@@ -912,14 +922,25 @@ fn encode_avif_target(
AVIF_TARGET_MIN_QUALITY,
95,
deadline,
|img, q| {
|img| {
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
encode_avif_raw(rgba.as_raw(), w, h, q)
TargetPixels {
bytes: rgba.into_raw(),
width: w,
height: h,
}
},
encode_avif_raw,
)
}
struct TargetPixels {
bytes: Vec<u8>,
width: u32,
height: u32,
}
/// 目标体积压缩(质量优先 + 有边界的降尺寸)
///
/// 策略:
@@ -927,16 +948,18 @@ fn encode_avif_target(
/// 2) 原尺寸失败时先验证最低允许尺寸,快速识别不可达目标;
/// 3) 最低尺寸可达时,二分查找满足目标的最高分辨率;
/// 4) 严格限制最小缩放比例,避免“过度糊图”。
fn encode_with_auto_resize<F>(
fn encode_with_auto_resize<P, E>(
image: DynamicImage,
target_size: u64,
min_q: u8,
max_q: u8,
deadline: &CompressionDeadline,
mut encode_fn: F,
mut prepare_fn: P,
mut encode_fn: E,
) -> Result<Vec<u8>, AppError>
where
F: FnMut(&DynamicImage, u8) -> Result<Vec<u8>, AppError>,
P: FnMut(&DynamicImage) -> TargetPixels,
E: FnMut(&[u8], u32, u32, u8) -> Result<Vec<u8>, AppError>,
{
deadline.check()?;
let (orig_w, orig_h) = image.dimensions();
@@ -947,10 +970,10 @@ where
let (full_result, full_dimensions) = encode_target_at_scale(
&image,
1.0,
min_q,
max_q,
(min_q, max_q),
target_size,
deadline,
&mut prepare_fn,
&mut encode_fn,
)?;
if full_result.len() as u64 <= target_size {
@@ -968,10 +991,10 @@ where
let (min_result, min_dimensions) = encode_target_at_scale(
&image,
min_scale,
min_q,
max_q,
(min_q, max_q),
target_size,
deadline,
&mut prepare_fn,
&mut encode_fn,
)?;
if min_result.len() as u64 > target_size {
@@ -1006,10 +1029,10 @@ where
let (candidate_result, _) = encode_target_at_scale(
&image,
candidate_scale,
min_q,
max_q,
(min_q, max_q),
target_size,
deadline,
&mut prepare_fn,
&mut encode_fn,
)?;
if candidate_result.len() as u64 <= target_size {
@@ -1040,17 +1063,18 @@ fn dimensions_at_scale(orig_w: u32, orig_h: u32, scale: f64) -> (u32, u32) {
)
}
fn encode_target_at_scale<F>(
fn encode_target_at_scale<P, E>(
image: &DynamicImage,
scale: f64,
min_q: u8,
max_q: u8,
quality_range: (u8, u8),
target_size: u64,
deadline: &CompressionDeadline,
encode_fn: &mut F,
prepare_fn: &mut P,
encode_fn: &mut E,
) -> Result<(Vec<u8>, (u32, u32)), AppError>
where
F: FnMut(&DynamicImage, u8) -> Result<Vec<u8>, AppError>,
P: FnMut(&DynamicImage) -> TargetPixels,
E: FnMut(&[u8], u32, u32, u8) -> Result<Vec<u8>, AppError>,
{
deadline.check()?;
let (orig_w, orig_h) = image.dimensions();
@@ -1064,40 +1088,36 @@ where
});
deadline.check()?;
let candidate = resized.as_ref().unwrap_or(image);
let result = encode_target_quality_with_image(
candidate,
min_q,
max_q,
target_size,
deadline,
encode_fn,
)?;
let pixels = prepare_fn(candidate);
deadline.check()?;
let (min_q, max_q) = quality_range;
let result = encode_target_quality(&pixels, min_q, max_q, target_size, deadline, encode_fn)?;
Ok((result, dimensions))
}
/// 对给定图片进行二分质量搜索
fn encode_target_quality_with_image<F>(
image: &DynamicImage,
fn encode_target_quality<E>(
pixels: &TargetPixels,
min_q: u8,
max_q: u8,
target_size: u64,
deadline: &CompressionDeadline,
encode_fn: &mut F,
encode_fn: &mut E,
) -> Result<Vec<u8>, AppError>
where
F: FnMut(&DynamicImage, u8) -> Result<Vec<u8>, AppError>,
E: FnMut(&[u8], u32, u32, u8) -> Result<Vec<u8>, AppError>,
{
deadline.check()?;
// Start with the highest quality. If it already fits, no lower-quality
// encodes can improve the result.
let max_quality = encode_fn(image, max_q)?;
let max_quality = encode_fn(&pixels.bytes, pixels.width, pixels.height, max_q)?;
let max_size = max_quality.len() as u64;
if max_size <= target_size || min_q == max_q {
return Ok(max_quality);
}
deadline.check()?;
let min_quality = encode_fn(image, min_q)?;
let min_quality = encode_fn(&pixels.bytes, pixels.width, pixels.height, min_q)?;
let min_size = min_quality.len() as u64;
if min_size > target_size {
return Ok(min_quality);
@@ -1132,7 +1152,7 @@ where
};
let candidate_q =
under_q.saturating_add(estimated_offset.clamp(1, quality_span.saturating_sub(1)) as u8);
let bytes = encode_fn(image, candidate_q)?;
let bytes = encode_fn(&pixels.bytes, pixels.width, pixels.height, candidate_q)?;
let size = bytes.len() as u64;
if size > target_size {
over_q = candidate_q;
@@ -1149,7 +1169,7 @@ where
return Ok(best_under);
}
let mid = under_q + (over_q - under_q) / 2;
let bytes = encode_fn(image, mid)?;
let bytes = encode_fn(&pixels.bytes, pixels.width, pixels.height, mid)?;
let size = bytes.len() as u64;
if size > target_size {
over_q = mid;
@@ -1371,6 +1391,15 @@ fn is_animated_image(input: &[u8], format: ImageFmt) -> Result<bool, AppError> {
mod tests {
use super::*;
fn empty_target_pixels(image: &DynamicImage) -> TargetPixels {
let (width, height) = image.dimensions();
TargetPixels {
bytes: Vec::new(),
width,
height,
}
}
fn crc32(bytes: &[u8]) -> u32 {
let mut crc = u32::MAX;
for byte in bytes {
@@ -1582,7 +1611,8 @@ mod tests {
40,
95,
&CompressionDeadline::unlimited(),
|_image, quality| {
empty_target_pixels,
|_raw, _width, _height, quality| {
calls.set(calls.get() + 1);
Ok(vec![0; quality as usize])
},
@@ -1598,14 +1628,18 @@ mod tests {
use std::cell::RefCell;
let calls = RefCell::new(Vec::new());
let image = DynamicImage::new_rgb8(32, 32);
let result = encode_target_quality_with_image(
&image,
let pixels = TargetPixels {
bytes: Vec::new(),
width: 32,
height: 32,
};
let result = encode_target_quality(
&pixels,
25,
95,
50_000,
&CompressionDeadline::unlimited(),
&mut |_image, quality| {
&mut |_raw, _width, _height, quality| {
calls.borrow_mut().push(quality);
Ok(vec![0; usize::from(quality) * 1_000])
},
@@ -1625,10 +1659,8 @@ mod tests {
40,
40,
&CompressionDeadline::unlimited(),
|image, _quality| {
let (width, height) = image.dimensions();
Ok(vec![0; (width as usize * height as usize) / 10])
},
empty_target_pixels,
|_raw, width, height, _quality| Ok(vec![0; (width as usize * height as usize) / 10]),
)
.unwrap();
@@ -1646,7 +1678,8 @@ mod tests {
40,
40,
&CompressionDeadline::unlimited(),
|_image, _quality| {
empty_target_pixels,
|_raw, _width, _height, _quality| {
calls.set(calls.get() + 1);
Ok(vec![0; 50_000])
},
@@ -1671,7 +1704,8 @@ mod tests {
40,
40,
&deadline,
|_image, _quality| {
empty_target_pixels,
|_raw, _width, _height, _quality| {
calls.set(calls.get() + 1);
Ok(vec![0; 1])
},
@@ -1704,12 +1738,12 @@ mod tests {
40,
40,
&deadline,
|image, _quality| {
empty_target_pixels,
|_raw, width, height, _quality| {
calls.set(calls.get() + 1);
if calls.get() == 2 {
controller.cancel();
}
let (width, height) = image.dimensions();
Ok(vec![0; (width as usize * height as usize) / 10])
},
)
@@ -1719,6 +1753,34 @@ mod tests {
assert_eq!(calls.get(), 2);
}
#[test]
fn target_encoder_prepares_pixels_once_for_each_scale() {
use std::cell::Cell;
let preparations = Cell::new(0);
let encodes = Cell::new(0);
let result = encode_with_auto_resize(
DynamicImage::new_rgb8(800, 600),
50_000,
25,
95,
&CompressionDeadline::unlimited(),
|image| {
preparations.set(preparations.get() + 1);
empty_target_pixels(image)
},
|_raw, _width, _height, quality| {
encodes.set(encodes.get() + 1);
Ok(vec![0; usize::from(quality) * 1_000])
},
)
.unwrap();
assert_eq!(result.len(), 50_000);
assert_eq!(preparations.get(), 1);
assert!(encodes.get() > 1);
}
#[test]
fn large_images_use_a_smaller_scale_search_budget() {
assert_eq!(scale_search_attempts(4_000, 3_000), 3);