fix: honor target image size across compression paths
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -33,9 +33,9 @@ const AVIF_TARGET_MIN_QUALITY: u8 = 38;
|
||||
const JPEG_PERCEPTUAL_QUALITY: u8 = 72;
|
||||
const WEBP_PERCEPTUAL_QUALITY: u8 = 70;
|
||||
const AVIF_PERCEPTUAL_QUALITY: u8 = 55;
|
||||
const JPEG_TARGET_MAX_QUALITY: u8 = 90;
|
||||
const WEBP_TARGET_MAX_QUALITY: u8 = 92;
|
||||
const AVIF_TARGET_MAX_QUALITY: u8 = 90;
|
||||
const JPEG_TARGET_MAX_QUALITY: u8 = 100;
|
||||
const WEBP_TARGET_MAX_QUALITY: u8 = 100;
|
||||
const AVIF_TARGET_MAX_QUALITY: u8 = 100;
|
||||
const AVIF_ENCODER_SPEED: u8 = 5;
|
||||
const WEBP_TARGET_SAFETY_PERCENT: u64 = 97;
|
||||
const METADATA_TARGET_OVERHEAD: u64 = 1024;
|
||||
@@ -927,6 +927,23 @@ fn encode_webp_target(
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
deadline.check()?;
|
||||
let pixels = prepare_target_pixels(&image);
|
||||
let lossless_candidate = encode_webp_lossless_pixels(&pixels);
|
||||
deadline.check()?;
|
||||
match lossless_candidate {
|
||||
Ok(lossless) if lossless.len() as u64 <= target_size => return Ok(lossless),
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
tracing::debug!(error = %error, "WebP 无损候选编码失败,继续尝试有损编码");
|
||||
}
|
||||
}
|
||||
deadline.check()?;
|
||||
|
||||
let max_lossy = encode_webp_pixels(&pixels, WEBP_TARGET_MAX_QUALITY)?;
|
||||
deadline.check()?;
|
||||
if max_lossy.len() as u64 <= target_size {
|
||||
return Ok(max_lossy);
|
||||
}
|
||||
|
||||
let native_min_quality = if allow_resize {
|
||||
WEBP_PERCEPTUAL_QUALITY
|
||||
} else {
|
||||
@@ -987,6 +1004,28 @@ fn encode_webp_native_target(
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_webp_lossless_pixels(pixels: &TargetPixels) -> Result<Vec<u8>, AppError> {
|
||||
let mut config = webp::WebPConfig::new()
|
||||
.map_err(|_| AppError::new(ErrorCode::CompressionFailed, "初始化 WebP 无损配置失败"))?;
|
||||
config.lossless = 1;
|
||||
config.quality = 100.0;
|
||||
config.method = 6;
|
||||
config.alpha_compression = 1;
|
||||
config.near_lossless = 100;
|
||||
config.exact = 1;
|
||||
config.thread_level = 0;
|
||||
|
||||
webp_encoder(pixels)
|
||||
.encode_advanced(&config)
|
||||
.map(|bytes| bytes.to_vec())
|
||||
.map_err(|err| {
|
||||
AppError::new(
|
||||
ErrorCode::CompressionFailed,
|
||||
format!("WebP 无损编码失败: {err:?}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_avif_target(
|
||||
image: DynamicImage,
|
||||
target_size: u64,
|
||||
@@ -1942,6 +1981,60 @@ mod tests {
|
||||
assert_eq!(detect_format(&output).unwrap(), ImageFmt::Webp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webp_target_prefers_lossless_when_it_fits() {
|
||||
let image = DynamicImage::ImageRgb8(RgbImage::from_fn(160, 120, |x, y| {
|
||||
let block = ((x / 20) + (y / 20) * 3) as u8;
|
||||
Rgb([
|
||||
block.wrapping_mul(31),
|
||||
block.wrapping_mul(17),
|
||||
block.wrapping_mul(11),
|
||||
])
|
||||
}));
|
||||
let pixels = prepare_target_pixels(&image);
|
||||
let lossless = encode_webp_lossless_pixels(&pixels).unwrap();
|
||||
let output = encode_webp_target(
|
||||
image.clone(),
|
||||
lossless.len() as u64,
|
||||
true,
|
||||
&CompressionDeadline::unlimited(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, lossless);
|
||||
assert_eq!(
|
||||
image::load_from_memory(&output).unwrap().to_rgb8(),
|
||||
image.to_rgb8()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webp_target_uses_quality_100_when_lossless_exceeds_the_cap() {
|
||||
let mut state = 0x7f4a_7c15_u32;
|
||||
let image = DynamicImage::ImageRgb8(RgbImage::from_fn(160, 120, |_x, _y| {
|
||||
let mut channel = || {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 17;
|
||||
state ^= state << 5;
|
||||
state as u8
|
||||
};
|
||||
Rgb([channel(), channel(), channel()])
|
||||
}));
|
||||
let pixels = prepare_target_pixels(&image);
|
||||
let max_lossy = encode_webp_pixels(&pixels, WEBP_TARGET_MAX_QUALITY).unwrap();
|
||||
let lossless = encode_webp_lossless_pixels(&pixels).unwrap();
|
||||
assert!(max_lossy.len() < lossless.len());
|
||||
|
||||
let output = encode_webp_target(
|
||||
image,
|
||||
max_lossy.len() as u64,
|
||||
true,
|
||||
&CompressionDeadline::unlimited(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(output, max_lossy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jpeg_target_encoder_prefers_perceptual_downscaling() {
|
||||
let image = DynamicImage::ImageRgb8(RgbImage::from_fn(800, 600, |x, y| {
|
||||
|
||||
Reference in New Issue
Block a user