Skip to main content

hayro_interpret/
gradient.rs

1//! SVG-like gradient conversion for encoded shadings.
2//!
3//! PDF shadings are very expensive to evaluate. Because of this, at least for
4//! radial-axial shadings it's useful if we can instead approximate them by
5//! SVG-like sRGB gradients.
6
7use crate::color::AlphaColor;
8use crate::encode::{EncodedRadialAxialShading, EncodedShadingPattern, RadialAxialParams};
9use crate::function::StitchingBounds;
10use kurbo::{Affine, Circle, Point, Rect, Shape};
11use smallvec::smallvec;
12
13const MAX_GRADIENT_SUBDIVISION_DEPTH: u8 = 10;
14const RADIAL_EPSILON: f64 = 1.0e-6;
15
16/// An SVG-like gradient.
17pub struct SvgGradient {
18    /// The kind of the gradient.
19    pub kind: SvgGradientKind,
20    /// The transform from encoded shading coordinates into the renderer's coordinate space.
21    pub transform: Affine,
22    /// The stops of the gradient in the sRGB color space.
23    pub stops: Vec<SvgGradientStop>,
24}
25
26/// The kind of the SVG gradient.
27pub enum SvgGradientKind {
28    /// Linear gradient.
29    Linear {
30        /// The normalized start point of the gradient line.
31        start: Point,
32        /// The normalized end point of the gradient line.
33        end: Point,
34    },
35    /// Radial gradient.
36    Radial {
37        /// The center point of the start circle.
38        start_center: Point,
39        /// The radius of the start circle.
40        start_radius: f32,
41        /// The center point of the end circle.
42        end_center: Point,
43        /// The radius of the end circle.
44        end_radius: f32,
45    },
46}
47
48/// A color stop for an SVG-like gradient.
49pub struct SvgGradientStop {
50    /// Normalized offset.
51    pub offset: f32,
52    /// RGBA color components in sRGB.
53    pub color: [f32; 4],
54}
55
56impl EncodedRadialAxialShading {
57    /// Convert an axial/radial PDF shading into an SVG-like gradient approximation.
58    pub fn as_svg_gradient(
59        &self,
60        pattern: &EncodedShadingPattern,
61        path_bbox: Rect,
62        tolerance: f32,
63    ) -> Option<SvgGradient> {
64        // A couple of cases cannot be losslessly represented by an SVG gradient.
65        match self.params {
66            RadialAxialParams::Axial => {
67                if !self.axial_extend_covers_bbox(pattern, path_bbox) {
68                    return None;
69                }
70            }
71            RadialAxialParams::Radial { .. } => {
72                if !self.radial_extend_covers_bbox(pattern, path_bbox)
73                    || pattern.background_color.components()[3] > 0.0
74                {
75                    return None;
76                }
77            }
78        }
79
80        Some(SvgGradient {
81            kind: self.native_gradient_kind(),
82            transform: pattern.base_transform.inverse(),
83            stops: approximate_gradient_stops(
84                |t| self.sample_t(pattern, t),
85                &self.normalized_stitching_bounds(),
86                tolerance,
87            ),
88        })
89    }
90
91    fn native_gradient_kind(&self) -> SvgGradientKind {
92        match &self.params {
93            RadialAxialParams::Axial => SvgGradientKind::Linear {
94                start: Point::ZERO,
95                end: Point::new(1.0, 0.0),
96            },
97            RadialAxialParams::Radial { p1, r } => SvgGradientKind::Radial {
98                start_center: Point::ZERO,
99                start_radius: r.x as f32,
100                end_center: *p1,
101                end_radius: r.y as f32,
102            },
103        }
104    }
105
106    fn sample_t(&self, pattern: &EncodedShadingPattern, t: f32) -> [f32; 4] {
107        let (t0, t1) = (self.domain[0], self.domain[1]);
108        let t = t0 + (t1 - t0) * t;
109        let Some(out) = self.function.eval(&smallvec![t]) else {
110            return [0.0, 0.0, 0.0, 0.0];
111        };
112        let mut components = pattern.color_space.to_rgba(&out, 1.0).components();
113        components[3] *= pattern.opacity;
114
115        if let Some(tf) = &pattern.transfer_function {
116            return tf.apply(&AlphaColor::new(components)).components();
117        }
118
119        components
120    }
121
122    fn axial_extend_covers_bbox(&self, pattern: &EncodedShadingPattern, path_bbox: Rect) -> bool {
123        let path_bbox = pattern.base_transform.transform_rect_bbox(path_bbox);
124
125        let min_x = path_bbox.min_x();
126        let max_x = path_bbox.max_x();
127
128        (self.extend[0] || min_x >= 0.0) && (self.extend[1] || max_x <= 1.0)
129    }
130
131    fn radial_extend_covers_bbox(&self, pattern: &EncodedShadingPattern, path_bbox: Rect) -> bool {
132        let RadialAxialParams::Radial { p1, r } = self.params else {
133            return false;
134        };
135
136        let path_bbox = pattern.base_transform.transform_rect_bbox(path_bbox);
137
138        if p1.x.abs() <= RADIAL_EPSILON && p1.y.abs() <= RADIAL_EPSILON {
139            return self.same_center_radial_extend_covers_bbox(path_bbox, r.x, r.y);
140        }
141
142        let start_extend_ok = self.extend[0] || r.x.abs() <= RADIAL_EPSILON;
143        let end_extend_ok = self.extend[1] || circle_covers_bbox(Circle::new(p1, r.y), path_bbox);
144
145        start_extend_ok && end_extend_ok
146    }
147
148    fn same_center_radial_extend_covers_bbox(&self, path_bbox: Rect, r0: f64, r1: f64) -> bool {
149        let mut inner_circle = Circle::new(Point::ZERO, r0);
150        let mut outer_circle = Circle::new(Point::ZERO, r1);
151        let mut inner_extend = self.extend[0];
152        let mut outer_extend = self.extend[1];
153
154        if inner_circle.radius > outer_circle.radius {
155            std::mem::swap(&mut inner_circle, &mut outer_circle);
156            std::mem::swap(&mut inner_extend, &mut outer_extend);
157        }
158
159        let inner_extend_ok = inner_extend
160            || inner_circle.radius <= RADIAL_EPSILON
161            || circle_outside_bbox(inner_circle, path_bbox);
162        let outer_extend_ok = outer_extend || circle_covers_bbox(outer_circle, path_bbox);
163
164        inner_extend_ok && outer_extend_ok
165    }
166
167    fn normalized_stitching_bounds(&self) -> StitchingBounds {
168        let [t0, t1] = self.domain;
169        let dt = t1 - t0;
170        if dt.abs() <= f32::EPSILON {
171            return StitchingBounds::new();
172        }
173
174        let mut bounds = self
175            .function
176            .stitching_bounds()
177            .into_iter()
178            .map(|bound| (bound - t0) / dt)
179            .filter(|bound| *bound > 0.0 && *bound < 1.0)
180            .collect::<StitchingBounds>();
181
182        if dt < 0.0 {
183            bounds.reverse();
184        }
185
186        bounds
187    }
188}
189
190fn circle_covers_bbox(circle: Circle, bbox: Rect) -> bool {
191    if circle.radius < 0.0 {
192        return false;
193    }
194
195    let radius_squared = circle.radius * circle.radius;
196    [
197        Point::new(bbox.x0, bbox.y0),
198        Point::new(bbox.x1, bbox.y0),
199        Point::new(bbox.x0, bbox.y1),
200        Point::new(bbox.x1, bbox.y1),
201    ]
202    .into_iter()
203    .all(|point| {
204        let delta = point - circle.center;
205        delta.hypot2() <= radius_squared
206    })
207}
208
209fn circle_outside_bbox(circle: Circle, bbox: Rect) -> bool {
210    if circle.radius <= 0.0 {
211        return true;
212    }
213
214    let closest = Point::new(
215        circle.center.x.clamp(bbox.x0, bbox.x1),
216        circle.center.y.clamp(bbox.y0, bbox.y1),
217    );
218
219    !circle.contains(closest)
220}
221
222fn approximate_gradient_stops(
223    mut sample: impl FnMut(f32) -> [f32; 4],
224    breakpoints: &[f32],
225    tolerance: f32,
226) -> Vec<SvgGradientStop> {
227    let mut interval_bounds = Vec::with_capacity(breakpoints.len() + 2);
228    interval_bounds.push(0.0);
229    interval_bounds.extend(breakpoints.iter().copied());
230    interval_bounds.push(1.0);
231
232    let mut stops = Vec::new();
233    for interval in interval_bounds.windows(2) {
234        let start_offset = interval[0];
235        let end_offset = interval[1];
236
237        let start_color = sample(start_offset);
238        stops.push(SvgGradientStop {
239            offset: start_offset,
240            color: start_color,
241        });
242
243        let end_color = sample(end_offset);
244
245        approximate_gradient_interval(
246            &mut sample,
247            &mut stops,
248            GradientSample {
249                offset: start_offset,
250                color: start_color,
251            },
252            GradientSample {
253                offset: end_offset,
254                color: end_color,
255            },
256            tolerance.max(0.0),
257            0,
258        );
259    }
260
261    stops
262}
263
264#[derive(Clone, Copy)]
265struct GradientSample {
266    offset: f32,
267    color: [f32; 4],
268}
269
270fn approximate_gradient_interval(
271    sample: &mut impl FnMut(f32) -> [f32; 4],
272    stops: &mut Vec<SvgGradientStop>,
273    start: GradientSample,
274    end: GradientSample,
275    tolerance: f32,
276    depth: u8,
277) {
278    let offset_delta = end.offset - start.offset;
279    let quarter_offset = start.offset + offset_delta * 0.25;
280    let mid_offset = start.offset + offset_delta * 0.5;
281    let three_quarter_offset = start.offset + offset_delta * 0.75;
282
283    let quarter_color = sample(quarter_offset);
284    let mid_color = sample(mid_offset);
285    let three_quarter_color = sample(three_quarter_offset);
286
287    if color_error(quarter_color, lerp_color(start.color, end.color, 0.25)) <= tolerance
288        && color_error(mid_color, lerp_color(start.color, end.color, 0.5)) <= tolerance
289        && color_error(
290            three_quarter_color,
291            lerp_color(start.color, end.color, 0.75),
292        ) <= tolerance
293        || depth == MAX_GRADIENT_SUBDIVISION_DEPTH
294    {
295        stops.push(SvgGradientStop {
296            offset: end.offset,
297            color: end.color,
298        });
299
300        return;
301    }
302
303    let mid = GradientSample {
304        offset: mid_offset,
305        color: mid_color,
306    };
307
308    approximate_gradient_interval(sample, stops, start, mid, tolerance, depth + 1);
309    approximate_gradient_interval(sample, stops, mid, end, tolerance, depth + 1);
310}
311
312fn lerp_color(c0: [f32; 4], c1: [f32; 4], t: f32) -> [f32; 4] {
313    [
314        c0[0] + (c1[0] - c0[0]) * t,
315        c0[1] + (c1[1] - c0[1]) * t,
316        c0[2] + (c1[2] - c0[2]) * t,
317        c0[3] + (c1[3] - c0[3]) * t,
318    ]
319}
320
321fn color_error(c0: [f32; 4], c1: [f32; 4]) -> f32 {
322    c0.iter()
323        .zip(c1)
324        .map(|(a, b)| (a - b).abs())
325        .fold(0.0, f32::max)
326}