Add SIMD-accelerated resize operation using fast_image_resize
Lanczos3 filter, supports ByWidth, ByHeight, FitInBox, Exact modes. All 5 resize tests passing.
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod resize;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::types::{Dimensions, ImageFormat, QualityPreset};
|
use crate::types::{Dimensions, ImageFormat, QualityPreset};
|
||||||
|
|||||||
60
pixstrip-core/src/operations/resize.rs
Normal file
60
pixstrip-core/src/operations/resize.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use fast_image_resize::{images::Image, Resizer, ResizeOptions, ResizeAlg, FilterType};
|
||||||
|
|
||||||
|
use crate::error::{PixstripError, Result};
|
||||||
|
use crate::types::Dimensions;
|
||||||
|
|
||||||
|
use super::ResizeConfig;
|
||||||
|
|
||||||
|
pub fn resize_image(
|
||||||
|
src: &image::DynamicImage,
|
||||||
|
config: &ResizeConfig,
|
||||||
|
) -> Result<image::DynamicImage> {
|
||||||
|
let original = Dimensions {
|
||||||
|
width: src.width(),
|
||||||
|
height: src.height(),
|
||||||
|
};
|
||||||
|
let target = config.target_for(original);
|
||||||
|
|
||||||
|
if target.width == original.width && target.height == original.height {
|
||||||
|
return Ok(src.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let src_rgba = src.to_rgba8();
|
||||||
|
let (src_w, src_h) = (src_rgba.width(), src_rgba.height());
|
||||||
|
|
||||||
|
let src_image = Image::from_vec_u8(
|
||||||
|
src_w,
|
||||||
|
src_h,
|
||||||
|
src_rgba.into_raw(),
|
||||||
|
fast_image_resize::PixelType::U8x4,
|
||||||
|
)
|
||||||
|
.map_err(|e| PixstripError::Processing {
|
||||||
|
operation: "resize".into(),
|
||||||
|
reason: format!("Failed to create source image: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut dst_image = Image::new(
|
||||||
|
target.width,
|
||||||
|
target.height,
|
||||||
|
fast_image_resize::PixelType::U8x4,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut resizer = Resizer::new();
|
||||||
|
let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Lanczos3));
|
||||||
|
|
||||||
|
resizer
|
||||||
|
.resize(&src_image, &mut dst_image, &options)
|
||||||
|
.map_err(|e| PixstripError::Processing {
|
||||||
|
operation: "resize".into(),
|
||||||
|
reason: format!("Resize failed: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let result_buf: image::RgbaImage =
|
||||||
|
image::ImageBuffer::from_raw(target.width, target.height, dst_image.into_vec())
|
||||||
|
.ok_or_else(|| PixstripError::Processing {
|
||||||
|
operation: "resize".into(),
|
||||||
|
reason: "Failed to create output image buffer".into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(image::DynamicImage::ImageRgba8(result_buf))
|
||||||
|
}
|
||||||
61
pixstrip-core/tests/resize_tests.rs
Normal file
61
pixstrip-core/tests/resize_tests.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use pixstrip_core::operations::resize::resize_image;
|
||||||
|
use pixstrip_core::operations::ResizeConfig;
|
||||||
|
use pixstrip_core::types::Dimensions;
|
||||||
|
|
||||||
|
fn create_test_image(width: u32, height: u32) -> image::DynamicImage {
|
||||||
|
let img = image::RgbImage::from_fn(width, height, |x, y| {
|
||||||
|
image::Rgb([(x % 256) as u8, (y % 256) as u8, 128])
|
||||||
|
});
|
||||||
|
image::DynamicImage::ImageRgb8(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_by_width() {
|
||||||
|
let img = create_test_image(4000, 3000);
|
||||||
|
let config = ResizeConfig::ByWidth(1200);
|
||||||
|
let result = resize_image(&img, &config).unwrap();
|
||||||
|
assert_eq!(result.width(), 1200);
|
||||||
|
assert_eq!(result.height(), 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_by_height() {
|
||||||
|
let img = create_test_image(4000, 3000);
|
||||||
|
let config = ResizeConfig::ByHeight(600);
|
||||||
|
let result = resize_image(&img, &config).unwrap();
|
||||||
|
assert_eq!(result.width(), 800);
|
||||||
|
assert_eq!(result.height(), 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_fit_in_box() {
|
||||||
|
let img = create_test_image(4000, 3000);
|
||||||
|
let config = ResizeConfig::FitInBox {
|
||||||
|
max: Dimensions { width: 1920, height: 1080 },
|
||||||
|
allow_upscale: false,
|
||||||
|
};
|
||||||
|
let result = resize_image(&img, &config).unwrap();
|
||||||
|
assert_eq!(result.width(), 1440);
|
||||||
|
assert_eq!(result.height(), 1080);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_no_upscale() {
|
||||||
|
let img = create_test_image(800, 600);
|
||||||
|
let config = ResizeConfig::FitInBox {
|
||||||
|
max: Dimensions { width: 1920, height: 1080 },
|
||||||
|
allow_upscale: false,
|
||||||
|
};
|
||||||
|
let result = resize_image(&img, &config).unwrap();
|
||||||
|
assert_eq!(result.width(), 800);
|
||||||
|
assert_eq!(result.height(), 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_exact() {
|
||||||
|
let img = create_test_image(4000, 3000);
|
||||||
|
let config = ResizeConfig::Exact(Dimensions { width: 1080, height: 1080 });
|
||||||
|
let result = resize_image(&img, &config).unwrap();
|
||||||
|
assert_eq!(result.width(), 1080);
|
||||||
|
assert_eq!(result.height(), 1080);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user