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}