Skip to main content

pdf_interpret/
encode.rs

1//! Encoding shading patterns for easy sampling.
2
3use crate::color::{AlphaColor, ColorComponents, ColorSpace};
4use crate::interpret::state::ActiveTransferFunction;
5use crate::pattern::ShadingPattern;
6use crate::shading::{ShadingFunction, ShadingType, Triangle};
7use kurbo::{Affine, Point};
8use rustc_hash::FxHashMap;
9use smallvec::{ToSmallVec, smallvec};
10
11/// A shading pattern that was encoded so it can be sampled.
12#[derive(Debug)]
13pub struct EncodedShadingPattern {
14    /// The base transform of the shading pattern.
15    pub base_transform: Affine,
16    pub(crate) color_space: ColorSpace,
17    pub(crate) background_color: AlphaColor,
18    pub(crate) shading_type: EncodedShadingType,
19    pub(crate) opacity: f32,
20    pub(crate) transfer_function: Option<ActiveTransferFunction>,
21}
22
23impl EncodedShadingPattern {
24    /// Sample the shading at the given position.
25    #[inline]
26    pub fn sample(&self, pos: Point) -> [f32; 4] {
27        self.shading_type
28            .eval(pos, self.background_color, &self.color_space)
29            .map(|v| {
30                let mut components = v.components();
31                components[3] *= self.opacity;
32
33                if let Some(tf) = &self.transfer_function {
34                    return tf.apply(&AlphaColor::new(components)).components();
35                }
36
37                components
38            })
39            .unwrap_or([0.0, 0.0, 0.0, 0.0])
40    }
41}
42
43impl ShadingPattern {
44    /// Encode the shading pattern.
45    pub fn encode(&self) -> EncodedShadingPattern {
46        let base_transform;
47
48        let shading_type = match self.shading.shading_type.as_ref() {
49            ShadingType::FunctionBased {
50                domain,
51                matrix,
52                function,
53            } => {
54                base_transform = (self.matrix * *matrix).inverse();
55                encode_function_shading(domain, function)
56            }
57            ShadingType::RadialAxial {
58                coords,
59                domain,
60                function,
61                extend,
62                axial,
63            } => {
64                let (encoded, initial_transform) =
65                    encode_axial_shading(*coords, *domain, function, *extend, *axial);
66
67                base_transform = initial_transform * self.matrix.inverse();
68
69                encoded
70            }
71            ShadingType::TriangleMesh {
72                triangles,
73                function,
74            } => {
75                let full_transform = self.matrix;
76                let samples = sample_triangles(triangles, full_transform);
77
78                base_transform = Affine::IDENTITY;
79
80                EncodedShadingType::Sampled {
81                    samples,
82                    function: function.clone(),
83                }
84            }
85            ShadingType::CoonsPatchMesh { patches, function } => {
86                let mut triangles = vec![];
87                for patch in patches {
88                    patch.to_triangles(&mut triangles);
89                }
90
91                let full_transform = self.matrix;
92                let samples = sample_triangles(&triangles, full_transform);
93
94                base_transform = Affine::IDENTITY;
95
96                EncodedShadingType::Sampled {
97                    samples,
98                    function: function.clone(),
99                }
100            }
101            ShadingType::TensorProductPatchMesh { patches, function } => {
102                let mut triangles = vec![];
103                for patch in patches {
104                    patch.to_triangles(&mut triangles);
105                }
106
107                let full_transform = self.matrix;
108                let samples = sample_triangles(&triangles, full_transform);
109
110                base_transform = Affine::IDENTITY;
111
112                EncodedShadingType::Sampled {
113                    samples,
114                    function: function.clone(),
115                }
116            }
117            ShadingType::Dummy => {
118                base_transform = Affine::IDENTITY;
119
120                EncodedShadingType::Dummy
121            }
122        };
123
124        let color_space = self.shading.color_space.clone();
125
126        let background_color = self
127            .shading
128            .background
129            .as_ref()
130            .map(|b| color_space.to_rgba(b, 1.0, false))
131            .unwrap_or(AlphaColor::TRANSPARENT);
132
133        EncodedShadingPattern {
134            color_space,
135            background_color,
136            shading_type,
137            base_transform,
138            opacity: self.opacity,
139            transfer_function: self.transfer_function.clone(),
140        }
141    }
142}
143
144fn encode_axial_shading(
145    coords: [f32; 6],
146    domain: [f32; 2],
147    function: &ShadingFunction,
148    extend: [bool; 2],
149    is_axial: bool,
150) -> (EncodedShadingType, Affine) {
151    let initial_transform;
152
153    let params = if is_axial {
154        let [x_0, y_0, x_1, y_1, _, _] = coords;
155
156        initial_transform = ts_from_line_to_line(
157            Point::new(x_0 as f64, y_0 as f64),
158            Point::new(x_1 as f64, y_1 as f64),
159            Point::ZERO,
160            Point::new(1.0, 0.0),
161        );
162
163        RadialAxialParams::Axial
164    } else {
165        let [x_0, y_0, r0, x_1, y_1, r_1] = coords;
166
167        initial_transform = Affine::translate((-x_0 as f64, -y_0 as f64));
168        let new_x1 = x_1 - x_0;
169        let new_y1 = y_1 - y_0;
170
171        let p1 = Point::new(new_x1 as f64, new_y1 as f64);
172        let r = Point::new(r0 as f64, r_1 as f64);
173
174        RadialAxialParams::Radial { p1, r }
175    };
176
177    (
178        EncodedShadingType::RadialAxial {
179            function: function.clone(),
180            params,
181            domain,
182            extend,
183        },
184        initial_transform,
185    )
186}
187
188fn sample_triangles(
189    triangles: &[Triangle],
190    transform: Affine,
191) -> FxHashMap<(i32, i32), ColorComponents> {
192    let mut map = FxHashMap::default();
193
194    for t in triangles {
195        let t = {
196            let p0 = transform * t.p0.point;
197            let p1 = transform * t.p1.point;
198            let p2 = transform * t.p2.point;
199
200            let mut v0 = t.p0.clone();
201            v0.point = p0;
202            let mut v1 = t.p1.clone();
203            v1.point = p1;
204            let mut v2 = t.p2.clone();
205            v2.point = p2;
206
207            Triangle::new(v0, v1, v2)
208        };
209
210        let bbox = t.bounding_box();
211
212        // Use i32 keys so that negative coordinates (e.g. patterns that start
213        // left/above the page origin) and large coordinate values (> 65535)
214        // are represented correctly without wrapping or saturation.
215        for y in (bbox.y0.floor() as i32)..(bbox.y1.ceil() as i32) {
216            for x in (bbox.x0.floor() as i32)..(bbox.x1.ceil() as i32) {
217                let point = Point::new(x as f64, y as f64);
218                if t.contains_point(point) {
219                    map.insert((x, y), t.interpolate(point));
220                }
221            }
222        }
223    }
224
225    map
226}
227
228fn encode_function_shading(domain: &[f32; 4], function: &ShadingFunction) -> EncodedShadingType {
229    let domain = kurbo::Rect::new(
230        domain[0] as f64,
231        domain[2] as f64,
232        domain[1] as f64,
233        domain[3] as f64,
234    );
235
236    EncodedShadingType::FunctionBased {
237        domain,
238        function: function.clone(),
239    }
240}
241
242#[derive(Debug)]
243pub(crate) enum RadialAxialParams {
244    Axial,
245    Radial { p1: Point, r: Point },
246}
247
248#[derive(Debug)]
249pub(crate) enum EncodedShadingType {
250    FunctionBased {
251        domain: kurbo::Rect,
252        function: ShadingFunction,
253    },
254    RadialAxial {
255        function: ShadingFunction,
256        params: RadialAxialParams,
257        domain: [f32; 2],
258        extend: [bool; 2],
259    },
260    Sampled {
261        samples: FxHashMap<(i32, i32), ColorComponents>,
262        function: Option<ShadingFunction>,
263    },
264    Dummy,
265}
266
267impl EncodedShadingType {
268    pub(crate) fn eval(
269        &self,
270        pos: Point,
271        bg_color: AlphaColor,
272        color_space: &ColorSpace,
273    ) -> Option<AlphaColor> {
274        match self {
275            Self::FunctionBased { domain, function } => {
276                if !domain.contains(pos) {
277                    Some(bg_color)
278                } else {
279                    let out = function.eval(&smallvec![pos.x as f32, pos.y as f32])?;
280                    // TODO: Clamp out-of-range values.
281                    Some(color_space.to_rgba(&out, 1.0, false))
282                }
283            }
284            Self::RadialAxial {
285                function,
286                params,
287                domain,
288                extend,
289            } => {
290                let (t0, t1) = (domain[0], domain[1]);
291
292                let mut t = match params {
293                    RadialAxialParams::Axial => pos.x as f32,
294                    RadialAxialParams::Radial { p1, r } => {
295                        radial_pos(&pos, p1, *r, extend[0], extend[1]).unwrap_or(f32::MIN)
296                    }
297                };
298
299                if t == f32::MIN {
300                    return Some(bg_color);
301                }
302
303                if t < 0.0 {
304                    if extend[0] {
305                        t = 0.0;
306                    } else {
307                        return Some(bg_color);
308                    }
309                } else if t > 1.0 {
310                    if extend[1] {
311                        t = 1.0;
312                    } else {
313                        return Some(bg_color);
314                    }
315                }
316
317                let t = t0 + (t1 - t0) * t;
318
319                let val = function.eval(&smallvec![t])?;
320
321                Some(color_space.to_rgba(&val, 1.0, false))
322            }
323            Self::Sampled { samples, function } => {
324                // Use i32 keys (matching sample_triangles) and round rather
325                // than truncate so the 0.5-offset centre point hits the
326                // correct bucket and we avoid systematic off-by-one gaps
327                // along triangle edges.
328                let sample_point = (pos.x.round() as i32, pos.y.round() as i32);
329
330                if let Some(color) = samples.get(&sample_point) {
331                    if let Some(function) = function {
332                        let val = function.eval(&color.to_smallvec())?;
333                        Some(color_space.to_rgba(&val, 1.0, false))
334                    } else {
335                        Some(color_space.to_rgba(color, 1.0, false))
336                    }
337                } else {
338                    Some(bg_color)
339                }
340            }
341            Self::Dummy => Some(AlphaColor::TRANSPARENT),
342        }
343    }
344}
345
346fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine {
347    let unit_to_line1 = unit_to_line(src1, src2);
348    let line1_to_unit = unit_to_line1.inverse();
349    let unit_to_line2 = unit_to_line(dst1, dst2);
350
351    unit_to_line2 * line1_to_unit
352}
353
354fn unit_to_line(p0: Point, p1: Point) -> Affine {
355    Affine::new([
356        p1.y - p0.y,
357        p0.x - p1.x,
358        p1.x - p0.x,
359        p1.y - p0.y,
360        p0.x,
361        p0.y,
362    ])
363}
364
365fn radial_pos(
366    pos: &Point,
367    p1: &Point,
368    r: Point,
369    min_extend: bool,
370    max_extend: bool,
371) -> Option<f32> {
372    let r0 = r.x as f32;
373    let dx = p1.x as f32;
374    let dy = p1.y as f32;
375    let dr = r.y as f32 - r0;
376
377    let px = pos.x as f32;
378    let py = pos.y as f32;
379
380    let a = dx * dx + dy * dy - dr * dr;
381    let b = -2.0 * (px * dx + py * dy + r0 * dr);
382    let c = px * px + py * py - r0 * r0;
383
384    let discriminant = b * b - 4.0 * a * c;
385
386    // No solution available.
387    if discriminant < 0.0 {
388        return None;
389    }
390
391    if a.abs() < 1e-6 {
392        if b.abs() < 1e-6 {
393            return None;
394        }
395
396        let t = -c / b;
397
398        if (!min_extend && t < 0.0) || (!max_extend && t > 1.0) {
399            return None;
400        }
401
402        let r_t = r0 + dr * t;
403        if r_t < 0.0 {
404            return None;
405        }
406
407        return Some(t);
408    }
409
410    let sqrt_d = discriminant.sqrt();
411    let t1 = (-b - sqrt_d) / (2.0 * a);
412    let t2 = (-b + sqrt_d) / (2.0 * a);
413
414    let max = t1.max(t2);
415    let mut take_max = Some(max);
416    let min = t1.min(t2);
417    let mut take_min = Some(min);
418
419    if (!min_extend && min < 0.0) || r0 + dr * min < 0.0 {
420        take_min = None;
421    }
422
423    if (!max_extend && max > 1.0) || r0 + dr * max < 0.0 {
424        take_max = None;
425    }
426
427    match (take_min, take_max) {
428        (Some(_), Some(max)) => Some(max),
429        (Some(min), None) => Some(min),
430        (None, Some(max)) => Some(max),
431        (None, None) => None,
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::shading::{Triangle, TriangleVertex};
439    use kurbo::{Affine, Point};
440    use rustc_hash::FxHashMap;
441    use smallvec::smallvec;
442
443    fn make_vertex(x: f64, y: f64, color: f32) -> TriangleVertex {
444        TriangleVertex::new(0, Point::new(x, y), smallvec![color])
445    }
446
447    /// sample_triangles must use i32 keys so that negative coordinates (e.g.
448    /// patterns starting above/left of the page origin) are stored and looked
449    /// up consistently without wrapping at 0.
450    #[test]
451    fn sample_triangles_negative_coords() {
452        let v0 = make_vertex(-2.0, -2.0, 0.0);
453        let v1 = make_vertex(2.0, -2.0, 1.0);
454        let v2 = make_vertex(0.0, 2.0, 0.5);
455        let tri = Triangle::new(v0, v1, v2);
456
457        let map = sample_triangles(&[tri], Affine::IDENTITY);
458
459        // The map should contain entries for negative coordinate pixels.
460        assert!(
461            map.keys().any(|(x, _)| *x < 0),
462            "expected negative x keys in sample map"
463        );
464        assert!(
465            map.keys().any(|(_, y)| *y < 0),
466            "expected negative y keys in sample map"
467        );
468    }
469
470    /// The Sampled::eval lookup must match the i32 keys written by
471    /// sample_triangles, and rounding must agree so samples are not missed.
472    #[test]
473    fn sampled_eval_roundtrip() {
474        use crate::color::ColorSpace;
475
476        let mut samples: FxHashMap<(i32, i32), ColorComponents> = FxHashMap::default();
477        samples.insert((10, 20), smallvec![0.5]);
478
479        let stype = EncodedShadingType::Sampled {
480            samples,
481            function: None,
482        };
483
484        let cs = ColorSpace::device_gray();
485        let bg = AlphaColor::TRANSPARENT;
486
487        // Exact integer lookup should find the sample.
488        let hit = stype.eval(Point::new(10.0, 20.0), bg, &cs);
489        assert!(hit.is_some(), "exact integer lookup should find sample");
490        let color = hit.unwrap();
491        assert!(color.components()[3] > 0.0, "sample should be opaque");
492
493        // A point 0.4 away rounds to the same bucket.
494        let hit2 = stype.eval(Point::new(10.4, 20.4), bg, &cs);
495        assert!(
496            hit2.is_some(),
497            "nearby point (0.4 offset) should hit same bucket"
498        );
499
500        // A point 0.6 away rounds to (11, 21) which is not in the map → bg.
501        let miss = stype.eval(Point::new(10.6, 20.6), bg, &cs);
502        assert_eq!(
503            miss.map(|c| c.components()[3]),
504            Some(bg.components()[3]),
505            "point rounding to (11,21) should return bg"
506        );
507    }
508}