use crate::processing::options::{Crop, Gravity, ParsedOptions, Resize, Watermark};
use crate::processing::process_image;
use crate::processing::save;
use crate::processing::transform;
use crate::processing::watermark;
use bytes::Bytes;
use image::GenericImageView;
use libvips::VipsImage;
use super::tests_support::*;
#[test]
fn test_crop_then_resize() {
init_vips();
let img = image_from(create_test_image(400, 400));
let crop = Crop {
x: 50,
y: 50,
width: 200,
height: 200,
gravity: None,
};
let cropped = transform::crop_image(img, crop).unwrap();
let resize = Resize {
resizing_type: "fit".to_string(),
width: 100,
height: 100,
};
let final_img = transform::apply_resize(cropped, &resize, &None, None, true).unwrap();
assert_eq!(final_img.get_width(), 100);
assert_eq!(final_img.get_height(), 100);
}
#[test]
fn test_resize_then_blur() {
init_vips();
let img = image_from(create_test_image(200, 200));
let resize = Resize {
resizing_type: "fit".to_string(),
width: 100,
height: 100,
};
let resized = transform::apply_resize(img, &resize, &None, None, true).unwrap();
let blurred = transform::apply_blur(resized, 3.0).unwrap();
assert_eq!(blurred.get_width(), 100);
assert_eq!(blurred.get_height(), 100);
}
#[test]
fn test_resize_then_sharpen() {
init_vips();
let img = image_from(create_test_image(200, 200));
let resize = Resize {
resizing_type: "fit".to_string(),
width: 300,
height: 300,
};
let resized = transform::apply_resize(img, &resize, &None, None, true).unwrap();
let sharpened = transform::apply_sharpen(resized, 1.0).unwrap();
assert_eq!(sharpened.get_width(), 300);
assert_eq!(sharpened.get_height(), 300);
}
#[test]
fn test_rotation_then_resize() {
init_vips();
let img = image_from(create_test_image(100, 200));
let rotated = transform::apply_rotation(img, 90).unwrap();
let resize = Resize {
resizing_type: "fit".to_string(),
width: 100,
height: 100,
};
let resized = transform::apply_resize(rotated, &resize, &None, None, true).unwrap();
assert_eq!(resized.get_width(), 100);
assert_eq!(resized.get_height(), 50);
}
#[test]
fn test_complex_pipeline_crop_resize_blur_rotate() {
init_vips();
let img = image_from(create_test_image(400, 400));
let crop = Crop {
x: 50,
y: 50,
width: 300,
height: 300,
gravity: None,
};
let img = transform::crop_image(img, crop).unwrap();
assert_eq!(img.get_width(), 300);
let resize = Resize {
resizing_type: "fit".to_string(),
width: 200,
height: 200,
};
let img = transform::apply_resize(img, &resize, &None, None, true).unwrap();
assert_eq!(img.get_width(), 200);
let img = transform::apply_blur(img, 2.0).unwrap();
let img = transform::apply_rotation(img, 90).unwrap();
assert_eq!(img.get_width(), 200);
assert_eq!(img.get_height(), 200);
}
#[test]
fn test_complex_pipeline_resize_padding_watermark() {
init_vips();
let img = image_from(create_test_image(200, 200));
let resize = Resize {
resizing_type: "fit".to_string(),
width: 150,
height: 150,
};
let img = transform::apply_resize(img, &resize, &None, None, true).unwrap();
let img = transform::apply_padding(img, 10, 10, 10, 10, &Some([255, 255, 255, 255])).unwrap();
assert_eq!(img.get_width(), 170);
assert_eq!(img.get_height(), 170);
let watermark = cached_watermark_from_bytes(create_test_image(30, 30));
let watermark_opts = Watermark {
opacity: 0.7,
position: "soea".to_string(),
};
let img = watermark::apply_watermark(img, &watermark, &watermark_opts, None).unwrap();
assert_eq!(img.get_width(), 170);
}
#[test]
fn test_process_image_extend_uses_current_dimensions_after_min_height() {
init_vips();
let source_bytes = Bytes::from(create_test_image(200, 100));
let img = VipsImage::new_from_buffer(&source_bytes, "").unwrap();
let parsed_options = ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 100,
height: 200,
}),
format: Some("png".to_string()),
enlarge: true,
extend: true,
min_height: Some(150),
..ParsedOptions::default()
};
let output = process_image(img, parsed_options, &source_bytes, None).unwrap();
let decoded = image::load_from_memory(&output).unwrap();
assert_eq!(decoded.dimensions(), (300, 200));
}
#[test]
fn test_max_result_dimension_rejects_oversized_output() {
init_vips();
let source_bytes = Bytes::from(create_test_image(64, 64));
let img = VipsImage::new_from_buffer(&source_bytes, "").unwrap();
let parsed_options = ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 4000,
height: 4000,
}),
format: Some("png".to_string()),
enlarge: true,
max_result_dimension: Some("2000".parse().unwrap()),
..ParsedOptions::default()
};
let err = process_image(img, parsed_options, &source_bytes, None).expect_err("should be rejected");
let message = err.to_string();
assert!(
message.contains("4000") && message.contains("2000"),
"error should name the result and the limit, got: {message}"
);
}
#[test]
fn test_max_result_dimension_allows_output_within_limit() {
init_vips();
let source_bytes = Bytes::from(create_test_image(64, 64));
let img = VipsImage::new_from_buffer(&source_bytes, "").unwrap();
let parsed_options = ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 500,
height: 500,
}),
format: Some("png".to_string()),
enlarge: true,
max_result_dimension: Some("500".parse().unwrap()),
..ParsedOptions::default()
};
let output = process_image(img, parsed_options, &source_bytes, None).unwrap();
let decoded = image::load_from_memory(&output).unwrap();
assert_eq!(decoded.dimensions(), (500, 500));
}
#[test]
fn test_min_dimensions_upscale_regardless_of_enlarge() {
init_vips();
let source_bytes = Bytes::from(create_test_image(100, 100));
let img = VipsImage::new_from_buffer(&source_bytes, "").unwrap();
let parsed_options = ParsedOptions {
min_width: Some(500),
format: Some("png".to_string()),
enlarge: false,
..ParsedOptions::default()
};
let output = process_image(img, parsed_options, &source_bytes, None).unwrap();
let decoded = image::load_from_memory(&output).unwrap();
assert_eq!(decoded.dimensions(), (500, 500));
}
#[test]
fn test_fit_inside_a_square_box_downscales_a_wide_source() {
init_vips();
let source_bytes = Bytes::from(create_test_image(1600, 400));
let img = VipsImage::new_from_buffer(&source_bytes, "").unwrap();
let parsed_options = ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 500,
height: 500,
}),
format: Some("png".to_string()),
enlarge: false,
..ParsedOptions::default()
};
let output = process_image(img, parsed_options, &source_bytes, None).unwrap();
assert_eq!(image::load_from_memory(&output).unwrap().dimensions(), (500, 125));
}
#[test]
fn test_load_shrink_never_undershoots_the_target() {
use crate::processing::load_shrink_factor;
let plan = |w: u32, h: u32| ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: w,
height: h,
}),
..ParsedOptions::default()
};
for (sw, sh, tw, th) in [
(4000u32, 3000u32, 200u32, 200u32),
(9000, 7000, 450, 450),
(1000, 1000, 999, 999),
(4000, 100, 200, 50),
(100, 4000, 50, 200),
(5000, 5000, 1, 1),
] {
let factor = load_shrink_factor(&plan(tw, th), sw, sh);
assert!(factor.is_power_of_two() && factor <= 8, "odd factor {factor}");
let (after_w, after_h) = (sw / factor, sh / factor);
assert!(
after_w >= tw && after_h >= th,
"shrink 1/{factor} took {sw}x{sh} to {after_w}x{after_h}, below the {tw}x{th} target"
);
}
}
#[test]
fn test_load_shrink_declines_when_it_cannot_reason_about_the_target() {
use crate::processing::load_shrink_factor;
let with = |f: fn(&mut ParsedOptions)| {
let mut o = ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 1000,
height: 1000,
}),
..ParsedOptions::default()
};
f(&mut o);
load_shrink_factor(&o, 4000, 4000)
};
assert_eq!(with(|_| ()), 4, "baseline");
assert_eq!(
with(|o| o.crop = Some(Crop {
x: 0,
y: 0,
width: 50,
height: 50,
gravity: None
})),
1
);
assert_eq!(with(|o| o.raw = true), 1);
assert_eq!(with(|o| o.resize = None), 1);
assert_eq!(with(|o| o.dpr = Some(2.0)), 2, "dpr doubles the pixels needed");
assert_eq!(with(|o| o.zoom = Some(2.0)), 2, "zoom doubles the pixels needed");
assert_eq!(
with(|o| o.min_width = Some(2000)),
2,
"a minimum above the resize target raises the floor"
);
}
#[test]
fn test_shrink_on_load_does_not_change_output_dimensions() {
init_vips();
let source_bytes = Bytes::from(create_test_image_jpeg(2000, 1600));
let options = || ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 200,
height: 200,
}),
format: Some("png".to_string()),
..ParsedOptions::default()
};
let full = process_image(image_from(source_bytes.to_vec()), options(), &source_bytes, None).unwrap();
let factor = crate::processing::load_shrink_factor(&options(), 2000, 1600);
assert!(factor > 1, "this plan should qualify for shrink-on-load");
let shrunk = VipsImage::new_from_buffer(&source_bytes, &format!("shrink={factor}")).unwrap();
let reduced = process_image(shrunk, options(), &source_bytes, None).unwrap();
assert_eq!(
image::load_from_memory(&full).unwrap().dimensions(),
image::load_from_memory(&reduced).unwrap().dimensions()
);
}
#[test]
fn test_load_shrink_declines_for_force_with_an_unset_axis() {
use crate::processing::load_shrink_factor;
use crate::processing::transform::resolve_resize_dimensions;
let plan = |kind: &str, w: u32, h: u32| ParsedOptions {
resize: Some(Resize {
resizing_type: kind.to_string(),
width: w,
height: h,
}),
..ParsedOptions::default()
};
for (w, h) in [(0, 500), (500, 0)] {
let options = plan("force", w, h);
assert_eq!(
load_shrink_factor(&options, 4000, 3000),
1,
"force with an unset axis must decline shrink-on-load"
);
}
let fit = plan("fit", 0, 500);
let resize = fit.resize.as_ref().unwrap();
let full = resolve_resize_dimensions(resize, 4000, 3000).unwrap();
let shrunk = resolve_resize_dimensions(resize, 1000, 750).unwrap();
assert_eq!(full, shrunk, "an aspect-derived axis should survive a shrink");
assert!(load_shrink_factor(&fit, 4000, 3000) > 1, "fit should still qualify");
}
#[test]
fn test_load_shrink_uses_displayed_dimensions_for_rotated_sources() {
use crate::processing::load_shrink_factor;
let options = ParsedOptions {
resize: Some(Resize {
resizing_type: "fill".to_string(),
width: 2000,
height: 1000,
}),
..ParsedOptions::default()
};
let stored = load_shrink_factor(&options, 8000, 4000);
let displayed = load_shrink_factor(&options, 4000, 8000);
assert_eq!(stored, 4);
assert_eq!(displayed, 2, "the rotated shape needs a gentler shrink");
assert!(
displayed < stored,
"choosing against stored dimensions over-shrinks a transposed source"
);
let (after_w, after_h) = (8000 / displayed, 4000 / displayed);
let (rotated_w, rotated_h) = (after_h, after_w);
assert!(
rotated_w >= 2000 && rotated_h >= 1000,
"rotated {rotated_w}x{rotated_h} cannot fill a 2000x1000 box"
);
}
#[test]
fn test_webp_load_scale_never_undershoots_the_target() {
use crate::processing::load_scale_factor;
init_vips();
if !webp_loader_takes_a_scale() {
eprintln!("skipping: this libvips build cannot decode WebP at a scale");
return;
}
for (sw, sh, tw, th) in [
(4000u32, 3000u32, 1333u32, 1000u32), (4000, 3000, 999, 749),
(1000, 1000, 333, 333),
(1000, 1000, 667, 667),
(2000, 1000, 700, 300),
(999, 1001, 333, 334),
] {
let options = ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: tw,
height: th,
}),
..ParsedOptions::default()
};
let Some(scale) = load_scale_factor(&options, sw, sh) else {
continue;
};
let webp = save::save_image(image_from(create_test_image(sw, sh)), "webp", 80).unwrap();
let decoded = VipsImage::new_from_buffer(&webp, &format!("scale={scale}")).unwrap();
let (dw, dh) = (decoded.get_width() as u32, decoded.get_height() as u32);
assert!(
dw >= tw && dh >= th,
"scale {scale} took {sw}x{sh} to {dw}x{dh}, below the {tw}x{th} target"
);
}
}
#[test]
fn test_webp_scale_is_finer_than_the_jpeg_shrink() {
use crate::processing::{load_scale_factor, load_shrink_factor};
let options = ParsedOptions {
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 1000,
height: 1000,
}),
..ParsedOptions::default()
};
assert_eq!(load_shrink_factor(&options, 3000, 3000), 2);
let scale = load_scale_factor(&options, 3000, 3000).unwrap();
assert!(
(scale - 1.0 / 3.0).abs() < 0.01,
"expected roughly a third, got {scale}"
);
assert_eq!(load_shrink_factor(&options, 1800, 1800), 1);
assert!(load_scale_factor(&options, 1800, 1800).is_some());
}
fn webp_loader_takes_a_scale() -> bool {
if !crate::processing::save::is_format_supported("webp") {
return false;
}
let Ok(encoded) = save::save_image(image_from(create_test_image(32, 32)), "webp", 80) else {
return false;
};
VipsImage::new_from_buffer(&encoded, "scale=0.5").is_ok()
}
#[test]
fn test_load_shrink_measures_the_crop_region_not_the_source() {
use crate::processing::load_shrink_factor;
let plan = |crop: Option<(u32, u32)>| ParsedOptions {
crop: crop.map(|(w, h)| Crop {
x: 0,
y: 0,
width: w,
height: h,
gravity: None,
}),
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 500,
height: 375,
}),
..ParsedOptions::default()
};
assert_eq!(load_shrink_factor(&plan(Some((2000, 1500))), 8000, 6000), 4);
assert_eq!(load_shrink_factor(&plan(None), 8000, 6000), 8);
assert_eq!(load_shrink_factor(&plan(Some((600, 450))), 8000, 6000), 1);
}
#[test]
fn test_cropped_request_survives_a_reduced_decode() {
init_vips();
let source_bytes = Bytes::from(create_test_image_jpeg(2000, 1600));
let options = || ParsedOptions {
crop: Some(Crop {
x: 0,
y: 0,
width: 1000,
height: 800,
gravity: Some(Gravity::SouthEast),
}),
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 250,
height: 200,
}),
format: Some("png".to_string()),
..ParsedOptions::default()
};
let full = process_image(image_from(source_bytes.to_vec()), options(), &source_bytes, None).unwrap();
let factor = crate::processing::load_shrink_factor(&options(), 2000, 1600);
assert!(factor > 1, "a cropped request should now qualify for shrink-on-load");
let shrunk = VipsImage::new_from_buffer(&source_bytes, &format!("shrink={factor}")).unwrap();
let mut scaled_options = options();
if let Some(crop) = scaled_options.crop.as_mut() {
crop.width /= factor;
crop.height /= factor;
}
let reduced = process_image(shrunk, scaled_options, &source_bytes, None).unwrap();
assert_eq!(
image::load_from_memory(&full).unwrap().dimensions(),
image::load_from_memory(&reduced).unwrap().dimensions(),
"the cropped result changed size when the source was decoded smaller"
);
}
#[test]
fn test_trim_disables_scale_on_load() {
use crate::processing::load_shrink_factor;
use crate::processing::options::Trim;
let plan = |trim: Option<Trim>| ParsedOptions {
trim,
resize: Some(Resize {
resizing_type: "fit".to_string(),
width: 200,
height: 200,
}),
..ParsedOptions::default()
};
assert!(
load_shrink_factor(&plan(None), 4000, 4000) > 1,
"baseline should shrink"
);
assert_eq!(
load_shrink_factor(
&plan(Some(Trim {
threshold: 10.0,
color: None,
equal_hor: false,
equal_ver: false
})),
4000,
4000
),
1
);
}