Skip to main content

rasterrocket_render/shading/
function.rs

1//! Function-based shading pattern.
2//!
3//! A PDF Type 1 shading applies an arbitrary function `f(x, y) → colour`.
4//! The function is supplied as a trait object from `pdf_bridge`; this module
5//! wraps it in the [`Pattern`] interface.
6//!
7//! The function receives pixel centres in device space and returns an RGB
8//! triple already in device colour space.  Any colour-space conversion is the
9//! caller's responsibility — this type only forwards the closure's output.
10//!
11//! # Panics
12//!
13//! The closure itself must not panic.  Panics inside the closure propagate
14//! through `fill_span` to the rasterizer caller.
15
16use crate::pipe::Pattern;
17
18/// A shading pattern driven by an arbitrary `f(x, y) → [R, G, B]` closure.
19///
20/// The closure is boxed so it can capture ICC profiles, LUTs, or any other
21/// per-page state from `pdf_bridge`.  It must be `Send + Sync` because
22/// [`Pattern`] is used across rayon threads.
23pub struct FunctionPattern {
24    func: Box<dyn Fn(f64, f64) -> [u8; 3] + Send + Sync>,
25}
26
27impl FunctionPattern {
28    /// Create a function-based pattern from a closure `f(x, y) → [R, G, B]`.
29    ///
30    /// `x` and `y` are the pixel-centre coordinates in device space.
31    #[must_use]
32    pub fn new<F>(func: F) -> Self
33    where
34        F: Fn(f64, f64) -> [u8; 3] + Send + Sync + 'static,
35    {
36        Self {
37            func: Box::new(func),
38        }
39    }
40}
41
42impl Pattern for FunctionPattern {
43    fn fill_span(&self, y: i32, x0: i32, x1: i32, out: &mut [u8]) {
44        // out.len() == (x1-x0+1)*3 is an invariant guaranteed by render_span.
45        let mut off = 0usize;
46        for x in x0..=x1 {
47            let rgb = (self.func)(f64::from(x), f64::from(y));
48            out[off] = rgb[0];
49            out[off + 1] = rgb[1];
50            out[off + 2] = rgb[2];
51            off += 3;
52        }
53    }
54
55    fn is_static_color(&self) -> bool {
56        false
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn encodes_x_coordinate_in_output() {
66        // Each pixel's R/G/B equals its x coordinate (mod 256). fill_span is
67        // called with integer x ∈ [10, 13], so f64-as-u8 is exact here.
68        #[expect(
69            clippy::cast_possible_truncation,
70            clippy::cast_sign_loss,
71            reason = "test passes integer x ∈ [10,13]; cast is exact"
72        )]
73        let p = FunctionPattern::new(|x, _y| {
74            let v = x as u8;
75            [v, v, v]
76        });
77        let mut out = vec![0u8; 4 * 3];
78        p.fill_span(0, 10, 13, &mut out);
79        for i in 0..4usize {
80            #[expect(
81                clippy::cast_possible_truncation,
82                reason = "i ∈ [0,4); 10 + i ≤ 13 fits in u8"
83            )]
84            let expected = (10 + i) as u8;
85            assert_eq!(out[i * 3], expected, "pixel {i} R");
86            assert_eq!(out[i * 3 + 1], expected, "pixel {i} G");
87            assert_eq!(out[i * 3 + 2], expected, "pixel {i} B");
88        }
89    }
90
91    #[test]
92    fn constant_function_fills_uniform_color() {
93        let p = FunctionPattern::new(|_, _| [128, 64, 32]);
94        let mut out = vec![0u8; 3 * 3];
95        p.fill_span(5, 0, 2, &mut out);
96        for i in 0..3usize {
97            assert_eq!(out[i * 3], 128, "pixel {i} R");
98            assert_eq!(out[i * 3 + 1], 64, "pixel {i} G");
99            assert_eq!(out[i * 3 + 2], 32, "pixel {i} B");
100        }
101    }
102
103    #[test]
104    fn encodes_y_coordinate_in_output() {
105        #[expect(
106            clippy::cast_possible_truncation,
107            clippy::cast_sign_loss,
108            reason = "test passes integer y = 42; cast is exact"
109        )]
110        let p = FunctionPattern::new(|_x, y| [0, 0, y as u8]);
111        let mut out = [0u8; 3];
112        p.fill_span(42, 0, 0, &mut out);
113        assert_eq!(out[2], 42, "blue channel should encode y");
114    }
115
116    #[test]
117    fn single_pixel_span_writes_three_bytes() {
118        let p = FunctionPattern::new(|_, _| [1, 2, 3]);
119        let mut out = [0u8; 3];
120        p.fill_span(0, 7, 7, &mut out);
121        assert_eq!(out, [1, 2, 3]);
122    }
123}