Skip to main content

pdfrum_page/shading/
function_based.rs

1//! Type 1: function-based shadings (ISO 32000-1 §8.7.4.5.2).
2//!
3//! A two-input function evaluated over a rectangle of the shading's own
4//! coordinate space. Two things differ from every other type:
5//!
6//! - **`/Domain` is `[xmin xmax ymin ymax]`**, not `[xmin ymin xmax ymax]`.
7//!   The pairing is per axis, not per corner.
8//! - **There is no colour lookup table.** The function is evaluated per pixel
9//!   and its result truncated to bytes, where axial and radial round through
10//!   their 256-entry ramp. On a smooth gradient the two differ by one level.
11//!
12//! There is no `/Extend` for this type: outside the domain box the pixel is
13//! left untouched.
14
15use crate::names;
16use kurbo::{Affine, Point};
17use pdfrum_object::{Dict, Resolve};
18
19/// A type 1 shading's geometry.
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct FunctionBased {
22    /// The domain rectangle, `[xmin, xmax, ymin, ymax]` — note the ordering.
23    pub domain: [f32; 4],
24    /// The shading's own `/Matrix`, mapping the domain into the shading's
25    /// space. Distinct from a pattern's `/Matrix`.
26    pub matrix: Affine,
27}
28
29impl FunctionBased {
30    /// Load from a shading dictionary.
31    pub(super) fn load(dict: &Dict, r: &impl Resolve) -> Self {
32        let domain = match dict.array(names::DOMAIN, r) {
33            Some(a) => [
34                a.number_at_or_zero(0),
35                a.number_at_or_zero(1),
36                a.number_at_or_zero(2),
37                a.number_at_or_zero(3),
38            ],
39            None => [0.0, 1.0, 0.0, 1.0],
40        };
41        Self {
42            domain,
43            matrix: dict.matrix(names::MATRIX, r),
44        }
45    }
46
47    /// Whether a point in the domain's coordinate space is inside it.
48    ///
49    /// The test is **inclusive** at both ends.
50    #[must_use]
51    pub fn contains(&self, p: Point) -> bool {
52        let x = p.x;
53        let y = p.y;
54        x >= f64::from(self.domain[0])
55            && x <= f64::from(self.domain[1])
56            && y >= f64::from(self.domain[2])
57            && y <= f64::from(self.domain[3])
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    // Test fixtures quote the oracle's own vectors, compare floats exactly
64    // where the behaviour being pinned is exact, and index arrays whose
65    // length the fixture itself fixes.
66    #![allow(
67        clippy::unreadable_literal,
68        clippy::float_cmp,
69        clippy::indexing_slicing,
70        clippy::cast_precision_loss,
71        clippy::cast_possible_truncation,
72        reason = "test fixtures quote oracle vectors verbatim and compare exactly"
73    )]
74
75    use super::FunctionBased;
76    use kurbo::{Affine, Point};
77
78    #[test]
79    fn the_domain_pairs_per_axis_not_per_corner() {
80        let s = FunctionBased {
81            // x spans 0..10 and y spans 100..200.
82            domain: [0.0, 10.0, 100.0, 200.0],
83            matrix: Affine::IDENTITY,
84        };
85        assert!(s.contains(Point::new(5.0, 150.0)));
86        // Reading the array as `[xmin ymin xmax ymax]` would put this inside.
87        assert!(!s.contains(Point::new(5.0, 50.0)));
88        // The bounds are inclusive.
89        assert!(s.contains(Point::new(0.0, 100.0)));
90        assert!(s.contains(Point::new(10.0, 200.0)));
91        assert!(!s.contains(Point::new(10.001, 200.0)));
92    }
93}