Skip to main content

agg_rust/
pattern_filters_rgba.rs

1//! Pattern filters for RGBA pixel formats.
2//!
3//! Port of `agg_pattern_filters_rgba.h`.
4//! Provides nearest-neighbor and bilinear pattern filter implementations
5//! for use with line pattern rendering.
6//!
7//! Copyright 2025.
8
9use crate::color::Rgba8;
10
11/// Subpixel shift for line coordinate precision (same as line_aa_basics).
12pub const LINE_SUBPIXEL_SHIFT: u32 = 8;
13pub const LINE_SUBPIXEL_SCALE: i32 = 1 << LINE_SUBPIXEL_SHIFT;
14pub const LINE_SUBPIXEL_MASK: i32 = LINE_SUBPIXEL_SCALE - 1;
15
16/// Trait for pattern pixel access.
17///
18/// The buffer is a 2D grid stored as `&[Vec<Rgba8>]` where each inner Vec
19/// is one row of pixels. This maps to the C++ `row_ptr_cache` pattern.
20pub trait PatternFilter {
21    /// Number of extra pixels needed on each side of the pattern.
22    fn dilation() -> u32;
23
24    /// Get pixel at integer coordinates.
25    fn pixel_low_res(buf: &[Vec<Rgba8>], x: i32, y: i32) -> Rgba8;
26
27    /// Get pixel at subpixel coordinates (shifted by LINE_SUBPIXEL_SHIFT).
28    /// Writes the result into `p`.
29    fn pixel_high_res(buf: &[Vec<Rgba8>], p: &mut Rgba8, x: i32, y: i32);
30}
31
32/// Nearest-neighbor pattern filter.
33///
34/// Port of C++ `pattern_filter_nn<rgba8>`.
35/// Simple point sampling — no interpolation.
36pub struct PatternFilterNn;
37
38impl PatternFilter for PatternFilterNn {
39    fn dilation() -> u32 {
40        0
41    }
42
43    #[inline]
44    fn pixel_low_res(buf: &[Vec<Rgba8>], x: i32, y: i32) -> Rgba8 {
45        buf[y as usize][x as usize]
46    }
47
48    #[inline]
49    fn pixel_high_res(buf: &[Vec<Rgba8>], p: &mut Rgba8, x: i32, y: i32) {
50        *p = buf[(y >> LINE_SUBPIXEL_SHIFT) as usize][(x >> LINE_SUBPIXEL_SHIFT) as usize];
51    }
52}
53
54/// Bilinear pattern filter.
55///
56/// Port of C++ `pattern_filter_bilinear_rgba<rgba8>`.
57/// Interpolates between 4 neighboring pixels for smooth pattern rendering.
58pub struct PatternFilterBilinearRgba;
59
60impl PatternFilter for PatternFilterBilinearRgba {
61    fn dilation() -> u32 {
62        1
63    }
64
65    #[inline]
66    fn pixel_low_res(buf: &[Vec<Rgba8>], x: i32, y: i32) -> Rgba8 {
67        buf[y as usize][x as usize]
68    }
69
70    #[inline]
71    fn pixel_high_res(buf: &[Vec<Rgba8>], p: &mut Rgba8, x: i32, y: i32) {
72        let x_lr = x >> LINE_SUBPIXEL_SHIFT;
73        let y_lr = y >> LINE_SUBPIXEL_SHIFT;
74
75        let x_hr = x & LINE_SUBPIXEL_MASK;
76        let y_hr = y & LINE_SUBPIXEL_MASK;
77
78        let row0 = &buf[y_lr as usize];
79        let row1 = &buf[y_lr as usize + 1];
80
81        let p00 = &row0[x_lr as usize];
82        let p01 = &row0[x_lr as usize + 1];
83        let p10 = &row1[x_lr as usize];
84        let p11 = &row1[x_lr as usize + 1];
85
86        let weight = LINE_SUBPIXEL_SCALE;
87
88        let r = (p00.r as i32 * (weight - x_hr) * (weight - y_hr)
89            + p01.r as i32 * x_hr * (weight - y_hr)
90            + p10.r as i32 * (weight - x_hr) * y_hr
91            + p11.r as i32 * x_hr * y_hr)
92            >> (LINE_SUBPIXEL_SHIFT * 2);
93
94        let g = (p00.g as i32 * (weight - x_hr) * (weight - y_hr)
95            + p01.g as i32 * x_hr * (weight - y_hr)
96            + p10.g as i32 * (weight - x_hr) * y_hr
97            + p11.g as i32 * x_hr * y_hr)
98            >> (LINE_SUBPIXEL_SHIFT * 2);
99
100        let b = (p00.b as i32 * (weight - x_hr) * (weight - y_hr)
101            + p01.b as i32 * x_hr * (weight - y_hr)
102            + p10.b as i32 * (weight - x_hr) * y_hr
103            + p11.b as i32 * x_hr * y_hr)
104            >> (LINE_SUBPIXEL_SHIFT * 2);
105
106        let a = (p00.a as i32 * (weight - x_hr) * (weight - y_hr)
107            + p01.a as i32 * x_hr * (weight - y_hr)
108            + p10.a as i32 * (weight - x_hr) * y_hr
109            + p11.a as i32 * x_hr * y_hr)
110            >> (LINE_SUBPIXEL_SHIFT * 2);
111
112        *p = Rgba8::new(r as u32, g as u32, b as u32, a as u32);
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    fn make_row(colors: &[Rgba8]) -> Vec<Rgba8> {
121        colors.to_vec()
122    }
123
124    #[test]
125    fn test_nn_low_res() {
126        let row0 = make_row(&[Rgba8::new(255, 0, 0, 255), Rgba8::new(0, 255, 0, 255)]);
127        let row1 = make_row(&[Rgba8::new(0, 0, 255, 255), Rgba8::new(255, 255, 0, 255)]);
128        let buf: Vec<Vec<Rgba8>> = vec![row0, row1];
129
130        let p = PatternFilterNn::pixel_low_res(&buf, 0, 0);
131        assert_eq!(p.r, 255);
132        assert_eq!(p.g, 0);
133
134        let p = PatternFilterNn::pixel_low_res(&buf, 1, 0);
135        assert_eq!(p.r, 0);
136        assert_eq!(p.g, 255);
137
138        let p = PatternFilterNn::pixel_low_res(&buf, 0, 1);
139        assert_eq!(p.b, 255);
140    }
141
142    #[test]
143    fn test_nn_high_res() {
144        let row0 = make_row(&[Rgba8::new(255, 0, 0, 255), Rgba8::new(0, 255, 0, 255)]);
145        let row1 = make_row(&[Rgba8::new(0, 0, 255, 255), Rgba8::new(255, 255, 0, 255)]);
146        let buf: Vec<Vec<Rgba8>> = vec![row0, row1];
147
148        // High-res coord (128, 0) → low-res (0, 0) since 128 >> 8 = 0
149        let mut p = Rgba8::new(0, 0, 0, 0);
150        PatternFilterNn::pixel_high_res(&buf, &mut p, 128, 0);
151        assert_eq!(p.r, 255);
152
153        // High-res coord (256, 0) → low-res (1, 0)
154        PatternFilterNn::pixel_high_res(&buf, &mut p, 256, 0);
155        assert_eq!(p.r, 0);
156        assert_eq!(p.g, 255);
157    }
158
159    #[test]
160    fn test_bilinear_at_integer_coord() {
161        let row0 = make_row(&[
162            Rgba8::new(255, 0, 0, 255),
163            Rgba8::new(0, 255, 0, 255),
164            Rgba8::new(0, 0, 0, 255),
165        ]);
166        let row1 = make_row(&[
167            Rgba8::new(0, 0, 255, 255),
168            Rgba8::new(255, 255, 0, 255),
169            Rgba8::new(0, 0, 0, 255),
170        ]);
171        let row2 = make_row(&[
172            Rgba8::new(0, 0, 0, 255),
173            Rgba8::new(0, 0, 0, 255),
174            Rgba8::new(0, 0, 0, 255),
175        ]);
176        let buf: Vec<Vec<Rgba8>> = vec![row0, row1, row2];
177
178        // At exact integer coord (0,0)*256 → should return (255,0,0,255)
179        let mut p = Rgba8::new(0, 0, 0, 0);
180        PatternFilterBilinearRgba::pixel_high_res(&buf, &mut p, 0, 0);
181        assert_eq!(p.r, 255);
182        assert_eq!(p.g, 0);
183        assert_eq!(p.b, 0);
184    }
185
186    #[test]
187    fn test_bilinear_midpoint() {
188        // 2x2 pattern: top-left=white, top-right=black, bottom-left=black, bottom-right=black
189        let white = Rgba8::new(255, 255, 255, 255);
190        let black = Rgba8::new(0, 0, 0, 255);
191        let row0 = make_row(&[white, black]);
192        let row1 = make_row(&[black, black]);
193        let buf: Vec<Vec<Rgba8>> = vec![row0, row1];
194
195        // At midpoint (128, 128) → should blend all 4 corners
196        let mut p = Rgba8::new(0, 0, 0, 0);
197        PatternFilterBilinearRgba::pixel_high_res(&buf, &mut p, 128, 128);
198        // weight=(256-128)*(256-128)*255 / 65536 ≈ 64 for top-left only
199        assert!(p.r > 50 && p.r < 80, "r={}", p.r);
200    }
201}