Coverage Report

Created: 2026-10-17 08:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/imageops/resize.rs
Line
Count
Source
1
//! Implementation of resizing delegated to pic-scale-safe crate.
2
//! It is an all-safe-code implementation with competitive performance
3
//! and with optional parallelism to boost it even further.
4
//!
5
//! Everything that references pic-scale-safe crate is contained to this file
6
//! so that it could easily be made an optional dependency in the future.
7
8
use std::ops::{AddAssign, BitXor};
9
10
use crate::{imageops::FilterType, DynamicImage, ImageBuffer, Pixel};
11
use pic_scale_safe::ResamplingFunction;
12
13
0
pub(crate) fn resize_impl(
14
0
    image: &mut DynamicImage,
15
0
    dst_width: u32,
16
0
    dst_height: u32,
17
0
    algorithm: FilterType,
18
0
) -> Result<(), String> {
19
0
    if image.width() == dst_width && image.height() == dst_height {
20
0
        return Ok(());
21
0
    }
22
0
    let algorithm = convert_filter_type(algorithm);
23
0
    let alg = algorithm; // otherwise rustfmt breaks up too-long-lines and the formatting is a mess
24
0
    let src_size = ImageSize::new(image.width() as usize, image.height() as usize);
25
0
    let dst_size = ImageSize::new(dst_width as usize, dst_height as usize);
26
27
    // Premultiply the image by alpha channel to avoid color bleed from fully transparent pixels.
28
0
    let mut premultiplied_by_alpha = false;
29
    // There is no need to premultiply for nearest-neighbor resampling because it does not perform any blending.
30
    // This is actually a valuable optimization because thumbnailing uses nearest-neighbor for a part of the process.
31
0
    if algorithm != ResamplingFunction::Nearest && !has_constant_alpha(image) {
32
0
        premultiply_alpha(image);
33
0
        premultiplied_by_alpha = true;
34
0
    }
35
36
    use pic_scale_safe::*;
37
    use DynamicImage::*;
38
0
    let mut resized = match image {
39
0
        ImageLuma8(src) => {
40
0
            let resized = resize_plane8(src.subpixels(), src_size, dst_size, alg)?;
41
0
            ImageLuma8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
42
        }
43
0
        ImageLumaA8(src) => {
44
0
            let resized = resize_plane8_with_alpha(src.subpixels(), src_size, dst_size, alg)?;
45
0
            ImageLumaA8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
46
        }
47
0
        ImageRgb8(src) => {
48
0
            let resized = resize_rgb8(src.subpixels(), src_size, dst_size, alg)?;
49
0
            ImageRgb8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
50
        }
51
0
        ImageRgba8(src) => {
52
0
            let resized = resize_rgba8(src.subpixels(), src_size, dst_size, alg)?;
53
0
            ImageRgba8(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
54
        }
55
0
        ImageLuma16(src) => {
56
0
            let resized = resize_plane16(src.subpixels(), src_size, dst_size, 16, alg)?;
57
0
            ImageLuma16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
58
        }
59
0
        ImageLumaA16(src) => {
60
0
            let resized = resize_plane16_with_alpha(src.subpixels(), src_size, dst_size, 16, alg)?;
61
0
            ImageLumaA16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
62
        }
63
0
        ImageRgb16(src) => {
64
0
            let resized = resize_rgb16(src.subpixels(), src_size, dst_size, 16, alg)?;
65
0
            ImageRgb16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
66
        }
67
0
        ImageRgba16(src) => {
68
0
            let resized = resize_rgba16(src.subpixels(), src_size, dst_size, 16, alg)?;
69
0
            ImageRgba16(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
70
        }
71
0
        ImageLuma32F(src) => {
72
0
            let resized = resize_plane_f32(src.as_raw(), src_size, dst_size, alg)?;
73
0
            ImageLuma32F(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
74
        }
75
0
        ImageLumaA32F(src) => {
76
0
            let resized = resize_luma_alpha_f32(src.as_raw(), src_size, dst_size, alg)?;
77
0
            ImageLumaA32F(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
78
        }
79
0
        ImageRgb32F(src) => {
80
0
            let resized = resize_rgb_f32(src.subpixels(), src_size, dst_size, alg)?;
81
0
            ImageRgb32F(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
82
        }
83
0
        ImageRgba32F(src) => {
84
0
            let resized = resize_rgba_f32(src.subpixels(), src_size, dst_size, alg)?;
85
0
            ImageRgba32F(ImageBuffer::from_raw(dst_width, dst_height, resized).unwrap())
86
        }
87
    };
88
0
    if premultiplied_by_alpha {
89
0
        unpremultiply_alpha(&mut resized);
90
0
    }
91
0
    *image = resized;
92
0
    Ok(())
93
0
}
94
95
0
fn convert_filter_type(filter: FilterType) -> ResamplingFunction {
96
0
    match filter {
97
0
        FilterType::Nearest => ResamplingFunction::Nearest,
98
0
        FilterType::Triangle => ResamplingFunction::Bilinear,
99
0
        FilterType::CatmullRom => ResamplingFunction::CatmullRom,
100
0
        FilterType::Gaussian => ResamplingFunction::Gaussian,
101
0
        FilterType::Lanczos3 => ResamplingFunction::Lanczos3,
102
    }
103
0
}
104
105
0
fn premultiply_alpha(image: &mut DynamicImage) {
106
    use pic_scale_safe::*;
107
0
    match image {
108
0
        DynamicImage::ImageLuma8(_) => (),
109
0
        DynamicImage::ImageLumaA8(buf) => {
110
0
            premultiply_la8(buf.subpixels_mut());
111
0
        }
112
0
        DynamicImage::ImageRgb8(_) => (),
113
0
        DynamicImage::ImageRgba8(buf) => {
114
0
            premultiply_rgba8(buf.subpixels_mut());
115
0
        }
116
0
        DynamicImage::ImageLuma16(_) => (),
117
0
        DynamicImage::ImageLumaA16(buf) => {
118
0
            premultiply_la16(buf.subpixels_mut(), 16);
119
0
        }
120
0
        DynamicImage::ImageRgb16(_) => (),
121
0
        DynamicImage::ImageRgba16(buf) => {
122
0
            premultiply_rgba16(buf.subpixels_mut(), 16);
123
0
        }
124
0
        DynamicImage::ImageLuma32F(_) => (),
125
0
        DynamicImage::ImageLumaA32F(buf) => {
126
0
            premultiply_luma_alpha_f32(buf.as_mut());
127
0
        }
128
0
        DynamicImage::ImageRgb32F(_) => (),
129
0
        DynamicImage::ImageRgba32F(buf) => {
130
0
            premultiply_rgba_f32(buf.subpixels_mut());
131
0
        }
132
    }
133
0
}
134
135
/// Reverses premultiplication by alpha
136
0
fn unpremultiply_alpha(image: &mut DynamicImage) {
137
    use pic_scale_safe::*;
138
0
    match image {
139
0
        DynamicImage::ImageLuma8(_) => (),
140
0
        DynamicImage::ImageLumaA8(buf) => unpremultiply_la8(buf.subpixels_mut()),
141
0
        DynamicImage::ImageRgb8(_) => (),
142
0
        DynamicImage::ImageRgba8(buf) => unpremultiply_rgba8(buf.subpixels_mut()),
143
0
        DynamicImage::ImageLuma16(_) => (),
144
0
        DynamicImage::ImageLumaA16(buf) => unpremultiply_la16(buf.subpixels_mut(), 16),
145
0
        DynamicImage::ImageRgb16(_) => (),
146
0
        DynamicImage::ImageRgba16(buf) => unpremultiply_rgba16(buf.subpixels_mut(), 16),
147
0
        DynamicImage::ImageLuma32F(_) => (),
148
0
        DynamicImage::ImageLumaA32F(buf) => unpremultiply_luma_alpha_f32(buf.subpixels_mut()),
149
0
        DynamicImage::ImageRgb32F(_) => (),
150
0
        DynamicImage::ImageRgba32F(buf) => unpremultiply_rgba_f32(buf.subpixels_mut()),
151
    }
152
0
}
153
154
#[must_use]
155
0
fn has_constant_alpha(image: &DynamicImage) -> bool {
156
0
    match image {
157
0
        DynamicImage::ImageLuma8(_) => true,
158
0
        DynamicImage::ImageLumaA8(buf) => has_constant_alpha_integer(buf),
159
0
        DynamicImage::ImageRgb8(_) => true,
160
0
        DynamicImage::ImageRgba8(buf) => has_constant_alpha_integer(buf),
161
0
        DynamicImage::ImageLuma16(_) => true,
162
0
        DynamicImage::ImageLumaA16(buf) => has_constant_alpha_integer(buf),
163
0
        DynamicImage::ImageRgb16(_) => true,
164
0
        DynamicImage::ImageRgba16(buf) => has_constant_alpha_integer(buf),
165
0
        DynamicImage::ImageLuma32F(_) => true,
166
0
        DynamicImage::ImageLumaA32F(buf) => has_constant_alpha_f32(buf),
167
0
        DynamicImage::ImageRgb32F(_) => true,
168
0
        DynamicImage::ImageRgba32F(buf) => has_constant_alpha_f32(buf),
169
    }
170
0
}
171
172
#[must_use]
173
0
fn has_constant_alpha_integer<P, Container>(img: &ImageBuffer<P, Container>) -> bool
174
0
where
175
0
    P: Pixel,
176
0
    Container: std::ops::Deref<Target = [P::Subpixel]>,
177
0
    P::Subpixel:
178
0
        Copy + PartialEq + BitXor<P::Subpixel, Output = P::Subpixel> + AddAssign + Into<u64>,
179
{
180
0
    let first_pixel_alpha = *match img.pixels().first() {
181
0
        Some(pixel) => pixel.channels().last().unwrap(), // there doesn't seem to be a better way to retrieve the alpha channel
182
0
        None => return true,                             // empty input image
183
    };
184
    // A naive, slower implementation that branches on every pixel:
185
    //
186
    // img.pixels().map(|pixel| pixel.channels().last().unwrap()).all(|alpha| alpha == first_pixel_alpha);
187
    //
188
    // Instead of doing that we scan every row first with cheap arithmetic instructions
189
    // and only compare the sum of divergences on every row, which should be 0
190
0
    let mut sum_of_diffs: u64 = 0;
191
0
    for row in img.rows() {
192
0
        row.iter().for_each(|pixel| {
193
0
            let alpha = pixel.alpha();
194
0
            sum_of_diffs += alpha.bitxor(first_pixel_alpha).into();
195
0
        });
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::Rgba<u8>, alloc::vec::Vec<u8>>::{closure#0}
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::Rgba<u16>, alloc::vec::Vec<u16>>::{closure#0}
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::LumaA<u8>, alloc::vec::Vec<u8>>::{closure#0}
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::LumaA<u16>, alloc::vec::Vec<u16>>::{closure#0}
196
0
        if sum_of_diffs != 0 {
197
0
            return false;
198
0
        }
199
    }
200
0
    true
201
0
}
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::Rgba<u8>, alloc::vec::Vec<u8>>
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::Rgba<u16>, alloc::vec::Vec<u16>>
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::LumaA<u8>, alloc::vec::Vec<u8>>
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_integer::<image::color::LumaA<u16>, alloc::vec::Vec<u16>>
202
203
#[must_use]
204
0
fn has_constant_alpha_f32<P, Container>(img: &ImageBuffer<P, Container>) -> bool
205
0
where
206
0
    P: Pixel<Subpixel = f32>,
207
0
    Container: std::ops::Deref<Target = [f32]>,
208
{
209
    // Optimizing correctly in presence of NaNs and infinities is tricky, so just do the naive thing for now
210
0
    let first_pixel_alpha = match img.pixels().first() {
211
0
        Some(pixel) => pixel.alpha(),
212
0
        None => return true, // empty input image
213
    };
214
0
    img.pixels()
215
0
        .iter()
216
0
        .map(|pixel| pixel.channels().last().unwrap())
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_f32::<image::color::Rgba<f32>, alloc::vec::Vec<f32>>::{closure#0}
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_f32::<image::color::LumaA<f32>, alloc::vec::Vec<f32>>::{closure#0}
217
0
        .all(|alpha| *alpha == first_pixel_alpha)
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_f32::<image::color::Rgba<f32>, alloc::vec::Vec<f32>>::{closure#1}
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_f32::<image::color::LumaA<f32>, alloc::vec::Vec<f32>>::{closure#1}
218
0
}
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_f32::<image::color::Rgba<f32>, alloc::vec::Vec<f32>>
Unexecuted instantiation: image::imageops::resize::has_constant_alpha_f32::<image::color::LumaA<f32>, alloc::vec::Vec<f32>>
219
220
#[cfg(test)]
221
mod tests {
222
    use crate::{imageops::resize::resize_impl, DynamicImage};
223
224
    #[test]
225
    fn smoke_test() {
226
        let mut image = DynamicImage::new(10, 10, crate::ColorType::Rgba8);
227
        resize_impl(&mut image, 5, 5, crate::imageops::FilterType::Lanczos3).unwrap();
228
        assert_eq!(image.height(), 5);
229
        assert_eq!(image.width(), 5);
230
    }
231
}