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