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 raw = src.as_raw();
39 let mut tmp = vec![[0f64; 3]; sh * dwu]; for y in 0..sh {
41 let src_row = &raw[y * sw * 3..(y + 1) * sw * 3];
42 let dst_row = &mut tmp[y * dwu..(y + 1) * dwu];
43 for (acc, ws) in dst_row.iter_mut().zip(hw.iter()) {
44 for &(si, w) in ws {
45 let p = &src_row[si * 3..si * 3 + 3];
46 acc[0] += p[0] as f64 * w;
47 acc[1] += p[1] as f64 * w;
48 acc[2] += p[2] as f64 * w;
49 }
50 }
51 }
52 let mut out = RgbImage::new(dw, dh);
53 let mut acc_row = vec![[0f64; 3]; dwu];
54 for (dy, ws) in vw.iter().enumerate() {
55 acc_row.fill([0f64; 3]);
56 for &(si, w) in ws {
59 let row = &tmp[si * dwu..(si + 1) * dwu];
60 for (acc, t) in acc_row.iter_mut().zip(row) {
61 acc[0] += t[0] * w;
62 acc[1] += t[1] * w;
63 acc[2] += t[2] * w;
64 }
65 }
66 let out_row = &mut (*out)[dy * dwu * 3..(dy + 1) * dwu * 3];
67 for (px, acc) in out_row.chunks_exact_mut(3).zip(&acc_row) {
68 px[0] = round_u8(acc[0]);
69 px[1] = round_u8(acc[1]);
70 px[2] = round_u8(acc[2]);
71 }
72 }
73 out
74}
75
76fn round_u8(v: f64) -> u8 {
77 v.round().clamp(0.0, 255.0) as u8
78}
79
80const PIL_PRECISION_BITS: i32 = 22;
92
93#[derive(Clone, Copy)]
95pub enum PilFilter {
96 Bilinear,
98 Bicubic,
101}
102
103impl PilFilter {
104 fn support(self) -> f64 {
105 match self {
106 Self::Bilinear => 1.0,
107 Self::Bicubic => 2.0,
108 }
109 }
110
111 fn eval(self, x: f64) -> f64 {
112 match self {
113 Self::Bilinear => {
114 let x = x.abs();
115 if x < 1.0 {
116 1.0 - x
117 } else {
118 0.0
119 }
120 }
121 Self::Bicubic => {
122 const A: f64 = -0.5;
123 let x = x.abs();
124 if x < 1.0 {
125 ((A + 2.0) * x - (A + 3.0)) * x * x + 1.0
126 } else if x < 2.0 {
127 (((x - 5.0) * x + 8.0) * x - 4.0) * A
128 } else {
129 0.0
130 }
131 }
132 }
133 }
134}
135
136fn pil_coeffs(in_size: usize, out_size: usize, filter: PilFilter) -> Vec<(usize, Vec<i32>)> {
139 let scale = in_size as f64 / out_size as f64;
140 let filterscale = scale.max(1.0);
141 let support = filter.support() * filterscale;
142 let ss = 1.0 / filterscale;
143 (0..out_size)
144 .map(|xx| {
145 let center = (xx as f64 + 0.5) * scale;
146 let xmin = ((center - support + 0.5) as i64).max(0) as usize;
147 let xmax = (((center + support + 0.5) as i64).min(in_size as i64) as usize) - xmin;
148 let mut k: Vec<f64> = (0..xmax)
149 .map(|x| filter.eval(((x + xmin) as f64 - center + 0.5) * ss))
150 .collect();
151 let ww: f64 = k.iter().sum();
152 if ww != 0.0 {
153 for w in &mut k {
154 *w /= ww;
155 }
156 }
157 let quant: Vec<i32> = k
160 .iter()
161 .map(|&w| {
162 let s = w * f64::from(1i32 << PIL_PRECISION_BITS);
163 if s < 0.0 {
164 (s - 0.5) as i32
165 } else {
166 (s + 0.5) as i32
167 }
168 })
169 .collect();
170 (xmin, quant)
171 })
172 .collect()
173}
174
175fn pil_clip8(v: i32) -> u8 {
178 (v >> PIL_PRECISION_BITS).clamp(0, 255) as u8
179}
180
181pub fn pil_resize(src: &RgbImage, dw: u32, dh: u32, filter: PilFilter) -> RgbImage {
185 let (sw, sh) = (src.width() as usize, src.height() as usize);
186 let (dwu, dhu) = (dw as usize, dh as usize);
187 let bias = 1i32 << (PIL_PRECISION_BITS - 1);
188
189 let hpass: RgbImage = if dwu != sw {
191 let coeffs = pil_coeffs(sw, dwu, filter);
192 let mut out = RgbImage::new(dw, sh as u32);
193 for y in 0..sh {
194 for (xx, (xmin, k)) in coeffs.iter().enumerate() {
195 let mut acc = [bias; 3];
196 for (x, &w) in k.iter().enumerate() {
197 let p = src.get_pixel((xmin + x) as u32, y as u32);
198 acc[0] += i32::from(p[0]) * w;
199 acc[1] += i32::from(p[1]) * w;
200 acc[2] += i32::from(p[2]) * w;
201 }
202 out.put_pixel(
203 xx as u32,
204 y as u32,
205 Rgb([pil_clip8(acc[0]), pil_clip8(acc[1]), pil_clip8(acc[2])]),
206 );
207 }
208 }
209 out
210 } else {
211 src.clone()
212 };
213
214 if dhu == sh {
216 return hpass;
217 }
218 let coeffs = pil_coeffs(sh, dhu, filter);
219 let mut out = RgbImage::new(dw, dh);
220 for (yy, (ymin, k)) in coeffs.iter().enumerate() {
221 for x in 0..dwu {
222 let mut acc = [bias; 3];
223 for (y, &w) in k.iter().enumerate() {
224 let p = hpass.get_pixel(x as u32, (ymin + y) as u32);
225 acc[0] += i32::from(p[0]) * w;
226 acc[1] += i32::from(p[1]) * w;
227 acc[2] += i32::from(p[2]) * w;
228 }
229 out.put_pixel(
230 x as u32,
231 yy as u32,
232 Rgb([pil_clip8(acc[0]), pil_clip8(acc[1]), pil_clip8(acc[2])]),
233 );
234 }
235 }
236 out
237}
238
239#[cfg(test)]
240mod pil_tests {
241 use super::*;
242
243 fn lcg_image(w: u32, h: u32) -> RgbImage {
246 let mut state = 0x2545f491u64;
247 let mut next = || {
248 state = state
249 .wrapping_mul(6364136223846793005)
250 .wrapping_add(1442695040888963407);
251 (state >> 33) as u8
252 };
253 let mut img = RgbImage::new(w, h);
254 for y in 0..h {
255 for x in 0..w {
256 img.put_pixel(x, y, Rgb([next(), next(), next()]));
257 }
258 }
259 img
260 }
261
262 fn fnv1a(bytes: &[u8]) -> u64 {
263 let mut h = 0xcbf29ce484222325u64;
264 for &b in bytes {
265 h ^= u64::from(b);
266 h = h.wrapping_mul(0x100000001b3);
267 }
268 h
269 }
270
271 #[test]
275 fn matches_pillow_reference_hashes() {
276 let img = lcg_image(61, 47);
277 for (dw, dh, filter, want) in [
278 (40u32, 30u32, PilFilter::Bilinear, PIL_HASH_BILINEAR_DOWN),
279 (97, 83, PilFilter::Bilinear, PIL_HASH_BILINEAR_UP),
280 (40, 30, PilFilter::Bicubic, PIL_HASH_BICUBIC_DOWN),
281 (97, 83, PilFilter::Bicubic, PIL_HASH_BICUBIC_UP),
282 (640, 640, PilFilter::Bilinear, PIL_HASH_BILINEAR_640),
283 ] {
284 let out = pil_resize(&img, dw, dh, filter);
285 assert_eq!(
286 fnv1a(out.as_raw()),
287 want,
288 "PIL mismatch at {dw}x{dh} {:?}",
289 match filter {
290 PilFilter::Bilinear => "bilinear",
291 PilFilter::Bicubic => "bicubic",
292 }
293 );
294 }
295 }
296
297 const PIL_HASH_BILINEAR_DOWN: u64 = 0x2ac8262283746b4c;
299 const PIL_HASH_BILINEAR_UP: u64 = 0x031c9b4dae3ce142;
300 const PIL_HASH_BICUBIC_DOWN: u64 = 0xb450da21946e06c3;
301 const PIL_HASH_BICUBIC_UP: u64 = 0xc3134a9cff63718d;
302 const PIL_HASH_BILINEAR_640: u64 = 0x967d65f732845b9f;
303}