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::length::NormalizeValues;
15
use crate::paint_server::PaintSource;
16
use crate::path_builder::{
17
    arc_segment, ArcParameterization, CubicBezierCurve, EllipticalArc, Path, PathCommand,
18
};
19
use crate::properties::StrokeLinecap;
20
use crate::rect::Rect;
21
use crate::transform::Transform;
22

            
23
use cairo::PathSegment;
24

            
25
/// Sees if any of the coordinates in the segment is not representable in Cairo's fixed-point numbers.
26
///
27
/// See the documentation for [`CairoPath::has_unsuitable_coordinates`].
28
5695678
fn segment_has_unsuitable_coordinates(segment: &PathSegment, transform: &Transform) -> bool {
29
5695678
    match *segment {
30
948873
        PathSegment::MoveTo((x, y)) => coordinates_are_unsuitable(x, y, transform),
31
3788924
        PathSegment::LineTo((x, y)) => coordinates_are_unsuitable(x, y, transform),
32
10312
        PathSegment::CurveTo((x1, y1), (x2, y2), (x3, y3)) => {
33
10313
            coordinates_are_unsuitable(x1, y1, transform)
34
10313
                || coordinates_are_unsuitable(x2, y2, transform)
35
10312
                || coordinates_are_unsuitable(x3, y3, transform)
36
        }
37
947569
        PathSegment::ClosePath => false,
38
    }
39
5695678
}
40

            
41
4767971
fn coordinates_are_unsuitable(x: f64, y: f64, transform: &Transform) -> bool {
42
4767971
    let fixed_point_range = CAIRO_FIXED_MIN_DOUBLE..=CAIRO_FIXED_MAX_DOUBLE;
43

            
44
4767971
    let (x, y) = transform.transform_point(x, y);
45

            
46
4767971
    !(fixed_point_range.contains(&x) && fixed_point_range.contains(&y))
47
4767971
}
48

            
49
/// Our own version of a Cairo path, lower-level than [layout::Path].
50
///
51
/// Cairo paths can only represent move_to/line_to/curve_to/close_path, unlike
52
/// librsvg's, which also have elliptical arcs.  Moreover, not all candidate paths
53
/// can be represented by Cairo, due to limitations on its fixed-point coordinates.
54
///
55
/// This struct represents a path that we have done our best to ensure that Cairo
56
/// can represent.
57
///
58
/// This struct is not just a [cairo::Path] since that type is read-only; it cannot
59
/// be constructed from raw data and must be first obtained from a [cairo::Context].
60
/// However, we can reuse [cairo::PathSegment] here which is just a path command.
61
pub struct CairoPath(Vec<PathSegment>);
62

            
63
impl CairoPath {
64
1898733
    pub fn to_cairo_context(&self, cr: &cairo::Context) -> Result<(), InternalRenderingError> {
65
13803893
        for segment in &self.0 {
66
11905160
            match *segment {
67
1936260
                PathSegment::MoveTo((x, y)) => cr.move_to(x, y),
68
7742295
                PathSegment::LineTo((x, y)) => cr.line_to(x, y),
69
292838
                PathSegment::CurveTo((x1, y1), (x2, y2), (x3, y3)) => {
70
292838
                    cr.curve_to(x1, y1, x2, y2, x3, y3)
71
                }
72
1933767
                PathSegment::ClosePath => cr.close_path(),
73
            }
74
        }
75

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

            
84
1898733
        cr.status().map_err(|e| e.into())
85
1898733
    }
86

            
87
    /// Converts a `cairo::Path` to a librsvg `CairoPath`.
88
1027
    pub fn from_cairo(cairo_path: cairo::Path) -> Self {
89
        // Cairo has the habit of appending a MoveTo to some paths, but we don't want a
90
        // path for empty text to generate that lone point.  So, strip out paths composed
91
        // only of MoveTo.
92

            
93
2054
        if cairo_path_is_only_move_tos(&cairo_path) {
94
59
            Self(Vec::new())
95
        } else {
96
968
            Self(cairo_path.iter().collect())
97
        }
98
1027
    }
99

            
100
1026
    pub fn is_empty(&self) -> bool {
101
1026
        self.0.is_empty()
102
1026
    }
103

            
104
    /// Sees if any of the coordinates in the path is not representable in Cairo's fixed-point numbers.
105
    ///
106
    /// See https://gitlab.gnome.org/GNOME/librsvg/-/issues/1088 and
107
    /// for the root cause
108
    /// https://gitlab.freedesktop.org/cairo/cairo/-/issues/852.
109
    ///
110
    /// This function does a poor job, but a hopefully serviceable one, of seeing if a path's coordinates
111
    /// are prone to causing trouble when passed to Cairo.  The caller of this function takes note of
112
    /// that situation and in the end avoids rendering the path altogether.
113
    ///
114
    /// Cairo has trouble when given path coordinates that are outside of the range it can represent
115
    /// in cairo_fixed_t: 24 bits integer part, and 8 bits fractional part.  Coordinates outside
116
    /// of ±8 million get clamped.  These, or valid coordinates that are close to the limits,
117
    /// subsequently cause integer overflow while Cairo does arithmetic on the path's points.
118
    /// Fixing this in Cairo is a long-term project.
119
948656
    pub fn has_unsuitable_coordinates(&self, transform: &Transform) -> bool {
120
948656
        self.0
121
            .iter()
122
5693528
            .any(|segment| segment_has_unsuitable_coordinates(segment, transform))
123
948656
    }
124
}
125

            
126
949595
fn compute_path_extents(path: &Path) -> Result<Option<Rect>, InternalRenderingError> {
127
949595
    if path.is_empty() {
128
18
        return Ok(None);
129
    }
130

            
131
949577
    let surface = cairo::RecordingSurface::create(cairo::Content::ColorAlpha, None)?;
132
949577
    let cr = cairo::Context::new(&surface)?;
133

            
134
1898710
    path.to_cairo(&cr, false)?;
135
948568
    let (x0, y0, x1, y1) = cr.path_extents()?;
136

            
137
948311
    Ok(Some(Rect::new(x0, y0, x1, y1)))
138
948465
}
139

            
140
impl Path {
141
1896539
    pub fn to_cairo_path(
142
        &self,
143
        is_square_linecap: bool,
144
    ) -> Result<CairoPath, InternalRenderingError> {
145
1896539
        let mut segments = Vec::new();
146

            
147
3789042
        for subpath in self.iter_subpath() {
148
            // If a subpath is empty and the linecap is a square, then draw a square centered on
149
            // the origin of the subpath. See #165.
150
1894933
            if is_square_linecap {
151
34
                let (x, y) = subpath.origin();
152
34
                if subpath.is_zero_length() {
153
1
                    let stroke_size = 0.002;
154

            
155
1
                    segments.push(PathSegment::MoveTo((x - stroke_size / 2., y)));
156
1
                    segments.push(PathSegment::LineTo((x + stroke_size / 2., y)));
157
                }
158
            }
159

            
160
13297724
            for cmd in subpath.iter_commands() {
161
11402409
                cmd.to_path_segments(&mut segments);
162
            }
163
        }
164

            
165
1889145
        Ok(CairoPath(segments))
166
1889145
    }
167

            
168
948253
    pub fn to_cairo(
169
        &self,
170
        cr: &cairo::Context,
171
        is_square_linecap: bool,
172
    ) -> Result<(), InternalRenderingError> {
173
948253
        let cairo_path = self.to_cairo_path(is_square_linecap)?;
174
948253
        cairo_path.to_cairo_context(cr)
175
948253
    }
176
}
177

            
178
1026
fn cairo_path_is_only_move_tos(path: &cairo::Path) -> bool {
179
1026
    path.iter()
180
1995
        .all(|seg| matches!(seg, cairo::PathSegment::MoveTo((_, _))))
181
1026
}
182

            
183
impl PathCommand {
184
11405262
    fn to_path_segments(&self, segments: &mut Vec<PathSegment>) {
185
11405262
        match *self {
186
1899199
            PathCommand::MoveTo(x, y) => segments.push(PathSegment::MoveTo((x, y))),
187
7589969
            PathCommand::LineTo(x, y) => segments.push(PathSegment::LineTo((x, y))),
188
16341
            PathCommand::CurveTo(ref curve) => curve.to_path_segments(segments),
189
2858
            PathCommand::Arc(ref arc) => arc.to_path_segments(segments),
190
1896895
            PathCommand::ClosePath => segments.push(PathSegment::ClosePath),
191
        }
192
11405262
    }
193
}
194

            
195
impl EllipticalArc {
196
2858
    fn to_path_segments(&self, segments: &mut Vec<PathSegment>) {
197
2858
        match self.center_parameterization() {
198
            ArcParameterization::CenterParameters {
199
2857
                center,
200
2857
                radii,
201
2857
                theta1,
202
2857
                delta_theta,
203
            } => {
204
2857
                let n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as u32;
205
2857
                let d_theta = delta_theta / f64::from(n_segs);
206

            
207
2857
                let mut theta = theta1;
208
7137
                for _ in 0..n_segs {
209
4280
                    arc_segment(center, radii, self.x_axis_rotation, theta, theta + d_theta)
210
                        .to_path_segments(segments);
211
4280
                    theta += d_theta;
212
                }
213
            }
214
            ArcParameterization::LineTo => {
215
                let (x2, y2) = self.to;
216
                segments.push(PathSegment::LineTo((x2, y2)));
217
            }
218
            ArcParameterization::Omit => {}
219
        }
220
2858
    }
221
}
222

            
223
impl CubicBezierCurve {
224
20620
    fn to_path_segments(&self, segments: &mut Vec<PathSegment>) {
225
20620
        let Self { pt1, pt2, to } = *self;
226
20620
        segments.push(PathSegment::CurveTo(
227
            (pt1.0, pt1.1),
228
            (pt2.0, pt2.1),
229
            (to.0, to.1),
230
        ));
231
20620
    }
232
}
233

            
234
948849
pub fn validate_path(
235
    path: &Rc<Path>,
236
    stroke: &Stroke,
237
    viewport: &Viewport,
238
    normalize_values: &NormalizeValues,
239
    stroke_paint: &PaintSource,
240
    fill_paint: &PaintSource,
241
) -> Result<layout::Path, InternalRenderingError> {
242
948849
    let is_square_linecap = stroke.line_cap == StrokeLinecap::Square;
243
948849
    let cairo_path = path.to_cairo_path(is_square_linecap)?;
244

            
245
948849
    if cairo_path.has_unsuitable_coordinates(&viewport.transform) {
246
4
        return Ok(layout::Path::Invalid(String::from(
247
            "path has coordinates that are unsuitable for Cairo",
248
        )));
249
    }
250

            
251
947254
    let extents = compute_path_extents(path)?;
252
948847
    let stroke_paint = stroke_paint.to_user_space(&extents, viewport, normalize_values);
253
948841
    let fill_paint = fill_paint.to_user_space(&extents, viewport, normalize_values);
254

            
255
948841
    Ok(layout::Path::Validated {
256
948841
        cairo_path,
257
948841
        path: Rc::clone(path),
258
948841
        extents,
259
948841
        stroke_paint,
260
948841
        fill_paint,
261
    })
262
948845
}
263

            
264
#[cfg(test)]
265
mod tests {
266
    use super::*;
267
    use crate::path_builder::PathBuilder;
268

            
269
    #[test]
270
2
    fn rsvg_path_from_cairo_path() {
271
1
        let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, 10, 10).unwrap();
272
1
        let cr = cairo::Context::new(&surface).unwrap();
273

            
274
1
        cr.move_to(1.0, 2.0);
275
1
        cr.line_to(3.0, 4.0);
276
1
        cr.curve_to(5.0, 6.0, 7.0, 8.0, 9.0, 10.0);
277
1
        cr.close_path();
278

            
279
1
        let cr_path = cr.copy_path().unwrap();
280
1
        let cairo_path = CairoPath::from_cairo(cr_path);
281

            
282
2
        assert_eq!(
283
            cairo_path.0,
284
1
            vec![
285
1
                PathSegment::MoveTo((1.0, 2.0)),
286
1
                PathSegment::LineTo((3.0, 4.0)),
287
1
                PathSegment::CurveTo((5.0, 6.0), (7.0, 8.0), (9.0, 10.0)),
288
1
                PathSegment::ClosePath,
289
1
                PathSegment::MoveTo((1.0, 2.0)), // cairo inserts a MoveTo after ClosePath
290
            ],
291
        );
292
2
    }
293

            
294
    #[test]
295
2
    fn detects_suitable_coordinates() {
296
1
        let mut builder = PathBuilder::default();
297
1
        builder.move_to(900000.0, 33.0);
298
1
        builder.line_to(-900000.0, 3.0);
299

            
300
1
        let path = builder.into_path();
301
1
        let cairo_path = path.to_cairo_path(false).map_err(|_| ()).unwrap();
302
1
        assert!(!cairo_path.has_unsuitable_coordinates(&Transform::identity()));
303
2
    }
304

            
305
    #[test]
306
2
    fn detects_unsuitable_coordinates() {
307
1
        let mut builder = PathBuilder::default();
308
1
        builder.move_to(9000000.0, 33.0);
309
1
        builder.line_to(-9000000.0, 3.0);
310

            
311
1
        let path = builder.into_path();
312
1
        let cairo_path = path.to_cairo_path(false).map_err(|_| ()).unwrap();
313
1
        assert!(cairo_path.has_unsuitable_coordinates(&Transform::identity()));
314
2
    }
315
}