Skip to main content

pdfplumber_core/
path.rs

1use crate::geometry::{Ctm, Point};
2
3/// A segment of a PDF path.
4#[derive(Debug, Clone, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub enum PathSegment {
7    /// Move to a new point (starts a new subpath).
8    MoveTo(Point),
9    /// Straight line from current point to target.
10    LineTo(Point),
11    /// Cubic Bezier curve with two control points and an endpoint.
12    CurveTo {
13        /// First control point.
14        cp1: Point,
15        /// Second control point.
16        cp2: Point,
17        /// Endpoint of the curve.
18        end: Point,
19    },
20    /// Close the current subpath (line back to the subpath start).
21    ClosePath,
22}
23
24/// A complete path consisting of segments.
25#[derive(Debug, Clone, PartialEq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct Path {
28    /// The path segments.
29    pub segments: Vec<PathSegment>,
30}
31
32/// Builder for constructing paths from PDF path operators.
33///
34/// Coordinates are transformed through the CTM before storage.
35#[derive(Debug, Clone)]
36pub struct PathBuilder {
37    segments: Vec<PathSegment>,
38    current_point: Option<Point>,
39    subpath_start: Option<Point>,
40    ctm: Ctm,
41}
42
43impl PathBuilder {
44    /// Create a new PathBuilder with the given CTM.
45    pub fn new(ctm: Ctm) -> Self {
46        Self {
47            segments: Vec::new(),
48            current_point: None,
49            subpath_start: None,
50            ctm,
51        }
52    }
53
54    /// Update the CTM.
55    pub fn set_ctm(&mut self, ctm: Ctm) {
56        self.ctm = ctm;
57    }
58
59    /// Get the current CTM.
60    pub fn ctm(&self) -> &Ctm {
61        &self.ctm
62    }
63
64    /// `m` operator: move to a new point, starting a new subpath.
65    pub fn move_to(&mut self, x: f64, y: f64) {
66        let p = self.ctm.transform_point(Point::new(x, y));
67        self.segments.push(PathSegment::MoveTo(p));
68        self.current_point = Some(p);
69        self.subpath_start = Some(p);
70    }
71
72    /// `l` operator: straight line from current point to `(x, y)`.
73    pub fn line_to(&mut self, x: f64, y: f64) {
74        let p = self.ctm.transform_point(Point::new(x, y));
75        self.segments.push(PathSegment::LineTo(p));
76        self.current_point = Some(p);
77    }
78
79    /// `c` operator: cubic Bezier curve with three coordinate pairs.
80    pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) {
81        let cp1 = self.ctm.transform_point(Point::new(x1, y1));
82        let cp2 = self.ctm.transform_point(Point::new(x2, y2));
83        let end = self.ctm.transform_point(Point::new(x3, y3));
84        self.segments.push(PathSegment::CurveTo { cp1, cp2, end });
85        self.current_point = Some(end);
86    }
87
88    /// `v` operator: cubic Bezier where first control point equals current point.
89    pub fn curve_to_v(&mut self, x2: f64, y2: f64, x3: f64, y3: f64) {
90        let Some(cp1) = self.current_point else {
91            return;
92        };
93        let cp2 = self.ctm.transform_point(Point::new(x2, y2));
94        let end = self.ctm.transform_point(Point::new(x3, y3));
95        self.segments.push(PathSegment::CurveTo { cp1, cp2, end });
96        self.current_point = Some(end);
97    }
98
99    /// `y` operator: cubic Bezier where last control point equals endpoint.
100    pub fn curve_to_y(&mut self, x1: f64, y1: f64, x3: f64, y3: f64) {
101        let cp1 = self.ctm.transform_point(Point::new(x1, y1));
102        let end = self.ctm.transform_point(Point::new(x3, y3));
103        self.segments
104            .push(PathSegment::CurveTo { cp1, cp2: end, end });
105        self.current_point = Some(end);
106    }
107
108    /// `h` operator: close the current subpath.
109    pub fn close_path(&mut self) {
110        self.segments.push(PathSegment::ClosePath);
111        if let Some(start) = self.subpath_start {
112            self.current_point = Some(start);
113        }
114    }
115
116    /// `re` operator: append a rectangle as moveto + 3 lineto + closepath.
117    pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) {
118        self.move_to(x, y);
119        self.line_to(x + width, y);
120        self.line_to(x + width, y + height);
121        self.line_to(x, y + height);
122        self.close_path();
123    }
124
125    /// Get the current point (already CTM-transformed).
126    pub fn current_point(&self) -> Option<Point> {
127        self.current_point
128    }
129
130    /// Consume the builder and return the constructed path.
131    pub fn build(self) -> Path {
132        Path {
133            segments: self.segments,
134        }
135    }
136
137    /// Check if the builder has no segments.
138    pub fn is_empty(&self) -> bool {
139        self.segments.is_empty()
140    }
141
142    /// Take the accumulated segments as a `Path` and reset the builder.
143    ///
144    /// After this call, the builder is empty and ready for a new path.
145    /// The current point and subpath start are also reset.
146    pub fn take_and_reset(&mut self) -> Path {
147        let segments = std::mem::take(&mut self.segments);
148        self.current_point = None;
149        self.subpath_start = None;
150        Path { segments }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    fn assert_point_approx(p: Point, x: f64, y: f64) {
159        assert!((p.x - x).abs() < 1e-10, "x: expected {x}, got {}", p.x);
160        assert!((p.y - y).abs() < 1e-10, "y: expected {y}, got {}", p.y);
161    }
162
163    // --- PathBuilder: empty state ---
164
165    #[test]
166    fn test_new_builder_is_empty() {
167        let builder = PathBuilder::new(Ctm::identity());
168        assert!(builder.is_empty());
169        assert!(builder.current_point().is_none());
170    }
171
172    // --- m (moveto) ---
173
174    #[test]
175    fn test_move_to() {
176        let mut builder = PathBuilder::new(Ctm::identity());
177        builder.move_to(10.0, 20.0);
178
179        assert!(!builder.is_empty());
180        let cp = builder.current_point().unwrap();
181        assert_point_approx(cp, 10.0, 20.0);
182
183        let path = builder.build();
184        assert_eq!(path.segments.len(), 1);
185        assert_eq!(
186            path.segments[0],
187            PathSegment::MoveTo(Point::new(10.0, 20.0))
188        );
189    }
190
191    #[test]
192    fn test_move_to_updates_subpath_start() {
193        let mut builder = PathBuilder::new(Ctm::identity());
194        builder.move_to(10.0, 20.0);
195        builder.line_to(30.0, 40.0);
196        builder.close_path();
197
198        // After close, current point should return to subpath start (10, 20)
199        let cp = builder.current_point().unwrap();
200        assert_point_approx(cp, 10.0, 20.0);
201    }
202
203    // --- l (lineto) ---
204
205    #[test]
206    fn test_line_to() {
207        let mut builder = PathBuilder::new(Ctm::identity());
208        builder.move_to(0.0, 0.0);
209        builder.line_to(100.0, 50.0);
210
211        let cp = builder.current_point().unwrap();
212        assert_point_approx(cp, 100.0, 50.0);
213
214        let path = builder.build();
215        assert_eq!(path.segments.len(), 2);
216        assert_eq!(
217            path.segments[1],
218            PathSegment::LineTo(Point::new(100.0, 50.0))
219        );
220    }
221
222    // --- c (curveto) ---
223
224    #[test]
225    fn test_curve_to() {
226        let mut builder = PathBuilder::new(Ctm::identity());
227        builder.move_to(0.0, 0.0);
228        builder.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
229
230        let cp = builder.current_point().unwrap();
231        assert_point_approx(cp, 50.0, 60.0);
232
233        let path = builder.build();
234        assert_eq!(path.segments.len(), 2);
235        assert_eq!(
236            path.segments[1],
237            PathSegment::CurveTo {
238                cp1: Point::new(10.0, 20.0),
239                cp2: Point::new(30.0, 40.0),
240                end: Point::new(50.0, 60.0),
241            }
242        );
243    }
244
245    // --- v (curveto variant: first CP = current point) ---
246
247    #[test]
248    fn test_curve_to_v() {
249        let mut builder = PathBuilder::new(Ctm::identity());
250        builder.move_to(5.0, 10.0);
251        builder.curve_to_v(30.0, 40.0, 50.0, 60.0);
252
253        let cp = builder.current_point().unwrap();
254        assert_point_approx(cp, 50.0, 60.0);
255
256        let path = builder.build();
257        assert_eq!(path.segments.len(), 2);
258        assert_eq!(
259            path.segments[1],
260            PathSegment::CurveTo {
261                cp1: Point::new(5.0, 10.0), // first CP = current point at time of call
262                cp2: Point::new(30.0, 40.0),
263                end: Point::new(50.0, 60.0),
264            }
265        );
266    }
267
268    #[test]
269    fn test_curve_to_v_without_current_point_is_noop() {
270        let mut builder = PathBuilder::new(Ctm::identity());
271        builder.curve_to_v(30.0, 40.0, 50.0, 60.0);
272
273        assert!(builder.is_empty());
274        assert!(builder.current_point().is_none());
275    }
276
277    // --- y (curveto variant: last CP = endpoint) ---
278
279    #[test]
280    fn test_curve_to_y() {
281        let mut builder = PathBuilder::new(Ctm::identity());
282        builder.move_to(0.0, 0.0);
283        builder.curve_to_y(10.0, 20.0, 50.0, 60.0);
284
285        let cp = builder.current_point().unwrap();
286        assert_point_approx(cp, 50.0, 60.0);
287
288        let path = builder.build();
289        assert_eq!(path.segments.len(), 2);
290        assert_eq!(
291            path.segments[1],
292            PathSegment::CurveTo {
293                cp1: Point::new(10.0, 20.0),
294                cp2: Point::new(50.0, 60.0), // last CP = endpoint
295                end: Point::new(50.0, 60.0),
296            }
297        );
298    }
299
300    // --- h (closepath) ---
301
302    #[test]
303    fn test_close_path() {
304        let mut builder = PathBuilder::new(Ctm::identity());
305        builder.move_to(10.0, 20.0);
306        builder.line_to(30.0, 40.0);
307        builder.line_to(50.0, 20.0);
308        builder.close_path();
309
310        // Current point returns to subpath start
311        let cp = builder.current_point().unwrap();
312        assert_point_approx(cp, 10.0, 20.0);
313
314        let path = builder.build();
315        assert_eq!(path.segments.len(), 4);
316        assert_eq!(path.segments[3], PathSegment::ClosePath);
317    }
318
319    // --- re (rectangle) ---
320
321    #[test]
322    fn test_rectangle() {
323        let mut builder = PathBuilder::new(Ctm::identity());
324        builder.rectangle(10.0, 20.0, 100.0, 50.0);
325
326        let path = builder.build();
327        // re produces: moveto + 3 lineto + closepath = 5 segments
328        assert_eq!(path.segments.len(), 5);
329        assert_eq!(
330            path.segments[0],
331            PathSegment::MoveTo(Point::new(10.0, 20.0))
332        );
333        assert_eq!(
334            path.segments[1],
335            PathSegment::LineTo(Point::new(110.0, 20.0))
336        );
337        assert_eq!(
338            path.segments[2],
339            PathSegment::LineTo(Point::new(110.0, 70.0))
340        );
341        assert_eq!(
342            path.segments[3],
343            PathSegment::LineTo(Point::new(10.0, 70.0))
344        );
345        assert_eq!(path.segments[4], PathSegment::ClosePath);
346    }
347
348    #[test]
349    fn test_rectangle_current_point_at_start() {
350        let mut builder = PathBuilder::new(Ctm::identity());
351        builder.rectangle(10.0, 20.0, 100.0, 50.0);
352
353        // After re + close, current point is the rectangle origin
354        let cp = builder.current_point().unwrap();
355        assert_point_approx(cp, 10.0, 20.0);
356    }
357
358    // --- Combined path construction ---
359
360    #[test]
361    fn test_combined_path_triangle() {
362        let mut builder = PathBuilder::new(Ctm::identity());
363        builder.move_to(0.0, 0.0);
364        builder.line_to(100.0, 0.0);
365        builder.line_to(50.0, 80.0);
366        builder.close_path();
367
368        let path = builder.build();
369        assert_eq!(path.segments.len(), 4);
370        assert_eq!(path.segments[0], PathSegment::MoveTo(Point::new(0.0, 0.0)));
371        assert_eq!(
372            path.segments[1],
373            PathSegment::LineTo(Point::new(100.0, 0.0))
374        );
375        assert_eq!(
376            path.segments[2],
377            PathSegment::LineTo(Point::new(50.0, 80.0))
378        );
379        assert_eq!(path.segments[3], PathSegment::ClosePath);
380    }
381
382    #[test]
383    fn test_combined_path_with_curves() {
384        let mut builder = PathBuilder::new(Ctm::identity());
385        builder.move_to(0.0, 0.0);
386        builder.line_to(50.0, 0.0);
387        builder.curve_to(60.0, 0.0, 70.0, 10.0, 70.0, 20.0);
388        builder.line_to(70.0, 50.0);
389        builder.close_path();
390
391        let path = builder.build();
392        assert_eq!(path.segments.len(), 5);
393    }
394
395    #[test]
396    fn test_multiple_subpaths() {
397        let mut builder = PathBuilder::new(Ctm::identity());
398        // First subpath: a line
399        builder.move_to(0.0, 0.0);
400        builder.line_to(100.0, 0.0);
401        // Second subpath: another line
402        builder.move_to(0.0, 50.0);
403        builder.line_to(100.0, 50.0);
404
405        let path = builder.build();
406        assert_eq!(path.segments.len(), 4);
407
408        // After second moveto, close should go back to second subpath start
409    }
410
411    #[test]
412    fn test_multiple_subpaths_close_returns_to_latest_start() {
413        let mut builder = PathBuilder::new(Ctm::identity());
414        builder.move_to(0.0, 0.0);
415        builder.line_to(100.0, 100.0);
416        builder.move_to(200.0, 200.0);
417        builder.line_to(300.0, 300.0);
418        builder.close_path();
419
420        // Close returns to the most recent moveto (200, 200)
421        let cp = builder.current_point().unwrap();
422        assert_point_approx(cp, 200.0, 200.0);
423    }
424
425    // --- CTM-transformed paths ---
426
427    #[test]
428    fn test_ctm_translation_moveto() {
429        // CTM translates by (100, 200)
430        let ctm = Ctm::new(1.0, 0.0, 0.0, 1.0, 100.0, 200.0);
431        let mut builder = PathBuilder::new(ctm);
432        builder.move_to(10.0, 20.0);
433
434        let cp = builder.current_point().unwrap();
435        assert_point_approx(cp, 110.0, 220.0);
436    }
437
438    #[test]
439    fn test_ctm_scaling_lineto() {
440        // CTM scales by 2x
441        let ctm = Ctm::new(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
442        let mut builder = PathBuilder::new(ctm);
443        builder.move_to(5.0, 10.0);
444        builder.line_to(15.0, 25.0);
445
446        let path = builder.build();
447        assert_eq!(
448            path.segments[0],
449            PathSegment::MoveTo(Point::new(10.0, 20.0))
450        );
451        assert_eq!(
452            path.segments[1],
453            PathSegment::LineTo(Point::new(30.0, 50.0))
454        );
455    }
456
457    #[test]
458    fn test_ctm_transformed_rectangle() {
459        // CTM: scale 2x + translate (10, 10)
460        let ctm = Ctm::new(2.0, 0.0, 0.0, 2.0, 10.0, 10.0);
461        let mut builder = PathBuilder::new(ctm);
462        builder.rectangle(0.0, 0.0, 50.0, 30.0);
463
464        let path = builder.build();
465        // (0,0) -> (10, 10)
466        assert_eq!(
467            path.segments[0],
468            PathSegment::MoveTo(Point::new(10.0, 10.0))
469        );
470        // (50,0) -> (110, 10)
471        assert_eq!(
472            path.segments[1],
473            PathSegment::LineTo(Point::new(110.0, 10.0))
474        );
475        // (50,30) -> (110, 70)
476        assert_eq!(
477            path.segments[2],
478            PathSegment::LineTo(Point::new(110.0, 70.0))
479        );
480        // (0,30) -> (10, 70)
481        assert_eq!(
482            path.segments[3],
483            PathSegment::LineTo(Point::new(10.0, 70.0))
484        );
485        assert_eq!(path.segments[4], PathSegment::ClosePath);
486    }
487
488    #[test]
489    fn test_ctm_transformed_curveto() {
490        // CTM scales x by 2, y by 3
491        let ctm = Ctm::new(2.0, 0.0, 0.0, 3.0, 0.0, 0.0);
492        let mut builder = PathBuilder::new(ctm);
493        builder.move_to(0.0, 0.0);
494        builder.curve_to(10.0, 10.0, 20.0, 20.0, 30.0, 30.0);
495
496        let path = builder.build();
497        assert_eq!(
498            path.segments[1],
499            PathSegment::CurveTo {
500                cp1: Point::new(20.0, 30.0),
501                cp2: Point::new(40.0, 60.0),
502                end: Point::new(60.0, 90.0),
503            }
504        );
505    }
506
507    #[test]
508    fn test_ctm_transformed_curve_to_v() {
509        // CTM translates by (100, 0)
510        let ctm = Ctm::new(1.0, 0.0, 0.0, 1.0, 100.0, 0.0);
511        let mut builder = PathBuilder::new(ctm);
512        builder.move_to(5.0, 10.0); // transformed: (105, 10)
513        builder.curve_to_v(30.0, 40.0, 50.0, 60.0);
514
515        let path = builder.build();
516        assert_eq!(
517            path.segments[1],
518            PathSegment::CurveTo {
519                cp1: Point::new(105.0, 10.0), // current point (already transformed)
520                cp2: Point::new(130.0, 40.0),
521                end: Point::new(150.0, 60.0),
522            }
523        );
524    }
525
526    #[test]
527    fn test_ctm_transformed_curve_to_y() {
528        // CTM scales by 0.5
529        let ctm = Ctm::new(0.5, 0.0, 0.0, 0.5, 0.0, 0.0);
530        let mut builder = PathBuilder::new(ctm);
531        builder.move_to(0.0, 0.0);
532        builder.curve_to_y(20.0, 40.0, 60.0, 80.0);
533
534        let path = builder.build();
535        assert_eq!(
536            path.segments[1],
537            PathSegment::CurveTo {
538                cp1: Point::new(10.0, 20.0),
539                cp2: Point::new(30.0, 40.0), // same as endpoint
540                end: Point::new(30.0, 40.0),
541            }
542        );
543    }
544
545    #[test]
546    fn test_ctm_close_path_returns_to_transformed_start() {
547        let ctm = Ctm::new(1.0, 0.0, 0.0, 1.0, 50.0, 50.0);
548        let mut builder = PathBuilder::new(ctm);
549        builder.move_to(10.0, 20.0); // transformed: (60, 70)
550        builder.line_to(100.0, 100.0);
551        builder.close_path();
552
553        let cp = builder.current_point().unwrap();
554        assert_point_approx(cp, 60.0, 70.0);
555    }
556
557    #[test]
558    fn test_set_ctm() {
559        let mut builder = PathBuilder::new(Ctm::identity());
560        builder.move_to(10.0, 20.0); // no transform
561
562        // Change CTM to translate by (100, 100)
563        builder.set_ctm(Ctm::new(1.0, 0.0, 0.0, 1.0, 100.0, 100.0));
564        builder.line_to(10.0, 20.0); // now transformed: (110, 120)
565
566        let path = builder.build();
567        assert_eq!(
568            path.segments[0],
569            PathSegment::MoveTo(Point::new(10.0, 20.0))
570        );
571        assert_eq!(
572            path.segments[1],
573            PathSegment::LineTo(Point::new(110.0, 120.0))
574        );
575    }
576
577    #[test]
578    fn test_ctm_accessor() {
579        let ctm = Ctm::new(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
580        let builder = PathBuilder::new(ctm);
581        assert_eq!(*builder.ctm(), ctm);
582    }
583}