1use image::RgbImage;
7
8fn area_weights(src: usize, dst: usize, scale: f64) -> Vec<(usize, Vec<f64>)> {
12 (0..dst)
13 .map(|d| {
14 let f1 = d as f64 * scale;
15 let f2 = (d + 1) as f64 * scale;
16 let s1 = f1.floor() as usize;
17 let s2 = (f2.ceil() as usize).min(src);
18 let ws = (s1..s2)
19 .map(|si| (((si + 1) as f64).min(f2) - (si as f64).max(f1)) / scale)
20 .collect();
21 (s1, ws)
22 })
23 .collect()
24}
25
26pub fn inter_area(src: &RgbImage, dw: u32, dh: u32) -> RgbImage {
42 let (sw, sh) = (src.width() as usize, src.height() as usize);
43 let (dwu, dhu) = (dw as usize, dh as usize);
44 let hw = area_weights(sw, dwu, sw as f64 / dw as f64);
45 let vw = area_weights(sh, dhu, sh as f64 / dh as f64);
46 let raw = src.as_raw();
47 let stride = dwu * 3;
48
49 let shrink_row = |sy: usize, dst: &mut [f64]| {
51 let src_row = &raw[sy * sw * 3..(sy + 1) * sw * 3];
52 for ((s1, ws), acc) in hw.iter().zip(dst.chunks_exact_mut(3)) {
53 let taps = &src_row[s1 * 3..(s1 + ws.len()) * 3];
54 let mut a = [0f64; 3];
55 for (p, &w) in taps.chunks_exact(3).zip(ws) {
56 a[0] += f64::from(p[0]) * w;
57 a[1] += f64::from(p[1]) * w;
58 a[2] += f64::from(p[2]) * w;
59 }
60 acc.copy_from_slice(&a);
61 }
62 };
63
64 let mut ring: Vec<(usize, Vec<f64>)> = Vec::new();
68 let mut spare: Vec<Vec<f64>> = Vec::new();
69 let mut out = vec![0u8; stride * dhu];
70 let mut acc = vec![0f64; stride];
71 for ((s1, ws), out_row) in vw.iter().zip(out.chunks_exact_mut(stride)) {
72 let mut i = 0;
73 while i < ring.len() {
74 if ring[i].0 < *s1 {
75 spare.push(ring.swap_remove(i).1);
76 } else {
77 i += 1;
78 }
79 }
80 acc.fill(0.0);
81 for (k, &w) in ws.iter().enumerate() {
82 let sy = s1 + k;
83 let row = match ring.iter().position(|(y, _)| *y == sy) {
84 Some(j) => &ring[j].1,
85 None => {
86 let mut buf = spare.pop().unwrap_or_else(|| vec![0f64; stride]);
87 shrink_row(sy, &mut buf);
88 ring.push((sy, buf));
89 &ring[ring.len() - 1].1
90 }
91 };
92 for (a, t) in acc.iter_mut().zip(row) {
93 *a += t * w;
94 }
95 }
96 for (o, &a) in out_row.iter_mut().zip(&acc) {
97 *o = round_u8(a);
98 }
99 }
100 RgbImage::from_raw(dw, dh, out).expect("inter_area buffer sized dw×dh×3")
101}
102
103fn round_u8(v: f64) -> u8 {
104 v.round().clamp(0.0, 255.0) as u8
105}
106
107const PIL_PRECISION_BITS: i32 = 22;
119
120#[derive(Clone, Copy)]
122pub enum PilFilter {
123 Bilinear,
125 Bicubic,
128}
129
130impl PilFilter {
131 fn support(self) -> f64 {
132 match self {
133 Self::Bilinear => 1.0,
134 Self::Bicubic => 2.0,
135 }
136 }
137
138 fn eval(self, x: f64) -> f64 {
139 match self {
140 Self::Bilinear => {
141 let x = x.abs();
142 if x < 1.0 {
143 1.0 - x
144 } else {
145 0.0
146 }
147 }
148 Self::Bicubic => {
149 const A: f64 = -0.5;
150 let x = x.abs();
151 if x < 1.0 {
152 ((A + 2.0) * x - (A + 3.0)) * x * x + 1.0
153 } else if x < 2.0 {
154 (((x - 5.0) * x + 8.0) * x - 4.0) * A
155 } else {
156 0.0
157 }
158 }
159 }
160 }
161}
162
163fn pil_coeffs(in_size: usize, out_size: usize, filter: PilFilter) -> Vec<(usize, Vec<i32>)> {
166 let scale = in_size as f64 / out_size as f64;
167 let filterscale = scale.max(1.0);
168 let support = filter.support() * filterscale;
169 let ss = 1.0 / filterscale;
170 (0..out_size)
171 .map(|xx| {
172 let center = (xx as f64 + 0.5) * scale;
173 let xmin = ((center - support + 0.5) as i64).max(0) as usize;
174 let xmax = (((center + support + 0.5) as i64).min(in_size as i64) as usize) - xmin;
175 let mut k: Vec<f64> = (0..xmax)
176 .map(|x| filter.eval(((x + xmin) as f64 - center + 0.5) * ss))
177 .collect();
178 let ww: f64 = k.iter().sum();
179 if ww != 0.0 {
180 for w in &mut k {
181 *w /= ww;
182 }
183 }
184 let quant: Vec<i32> = k
187 .iter()
188 .map(|&w| {
189 let s = w * f64::from(1i32 << PIL_PRECISION_BITS);
190 if s < 0.0 {
191 (s - 0.5) as i32
192 } else {
193 (s + 0.5) as i32
194 }
195 })
196 .collect();
197 (xmin, quant)
198 })
199 .collect()
200}
201
202fn pil_clip8(v: i32) -> u8 {
205 (v >> PIL_PRECISION_BITS).clamp(0, 255) as u8
206}
207
208pub fn pil_resize(src: &RgbImage, dw: u32, dh: u32, filter: PilFilter) -> RgbImage {
212 let (sw, sh) = (src.width() as usize, src.height() as usize);
213 let (dwu, dhu) = (dw as usize, dh as usize);
214 let bias = 1i32 << (PIL_PRECISION_BITS - 1);
215 let hpass: RgbImage = if dwu != sw {
226 let coeffs = pil_coeffs(sw, dwu, filter);
227 let src_raw = src.as_raw();
228 let (sstride, dstride) = (sw * 3, dwu * 3);
229 let mut out = vec![0u8; dstride * sh];
230 for (row, orow) in src_raw
231 .chunks_exact(sstride)
232 .zip(out.chunks_exact_mut(dstride))
233 {
234 for ((xmin, k), o) in coeffs.iter().zip(orow.chunks_exact_mut(3)) {
235 let mut acc = [bias; 3];
236 let taps = &row[xmin * 3..(xmin + k.len()) * 3];
237 for (px, &w) in taps.chunks_exact(3).zip(k) {
238 acc[0] += i32::from(px[0]) * w;
239 acc[1] += i32::from(px[1]) * w;
240 acc[2] += i32::from(px[2]) * w;
241 }
242 o[0] = pil_clip8(acc[0]);
243 o[1] = pil_clip8(acc[1]);
244 o[2] = pil_clip8(acc[2]);
245 }
246 }
247 RgbImage::from_raw(dw, sh as u32, out).expect("hpass buffer sized dw×sh×3")
248 } else {
249 src.clone()
250 };
251
252 if dhu == sh {
255 return hpass;
256 }
257 let coeffs = pil_coeffs(sh, dhu, filter);
258 let hraw = hpass.as_raw();
259 let stride = dwu * 3;
260 let mut out = vec![0u8; stride * dhu];
261 let mut acc = vec![0i32; stride];
262 for ((ymin, k), orow) in coeffs.iter().zip(out.chunks_exact_mut(stride)) {
263 acc.fill(bias);
264 for (y, &w) in k.iter().enumerate() {
265 let row = &hraw[(ymin + y) * stride..(ymin + y + 1) * stride];
266 for (a, &p) in acc.iter_mut().zip(row) {
267 *a += i32::from(p) * w;
268 }
269 }
270 for (o, &a) in orow.iter_mut().zip(&acc) {
271 *o = pil_clip8(a);
272 }
273 }
274 RgbImage::from_raw(dw, dh, out).expect("vpass buffer sized dw×dh×3")
275}
276
277#[cfg(test)]
278mod pil_tests {
279 use super::*;
280 use image::Rgb;
281
282 fn lcg_image(w: u32, h: u32) -> RgbImage {
285 let mut state = 0x2545f491u64;
286 let mut next = || {
287 state = state
288 .wrapping_mul(6364136223846793005)
289 .wrapping_add(1442695040888963407);
290 (state >> 33) as u8
291 };
292 let mut img = RgbImage::new(w, h);
293 for y in 0..h {
294 for x in 0..w {
295 img.put_pixel(x, y, Rgb([next(), next(), next()]));
296 }
297 }
298 img
299 }
300
301 fn fnv1a(bytes: &[u8]) -> u64 {
302 let mut h = 0xcbf29ce484222325u64;
303 for &b in bytes {
304 h ^= u64::from(b);
305 h = h.wrapping_mul(0x100000001b3);
306 }
307 h
308 }
309
310 #[test]
314 fn matches_pillow_reference_hashes() {
315 let img = lcg_image(61, 47);
316 for (dw, dh, filter, want) in [
317 (40u32, 30u32, PilFilter::Bilinear, PIL_HASH_BILINEAR_DOWN),
318 (97, 83, PilFilter::Bilinear, PIL_HASH_BILINEAR_UP),
319 (40, 30, PilFilter::Bicubic, PIL_HASH_BICUBIC_DOWN),
320 (97, 83, PilFilter::Bicubic, PIL_HASH_BICUBIC_UP),
321 (640, 640, PilFilter::Bilinear, PIL_HASH_BILINEAR_640),
322 ] {
323 let out = pil_resize(&img, dw, dh, filter);
324 assert_eq!(
325 fnv1a(out.as_raw()),
326 want,
327 "PIL mismatch at {dw}x{dh} {:?}",
328 match filter {
329 PilFilter::Bilinear => "bilinear",
330 PilFilter::Bicubic => "bicubic",
331 }
332 );
333 }
334 }
335
336 const PIL_HASH_BILINEAR_DOWN: u64 = 0x2ac8262283746b4c;
338 const PIL_HASH_BILINEAR_UP: u64 = 0x031c9b4dae3ce142;
339 const PIL_HASH_BICUBIC_DOWN: u64 = 0xb450da21946e06c3;
340 const PIL_HASH_BICUBIC_UP: u64 = 0xc3134a9cff63718d;
341 const PIL_HASH_BILINEAR_640: u64 = 0x967d65f732845b9f;
342}
343
344#[cfg(test)]
345mod area_tests {
346 use super::*;
347
348 fn lcg_image(w: u32, h: u32, seed: u64) -> RgbImage {
349 let mut state = seed;
350 let mut next = || {
351 state = state
352 .wrapping_mul(6364136223846793005)
353 .wrapping_add(1442695040888963407);
354 (state >> 33) as u8
355 };
356 let mut raw = vec![0u8; (w * h * 3) as usize];
357 for b in &mut raw {
358 *b = next();
359 }
360 RgbImage::from_raw(w, h, raw).unwrap()
361 }
362
363 fn inter_area_naive(src: &RgbImage, dw: u32, dh: u32) -> RgbImage {
367 let (sw, sh) = (src.width() as usize, src.height() as usize);
368 let hw = area_weights(sw, dw as usize, sw as f64 / dw as f64);
369 let vw = area_weights(sh, dh as usize, sh as f64 / dh as f64);
370 let mut out = RgbImage::new(dw, dh);
371 for (dy, vws) in vw.iter().enumerate() {
372 for (dx, hws) in hw.iter().enumerate() {
373 let mut acc = [0f64; 3];
374 for (ky, &wy) in vws.1.iter().enumerate() {
375 let sy = vws.0 + ky;
376 let mut t = [0f64; 3];
377 for (kx, &wx) in hws.1.iter().enumerate() {
378 let sx = hws.0 + kx;
379 let p = src.get_pixel(sx as u32, sy as u32).0;
380 for c in 0..3 {
381 t[c] += p[c] as f64 * wx;
382 }
383 }
384 for c in 0..3 {
385 acc[c] += t[c] * wy;
386 }
387 }
388 out.put_pixel(
389 dx as u32,
390 dy as u32,
391 image::Rgb([round_u8(acc[0]), round_u8(acc[1]), round_u8(acc[2])]),
392 );
393 }
394 }
395 out
396 }
397
398 #[test]
399 fn inter_area_matches_naive_order() {
400 for (i, (sw, sh, dw, dh)) in [
401 (1224u32, 1584u32, 791u32, 1024u32),
402 (1190, 1684, 723, 1024),
403 (1584, 1224, 1325, 1024),
404 (61, 47, 40, 30),
405 (100, 100, 100, 50),
406 (37, 91, 36, 90),
407 ]
408 .into_iter()
409 .enumerate()
410 {
411 let img = lcg_image(sw, sh, 0x9e3779b97f4a7c15 ^ i as u64);
412 assert_eq!(
413 inter_area(&img, dw, dh).as_raw(),
414 inter_area_naive(&img, dw, dh).as_raw(),
415 "{sw}x{sh} -> {dw}x{dh}"
416 );
417 }
418 }
419
420 #[test]
421 #[ignore = "timing only: cargo test --release -p docling-pdf --lib area_tests::bench -- --ignored --nocapture"]
422 fn bench_inter_area() {
423 let img = lcg_image(1224, 1584, 7);
424 let _ = inter_area(&img, 791, 1024);
425 let t = std::time::Instant::now();
426 let n = 20;
427 for _ in 0..n {
428 std::hint::black_box(inter_area(&img, 791, 1024));
429 }
430 eprintln!(
431 "inter_area 1224x1584 -> 791x1024: {:.1} ms",
432 t.elapsed().as_secs_f64() * 1e3 / n as f64
433 );
434 }
435}