perf: harden quotas and reduce compression overhead
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user