1use image::{Rgb, RgbImage};
7
8fn area_weights(src: usize, dst: usize, scale: f64) -> Vec<Vec<(usize, f64)>> {
10 (0..dst)
11 .map(|d| {
12 let f1 = d as f64 * scale;
13 let f2 = (d + 1) as f64 * scale;
14 let s1 = f1.floor() as usize;
15 let s2 = (f2.ceil() as usize).min(src);
16 (s1..s2)
17 .map(|si| {
18 let w = (((si + 1) as f64).min(f2) - (si as f64).max(f1)) / scale;
19 (si, w)
20 })
21 .collect()
22 })
23 .collect()
24}
25
26pub fn inter_area(src: &RgbImage, dw: u32, dh: u32) -> RgbImage {
29 let (sw, sh) = (src.width() as usize, src.height() as usize);
30 let (dwu, dhu) = (dw as usize, dh as usize);
31 let hw = area_weights(sw, dwu, sw as f64 / dw as f64);
32 let vw = area_weights(sh, dhu, sh as f64 / dh as f64);
33
34 let mut tmp = vec![[0f64; 3]; sh * dwu]; for y in 0..sh {
36 let row = y * dwu;
37 for (dx, ws) in hw.iter().enumerate() {
38 let mut acc = [0f64; 3];
39 for &(si, w) in ws {
40 let p = src.get_pixel(si as u32, y as u32);
41 acc[0] += p[0] as f64 * w;
42 acc[1] += p[1] as f64 * w;
43 acc[2] += p[2] as f64 * w;
44 }
45 tmp[row + dx] = acc;
46 }
47 }
48 let mut out = RgbImage::new(dw, dh);
49 for (dy, ws) in vw.iter().enumerate() {
50 for dx in 0..dwu {
51 let mut acc = [0f64; 3];
52 for &(si, w) in ws {
53 let t = tmp[si * dwu + dx];
54 acc[0] += t[0] * w;
55 acc[1] += t[1] * w;
56 acc[2] += t[2] * w;
57 }
58 out.put_pixel(
59 dx as u32,
60 dy as u32,
61 Rgb([round_u8(acc[0]), round_u8(acc[1]), round_u8(acc[2])]),
62 );
63 }
64 }
65 out
66}
67
68fn round_u8(v: f64) -> u8 {
69 v.round().clamp(0.0, 255.0) as u8
70}
71
72const PIL_PRECISION_BITS: i32 = 22;
84
85#[derive(Clone, Copy)]
87pub enum PilFilter {
88 Bilinear,
90 Bicubic,
93}
94
95impl PilFilter {
96 fn support(self) -> f64 {
97 match self {
98 Self::Bilinear => 1.0,
99 Self::Bicubic => 2.0,
100 }
101 }
102
103 fn eval(self, x: f64) -> f64 {
104 match self {
105 Self::Bilinear => {
106 let x = x.abs();
107 if x < 1.0 {
108 1.0 - x
109 } else {
110 0.0
111 }
112 }
113 Self::Bicubic => {
114 const A: f64 = -0.5;
115 let x = x.abs();
116 if x < 1.0 {
117 ((A + 2.0) * x - (A + 3.0)) * x * x + 1.0
118 } else if x < 2.0 {
119 (((x - 5.0) * x + 8.0) * x - 4.0) * A
120 } else {
121 0.0
122 }
123 }
124 }
125 }
126}
127
128fn pil_coeffs(in_size: usize, out_size: usize, filter: PilFilter) -> Vec<(usize, Vec<i32>)> {
131 let scale = in_size as f64 / out_size as f64;
132 let filterscale = scale.max(1.0);
133 let support = filter.support() * filterscale;
134 let ss = 1.0 / filterscale;
135 (0..out_size)
136 .map(|xx| {
137 let center = (xx as f64 + 0.5) * scale;
138 let xmin = ((center - support + 0.5) as i64).max(0) as usize;
139 let xmax = (((center + support + 0.5) as i64).min(in_size as i64) as usize) - xmin;
140 let mut k: Vec<f64> = (0..xmax)
141 .map(|x| filter.eval(((x + xmin) as f64 - center + 0.5) * ss))
142 .collect();
143 let ww: f64 = k.iter().sum();
144 if ww != 0.0 {
145 for w in &mut k {
146 *w /= ww;
147 }
148 }
149 let quant: Vec<i32> = k
152 .iter()
153 .map(|&w| {
154 let s = w * f64::from(1i32 << PIL_PRECISION_BITS);
155 if s < 0.0 {
156 (s - 0.5) as i32
157 } else {
158 (s + 0.5) as i32
159 }
160 })
161 .collect();
162 (xmin, quant)
163 })
164 .collect()
165}
166
167fn pil_clip8(v: i32) -> u8 {
170 (v >> PIL_PRECISION_BITS).clamp(0, 255) as u8
171}
172
173pub fn pil_resize(src: &RgbImage, dw: u32, dh: u32, filter: PilFilter) -> RgbImage {
177 let (sw, sh) = (src.width() as usize, src.height() as usize);
178 let (dwu, dhu) = (dw as usize, dh as usize);
179 let bias = 1i32 << (PIL_PRECISION_BITS - 1);
180
181 let hpass: RgbImage = if dwu != sw {
183 let coeffs = pil_coeffs(sw, dwu, filter);
184 let mut out = RgbImage::new(dw, sh as u32);
185 for y in 0..sh {
186 for (xx, (xmin, k)) in coeffs.iter().enumerate() {
187 let mut acc = [bias; 3];
188 for (x, &w) in k.iter().enumerate() {
189 let p = src.get_pixel((xmin + x) as u32, y as u32);
190 acc[0] += i32::from(p[0]) * w;
191 acc[1] += i32::from(p[1]) * w;
192 acc[2] += i32::from(p[2]) * w;
193 }
194 out.put_pixel(
195 xx as u32,
196 y as u32,
197 Rgb([pil_clip8(acc[0]), pil_clip8(acc[1]), pil_clip8(acc[2])]),
198 );
199 }
200 }
201 out
202 } else {
203 src.clone()
204 };
205
206 if dhu == sh {
208 return hpass;
209 }
210 let coeffs = pil_coeffs(sh, dhu, filter);
211 let mut out = RgbImage::new(dw, dh);
212 for (yy, (ymin, k)) in coeffs.iter().enumerate() {
213 for x in 0..dwu {
214 let mut acc = [bias; 3];
215 for (y, &w) in k.iter().enumerate() {
216 let p = hpass.get_pixel(x as u32, (ymin + y) as u32);
217 acc[0] += i32::from(p[0]) * w;
218 acc[1] += i32::from(p[1]) * w;
219 acc[2] += i32::from(p[2]) * w;
220 }
221 out.put_pixel(
222 x as u32,
223 yy as u32,
224 Rgb([pil_clip8(acc[0]), pil_clip8(acc[1]), pil_clip8(acc[2])]),
225 );
226 }
227 }
228 out
229}
230
231#[cfg(test)]
232mod pil_tests {
233 use super::*;
234
235 fn lcg_image(w: u32, h: u32) -> RgbImage {
238 let mut state = 0x2545f491u64;
239 let mut next = || {
240 state = state
241 .wrapping_mul(6364136223846793005)
242 .wrapping_add(1442695040888963407);
243 (state >> 33) as u8
244 };
245 let mut img = RgbImage::new(w, h);
246 for y in 0..h {
247 for x in 0..w {
248 img.put_pixel(x, y, Rgb([next(), next(), next()]));
249 }
250 }
251 img
252 }
253
254 fn fnv1a(bytes: &[u8]) -> u64 {
255 let mut h = 0xcbf29ce484222325u64;
256 for &b in bytes {
257 h ^= u64::from(b);
258 h = h.wrapping_mul(0x100000001b3);
259 }
260 h
261 }
262
263 #[test]
267 fn matches_pillow_reference_hashes() {
268 let img = lcg_image(61, 47);
269 for (dw, dh, filter, want) in [
270 (40u32, 30u32, PilFilter::Bilinear, PIL_HASH_BILINEAR_DOWN),
271 (97, 83, PilFilter::Bilinear, PIL_HASH_BILINEAR_UP),
272 (40, 30, PilFilter::Bicubic, PIL_HASH_BICUBIC_DOWN),
273 (97, 83, PilFilter::Bicubic, PIL_HASH_BICUBIC_UP),
274 (640, 640, PilFilter::Bilinear, PIL_HASH_BILINEAR_640),
275 ] {
276 let out = pil_resize(&img, dw, dh, filter);
277 assert_eq!(
278 fnv1a(out.as_raw()),
279 want,
280 "PIL mismatch at {dw}x{dh} {:?}",
281 match filter {
282 PilFilter::Bilinear => "bilinear",
283 PilFilter::Bicubic => "bicubic",
284 }
285 );
286 }
287 }
288
289 const PIL_HASH_BILINEAR_DOWN: u64 = 0x2ac8262283746b4c;
291 const PIL_HASH_BILINEAR_UP: u64 = 0x031c9b4dae3ce142;
292 const PIL_HASH_BICUBIC_DOWN: u64 = 0xb450da21946e06c3;
293 const PIL_HASH_BICUBIC_UP: u64 = 0xc3134a9cff63718d;
294 const PIL_HASH_BILINEAR_640: u64 = 0x967d65f732845b9f;
295}