Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c93005e
add pic-scale-safe as a dependency
Shnatsel Nov 9, 2025
d0a92af
Import the resize adapter from pic-scale-safe to DynamicImage from wo…
Shnatsel Nov 9, 2025
3e366f2
Refactoring to match image function signatures
Shnatsel Nov 9, 2025
428e52d
Refactor to accept input as a read-only reference; sadly incurs an un…
Shnatsel Nov 9, 2025
1e42609
Expose the correct filter type in the API
Shnatsel Nov 9, 2025
867b0d1
Add doc comment on the module
Shnatsel Nov 9, 2025
8a1df84
Switch resizing DynamicImage from using built-in resize impl to pic-s…
Shnatsel Nov 9, 2025
b3b43eb
Do not enable rayon on pic-scale-safe to avoid memory exhaustion; see…
Shnatsel Nov 9, 2025
a8c6e0d
Add a TODO to re-enable rayon
Shnatsel Nov 9, 2025
2d07f42
Fix resampling function conversion
Shnatsel Nov 9, 2025
d8d73bf
Eliminate the extra copy in resizing now that we are no longer bound …
Shnatsel Nov 16, 2025
17fd278
Add a basic smoke test for resizing
Shnatsel Nov 16, 2025
15f0653
wire up the resize without an extra copy to DynamicImage methods
Shnatsel Nov 16, 2025
f29ebce
Fix compile errors in sample.rs
Shnatsel Nov 16, 2025
8ea7f5c
Update example code
Shnatsel Nov 16, 2025
e6da22d
Merge remote-tracking branch 'origin/main' into correct-resize
Shnatsel Nov 16, 2025
f3714a9
Fix resize benchmark code
Shnatsel Nov 16, 2025
a983704
Fix resize benchmark code, for real this time
Shnatsel Nov 16, 2025
ee67d2e
cargo fmt
Shnatsel Nov 16, 2025
aae0516
Black-box the image properly in resizing benchmark
Shnatsel Nov 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ tiff = { version = "0.10.3", optional = true }
zune-core = { version = "0.5.0", default-features = false, optional = true }
zune-jpeg = { version = "0.5.5", optional = true }
serde = { version = "1.0.214", optional = true, features = ["derive"] }
pic-scale-safe = "0.1.5"

[dev-dependencies]
crc32fast = "1.2.0"
Expand Down Expand Up @@ -86,6 +87,8 @@ tiff = ["dep:tiff"]
webp = ["dep:image-webp"]

# Other features
# TODO: add pic-scale-safe rayon in the next breaking release,
# see https://github.com/image-rs/image/pull/2639#discussion_r2508356543
rayon = ["dep:rayon", "ravif?/threading", "exr?/rayon"] # Enables multi-threading
nasm = ["ravif?/asm"] # Enables use of nasm by rav1e (requires nasm to be installed)
color_quant = ["dep:color_quant"] # Enables color quantization
Expand Down
6 changes: 3 additions & 3 deletions examples/scaledown/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl fmt::Display for Elapsed {
}

fn main() {
let img = image::open("examples/scaledown/test.jpg").unwrap();
let mut img = image::open("examples/scaledown/test.jpg").unwrap();
for &(name, filter) in &[
("near", FilterType::Nearest),
("tri", FilterType::Triangle),
Expand All @@ -34,10 +34,10 @@ fn main() {
("lcz2", FilterType::Lanczos3),
] {
let timer = Instant::now();
let scaled = img.resize(400, 400, filter);
img.resize(400, 400, filter);
println!("Scaled by {} in {}", name, Elapsed::from(&timer));
let mut output = File::create(format!("test-{name}.png")).unwrap();
scaled.write_to(&mut output, ImageFormat::Png).unwrap();
img.write_to(&mut output, ImageFormat::Png).unwrap();
}

for size in &[20_u32, 40, 100, 200, 400] {
Expand Down
11 changes: 6 additions & 5 deletions examples/scaleup/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,25 @@ impl fmt::Display for Elapsed {
}

fn main() {
let tiny = image::open("examples/scaleup/tinycross.png").unwrap();
let mut tiny = image::open("examples/scaleup/tinycross.png").unwrap();
for &(name, filter) in &[
("near", FilterType::Nearest),
("tri", FilterType::Triangle),
("xcmr", FilterType::CatmullRom),
("ygauss", FilterType::Gaussian),
("zlcz2", FilterType::Lanczos3),
] {
let mut tiny1 = tiny.clone();
let timer = Instant::now();
let scaled = tiny.resize(32, 32, filter);
tiny1.resize(32, 32, filter);
println!("Scaled by {} in {}", name, Elapsed::from(&timer));
let mut output = File::create(format!("up2-{name}.png")).unwrap();
scaled.write_to(&mut output, ImageFormat::Png).unwrap();
tiny1.write_to(&mut output, ImageFormat::Png).unwrap();

let timer = Instant::now();
let scaled = tiny.resize(48, 48, filter);
tiny.resize(48, 48, filter);
println!("Scaled by {} in {}", name, Elapsed::from(&timer));
let mut output = File::create(format!("up3-{name}.png")).unwrap();
scaled.write_to(&mut output, ImageFormat::Png).unwrap();
tiny.write_to(&mut output, ImageFormat::Png).unwrap();
}
}
1 change: 1 addition & 0 deletions src/imageops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod affine;
pub mod colorops;
mod fast_blur;
mod filter_1d;
pub(crate) mod resize;
mod sample;

pub use fast_blur::fast_blur;
Expand Down
210 changes: 210 additions & 0 deletions src/imageops/resize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//! Implementation of resizing delegated to pic-scale-safe crate.
//! It is an all-safe-code implementation with competitive performance
//! and with optional parallelism to boost it even further.
//!
//! Everything that references pic-scale-safe crate is contained to this file
//! so that it could easily be made an optional dependency in the future.

use std::ops::{AddAssign, BitXor};

use crate::{imageops::FilterType, DynamicImage, ImageBuffer, Pixel};
use pic_scale_safe::ResamplingFunction;

pub(crate) fn resize_impl(
image: &mut DynamicImage,
dst_width: u32,
dst_height: u32,
algorithm: FilterType,
) -> Result<(), String> {
if image.width() == dst_width && image.height() == dst_height {
return Ok(());
}
let algorithm = convert_filter_type(algorithm);
let alg = algorithm; // otherwise rustfmt breaks up too-long-lines and the formatting is a mess
let src_size = ImageSize::new(image.width() as usize, image.height() as usize);
let dst_size = ImageSize::new(dst_width as usize, dst_height as usize);

// Premultiply the image by alpha channel to avoid color bleed from fully transparent pixels.
let mut premultiplied_by_alpha = false;
// There is no need to premultiply for nearest-neighbor resampling because it does not perform any blending.
// This is actually a valuable optimization because thumbnailing uses nearest-neighbor for a part of the process.
if algorithm != ResamplingFunction::Nearest && !has_constant_alpha(image) {
premultiply_alpha(image);
premultiplied_by_alpha = true;
}

use pic_scale_safe::*;
use DynamicImage::*;
let mut resized = match image {
ImageLuma8(src) => {
let resized = resize_plane8(src.as_raw(), src_size, dst_size, alg)?;
ImageLuma8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageLumaA8(src) => {
let resized = resize_plane8_with_alpha(src.as_raw(), src_size, dst_size, alg)?;
ImageLumaA8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageRgb8(src) => {
let resized = resize_rgb8(src.as_raw(), src_size, dst_size, alg)?;
ImageRgb8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageRgba8(src) => {
let resized = resize_rgba8(src.as_raw(), src_size, dst_size, alg)?;
ImageRgba8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageLuma16(src) => {
let resized = resize_plane16(src.as_raw(), src_size, dst_size, 16, alg)?;
ImageLuma16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageLumaA16(src) => {
let resized = resize_plane16_with_alpha(src.as_raw(), src_size, dst_size, 16, alg)?;
ImageLumaA16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageRgb16(src) => {
let resized = resize_rgb16(src.as_raw(), src_size, dst_size, 16, alg)?;
ImageRgb16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageRgba16(src) => {
let resized = resize_rgba16(src.as_raw(), src_size, dst_size, 16, alg)?;
ImageRgba16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageRgb32F(src) => {
let resized = resize_rgb_f32(src.as_raw(), src_size, dst_size, alg)?;
ImageRgb32F(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
ImageRgba32F(src) => {
let resized = resize_rgba_f32(src.as_raw(), src_size, dst_size, alg)?;
ImageRgba32F(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
}
};
if premultiplied_by_alpha {
unpremultiply_alpha(&mut resized);
}
*image = resized;
Ok(())
}

fn convert_filter_type(filter: FilterType) -> ResamplingFunction {
match filter {
FilterType::Nearest => ResamplingFunction::Nearest,
FilterType::Triangle => ResamplingFunction::Bilinear,
FilterType::CatmullRom => ResamplingFunction::CatmullRom,
FilterType::Gaussian => ResamplingFunction::Gaussian,
FilterType::Lanczos3 => ResamplingFunction::Lanczos3,
}
}

fn premultiply_alpha(image: &mut DynamicImage) {
use pic_scale_safe::*;
match image {
DynamicImage::ImageLuma8(_) => (),
DynamicImage::ImageLumaA8(buf) => {
premultiply_la8(buf.as_mut());
}
DynamicImage::ImageRgb8(_) => (),
DynamicImage::ImageRgba8(buf) => {
premultiply_rgba8(buf.as_mut());
}
DynamicImage::ImageLuma16(_) => (),
DynamicImage::ImageLumaA16(buf) => {
premultiply_la16(buf.as_mut(), 16);
}
DynamicImage::ImageRgb16(_) => (),
DynamicImage::ImageRgba16(buf) => {
premultiply_rgba16(buf.as_mut(), 16);
}
DynamicImage::ImageRgb32F(_) => (),
DynamicImage::ImageRgba32F(buf) => {
premultiply_rgba_f32(buf.as_mut());
}
}
}

/// Reverses premultiplication by alpha
fn unpremultiply_alpha(image: &mut DynamicImage) {
use pic_scale_safe::*;
match image {
DynamicImage::ImageLuma8(_) => (),
DynamicImage::ImageLumaA8(buf) => unpremultiply_la8(buf.as_mut()),
DynamicImage::ImageRgb8(_) => (),
DynamicImage::ImageRgba8(buf) => unpremultiply_rgba8(buf.as_mut()),
DynamicImage::ImageLuma16(_) => (),
DynamicImage::ImageLumaA16(buf) => unpremultiply_la16(buf.as_mut(), 16),
DynamicImage::ImageRgb16(_) => (),
DynamicImage::ImageRgba16(buf) => unpremultiply_rgba16(buf.as_mut(), 16),
DynamicImage::ImageRgb32F(_) => (),
DynamicImage::ImageRgba32F(buf) => unpremultiply_rgba_f32(buf),
}
}

#[must_use]
fn has_constant_alpha(image: &DynamicImage) -> bool {
match image {
DynamicImage::ImageLuma8(_) => true,
DynamicImage::ImageLumaA8(buf) => has_constant_alpha_integer(buf),
DynamicImage::ImageRgb8(_) => true,
DynamicImage::ImageRgba8(buf) => has_constant_alpha_integer(buf),
DynamicImage::ImageLuma16(_) => true,
DynamicImage::ImageLumaA16(buf) => has_constant_alpha_integer(buf),
DynamicImage::ImageRgb16(_) => true,
DynamicImage::ImageRgba16(buf) => has_constant_alpha_integer(buf),
DynamicImage::ImageRgb32F(_) => true,
DynamicImage::ImageRgba32F(buf) => has_constant_alpha_f32(buf),
}
}

#[must_use]
fn has_constant_alpha_integer<P, Container>(img: &ImageBuffer<P, Container>) -> bool
where
P: Pixel + 'static,
Container: std::ops::Deref<Target = [P::Subpixel]>,
P::Subpixel:
Copy + PartialEq + BitXor<P::Subpixel, Output = P::Subpixel> + AddAssign + Into<u64>,
{
let first_pixel_alpha = *match img.pixels().next() {
Some(pixel) => pixel.channels().last().unwrap(), // there doesn't seem to be a better way to retrieve the alpha channel
None => return true, // empty input image
};
// A naive, slower implementation that branches on every pixel:
//
// img.pixels().map(|pixel| pixel.channels().last().unwrap()).all(|alpha| alpha == first_pixel_alpha);
//
// Instead of doing that we scan every row first with cheap arithmetic instructions
// and only compare the sum of divergences on every row, which should be 0
let mut sum_of_diffs: u64 = 0;
for row in img.rows() {
row.for_each(|pixel| {
let alpha = pixel.alpha();
sum_of_diffs += alpha.bitxor(first_pixel_alpha).into();
});
if sum_of_diffs != 0 {
return false;
}
}
true
}

#[must_use]
fn has_constant_alpha_f32(img: &ImageBuffer<crate::Rgba<f32>, Vec<f32>>) -> bool {
// Optimizing correctly in presence of NaNs and infinities is tricky, so just do the naive thing for now
let first_pixel_alpha = match img.pixels().next() {
Some(pixel) => pixel.alpha(),
None => return true, // empty input image
};
img.pixels()
.map(|pixel| pixel.channels().last().unwrap())
.all(|alpha| *alpha == first_pixel_alpha)
}

#[cfg(test)]
mod tests {
use crate::{imageops::resize::resize_impl, DynamicImage};

#[test]
fn smoke_test() {
let mut image = DynamicImage::new(10, 10, crate::ColorType::Rgba8);
resize_impl(&mut image, 5, 5, crate::imageops::FilterType::Lanczos3).unwrap();
assert_eq!(image.height(), 5);
assert_eq!(image.width(), 5);
}
}
11 changes: 8 additions & 3 deletions src/imageops/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1633,8 +1633,9 @@ mod tests {
fn test_resize_same_size() {
use std::path::Path;
let img = crate::open(Path::new("./examples/fractal.png")).unwrap();
let resize = img.resize(img.width(), img.height(), FilterType::Triangle);
assert!(img.pixels().eq(resize.pixels()));
let mut resized = img.clone();
resized.resize(img.width(), img.height(), FilterType::Triangle);
assert!(img.pixels().eq(resized.pixels()));
}

#[test]
Expand Down Expand Up @@ -1755,7 +1756,11 @@ mod tests {
);
let image = crate::open(path).unwrap();
b.iter(|| {
test::black_box(image.resize(image.width(), image.height(), FilterType::CatmullRom));
test::black_box(image.clone()).resize(
image.width(),
image.height(),
FilterType::CatmullRom,
);
});
b.bytes = u64::from(image.width() * image.height() * 3);
}
Expand Down
Loading
Loading