1
//! Utilities for dealing with Cairo paths.
2
//!
3
//! Librsvg uses Cairo to render Bézier paths, and also depends on Cairo to
4
//! compute the extents of those paths.  This module holds a number of utilities
5
//! to convert between librsvg paths and Cairo paths.
6

            
7
use std::f64::consts::PI;
8
use std::rc::Rc;
9

            
10
use crate::drawing_ctx::Viewport;
11
use crate::error::InternalRenderingError;
12
use crate::float_eq_cairo::{CAIRO_FIXED_MAX_DOUBLE, CAIRO_FIXED_MIN_DOUBLE};
13
use crate::layout::{self, Stroke};
14
use crate::path_builder::{
15
    arc_segment, ArcParameterization, CubicBezierCurve, EllipticalArc, Path, PathCommand,
16
};
17
use crate::properties::StrokeLinecap;
18
use crate::rect::Rect;
19
use crate::transform::Transform;
20

            
21
use cairo::PathSegment;
22

            
23
/// A path that has been validated for being suitable for Cairo.
24
///
25
/// As of 2024/Sep/25, Cairo converts path coordinates to fixed point, but it has several problems:
26
///
27
/// * For coordinates that are outside of the representable range in
28
///   fixed point, Cairo just clamps them.  It is not able to return
29
///   this condition as an error to the caller.
30
///
31
/// * Then, it has multiple cases of possible arithmetic overflow
32
///   while processing the paths for rendering.  Fixing this is an
33
///   ongoing project.
34
///
35
/// While Cairo gets better in these respects, librsvg will try to do
36
/// some mitigations, mainly about catching problematic coordinates
37
/// early and not passing them on to Cairo.
38
pub enum ValidatedPath {
39
    /// Path that has been checked for being suitable for Cairo.
40
    ///
41
    /// Note that this also keeps a reference to the original [SvgPath], in addition to
42
    /// the lowered [CairoPath].  This is because the markers code still needs the former.
43
    Validated(layout::Path),
44

            
45
    /// Reason why the path was determined to be not suitable for Cairo.  This
46
    /// is just used for logging purposes.
47
    Invalid(String),
48
}
49

            
50
/// Sees if any of the coordinates in the segment is not representable in Cairo's fixed-point numbers.
51
///
52
/// See the documentation for [`CairoPath::has_unsuitable_coordinates`].
53
108402387
fn segment_has_unsuitable_coordinates(segment: &PathSegment, transform: &Transform) -> bool {
54
108402387
    match *segment {
55
18043449
        PathSegment::MoveTo((x, y)) => coordinates_are_unsuitable(x, y, transform),
56
72131723
        PathSegment::LineTo((x, y)) => coordinates_are_unsuitable(x, y, transform),
57
194123
        PathSegment::CurveTo((x1, y1), (x2, y2), (x3, y3)) => {
58
194123
            coordinates_are_unsuitable(x1, y1, transform)
59
194123
                || coordinates_are_unsuitable(x2, y2, transform)
60
194123
                || coordinates_are_unsuitable(x3, y3, transform)
61
        }
62
18033092
        PathSegment::ClosePath => false,
63
    }
64
108402387
}
65

            
66
90757541
fn coordinates_are_unsuitable(x: f64, y: f64, transform: &Transform) -> bool {
67
90757541
    let fixed_point_range = CAIRO_FIXED_MIN_DOUBLE..=CAIRO_FIXED_MAX_DOUBLE;
68
90757541

            
69
90757541
    let (x, y) = transform.transform_point(x, y);
70
90757541

            
71
90757541
    !(fixed_point_range.contains(&x) && fixed_point_range.contains(&y))
72
90757541
}
73

            
74
/// Our own version of a Cairo path, lower-level than [layout::Path].
75
///
76
/// Cairo paths can only represent move_to/line_to/curve_to/close_path, unlike
77
/// librsvg's, which also have elliptical arcs.  Moreover, not all candidate paths
78
/// can be represented by Cairo, due to limitations on its fixed-point coordinates.
79
///
80
/// This struct represents a path that we have done our best to ensure that Cairo
81
/// can represent.
82
///
83
/// This struct is not just a [cairo::Path] since that type is read-only; it cannot
84
/// be constructed from raw data and must be first obtained from a [cairo::Context].
85
/// However, we can reuse [cairo::PathSegment] here which is just a path command.
86
pub struct CairoPath(Vec<PathSegment>);
87

            
88
impl CairoPath {
89
36100591
    pub fn to_cairo_context(&self, cr: &cairo::Context) -> Result<(), InternalRenderingError> {
90
262589837
        for segment in &self.0 {
91
226489246
            match *segment {
92
36846740
                PathSegment::MoveTo((x, y)) => cr.move_to(x, y),
93
147212730
                PathSegment::LineTo((x, y)) => cr.line_to(x, y),
94
5641442
                PathSegment::CurveTo((x1, y1), (x2, y2), (x3, y3)) => {
95
5641442
                    cr.curve_to(x1, y1, x2, y2, x3, y3)
96
                }
97
36788334
                PathSegment::ClosePath => cr.close_path(),
98
            }
99
        }
100

            
101
        // We check the cr's status right after feeding it a new path for a few reasons:
102
        //
103
        // * Any of the individual path commands may cause the cr to enter an error state, for
104
        //   example, if they come with coordinates outside of Cairo's supported range.
105
        //
106
        // * The *next* call to the cr will probably be something that actually checks the status
107
        //   (i.e. in cairo-rs), and we don't want to panic there.
108

            
109
36100591
        cr.status().map_err(|e| e.into())
110
36100591
    }
111

            
112
    /// Converts a `cairo::Path` to a librsvg `CairoPath`.
113
19894
    pub fn from_cairo(cairo_path: cairo::Path) -> Self {
114
19894
        // Cairo has the habit of appending a MoveTo to some paths, but we don't want a
115
19894
        // path for empty text to generate that lone point.  So, strip out paths composed
116
19894
        // only of MoveTo.
117
19894

            
118
19894
        if cairo_path_is_only_move_tos(&cairo_path) {
119
1121
            Self(Vec::new())
120
        } else {
121
18773
            Self(cairo_path.iter().collect())
122
        }
123
19894
    }
124

            
125
19893
    pub fn is_empty(&self) -> bool {
126
19893
        self.0.is_empty()
127
19893
    }
128

            
129
    /// Sees if any of the coordinates in the path is not representable in Cairo's fixed-point numbers.
130
    ///
131
    /// See https://gitlab.gnome.org/GNOME/librsvg/-/issues/1088 and
132
    /// for the root cause
133
    /// https://gitlab.freedesktop.org/cairo/cairo/-/issues/852.
134
    ///
135
    /// This function does a poor job, but a hopefully serviceable one, of seeing if a path's coordinates
136
    /// are prone to causing trouble when passed to Cairo.  The caller of this function takes note of
137
    /// that situation and in the end avoids rendering the path altogether.
138
    ///
139
    /// Cairo has trouble when given path coordinates that are outside of the range it can represent
140
    /// in cairo_fixed_t: 24 bits integer part, and 8 bits fractional part.  Coordinates outside
141
    /// of ±8 million get clamped.  These, or valid coordinates that are close to the limits,
142
    /// subsequently cause integer overflow while Cairo does arithmetic on the path's points.
143
    /// Fixing this in Cairo is a long-term project.
144
18029199
    pub fn has_unsuitable_coordinates(&self, transform: &Transform) -> bool {
145
18029199
        self.0
146
18029199
            .iter()
147
108402387
            .any(|segment| segment_has_unsuitable_coordinates(segment, transform))
148
18029199
    }
149
}
150

            
151
18029121
fn compute_path_extents(path: &Path) -> Result<Option<Rect>, InternalRenderingError> {
152
18029121
    if path.is_empty() {
153
342
        return Ok(None);
154
18028779
    }
155

            
156
18028779
    let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None)?;
157
18028779
    let cr = cairo::Context::new(&surface)?;
158

            
159
18028779
    path.to_cairo(&cr, false)?;
160
18028779
    let (x0, y0, x1, y1) = cr.path_extents()?;
161

            
162
18028779
    Ok(Some(Rect::new(x0, y0, x1, y1)))
163
18029121
}
164

            
165
impl Path {
166
36057978
    pub fn to_cairo_path(
167
36057978
        &self,
168
36057978
        is_square_linecap: bool,
169
36057978
    ) -> Result<CairoPath, InternalRenderingError> {
170
36057978
        let mut segments = Vec::new();
171

            
172
36086782
        for subpath in self.iter_subpath() {
173
            // If a subpath is empty and the linecap is a square, then draw a square centered on
174
            // the origin of the subpath. See #165.
175
36086782
            if is_square_linecap {
176
646
                let (x, y) = subpath.origin();
177
646
                if subpath.is_zero_length() {
178
19
                    let stroke_size = 0.002;
179
19

            
180
19
                    segments.push(PathSegment::MoveTo((x - stroke_size / 2., y)));
181
19
                    segments.push(PathSegment::LineTo((x + stroke_size / 2., y)));
182
627
                }
183
36086136
            }
184

            
185
216778704
            for cmd in subpath.iter_commands() {
186
216778704
                cmd.to_path_segments(&mut segments);
187
216778704
            }
188
        }
189

            
190
36057978
        Ok(CairoPath(segments))
191
36057978
    }
192

            
193
18028779
    pub fn to_cairo(
194
18028779
        &self,
195
18028779
        cr: &cairo::Context,
196
18028779
        is_square_linecap: bool,
197
18028779
    ) -> Result<(), InternalRenderingError> {
198
18028779
        let cairo_path = self.to_cairo_path(is_square_linecap)?;
199
18028779
        cairo_path.to_cairo_context(cr)
200
18028779
    }
201
}
202

            
203
19894
fn cairo_path_is_only_move_tos(path: &cairo::Path) -> bool {
204
19894
    path.iter()
205
38667
        .all(|seg| matches!(seg, cairo::PathSegment::MoveTo((_, _))))
206
19894
}
207

            
208
impl PathCommand {
209
216778704
    fn to_path_segments(&self, segments: &mut Vec<PathSegment>) {
210
216778704
        match *self {
211
36086782
            PathCommand::MoveTo(x, y) => segments.push(PathSegment::MoveTo((x, y))),
212
144263655
            PathCommand::LineTo(x, y) => segments.push(PathSegment::LineTo((x, y))),
213
308560
            PathCommand::CurveTo(ref curve) => curve.to_path_segments(segments),
214
53466
            PathCommand::Arc(ref arc) => arc.to_path_segments(segments),
215
36066241
            PathCommand::ClosePath => segments.push(PathSegment::ClosePath),
216
        }
217
216778704
    }
218
}
219

            
220
impl EllipticalArc {
221
53466
    fn to_path_segments(&self, segments: &mut Vec<PathSegment>) {
222
53466
        match self.center_parameterization() {
223
            ArcParameterization::CenterParameters {
224
53466
                center,
225
53466
                radii,
226
53466
                theta1,
227
53466
                delta_theta,
228
53466
            } => {
229
53466
                let n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as u32;
230
53466
                let d_theta = delta_theta / f64::from(n_segs);
231
53466

            
232
53466
                let mut theta = theta1;
233
79686
                for _ in 0..n_segs {
234
79686
                    arc_segment(center, radii, self.x_axis_rotation, theta, theta + d_theta)
235
79686
                        .to_path_segments(segments);
236
79686
                    theta += d_theta;
237
79686
                }
238
            }
239
            ArcParameterization::LineTo => {
240
                let (x2, y2) = self.to;
241
                segments.push(PathSegment::LineTo((x2, y2)));
242
            }
243
            ArcParameterization::Omit => {}
244
        }
245
53466
    }
246
}
247

            
248
impl CubicBezierCurve {
249
388246
    fn to_path_segments(&self, segments: &mut Vec<PathSegment>) {
250
388246
        let Self { pt1, pt2, to } = *self;
251
388246
        segments.push(PathSegment::CurveTo(
252
388246
            (pt1.0, pt1.1),
253
388246
            (pt2.0, pt2.1),
254
388246
            (to.0, to.1),
255
388246
        ));
256
388246
    }
257
}
258

            
259
18029197
pub fn validate_path(
260
18029197
    path: &Rc<Path>,
261
18029197
    stroke: &Stroke,
262
18029197
    viewport: &Viewport,
263
18029197
) -> Result<ValidatedPath, InternalRenderingError> {
264
18029197
    let is_square_linecap = stroke.line_cap == StrokeLinecap::Square;
265
18029197
    let cairo_path = path.to_cairo_path(is_square_linecap)?;
266

            
267
18029197
    if cairo_path.has_unsuitable_coordinates(&viewport.transform) {
268
76
        return Ok(ValidatedPath::Invalid(String::from(
269
76
            "path has coordinates that are unsuitable for Cairo",
270
76
        )));
271
18029121
    }
272

            
273
18029121
    let extents = compute_path_extents(path)?;
274

            
275
18029121
    Ok(ValidatedPath::Validated(layout::Path {
276
18029121
        cairo_path,
277
18029121
        path: Rc::clone(path),
278
18029121
        extents,
279
18029121
    }))
280
18029197
}
281

            
282
#[cfg(test)]
283
mod tests {
284
    use super::*;
285
    use crate::path_builder::PathBuilder;
286

            
287
    #[test]
288
1
    fn rsvg_path_from_cairo_path() {
289
1
        let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 10, 10).unwrap();
290
1
        let cr = cairo::Context::new(&surface).unwrap();
291
1

            
292
1
        cr.move_to(1.0, 2.0);
293
1
        cr.line_to(3.0, 4.0);
294
1
        cr.curve_to(5.0, 6.0, 7.0, 8.0, 9.0, 10.0);
295
1
        cr.close_path();
296
1

            
297
1
        let cr_path = cr.copy_path().unwrap();
298
1
        let cairo_path = CairoPath::from_cairo(cr_path);
299
1

            
300
1
        assert_eq!(
301
1
            cairo_path.0,
302
1
            vec![
303
1
                PathSegment::MoveTo((1.0, 2.0)),
304
1
                PathSegment::LineTo((3.0, 4.0)),
305
1
                PathSegment::CurveTo((5.0, 6.0), (7.0, 8.0), (9.0, 10.0)),
306
1
                PathSegment::ClosePath,
307
1
                PathSegment::MoveTo((1.0, 2.0)), // cairo inserts a MoveTo after ClosePath
308
1
            ],
309
1
        );
310
1
    }
311

            
312
    #[test]
313
1
    fn detects_suitable_coordinates() {
314
1
        let mut builder = PathBuilder::default();
315
1
        builder.move_to(900000.0, 33.0);
316
1
        builder.line_to(-900000.0, 3.0);
317
1

            
318
1
        let path = builder.into_path();
319
1
        let cairo_path = path.to_cairo_path(false).map_err(|_| ()).unwrap();
320
1
        assert!(!cairo_path.has_unsuitable_coordinates(&Transform::identity()));
321
1
    }
322

            
323
    #[test]
324
1
    fn detects_unsuitable_coordinates() {
325
1
        let mut builder = PathBuilder::default();
326
1
        builder.move_to(9000000.0, 33.0);
327
1
        builder.line_to(-9000000.0, 3.0);
328
1

            
329
1
        let path = builder.into_path();
330
1
        let cairo_path = path.to_cairo_path(false).map_err(|_| ()).unwrap();
331
1
        assert!(cairo_path.has_unsuitable_coordinates(&Transform::identity()));
332
1
    }
333
}