use crate::processing::options::SaveOptions;
use crate::processing::save;
use image::{ImageBuffer, Rgb};
use libvips::{ops, VipsImage};
use super::tests_support::*;
fn create_textured_image(width: u32, height: u32) -> Vec<u8> {
let mut state: u32 = 0x2545_f491;
let mut img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(width, height);
for (_x, _y, pixel) in img.enumerate_pixels_mut() {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
*pixel = Rgb([
(state & 0xff) as u8,
((state >> 8) & 0xff) as u8,
((state >> 16) & 0xff) as u8,
]);
}
let mut bytes: Vec<u8> = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut bytes), image::ImageFormat::Png)
.unwrap();
bytes
}
#[test]
fn test_webp_save_honors_quality() {
init_vips();
let base = create_textured_image(400, 400);
let img = VipsImage::new_from_buffer(&base, "").unwrap();
let low = save::save_image(img, "webp", 20).unwrap();
let img = VipsImage::new_from_buffer(&base, "").unwrap();
let high = save::save_image(img, "webp", 95).unwrap();
assert!(!low.is_empty());
assert!(
low.len() < high.len(),
"webp quality ignored: q20 is {} bytes, q95 is {} bytes",
low.len(),
high.len()
);
}
#[test]
fn test_webp_save_lossless_applies() {
init_vips();
let base = create_textured_image(400, 400);
let mut options = SaveOptions::default();
options.webp.lossless = Some(true);
let img = VipsImage::new_from_buffer(&base, "").unwrap();
let lossless = save::save_image_with_options(img, "webp", 80, &options).unwrap();
let img = VipsImage::new_from_buffer(&base, "").unwrap();
let lossy = save::save_image(img, "webp", 80).unwrap();
assert!(!lossless.is_empty());
assert!(
lossless.len() > lossy.len(),
"webp lossless option had no effect: lossless {} bytes, lossy {} bytes",
lossless.len(),
lossy.len()
);
}
#[test]
fn test_webp_save_honors_max_bytes() {
init_vips();
let base = create_textured_image(400, 400);
let img = VipsImage::new_from_buffer(&base, "").unwrap();
let unbounded = save::save_image(img, "webp", 95).unwrap();
let img = VipsImage::new_from_buffer(&base, "").unwrap();
let budget = save::save_image(img, "webp", 20).unwrap().len();
let options = SaveOptions {
max_bytes: Some(budget),
..Default::default()
};
let img = VipsImage::new_from_buffer(&base, "").unwrap();
let bounded = save::save_image_with_options(img, "webp", 95, &options).unwrap();
assert!(
bounded.len() <= budget && bounded.len() < unbounded.len(),
"webp max_bytes not enforced: {} bytes for a {} byte budget (unbounded is {} bytes)",
bounded.len(),
budget,
unbounded.len()
);
}
#[test]
fn test_webp_save_suffix_carries_encoder_options() {
let mut options = SaveOptions::default();
assert_eq!(
save::webp_save_suffix(80, ops::ForeignKeep::All, &options),
".webp[Q=80,keep=all]"
);
options.webp.lossless = Some(true);
options.webp.smart_subsample = Some(true);
options.webp.preset = Some("photo".to_string());
assert_eq!(
save::webp_save_suffix(90, ops::ForeignKeep::None, &options),
".webp[Q=90,lossless,smart-subsample,preset=photo,keep=none]"
);
}
#[test]
fn test_webp_save_suffix_clamps_quality() {
let options = SaveOptions::default();
assert_eq!(
save::webp_save_suffix(0, ops::ForeignKeep::All, &options),
".webp[Q=1,keep=all]"
);
}
#[test]
fn test_webp_save_suffix_drops_unknown_preset() {
let mut options = SaveOptions::default();
options.webp.preset = Some("photo],lossless".to_string());
assert_eq!(
save::webp_save_suffix(75, ops::ForeignKeep::All, &options),
".webp[Q=75,keep=all]"
);
}
#[test]
fn test_heif_save_suffix_carries_encoder_options() {
let mut options = SaveOptions::default();
assert_eq!(
save::heif_save_suffix("avif", "av1", 80, 7, ops::ForeignKeep::All, &options),
".avif[Q=80,compression=av1,effort=7,subsample-mode=auto,keep=all]"
);
options.avif.no_subsample = Some(true);
assert_eq!(
save::heif_save_suffix("heif", "hevc", 80, 12, ops::ForeignKeep::None, &options),
".heif[Q=80,compression=hevc,effort=9,subsample-mode=off,keep=none]"
);
}
#[test]
fn test_gif_save_suffix_carries_encoder_options() {
assert_eq!(
save::gif_save_suffix(5, ops::ForeignKeep::All),
".gif[effort=5,keep=all]"
);
assert_eq!(
save::gif_save_suffix(0, ops::ForeignKeep::None),
".gif[effort=1,keep=none]"
);
}
#[test]
fn test_avif_save_produces_output() {
assert_encodes_without_property_error("avif");
}
#[test]
fn test_gif_save_produces_output() {
assert_encodes_without_property_error("gif");
}
fn assert_encodes_without_property_error(format: &str) {
init_vips();
if !save::is_format_supported(format) {
eprintln!("skipping {format}: this libvips build has no saver for it");
return;
}
let base = create_textured_image(64, 64);
let img = VipsImage::new_from_buffer(&base, "").unwrap();
clear_vips_error();
match save::save_image(img, format, 80) {
Ok(bytes) => assert!(!bytes.is_empty(), "{format} encode produced no bytes"),
Err(err) => {
let detail = vips_error_buffer();
assert!(
!detail.contains("no property named"),
"{format}: encoder options named a property this libvips does not have — {} ({err})",
detail.trim()
);
eprintln!(
"skipping {format}: not encodable by this libvips build — {}",
detail.trim()
);
}
}
}
#[test]
fn test_tiff_save_produces_output() {
assert_encodes_without_property_error("tiff");
}
#[test]
fn test_tiff_is_lossless_at_max_quality_and_lossy_below() {
init_vips();
if !save::is_format_supported("tiff") {
eprintln!("skipping: this libvips build has no TIFF saver");
return;
}
let source_bytes = create_textured_image(64, 64);
let expected = decode_rgba(&image_from(source_bytes.clone()));
let lossless = save::save_image(image_from(source_bytes.clone()), "tiff", 100).unwrap();
let lossy = save::save_image(image_from(source_bytes.clone()), "tiff", 60).unwrap();
let lossless_pixels = collect_rgba_pixels(&decode_rgba(&image_from(lossless)));
let lossy_pixels = collect_rgba_pixels(&decode_rgba(&image_from(lossy)));
let expected_pixels = collect_rgba_pixels(&expected);
assert_eq!(
lossless_pixels, expected_pixels,
"quality 100 should select LZW and round-trip every pixel"
);
assert_ne!(
lossy_pixels, expected_pixels,
"quality 60 should select JPEG compression, which is lossy"
);
}