Skip to main content

agg_rust/
image_filters.rs

1//! Image transformation filters and lookup table.
2//!
3//! Port of `agg_image_filters.h` and `agg_image_filters.cpp` — filter shape
4//! functions and a weight lookup table used by image resampling span generators.
5
6use crate::basics::{iround, uceil};
7use crate::math::besj;
8use std::f64::consts::PI;
9
10// ============================================================================
11// Constants
12// ============================================================================
13
14pub const IMAGE_FILTER_SHIFT: u32 = 14;
15pub const IMAGE_FILTER_SCALE: i32 = 1 << IMAGE_FILTER_SHIFT; // 16384
16pub const IMAGE_FILTER_MASK: i32 = IMAGE_FILTER_SCALE - 1;
17
18pub const IMAGE_SUBPIXEL_SHIFT: u32 = 8;
19pub const IMAGE_SUBPIXEL_SCALE: u32 = 1 << IMAGE_SUBPIXEL_SHIFT; // 256
20pub const IMAGE_SUBPIXEL_MASK: u32 = IMAGE_SUBPIXEL_SCALE - 1;
21
22// ============================================================================
23// ImageFilterFunction trait
24// ============================================================================
25
26/// Trait for image filter shape functions.
27///
28/// Port of C++ template duck-typing for filter functions with
29/// `radius()` and `calc_weight(x)` methods.
30pub trait ImageFilterFunction {
31    /// The radius of the filter kernel.
32    fn radius(&self) -> f64;
33    /// Calculate the filter weight at distance `x` from center.
34    fn calc_weight(&self, x: f64) -> f64;
35}
36
37// ============================================================================
38// ImageFilterLut — weight lookup table
39// ============================================================================
40
41/// Image filter weight lookup table.
42///
43/// Stores precomputed filter weights at subpixel resolution for fast
44/// image resampling. Weights are stored as 14-bit fixed-point integers.
45///
46/// Port of C++ `image_filter_lut`.
47pub struct ImageFilterLut {
48    radius: f64,
49    diameter: u32,
50    start: i32,
51    weight_array: Vec<i16>,
52}
53
54impl ImageFilterLut {
55    /// Create an empty (uninitialized) filter LUT.
56    pub fn new() -> Self {
57        Self {
58            radius: 0.0,
59            diameter: 0,
60            start: 0,
61            weight_array: Vec::new(),
62        }
63    }
64
65    /// Create and populate a filter LUT from a filter function.
66    pub fn new_with_filter<F: ImageFilterFunction>(filter: &F, normalization: bool) -> Self {
67        let mut lut = Self::new();
68        lut.calculate(filter, normalization);
69        lut
70    }
71
72    pub fn radius(&self) -> f64 {
73        self.radius
74    }
75
76    pub fn diameter(&self) -> u32 {
77        self.diameter
78    }
79
80    pub fn start(&self) -> i32 {
81        self.start
82    }
83
84    pub fn weight_array(&self) -> &[i16] {
85        &self.weight_array
86    }
87
88    /// Populate the LUT from a filter function.
89    ///
90    /// Port of C++ `image_filter_lut::calculate`.
91    pub fn calculate<F: ImageFilterFunction>(&mut self, filter: &F, normalization: bool) {
92        let r = filter.radius();
93        self.realloc_lut(r);
94        let pivot = (self.diameter << (IMAGE_SUBPIXEL_SHIFT - 1)) as usize;
95        for i in 0..pivot {
96            let x = i as f64 / IMAGE_SUBPIXEL_SCALE as f64;
97            let y = filter.calc_weight(x);
98            let w = iround(y * IMAGE_FILTER_SCALE as f64) as i16;
99            self.weight_array[pivot + i] = w;
100            self.weight_array[pivot - i] = w;
101        }
102        let end = ((self.diameter as usize) << IMAGE_SUBPIXEL_SHIFT) - 1;
103        self.weight_array[0] = self.weight_array[end];
104        if normalization {
105            self.normalize();
106        }
107    }
108
109    /// Normalize weights so that for every subpixel offset, the filter
110    /// weights sum to exactly `IMAGE_FILTER_SCALE`.
111    ///
112    /// Port of C++ `image_filter_lut::normalize`.
113    #[allow(clippy::needless_range_loop)]
114    pub fn normalize(&mut self) {
115        let mut flip: i32 = 1;
116        let subpixel_scale = IMAGE_SUBPIXEL_SCALE as usize;
117        let diameter = self.diameter as usize;
118
119        for i in 0..subpixel_scale {
120            loop {
121                let mut sum: i32 = 0;
122                for j in 0..diameter {
123                    sum += self.weight_array[j * subpixel_scale + i] as i32;
124                }
125
126                if sum == IMAGE_FILTER_SCALE {
127                    break;
128                }
129
130                let k = IMAGE_FILTER_SCALE as f64 / sum as f64;
131                sum = 0;
132                for j in 0..diameter {
133                    let idx = j * subpixel_scale + i;
134                    let v = iround(self.weight_array[idx] as f64 * k) as i16;
135                    self.weight_array[idx] = v;
136                    sum += v as i32;
137                }
138
139                sum -= IMAGE_FILTER_SCALE;
140                let inc: i32 = if sum > 0 { -1 } else { 1 };
141
142                let mut j = 0;
143                while j < diameter && sum != 0 {
144                    flip ^= 1;
145                    let idx = if flip != 0 {
146                        diameter / 2 + j / 2
147                    } else {
148                        diameter / 2 - j / 2
149                    };
150                    let arr_idx = idx * subpixel_scale + i;
151                    let v = self.weight_array[arr_idx] as i32;
152                    if v < IMAGE_FILTER_SCALE {
153                        self.weight_array[arr_idx] += inc as i16;
154                        sum += inc;
155                    }
156                    j += 1;
157                }
158            }
159        }
160
161        let pivot = diameter << (IMAGE_SUBPIXEL_SHIFT as usize - 1);
162        for i in 0..pivot {
163            self.weight_array[pivot + i] = self.weight_array[pivot - i];
164        }
165        let end = (diameter << IMAGE_SUBPIXEL_SHIFT as usize) - 1;
166        self.weight_array[0] = self.weight_array[end];
167    }
168
169    fn realloc_lut(&mut self, radius: f64) {
170        self.radius = radius;
171        self.diameter = uceil(radius) * 2;
172        self.start = -((self.diameter / 2 - 1) as i32);
173        let size = (self.diameter as usize) << IMAGE_SUBPIXEL_SHIFT;
174        if size > self.weight_array.len() {
175            self.weight_array.resize(size, 0);
176        }
177    }
178}
179
180impl Default for ImageFilterLut {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186// ============================================================================
187// Filter shape implementations
188// ============================================================================
189
190/// Bilinear filter — radius 1.0, linear interpolation.
191pub struct ImageFilterBilinear;
192impl ImageFilterFunction for ImageFilterBilinear {
193    fn radius(&self) -> f64 {
194        1.0
195    }
196    fn calc_weight(&self, x: f64) -> f64 {
197        1.0 - x
198    }
199}
200
201/// Hanning window filter — radius 1.0.
202pub struct ImageFilterHanning;
203impl ImageFilterFunction for ImageFilterHanning {
204    fn radius(&self) -> f64 {
205        1.0
206    }
207    fn calc_weight(&self, x: f64) -> f64 {
208        0.5 + 0.5 * (PI * x).cos()
209    }
210}
211
212/// Hamming window filter — radius 1.0.
213pub struct ImageFilterHamming;
214impl ImageFilterFunction for ImageFilterHamming {
215    fn radius(&self) -> f64 {
216        1.0
217    }
218    fn calc_weight(&self, x: f64) -> f64 {
219        0.54 + 0.46 * (PI * x).cos()
220    }
221}
222
223/// Hermite filter — radius 1.0, cubic Hermite interpolation.
224pub struct ImageFilterHermite;
225impl ImageFilterFunction for ImageFilterHermite {
226    fn radius(&self) -> f64 {
227        1.0
228    }
229    fn calc_weight(&self, x: f64) -> f64 {
230        (2.0 * x - 3.0) * x * x + 1.0
231    }
232}
233
234/// Quadric filter — radius 1.5, piecewise quadratic.
235pub struct ImageFilterQuadric;
236impl ImageFilterFunction for ImageFilterQuadric {
237    fn radius(&self) -> f64 {
238        1.5
239    }
240    fn calc_weight(&self, x: f64) -> f64 {
241        if x < 0.5 {
242            return 0.75 - x * x;
243        }
244        if x < 1.5 {
245            let t = x - 1.5;
246            return 0.5 * t * t;
247        }
248        0.0
249    }
250}
251
252/// Bicubic filter — radius 2.0, cubic B-spline.
253pub struct ImageFilterBicubic;
254impl ImageFilterBicubic {
255    fn pow3(x: f64) -> f64 {
256        if x <= 0.0 {
257            0.0
258        } else {
259            x * x * x
260        }
261    }
262}
263impl ImageFilterFunction for ImageFilterBicubic {
264    fn radius(&self) -> f64 {
265        2.0
266    }
267    fn calc_weight(&self, x: f64) -> f64 {
268        (1.0 / 6.0)
269            * (Self::pow3(x + 2.0) - 4.0 * Self::pow3(x + 1.0) + 6.0 * Self::pow3(x)
270                - 4.0 * Self::pow3(x - 1.0))
271    }
272}
273
274/// Kaiser filter — radius 1.0, parameterized by `b` (default 6.33).
275///
276/// Uses modified Bessel function of the first kind (order 0).
277pub struct ImageFilterKaiser {
278    a: f64,
279    i0a: f64,
280    epsilon: f64,
281}
282impl ImageFilterKaiser {
283    pub fn new(b: f64) -> Self {
284        let epsilon = 1e-12;
285        let i0a = 1.0 / Self::bessel_i0(b, epsilon);
286        Self { a: b, i0a, epsilon }
287    }
288
289    fn bessel_i0(x: f64, epsilon: f64) -> f64 {
290        let mut sum = 1.0;
291        let y = x * x / 4.0;
292        let mut t = y;
293        let mut i = 2;
294        while t > epsilon {
295            sum += t;
296            t *= y / (i * i) as f64;
297            i += 1;
298        }
299        sum
300    }
301}
302impl Default for ImageFilterKaiser {
303    fn default() -> Self {
304        Self::new(6.33)
305    }
306}
307impl ImageFilterFunction for ImageFilterKaiser {
308    fn radius(&self) -> f64 {
309        1.0
310    }
311    fn calc_weight(&self, x: f64) -> f64 {
312        Self::bessel_i0(self.a * (1.0 - x * x).sqrt(), self.epsilon) * self.i0a
313    }
314}
315
316/// Catmull-Rom spline filter — radius 2.0.
317pub struct ImageFilterCatrom;
318impl ImageFilterFunction for ImageFilterCatrom {
319    fn radius(&self) -> f64 {
320        2.0
321    }
322    fn calc_weight(&self, x: f64) -> f64 {
323        if x < 1.0 {
324            return 0.5 * (2.0 + x * x * (-5.0 + x * 3.0));
325        }
326        if x < 2.0 {
327            return 0.5 * (4.0 + x * (-8.0 + x * (5.0 - x)));
328        }
329        0.0
330    }
331}
332
333/// Mitchell-Netravali filter — radius 2.0, parameterized by `b` and `c`.
334///
335/// Default: b = 1/3, c = 1/3 (recommended for general image scaling).
336pub struct ImageFilterMitchell {
337    p0: f64,
338    p2: f64,
339    p3: f64,
340    q0: f64,
341    q1: f64,
342    q2: f64,
343    q3: f64,
344}
345impl ImageFilterMitchell {
346    pub fn new(b: f64, c: f64) -> Self {
347        Self {
348            p0: (6.0 - 2.0 * b) / 6.0,
349            p2: (-18.0 + 12.0 * b + 6.0 * c) / 6.0,
350            p3: (12.0 - 9.0 * b - 6.0 * c) / 6.0,
351            q0: (8.0 * b + 24.0 * c) / 6.0,
352            q1: (-12.0 * b - 48.0 * c) / 6.0,
353            q2: (6.0 * b + 30.0 * c) / 6.0,
354            q3: (-b - 6.0 * c) / 6.0,
355        }
356    }
357}
358impl Default for ImageFilterMitchell {
359    fn default() -> Self {
360        Self::new(1.0 / 3.0, 1.0 / 3.0)
361    }
362}
363impl ImageFilterFunction for ImageFilterMitchell {
364    fn radius(&self) -> f64 {
365        2.0
366    }
367    fn calc_weight(&self, x: f64) -> f64 {
368        if x < 1.0 {
369            return self.p0 + x * x * (self.p2 + x * self.p3);
370        }
371        if x < 2.0 {
372            return self.q0 + x * (self.q1 + x * (self.q2 + x * self.q3));
373        }
374        0.0
375    }
376}
377
378/// Spline16 filter — radius 2.0.
379pub struct ImageFilterSpline16;
380impl ImageFilterFunction for ImageFilterSpline16 {
381    fn radius(&self) -> f64 {
382        2.0
383    }
384    fn calc_weight(&self, x: f64) -> f64 {
385        if x < 1.0 {
386            return ((x - 9.0 / 5.0) * x - 1.0 / 5.0) * x + 1.0;
387        }
388        ((-1.0 / 3.0 * (x - 1.0) + 4.0 / 5.0) * (x - 1.0) - 7.0 / 15.0) * (x - 1.0)
389    }
390}
391
392/// Spline36 filter — radius 3.0.
393pub struct ImageFilterSpline36;
394impl ImageFilterFunction for ImageFilterSpline36 {
395    fn radius(&self) -> f64 {
396        3.0
397    }
398    fn calc_weight(&self, x: f64) -> f64 {
399        if x < 1.0 {
400            return ((13.0 / 11.0 * x - 453.0 / 209.0) * x - 3.0 / 209.0) * x + 1.0;
401        }
402        if x < 2.0 {
403            return ((-6.0 / 11.0 * (x - 1.0) + 270.0 / 209.0) * (x - 1.0) - 156.0 / 209.0)
404                * (x - 1.0);
405        }
406        ((1.0 / 11.0 * (x - 2.0) - 45.0 / 209.0) * (x - 2.0) + 26.0 / 209.0) * (x - 2.0)
407    }
408}
409
410/// Gaussian filter — radius 2.0.
411pub struct ImageFilterGaussian;
412impl ImageFilterFunction for ImageFilterGaussian {
413    fn radius(&self) -> f64 {
414        2.0
415    }
416    fn calc_weight(&self, x: f64) -> f64 {
417        (-2.0 * x * x).exp() * (2.0 / PI).sqrt()
418    }
419}
420
421/// Bessel filter — radius 3.2383.
422pub struct ImageFilterBessel;
423impl ImageFilterFunction for ImageFilterBessel {
424    fn radius(&self) -> f64 {
425        3.2383
426    }
427    fn calc_weight(&self, x: f64) -> f64 {
428        if x == 0.0 {
429            PI / 4.0
430        } else {
431            besj(PI * x, 1) / (2.0 * x)
432        }
433    }
434}
435
436// ============================================================================
437// Variable-radius filters
438// ============================================================================
439
440/// Sinc filter — variable radius (minimum 2.0).
441pub struct ImageFilterSinc {
442    radius: f64,
443}
444impl ImageFilterSinc {
445    pub fn new(r: f64) -> Self {
446        Self {
447            radius: if r < 2.0 { 2.0 } else { r },
448        }
449    }
450}
451impl ImageFilterFunction for ImageFilterSinc {
452    fn radius(&self) -> f64 {
453        self.radius
454    }
455    fn calc_weight(&self, x: f64) -> f64 {
456        if x == 0.0 {
457            return 1.0;
458        }
459        let x = x * PI;
460        x.sin() / x
461    }
462}
463
464/// Lanczos filter — variable radius (minimum 2.0).
465pub struct ImageFilterLanczos {
466    radius: f64,
467}
468impl ImageFilterLanczos {
469    pub fn new(r: f64) -> Self {
470        Self {
471            radius: if r < 2.0 { 2.0 } else { r },
472        }
473    }
474}
475impl ImageFilterFunction for ImageFilterLanczos {
476    fn radius(&self) -> f64 {
477        self.radius
478    }
479    fn calc_weight(&self, x: f64) -> f64 {
480        if x == 0.0 {
481            return 1.0;
482        }
483        if x > self.radius {
484            return 0.0;
485        }
486        let x = x * PI;
487        let xr = x / self.radius;
488        (x.sin() / x) * (xr.sin() / xr)
489    }
490}
491
492/// Blackman window filter — variable radius (minimum 2.0).
493pub struct ImageFilterBlackman {
494    radius: f64,
495}
496impl ImageFilterBlackman {
497    pub fn new(r: f64) -> Self {
498        Self {
499            radius: if r < 2.0 { 2.0 } else { r },
500        }
501    }
502}
503impl ImageFilterFunction for ImageFilterBlackman {
504    fn radius(&self) -> f64 {
505        self.radius
506    }
507    fn calc_weight(&self, x: f64) -> f64 {
508        if x == 0.0 {
509            return 1.0;
510        }
511        if x > self.radius {
512            return 0.0;
513        }
514        let x = x * PI;
515        let xr = x / self.radius;
516        (x.sin() / x) * (0.42 + 0.5 * xr.cos() + 0.08 * (2.0 * xr).cos())
517    }
518}
519
520// ============================================================================
521// Fixed-radius convenience wrappers
522// ============================================================================
523
524/// Sinc filter with radius 3.0.
525pub struct ImageFilterSinc36(ImageFilterSinc);
526impl ImageFilterSinc36 {
527    pub fn new() -> Self {
528        Self(ImageFilterSinc::new(3.0))
529    }
530}
531impl Default for ImageFilterSinc36 {
532    fn default() -> Self {
533        Self::new()
534    }
535}
536impl ImageFilterFunction for ImageFilterSinc36 {
537    fn radius(&self) -> f64 {
538        self.0.radius()
539    }
540    fn calc_weight(&self, x: f64) -> f64 {
541        self.0.calc_weight(x)
542    }
543}
544
545/// Sinc filter with radius 4.0.
546pub struct ImageFilterSinc64(ImageFilterSinc);
547impl ImageFilterSinc64 {
548    pub fn new() -> Self {
549        Self(ImageFilterSinc::new(4.0))
550    }
551}
552impl Default for ImageFilterSinc64 {
553    fn default() -> Self {
554        Self::new()
555    }
556}
557impl ImageFilterFunction for ImageFilterSinc64 {
558    fn radius(&self) -> f64 {
559        self.0.radius()
560    }
561    fn calc_weight(&self, x: f64) -> f64 {
562        self.0.calc_weight(x)
563    }
564}
565
566/// Sinc filter with radius 5.0.
567pub struct ImageFilterSinc100(ImageFilterSinc);
568impl ImageFilterSinc100 {
569    pub fn new() -> Self {
570        Self(ImageFilterSinc::new(5.0))
571    }
572}
573impl Default for ImageFilterSinc100 {
574    fn default() -> Self {
575        Self::new()
576    }
577}
578impl ImageFilterFunction for ImageFilterSinc100 {
579    fn radius(&self) -> f64 {
580        self.0.radius()
581    }
582    fn calc_weight(&self, x: f64) -> f64 {
583        self.0.calc_weight(x)
584    }
585}
586
587/// Sinc filter with radius 6.0.
588pub struct ImageFilterSinc144(ImageFilterSinc);
589impl ImageFilterSinc144 {
590    pub fn new() -> Self {
591        Self(ImageFilterSinc::new(6.0))
592    }
593}
594impl Default for ImageFilterSinc144 {
595    fn default() -> Self {
596        Self::new()
597    }
598}
599impl ImageFilterFunction for ImageFilterSinc144 {
600    fn radius(&self) -> f64 {
601        self.0.radius()
602    }
603    fn calc_weight(&self, x: f64) -> f64 {
604        self.0.calc_weight(x)
605    }
606}
607
608/// Sinc filter with radius 7.0.
609pub struct ImageFilterSinc196(ImageFilterSinc);
610impl ImageFilterSinc196 {
611    pub fn new() -> Self {
612        Self(ImageFilterSinc::new(7.0))
613    }
614}
615impl Default for ImageFilterSinc196 {
616    fn default() -> Self {
617        Self::new()
618    }
619}
620impl ImageFilterFunction for ImageFilterSinc196 {
621    fn radius(&self) -> f64 {
622        self.0.radius()
623    }
624    fn calc_weight(&self, x: f64) -> f64 {
625        self.0.calc_weight(x)
626    }
627}
628
629/// Sinc filter with radius 8.0.
630pub struct ImageFilterSinc256(ImageFilterSinc);
631impl ImageFilterSinc256 {
632    pub fn new() -> Self {
633        Self(ImageFilterSinc::new(8.0))
634    }
635}
636impl Default for ImageFilterSinc256 {
637    fn default() -> Self {
638        Self::new()
639    }
640}
641impl ImageFilterFunction for ImageFilterSinc256 {
642    fn radius(&self) -> f64 {
643        self.0.radius()
644    }
645    fn calc_weight(&self, x: f64) -> f64 {
646        self.0.calc_weight(x)
647    }
648}
649
650/// Lanczos filter with radius 3.0.
651pub struct ImageFilterLanczos36(ImageFilterLanczos);
652impl ImageFilterLanczos36 {
653    pub fn new() -> Self {
654        Self(ImageFilterLanczos::new(3.0))
655    }
656}
657impl Default for ImageFilterLanczos36 {
658    fn default() -> Self {
659        Self::new()
660    }
661}
662impl ImageFilterFunction for ImageFilterLanczos36 {
663    fn radius(&self) -> f64 {
664        self.0.radius()
665    }
666    fn calc_weight(&self, x: f64) -> f64 {
667        self.0.calc_weight(x)
668    }
669}
670
671/// Lanczos filter with radius 4.0.
672pub struct ImageFilterLanczos64(ImageFilterLanczos);
673impl ImageFilterLanczos64 {
674    pub fn new() -> Self {
675        Self(ImageFilterLanczos::new(4.0))
676    }
677}
678impl Default for ImageFilterLanczos64 {
679    fn default() -> Self {
680        Self::new()
681    }
682}
683impl ImageFilterFunction for ImageFilterLanczos64 {
684    fn radius(&self) -> f64 {
685        self.0.radius()
686    }
687    fn calc_weight(&self, x: f64) -> f64 {
688        self.0.calc_weight(x)
689    }
690}
691
692/// Lanczos filter with radius 5.0.
693pub struct ImageFilterLanczos100(ImageFilterLanczos);
694impl ImageFilterLanczos100 {
695    pub fn new() -> Self {
696        Self(ImageFilterLanczos::new(5.0))
697    }
698}
699impl Default for ImageFilterLanczos100 {
700    fn default() -> Self {
701        Self::new()
702    }
703}
704impl ImageFilterFunction for ImageFilterLanczos100 {
705    fn radius(&self) -> f64 {
706        self.0.radius()
707    }
708    fn calc_weight(&self, x: f64) -> f64 {
709        self.0.calc_weight(x)
710    }
711}
712
713/// Lanczos filter with radius 6.0.
714pub struct ImageFilterLanczos144(ImageFilterLanczos);
715impl ImageFilterLanczos144 {
716    pub fn new() -> Self {
717        Self(ImageFilterLanczos::new(6.0))
718    }
719}
720impl Default for ImageFilterLanczos144 {
721    fn default() -> Self {
722        Self::new()
723    }
724}
725impl ImageFilterFunction for ImageFilterLanczos144 {
726    fn radius(&self) -> f64 {
727        self.0.radius()
728    }
729    fn calc_weight(&self, x: f64) -> f64 {
730        self.0.calc_weight(x)
731    }
732}
733
734/// Lanczos filter with radius 7.0.
735pub struct ImageFilterLanczos196(ImageFilterLanczos);
736impl ImageFilterLanczos196 {
737    pub fn new() -> Self {
738        Self(ImageFilterLanczos::new(7.0))
739    }
740}
741impl Default for ImageFilterLanczos196 {
742    fn default() -> Self {
743        Self::new()
744    }
745}
746impl ImageFilterFunction for ImageFilterLanczos196 {
747    fn radius(&self) -> f64 {
748        self.0.radius()
749    }
750    fn calc_weight(&self, x: f64) -> f64 {
751        self.0.calc_weight(x)
752    }
753}
754
755/// Lanczos filter with radius 8.0.
756pub struct ImageFilterLanczos256(ImageFilterLanczos);
757impl ImageFilterLanczos256 {
758    pub fn new() -> Self {
759        Self(ImageFilterLanczos::new(8.0))
760    }
761}
762impl Default for ImageFilterLanczos256 {
763    fn default() -> Self {
764        Self::new()
765    }
766}
767impl ImageFilterFunction for ImageFilterLanczos256 {
768    fn radius(&self) -> f64 {
769        self.0.radius()
770    }
771    fn calc_weight(&self, x: f64) -> f64 {
772        self.0.calc_weight(x)
773    }
774}
775
776/// Blackman filter with radius 3.0.
777pub struct ImageFilterBlackman36(ImageFilterBlackman);
778impl ImageFilterBlackman36 {
779    pub fn new() -> Self {
780        Self(ImageFilterBlackman::new(3.0))
781    }
782}
783impl Default for ImageFilterBlackman36 {
784    fn default() -> Self {
785        Self::new()
786    }
787}
788impl ImageFilterFunction for ImageFilterBlackman36 {
789    fn radius(&self) -> f64 {
790        self.0.radius()
791    }
792    fn calc_weight(&self, x: f64) -> f64 {
793        self.0.calc_weight(x)
794    }
795}
796
797/// Blackman filter with radius 4.0.
798pub struct ImageFilterBlackman64(ImageFilterBlackman);
799impl ImageFilterBlackman64 {
800    pub fn new() -> Self {
801        Self(ImageFilterBlackman::new(4.0))
802    }
803}
804impl Default for ImageFilterBlackman64 {
805    fn default() -> Self {
806        Self::new()
807    }
808}
809impl ImageFilterFunction for ImageFilterBlackman64 {
810    fn radius(&self) -> f64 {
811        self.0.radius()
812    }
813    fn calc_weight(&self, x: f64) -> f64 {
814        self.0.calc_weight(x)
815    }
816}
817
818/// Blackman filter with radius 5.0.
819pub struct ImageFilterBlackman100(ImageFilterBlackman);
820impl ImageFilterBlackman100 {
821    pub fn new() -> Self {
822        Self(ImageFilterBlackman::new(5.0))
823    }
824}
825impl Default for ImageFilterBlackman100 {
826    fn default() -> Self {
827        Self::new()
828    }
829}
830impl ImageFilterFunction for ImageFilterBlackman100 {
831    fn radius(&self) -> f64 {
832        self.0.radius()
833    }
834    fn calc_weight(&self, x: f64) -> f64 {
835        self.0.calc_weight(x)
836    }
837}
838
839/// Blackman filter with radius 6.0.
840pub struct ImageFilterBlackman144(ImageFilterBlackman);
841impl ImageFilterBlackman144 {
842    pub fn new() -> Self {
843        Self(ImageFilterBlackman::new(6.0))
844    }
845}
846impl Default for ImageFilterBlackman144 {
847    fn default() -> Self {
848        Self::new()
849    }
850}
851impl ImageFilterFunction for ImageFilterBlackman144 {
852    fn radius(&self) -> f64 {
853        self.0.radius()
854    }
855    fn calc_weight(&self, x: f64) -> f64 {
856        self.0.calc_weight(x)
857    }
858}
859
860/// Blackman filter with radius 7.0.
861pub struct ImageFilterBlackman196(ImageFilterBlackman);
862impl ImageFilterBlackman196 {
863    pub fn new() -> Self {
864        Self(ImageFilterBlackman::new(7.0))
865    }
866}
867impl Default for ImageFilterBlackman196 {
868    fn default() -> Self {
869        Self::new()
870    }
871}
872impl ImageFilterFunction for ImageFilterBlackman196 {
873    fn radius(&self) -> f64 {
874        self.0.radius()
875    }
876    fn calc_weight(&self, x: f64) -> f64 {
877        self.0.calc_weight(x)
878    }
879}
880
881/// Blackman filter with radius 8.0.
882pub struct ImageFilterBlackman256(ImageFilterBlackman);
883impl ImageFilterBlackman256 {
884    pub fn new() -> Self {
885        Self(ImageFilterBlackman::new(8.0))
886    }
887}
888impl Default for ImageFilterBlackman256 {
889    fn default() -> Self {
890        Self::new()
891    }
892}
893impl ImageFilterFunction for ImageFilterBlackman256 {
894    fn radius(&self) -> f64 {
895        self.0.radius()
896    }
897    fn calc_weight(&self, x: f64) -> f64 {
898        self.0.calc_weight(x)
899    }
900}
901
902// ============================================================================
903// Tests
904// ============================================================================
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909
910    #[test]
911    fn test_bilinear_radius_and_weight() {
912        let f = ImageFilterBilinear;
913        assert_eq!(f.radius(), 1.0);
914        assert_eq!(f.calc_weight(0.0), 1.0);
915        assert_eq!(f.calc_weight(0.5), 0.5);
916        assert_eq!(f.calc_weight(1.0), 0.0);
917    }
918
919    #[test]
920    fn test_hermite_at_zero() {
921        let f = ImageFilterHermite;
922        assert_eq!(f.calc_weight(0.0), 1.0);
923    }
924
925    #[test]
926    fn test_bicubic_at_zero() {
927        let f = ImageFilterBicubic;
928        let w = f.calc_weight(0.0);
929        // (1/6)*(pow3(2) - 4*pow3(1) + 6*pow3(0) - 4*pow3(-1)) = (1/6)*(8-4+0-0) = 2/3
930        let expected = 2.0 / 3.0;
931        assert!(
932            (w - expected).abs() < 1e-10,
933            "bicubic at 0 should be ~{expected}, got {w}"
934        );
935    }
936
937    #[test]
938    fn test_gaussian_at_zero() {
939        let f = ImageFilterGaussian;
940        let expected = (2.0 / PI).sqrt();
941        assert!((f.calc_weight(0.0) - expected).abs() < 1e-10);
942    }
943
944    #[test]
945    fn test_kaiser_at_zero() {
946        let f = ImageFilterKaiser::default();
947        let w = f.calc_weight(0.0);
948        assert!(
949            (w - 1.0).abs() < 1e-10,
950            "kaiser at 0 should be ~1.0, got {w}"
951        );
952    }
953
954    #[test]
955    fn test_mitchell_at_zero() {
956        let f = ImageFilterMitchell::default();
957        let w = f.calc_weight(0.0);
958        let expected = f.p0; // p0 = (6 - 2/3) / 6 = 8/9
959        assert!((w - expected).abs() < 1e-10);
960    }
961
962    #[test]
963    fn test_sinc_at_zero() {
964        let f = ImageFilterSinc::new(3.0);
965        assert_eq!(f.calc_weight(0.0), 1.0);
966        assert_eq!(f.radius(), 3.0);
967    }
968
969    #[test]
970    fn test_sinc_minimum_radius() {
971        let f = ImageFilterSinc::new(1.0);
972        assert_eq!(f.radius(), 2.0); // minimum is 2.0
973    }
974
975    #[test]
976    fn test_lanczos_at_zero() {
977        let f = ImageFilterLanczos::new(3.0);
978        assert_eq!(f.calc_weight(0.0), 1.0);
979    }
980
981    #[test]
982    fn test_lanczos_beyond_radius() {
983        let f = ImageFilterLanczos::new(3.0);
984        assert_eq!(f.calc_weight(4.0), 0.0);
985    }
986
987    #[test]
988    fn test_blackman_at_zero() {
989        let f = ImageFilterBlackman::new(3.0);
990        assert_eq!(f.calc_weight(0.0), 1.0);
991    }
992
993    #[test]
994    fn test_lut_bilinear() {
995        let lut = ImageFilterLut::new_with_filter(&ImageFilterBilinear, true);
996        assert_eq!(lut.radius(), 1.0);
997        assert_eq!(lut.diameter(), 2);
998        assert_eq!(lut.start(), 0); // -(2/2 - 1) = 0
999                                    // Weight array size = diameter * 256 = 512
1000        assert_eq!(lut.weight_array().len(), 512);
1001    }
1002
1003    #[test]
1004    fn test_lut_weight_symmetry() {
1005        let lut = ImageFilterLut::new_with_filter(&ImageFilterBilinear, false);
1006        let weights = lut.weight_array();
1007        let pivot = (lut.diameter() as usize) << (IMAGE_SUBPIXEL_SHIFT as usize - 1);
1008        // Weights should be symmetric around pivot
1009        for i in 1..pivot {
1010            assert_eq!(
1011                weights[pivot + i],
1012                weights[pivot - i],
1013                "asymmetry at offset {i}"
1014            );
1015        }
1016    }
1017
1018    #[test]
1019    fn test_lut_normalization() {
1020        let lut = ImageFilterLut::new_with_filter(&ImageFilterBilinear, true);
1021        let weights = lut.weight_array();
1022        let diameter = lut.diameter() as usize;
1023        let subpixel_scale = IMAGE_SUBPIXEL_SCALE as usize;
1024        // For each subpixel offset, weights across diameter should sum to IMAGE_FILTER_SCALE
1025        for i in 0..subpixel_scale {
1026            let sum: i32 = (0..diameter)
1027                .map(|j| weights[j * subpixel_scale + i] as i32)
1028                .sum();
1029            assert_eq!(sum, IMAGE_FILTER_SCALE, "sum at offset {i} = {sum}");
1030        }
1031    }
1032
1033    #[test]
1034    fn test_lut_bicubic() {
1035        let lut = ImageFilterLut::new_with_filter(&ImageFilterBicubic, true);
1036        assert_eq!(lut.diameter(), 4);
1037        assert_eq!(lut.start(), -1); // -(4/2 - 1) = -1
1038    }
1039
1040    #[test]
1041    fn test_convenience_wrappers() {
1042        // Just verify the convenience types create correct radii
1043        assert_eq!(ImageFilterSinc36::new().radius(), 3.0);
1044        assert_eq!(ImageFilterSinc64::new().radius(), 4.0);
1045        assert_eq!(ImageFilterLanczos36::new().radius(), 3.0);
1046        assert_eq!(ImageFilterLanczos256::new().radius(), 8.0);
1047        assert_eq!(ImageFilterBlackman36::new().radius(), 3.0);
1048        assert_eq!(ImageFilterBlackman256::new().radius(), 8.0);
1049    }
1050
1051    #[test]
1052    fn test_bessel_filter() {
1053        let f = ImageFilterBessel;
1054        assert_eq!(f.radius(), 3.2383);
1055        let w0 = f.calc_weight(0.0);
1056        assert!((w0 - PI / 4.0).abs() < 1e-10);
1057    }
1058
1059    #[test]
1060    fn test_quadric_regions() {
1061        let f = ImageFilterQuadric;
1062        // x < 0.5: 0.75 - x²
1063        assert_eq!(f.calc_weight(0.0), 0.75);
1064        // x = 0.5: boundary
1065        assert!((f.calc_weight(0.5) - 0.5).abs() < 1e-10);
1066        // x >= 1.5: 0
1067        assert_eq!(f.calc_weight(1.5), 0.0);
1068    }
1069
1070    #[test]
1071    fn test_catrom_at_zero() {
1072        let f = ImageFilterCatrom;
1073        assert_eq!(f.calc_weight(0.0), 1.0);
1074    }
1075
1076    #[test]
1077    fn test_hanning_at_zero() {
1078        let f = ImageFilterHanning;
1079        assert_eq!(f.calc_weight(0.0), 1.0);
1080    }
1081
1082    #[test]
1083    fn test_spline16_at_zero() {
1084        let f = ImageFilterSpline16;
1085        assert_eq!(f.calc_weight(0.0), 1.0);
1086    }
1087}