Skip to main content

maple_render_core/
pixer.rs

1use image::{Rgba, RgbaImage};
2
3#[derive(Debug, Clone, Copy, Default)]
4pub struct Pixer {
5    pub r: f32,
6    pub g: f32,
7    pub b: f32,
8    pub a: f32,
9}
10
11impl Pixer {
12    #[inline(always)]
13    pub fn new() -> Self {
14        Pixer::default()
15    }
16
17    #[inline(always)]
18    pub fn from_rgba(pixel: &Rgba<u8>) -> Self {
19        Pixer { r: pixel[0] as f32, g: pixel[1] as f32, b: pixel[2] as f32, a: pixel[3] as f32 }
20    }
21
22    #[inline(always)]
23    pub fn to_rgba(&self) -> Rgba<u8> {
24        Rgba([clamp_u8(self.r), clamp_u8(self.g), clamp_u8(self.b), clamp_u8(self.a)])
25    }
26
27    #[inline(always)]
28    pub fn preblend(&mut self) {
29        self.r *= self.a;
30        self.g *= self.a;
31        self.b *= self.a;
32    }
33
34    #[inline(always)]
35    pub fn postblend(&mut self, scale: f32) {
36        if scale > 0.0001 {
37            self.r /= scale;
38            self.g /= scale;
39            self.b /= scale;
40        } else {
41            self.r = 0.0;
42            self.g = 0.0;
43            self.b = 0.0;
44        }
45    }
46
47    pub fn add(&mut self, other: &Pixer) {
48        self.r += other.r;
49        self.g += other.g;
50        self.b += other.b;
51        self.a += other.a;
52    }
53
54    pub fn add_rgba(&mut self, pixel: &Rgba<u8>) {
55        self.r += pixel[0] as f32;
56        self.g += pixel[1] as f32;
57        self.b += pixel[2] as f32;
58        self.a += pixel[3] as f32;
59    }
60
61    pub fn scale(&mut self, factor: f32) {
62        self.r *= factor;
63        self.g *= factor;
64        self.b *= factor;
65        self.a *= factor;
66    }
67
68    pub fn div(&mut self, factor: f32) {
69        if factor.abs() > 0.0001 {
70            self.r /= factor;
71            self.g /= factor;
72            self.b /= factor;
73            self.a /= factor;
74        }
75    }
76}
77
78impl std::ops::Add for Pixer {
79    type Output = Pixer;
80
81    fn add(self, other: Pixer) -> Pixer {
82        Pixer { r: self.r + other.r, g: self.g + other.g, b: self.b + other.b, a: self.a + other.a }
83    }
84}
85
86impl std::ops::AddAssign for Pixer {
87    fn add_assign(&mut self, other: Pixer) {
88        self.r += other.r;
89        self.g += other.g;
90        self.b += other.b;
91        self.a += other.a;
92    }
93}
94
95impl std::ops::Mul<f32> for Pixer {
96    type Output = Pixer;
97
98    fn mul(self, factor: f32) -> Pixer {
99        Pixer { r: self.r * factor, g: self.g * factor, b: self.b * factor, a: self.a * factor }
100    }
101}
102
103impl std::ops::Div<f32> for Pixer {
104    type Output = Pixer;
105
106    fn div(self, factor: f32) -> Pixer {
107        if factor.abs() > 0.0001 {
108            Pixer { r: self.r / factor, g: self.g / factor, b: self.b / factor, a: self.a / factor }
109        } else {
110            Pixer::new()
111        }
112    }
113}
114
115#[inline]
116fn clamp_u8(v: f32) -> u8 {
117    if v <= 0.0 {
118        0
119    } else if v >= 255.0 {
120        255
121    } else {
122        v as u8
123    }
124}
125
126#[inline(always)]
127pub fn safe_pixel(img: &RgbaImage, x: i32, y: i32) -> Rgba<u8> {
128    let w = img.width() as i32;
129    let h = img.height() as i32;
130
131    if x < 0 || y < 0 || x >= w || y >= h {
132        Rgba([0, 0, 0, 0])
133    } else {
134        *img.get_pixel(x as u32, y as u32)
135    }
136}
137
138/// Bilinear interpolation with alpha-weighted averaging.
139///
140/// Coordinates remain `f64` for sub-pixel accuracy (the heavy lifting is the
141/// per-pixel color blend, which is performed in `f32` for throughput). The
142/// `f64` geometry inputs are down-cast to `f32` when constructing the returned
143/// `Pixer`.
144#[inline(always)]
145pub fn sample_linear(img: &RgbaImage, x: f64, y: f64) -> Pixer {
146    let xx = x.floor() as i32;
147    let yy = y.floor() as i32;
148    let fx = x - xx as f64;
149    let fy = y - yy as f64;
150
151    let w00 = ((1.0 - fx) * (1.0 - fy)) as f32;
152    let w10 = (fx * (1.0 - fy)) as f32;
153    let w01 = ((1.0 - fx) * fy) as f32;
154    let w11 = (fx * fy) as f32;
155
156    let w = img.width() as i32;
157    let h = img.height() as i32;
158
159    if xx >= 0 && yy >= 0 && xx + 1 < w && yy + 1 < h {
160        let raw = img.as_raw();
161        let w_us = w as usize;
162        let row_stride = w_us * 4;
163
164        let row0 = yy as usize * row_stride;
165        let row1 = row0 + row_stride;
166        let col = xx as usize * 4;
167
168        let i00 = row0 + col;
169        let i10 = i00 + 4;
170        let i01 = row1 + col;
171        let i11 = i01 + 4;
172
173        let a00 = raw[i00 + 3] as f32;
174        let a10 = raw[i10 + 3] as f32;
175        let a01 = raw[i01 + 3] as f32;
176        let a11 = raw[i11 + 3] as f32;
177
178        let aa = w00 * a00 + w10 * a10 + w01 * a01 + w11 * a11;
179        let aa_safe = if aa < 0.0001 { 0.0001 } else { aa };
180
181        return Pixer {
182            r: (w00 * raw[i00] as f32 * a00
183                + w10 * raw[i10] as f32 * a10
184                + w01 * raw[i01] as f32 * a01
185                + w11 * raw[i11] as f32 * a11)
186                / aa_safe,
187            g: (w00 * raw[i00 + 1] as f32 * a00
188                + w10 * raw[i10 + 1] as f32 * a10
189                + w01 * raw[i01 + 1] as f32 * a01
190                + w11 * raw[i11 + 1] as f32 * a11)
191                / aa_safe,
192            b: (w00 * raw[i00 + 2] as f32 * a00
193                + w10 * raw[i10 + 2] as f32 * a10
194                + w01 * raw[i01 + 2] as f32 * a01
195                + w11 * raw[i11 + 2] as f32 * a11)
196                / aa_safe,
197            a: aa,
198        };
199    }
200
201    let p00 = safe_pixel(img, xx, yy);
202    let p10 = safe_pixel(img, xx + 1, yy);
203    let p01 = safe_pixel(img, xx, yy + 1);
204    let p11 = safe_pixel(img, xx + 1, yy + 1);
205
206    let a00 = p00[3] as f32;
207    let a10 = p10[3] as f32;
208    let a01 = p01[3] as f32;
209    let a11 = p11[3] as f32;
210
211    let aa = w00 * a00 + w10 * a10 + w01 * a01 + w11 * a11;
212    let aa_safe = if aa < 0.0001 { 0.0001 } else { aa };
213
214    Pixer {
215        r: (w00 * p00[0] as f32 * a00
216            + w10 * p10[0] as f32 * a10
217            + w01 * p01[0] as f32 * a01
218            + w11 * p11[0] as f32 * a11)
219            / aa_safe,
220        g: (w00 * p00[1] as f32 * a00
221            + w10 * p10[1] as f32 * a10
222            + w01 * p01[1] as f32 * a01
223            + w11 * p11[1] as f32 * a11)
224            / aa_safe,
225        b: (w00 * p00[2] as f32 * a00
226            + w10 * p10[2] as f32 * a10
227            + w01 * p01[2] as f32 * a01
228            + w11 * p11[2] as f32 * a11)
229            / aa_safe,
230        a: aa,
231    }
232}
233
234/// Bilinear interpolation in premultiplied-alpha space.
235///
236/// The sampled compositor combines several neighboring samples before
237/// converting back to straight alpha. Keeping RGB premultiplied here avoids a
238/// divide followed immediately by a multiply for every tap.
239#[inline(always)]
240pub(crate) fn sample_linear_premultiplied(img: &RgbaImage, x: f64, y: f64) -> Pixer {
241    let xx = x.floor() as i32;
242    let yy = y.floor() as i32;
243    let fx = x - xx as f64;
244    let fy = y - yy as f64;
245
246    let w00 = ((1.0 - fx) * (1.0 - fy)) as f32;
247    let w10 = (fx * (1.0 - fy)) as f32;
248    let w01 = ((1.0 - fx) * fy) as f32;
249    let w11 = (fx * fy) as f32;
250
251    let w = img.width() as i32;
252    let h = img.height() as i32;
253
254    if xx >= 0 && yy >= 0 && xx + 1 < w && yy + 1 < h {
255        let raw = img.as_raw();
256        let row_stride = w as usize * 4;
257
258        let i00 = yy as usize * row_stride + xx as usize * 4;
259        let i10 = i00 + 4;
260        let i01 = i00 + row_stride;
261        let i11 = i01 + 4;
262
263        let a00 = raw[i00 + 3] as f32;
264        let a10 = raw[i10 + 3] as f32;
265        let a01 = raw[i01 + 3] as f32;
266        let a11 = raw[i11 + 3] as f32;
267
268        return Pixer {
269            r: w00 * raw[i00] as f32 * a00
270                + w10 * raw[i10] as f32 * a10
271                + w01 * raw[i01] as f32 * a01
272                + w11 * raw[i11] as f32 * a11,
273            g: w00 * raw[i00 + 1] as f32 * a00
274                + w10 * raw[i10 + 1] as f32 * a10
275                + w01 * raw[i01 + 1] as f32 * a01
276                + w11 * raw[i11 + 1] as f32 * a11,
277            b: w00 * raw[i00 + 2] as f32 * a00
278                + w10 * raw[i10 + 2] as f32 * a10
279                + w01 * raw[i01 + 2] as f32 * a01
280                + w11 * raw[i11 + 2] as f32 * a11,
281            a: w00 * a00 + w10 * a10 + w01 * a01 + w11 * a11,
282        };
283    }
284
285    let p00 = safe_pixel(img, xx, yy);
286    let p10 = safe_pixel(img, xx + 1, yy);
287    let p01 = safe_pixel(img, xx, yy + 1);
288    let p11 = safe_pixel(img, xx + 1, yy + 1);
289
290    let a00 = p00[3] as f32;
291    let a10 = p10[3] as f32;
292    let a01 = p01[3] as f32;
293    let a11 = p11[3] as f32;
294
295    Pixer {
296        r: w00 * p00[0] as f32 * a00
297            + w10 * p10[0] as f32 * a10
298            + w01 * p01[0] as f32 * a01
299            + w11 * p11[0] as f32 * a11,
300        g: w00 * p00[1] as f32 * a00
301            + w10 * p10[1] as f32 * a10
302            + w01 * p01[1] as f32 * a01
303            + w11 * p11[1] as f32 * a11,
304        b: w00 * p00[2] as f32 * a00
305            + w10 * p10[2] as f32 * a10
306            + w01 * p01[2] as f32 * a01
307            + w11 * p11[2] as f32 * a11,
308        a: w00 * a00 + w10 * a10 + w01 * a01 + w11 * a11,
309    }
310}
311
312/// Bilinear interpolation for an image known to contain only opaque pixels.
313///
314/// This preserves the general sampler's floating-point operation order while
315/// replacing four alpha loads with the known value `255`.
316#[inline(always)]
317pub(crate) fn sample_linear_opaque(img: &RgbaImage, x: f64, y: f64) -> Pixer {
318    let xx = x.floor() as i32;
319    let yy = y.floor() as i32;
320    let fx = x - xx as f64;
321    let fy = y - yy as f64;
322
323    let w00 = ((1.0 - fx) * (1.0 - fy)) as f32;
324    let w10 = (fx * (1.0 - fy)) as f32;
325    let w01 = ((1.0 - fx) * fy) as f32;
326    let w11 = (fx * fy) as f32;
327
328    let w = img.width() as i32;
329    let h = img.height() as i32;
330
331    if xx >= 0 && yy >= 0 && xx + 1 < w && yy + 1 < h {
332        let raw = img.as_raw();
333        let row_stride = w as usize * 4;
334
335        let i00 = yy as usize * row_stride + xx as usize * 4;
336        let i10 = i00 + 4;
337        let i01 = i00 + row_stride;
338        let i11 = i01 + 4;
339
340        let alpha = 255.0;
341
342        return Pixer {
343            r: w00 * raw[i00] as f32 * alpha
344                + w10 * raw[i10] as f32 * alpha
345                + w01 * raw[i01] as f32 * alpha
346                + w11 * raw[i11] as f32 * alpha,
347            g: w00 * raw[i00 + 1] as f32 * alpha
348                + w10 * raw[i10 + 1] as f32 * alpha
349                + w01 * raw[i01 + 1] as f32 * alpha
350                + w11 * raw[i11 + 1] as f32 * alpha,
351            b: w00 * raw[i00 + 2] as f32 * alpha
352                + w10 * raw[i10 + 2] as f32 * alpha
353                + w01 * raw[i01 + 2] as f32 * alpha
354                + w11 * raw[i11 + 2] as f32 * alpha,
355            a: w00 * alpha + w10 * alpha + w01 * alpha + w11 * alpha,
356        };
357    }
358
359    let p00 = safe_pixel(img, xx, yy);
360    let p10 = safe_pixel(img, xx + 1, yy);
361    let p01 = safe_pixel(img, xx, yy + 1);
362    let p11 = safe_pixel(img, xx + 1, yy + 1);
363    let a00 = p00[3] as f32;
364    let a10 = p10[3] as f32;
365    let a01 = p01[3] as f32;
366    let a11 = p11[3] as f32;
367
368    Pixer {
369        r: w00 * p00[0] as f32 * a00
370            + w10 * p10[0] as f32 * a10
371            + w01 * p01[0] as f32 * a01
372            + w11 * p11[0] as f32 * a11,
373        g: w00 * p00[1] as f32 * a00
374            + w10 * p10[1] as f32 * a10
375            + w01 * p01[1] as f32 * a01
376            + w11 * p11[1] as f32 * a11,
377        b: w00 * p00[2] as f32 * a00
378            + w10 * p10[2] as f32 * a10
379            + w01 * p01[2] as f32 * a01
380            + w11 * p11[2] as f32 * a11,
381        a: w00 * a00 + w10 * a10 + w01 * a01 + w11 * a11,
382    }
383}
384
385pub fn sample_weakly(img: &RgbaImage, x: f64, y: f64) -> Pixer {
386    Pixer::from_rgba(&safe_pixel(img, x as i32, y as i32))
387}
388
389#[inline(always)]
390pub fn distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
391    ((x1 - x2).powi(2) + (y1 - y2).powi(2)).sqrt()
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn test_pixer_operations() {
400        let mut p = Pixer { r: 100.0, g: 150.0, b: 200.0, a: 255.0 };
401        p.scale(0.5);
402        assert_eq!(p.r, 50.0);
403        assert_eq!(p.g, 75.0);
404        assert_eq!(p.b, 100.0);
405    }
406
407    #[test]
408    fn test_clamp_u8() {
409        assert_eq!(clamp_u8(-10.0), 0);
410        assert_eq!(clamp_u8(128.0), 128);
411        assert_eq!(clamp_u8(300.0), 255);
412    }
413
414    #[test]
415    fn opaque_sampler_matches_general_premultiplied_sampler() {
416        let mut image = RgbaImage::new(3, 2);
417        for (i, pixel) in image.pixels_mut().enumerate() {
418            let value = u8::try_from(i * 31).expect("test value fits in u8");
419            *pixel = Rgba([value, value.wrapping_add(17), value.wrapping_add(83), 255]);
420        }
421
422        for (x, y) in [
423            (-1.25, -0.5),
424            (-0.25, 0.25),
425            (0.0, 0.0),
426            (0.3, 0.7),
427            (1.5, 0.25),
428            (2.0, 1.0),
429            (2.75, 1.75),
430            (4.0, 4.0),
431        ] {
432            let general = sample_linear_premultiplied(&image, x, y);
433            let opaque = sample_linear_opaque(&image, x, y);
434            assert_eq!(general.r.to_bits(), opaque.r.to_bits());
435            assert_eq!(general.g.to_bits(), opaque.g.to_bits());
436            assert_eq!(general.b.to_bits(), opaque.b.to_bits());
437            assert_eq!(general.a.to_bits(), opaque.a.to_bits());
438        }
439    }
440}