Skip to main content

pdfplumber_core/
painting.rs

1//! Path painting operators, graphics state, and ExtGState types.
2//!
3//! Implements PDF path painting operators (S, s, f, F, f*, B, B*, b, b*, n)
4//! that determine how constructed paths are rendered. Also provides
5//! `DashPattern`, `ExtGState`, and extended `GraphicsState` for the
6//! `gs` and `d` operators.
7
8use crate::path::{Path, PathBuilder};
9
10/// Color value from a PDF color space.
11///
12/// Supports the standard PDF color spaces: DeviceGray, DeviceRGB,
13/// DeviceCMYK, and other (e.g., indexed, ICC-based) spaces.
14#[derive(Debug, Clone, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub enum Color {
17    /// DeviceGray: single component in [0.0, 1.0].
18    Gray(f32),
19    /// DeviceRGB: (r, g, b) components in [0.0, 1.0].
20    Rgb(f32, f32, f32),
21    /// DeviceCMYK: (c, m, y, k) components in [0.0, 1.0].
22    Cmyk(f32, f32, f32, f32),
23    /// Other color space (e.g., indexed, ICC-based).
24    Other(Vec<f32>),
25}
26
27impl Color {
28    /// Black color (gray 0).
29    pub fn black() -> Self {
30        Self::Gray(0.0)
31    }
32
33    /// Convert this color to an RGB triple `(r, g, b)` with components in `[0.0, 1.0]`.
34    ///
35    /// Returns `None` for `Color::Other` since the color space is unknown.
36    pub fn to_rgb(&self) -> Option<(f32, f32, f32)> {
37        match self {
38            Color::Gray(g) => Some((*g, *g, *g)),
39            Color::Rgb(r, g, b) => Some((*r, *g, *b)),
40            Color::Cmyk(c, m, y, k) => {
41                // Standard CMYK to RGB conversion
42                let r = (1.0 - c) * (1.0 - k);
43                let g = (1.0 - m) * (1.0 - k);
44                let b = (1.0 - y) * (1.0 - k);
45                Some((r, g, b))
46            }
47            Color::Other(_) => None,
48        }
49    }
50}
51
52impl Default for Color {
53    fn default() -> Self {
54        Self::black()
55    }
56}
57
58/// Fill rule for path painting.
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub enum FillRule {
62    /// Nonzero winding number rule (default).
63    #[default]
64    NonZeroWinding,
65    /// Even-odd rule.
66    EvenOdd,
67}
68
69/// Dash pattern for stroking operations.
70///
71/// Corresponds to the PDF `d` operator and `/D` entry in ExtGState.
72/// A solid line has an empty `dash_array` and `dash_phase` of 0.
73#[derive(Debug, Clone, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct DashPattern {
76    /// Array of dash/gap lengths (alternating on/off).
77    /// Empty array means a solid line.
78    pub dash_array: Vec<f64>,
79    /// Phase offset into the dash pattern.
80    pub dash_phase: f64,
81}
82
83impl DashPattern {
84    /// Create a new dash pattern.
85    pub fn new(dash_array: Vec<f64>, dash_phase: f64) -> Self {
86        Self {
87            dash_array,
88            dash_phase,
89        }
90    }
91
92    /// Solid line (no dashes).
93    pub fn solid() -> Self {
94        Self {
95            dash_array: Vec::new(),
96            dash_phase: 0.0,
97        }
98    }
99
100    /// Returns true if this is a solid line (no dashes).
101    pub fn is_solid(&self) -> bool {
102        self.dash_array.is_empty()
103    }
104}
105
106impl Default for DashPattern {
107    fn default() -> Self {
108        Self::solid()
109    }
110}
111
112/// Graphics state relevant to path painting.
113#[derive(Debug, Clone, PartialEq)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115pub struct GraphicsState {
116    /// Current line width (default: 1.0 per PDF spec).
117    pub line_width: f64,
118    /// Current stroking color.
119    pub stroke_color: Color,
120    /// Current non-stroking (fill) color.
121    pub fill_color: Color,
122    /// Current dash pattern (default: solid line).
123    pub dash_pattern: DashPattern,
124    /// Stroking alpha / opacity (CA, default: 1.0 = fully opaque).
125    pub stroke_alpha: f64,
126    /// Non-stroking alpha / opacity (ca, default: 1.0 = fully opaque).
127    pub fill_alpha: f64,
128}
129
130impl Default for GraphicsState {
131    fn default() -> Self {
132        Self {
133            line_width: 1.0,
134            stroke_color: Color::black(),
135            fill_color: Color::black(),
136            dash_pattern: DashPattern::solid(),
137            stroke_alpha: 1.0,
138            fill_alpha: 1.0,
139        }
140    }
141}
142
143impl GraphicsState {
144    /// Apply an `ExtGState` dictionary to this graphics state.
145    ///
146    /// Only fields that are `Some` in the `ExtGState` are overridden.
147    pub fn apply_ext_gstate(&mut self, ext: &ExtGState) {
148        if let Some(lw) = ext.line_width {
149            self.line_width = lw;
150        }
151        if let Some(ref dp) = ext.dash_pattern {
152            self.dash_pattern = dp.clone();
153        }
154        if let Some(ca) = ext.stroke_alpha {
155            self.stroke_alpha = ca;
156        }
157        if let Some(ca) = ext.fill_alpha {
158            self.fill_alpha = ca;
159        }
160    }
161
162    /// Set the dash pattern directly (`d` operator).
163    pub fn set_dash_pattern(&mut self, dash_array: Vec<f64>, dash_phase: f64) {
164        self.dash_pattern = DashPattern::new(dash_array, dash_phase);
165    }
166}
167
168/// Extended Graphics State parameters (from `gs` operator).
169///
170/// Represents the parsed contents of an ExtGState dictionary.
171/// All fields are optional — only present entries override the current graphics state.
172#[derive(Debug, Clone, Default, PartialEq)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub struct ExtGState {
175    /// /LW — Line width override.
176    pub line_width: Option<f64>,
177    /// /D — Dash pattern override.
178    pub dash_pattern: Option<DashPattern>,
179    /// /CA — Stroking alpha (opacity).
180    pub stroke_alpha: Option<f64>,
181    /// /ca — Non-stroking alpha (opacity).
182    pub fill_alpha: Option<f64>,
183    /// /Font — Font name and size override (font_name, font_size).
184    pub font: Option<(String, f64)>,
185}
186
187/// A painted path — the result of a painting operator applied to a constructed path.
188#[derive(Debug, Clone, PartialEq)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190pub struct PaintedPath {
191    /// The path segments.
192    pub path: Path,
193    /// Whether the path is stroked.
194    pub stroke: bool,
195    /// Whether the path is filled.
196    pub fill: bool,
197    /// Fill rule used (only meaningful when `fill` is true).
198    pub fill_rule: FillRule,
199    /// Line width at the time of painting.
200    pub line_width: f64,
201    /// Stroking color at the time of painting.
202    pub stroke_color: Color,
203    /// Fill color at the time of painting.
204    pub fill_color: Color,
205    /// Dash pattern at the time of painting.
206    pub dash_pattern: DashPattern,
207    /// Stroking alpha at the time of painting.
208    pub stroke_alpha: f64,
209    /// Non-stroking alpha at the time of painting.
210    pub fill_alpha: f64,
211}
212
213impl PathBuilder {
214    /// Create a `PaintedPath` capturing the current graphics state.
215    fn paint(
216        &mut self,
217        gs: &GraphicsState,
218        stroke: bool,
219        fill: bool,
220        fill_rule: FillRule,
221    ) -> PaintedPath {
222        let path = self.take_path();
223        PaintedPath {
224            path,
225            stroke,
226            fill,
227            fill_rule,
228            line_width: gs.line_width,
229            stroke_color: gs.stroke_color.clone(),
230            fill_color: gs.fill_color.clone(),
231            dash_pattern: gs.dash_pattern.clone(),
232            stroke_alpha: gs.stroke_alpha,
233            fill_alpha: gs.fill_alpha,
234        }
235    }
236
237    /// `S` operator: stroke the current path.
238    ///
239    /// Paints the path outline using the current stroking color and line width.
240    /// Clears the current path after painting.
241    pub fn stroke(&mut self, gs: &GraphicsState) -> PaintedPath {
242        self.paint(gs, true, false, FillRule::NonZeroWinding)
243    }
244
245    /// `s` operator: close the current subpath, then stroke.
246    ///
247    /// Equivalent to `h S`.
248    pub fn close_and_stroke(&mut self, gs: &GraphicsState) -> PaintedPath {
249        self.close_path();
250        self.stroke(gs)
251    }
252
253    /// `f` or `F` operator: fill the current path using the nonzero winding rule.
254    ///
255    /// Any open subpaths are implicitly closed before filling.
256    pub fn fill(&mut self, gs: &GraphicsState) -> PaintedPath {
257        self.paint(gs, false, true, FillRule::NonZeroWinding)
258    }
259
260    /// `f*` operator: fill the current path using the even-odd rule.
261    pub fn fill_even_odd(&mut self, gs: &GraphicsState) -> PaintedPath {
262        self.paint(gs, false, true, FillRule::EvenOdd)
263    }
264
265    /// `B` operator: fill then stroke the current path (nonzero winding).
266    pub fn fill_and_stroke(&mut self, gs: &GraphicsState) -> PaintedPath {
267        self.paint(gs, true, true, FillRule::NonZeroWinding)
268    }
269
270    /// `B*` operator: fill (even-odd) then stroke the current path.
271    pub fn fill_even_odd_and_stroke(&mut self, gs: &GraphicsState) -> PaintedPath {
272        self.paint(gs, true, true, FillRule::EvenOdd)
273    }
274
275    /// `b` operator: close, fill (nonzero winding), then stroke.
276    ///
277    /// Equivalent to `h B`.
278    pub fn close_fill_and_stroke(&mut self, gs: &GraphicsState) -> PaintedPath {
279        self.close_path();
280        self.fill_and_stroke(gs)
281    }
282
283    /// `b*` operator: close, fill (even-odd), then stroke.
284    ///
285    /// Equivalent to `h B*`.
286    pub fn close_fill_even_odd_and_stroke(&mut self, gs: &GraphicsState) -> PaintedPath {
287        self.close_path();
288        self.fill_even_odd_and_stroke(gs)
289    }
290
291    /// `n` operator: end the path without painting.
292    ///
293    /// Discards the current path. Used primarily for clipping paths.
294    /// Returns `None` since no painted path is produced.
295    pub fn end_path(&mut self) -> Option<PaintedPath> {
296        self.take_path();
297        None
298    }
299
300    /// Take the current path segments and reset the builder for the next path.
301    fn take_path(&mut self) -> Path {
302        self.take_and_reset()
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::geometry::{Ctm, Point};
310    use crate::path::PathSegment;
311
312    fn default_gs() -> GraphicsState {
313        GraphicsState::default()
314    }
315
316    fn custom_gs() -> GraphicsState {
317        GraphicsState {
318            line_width: 2.5,
319            stroke_color: Color::Rgb(1.0, 0.0, 0.0),
320            fill_color: Color::Rgb(0.0, 0.0, 1.0),
321            ..GraphicsState::default()
322        }
323    }
324
325    fn build_triangle(builder: &mut PathBuilder) {
326        builder.move_to(0.0, 0.0);
327        builder.line_to(100.0, 0.0);
328        builder.line_to(50.0, 80.0);
329    }
330
331    fn build_rectangle(builder: &mut PathBuilder) {
332        builder.rectangle(10.0, 20.0, 100.0, 50.0);
333    }
334
335    // --- Color::to_rgb tests ---
336
337    #[test]
338    fn test_gray_to_rgb() {
339        let c = Color::Gray(0.5);
340        assert_eq!(c.to_rgb(), Some((0.5, 0.5, 0.5)));
341    }
342
343    #[test]
344    fn test_gray_black_to_rgb() {
345        let c = Color::Gray(0.0);
346        assert_eq!(c.to_rgb(), Some((0.0, 0.0, 0.0)));
347    }
348
349    #[test]
350    fn test_gray_white_to_rgb() {
351        let c = Color::Gray(1.0);
352        assert_eq!(c.to_rgb(), Some((1.0, 1.0, 1.0)));
353    }
354
355    #[test]
356    fn test_rgb_to_rgb_identity() {
357        let c = Color::Rgb(0.2, 0.4, 0.6);
358        assert_eq!(c.to_rgb(), Some((0.2, 0.4, 0.6)));
359    }
360
361    #[test]
362    fn test_cmyk_to_rgb() {
363        // Pure cyan: C=1, M=0, Y=0, K=0 → R=0, G=1, B=1
364        let c = Color::Cmyk(1.0, 0.0, 0.0, 0.0);
365        let (r, g, b) = c.to_rgb().unwrap();
366        assert!((r - 0.0).abs() < 0.01, "r={r}");
367        assert!((g - 1.0).abs() < 0.01, "g={g}");
368        assert!((b - 1.0).abs() < 0.01, "b={b}");
369    }
370
371    #[test]
372    fn test_cmyk_black_to_rgb() {
373        // K=1 → all black
374        let c = Color::Cmyk(0.0, 0.0, 0.0, 1.0);
375        let (r, g, b) = c.to_rgb().unwrap();
376        assert!((r - 0.0).abs() < 0.01, "r={r}");
377        assert!((g - 0.0).abs() < 0.01, "g={g}");
378        assert!((b - 0.0).abs() < 0.01, "b={b}");
379    }
380
381    #[test]
382    fn test_cmyk_white_to_rgb() {
383        // All zero → white
384        let c = Color::Cmyk(0.0, 0.0, 0.0, 0.0);
385        let (r, g, b) = c.to_rgb().unwrap();
386        assert!((r - 1.0).abs() < 0.01, "r={r}");
387        assert!((g - 1.0).abs() < 0.01, "g={g}");
388        assert!((b - 1.0).abs() < 0.01, "b={b}");
389    }
390
391    #[test]
392    fn test_other_to_rgb_returns_none() {
393        let c = Color::Other(vec![0.1, 0.2]);
394        assert_eq!(c.to_rgb(), None);
395    }
396
397    // --- Color tests ---
398
399    #[test]
400    fn test_color_gray() {
401        let c = Color::Gray(0.5);
402        assert_eq!(c, Color::Gray(0.5));
403    }
404
405    #[test]
406    fn test_color_rgb() {
407        let c = Color::Rgb(0.5, 0.6, 0.7);
408        assert_eq!(c, Color::Rgb(0.5, 0.6, 0.7));
409    }
410
411    #[test]
412    fn test_color_cmyk() {
413        let c = Color::Cmyk(0.1, 0.2, 0.3, 0.4);
414        assert_eq!(c, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
415    }
416
417    #[test]
418    fn test_color_other() {
419        let c = Color::Other(vec![0.1, 0.2, 0.3, 0.4, 0.5]);
420        if let Color::Other(ref v) = c {
421            assert_eq!(v.len(), 5);
422        } else {
423            panic!("expected Color::Other");
424        }
425    }
426
427    #[test]
428    fn test_color_black() {
429        let c = Color::black();
430        assert_eq!(c, Color::Gray(0.0));
431    }
432
433    #[test]
434    fn test_color_default_is_black() {
435        assert_eq!(Color::default(), Color::black());
436    }
437
438    #[test]
439    fn test_color_clone() {
440        let c = Color::Rgb(1.0, 0.0, 0.0);
441        let c2 = c.clone();
442        assert_eq!(c, c2);
443    }
444
445    // --- FillRule tests ---
446
447    #[test]
448    fn test_fill_rule_default() {
449        assert_eq!(FillRule::default(), FillRule::NonZeroWinding);
450    }
451
452    // --- DashPattern tests ---
453
454    #[test]
455    fn test_dash_pattern_solid() {
456        let dp = DashPattern::solid();
457        assert!(dp.dash_array.is_empty());
458        assert_eq!(dp.dash_phase, 0.0);
459        assert!(dp.is_solid());
460    }
461
462    #[test]
463    fn test_dash_pattern_default_is_solid() {
464        assert_eq!(DashPattern::default(), DashPattern::solid());
465    }
466
467    #[test]
468    fn test_dash_pattern_new() {
469        let dp = DashPattern::new(vec![3.0, 2.0], 1.0);
470        assert_eq!(dp.dash_array, vec![3.0, 2.0]);
471        assert_eq!(dp.dash_phase, 1.0);
472        assert!(!dp.is_solid());
473    }
474
475    #[test]
476    fn test_dash_pattern_complex() {
477        let dp = DashPattern::new(vec![5.0, 2.0, 1.0, 2.0], 0.0);
478        assert_eq!(dp.dash_array.len(), 4);
479        assert!(!dp.is_solid());
480    }
481
482    // --- GraphicsState tests ---
483
484    #[test]
485    fn test_graphics_state_default() {
486        let gs = GraphicsState::default();
487        assert_eq!(gs.line_width, 1.0);
488        assert_eq!(gs.stroke_color, Color::black());
489        assert_eq!(gs.fill_color, Color::black());
490        assert!(gs.dash_pattern.is_solid());
491        assert_eq!(gs.stroke_alpha, 1.0);
492        assert_eq!(gs.fill_alpha, 1.0);
493    }
494
495    #[test]
496    fn test_set_dash_pattern() {
497        let mut gs = GraphicsState::default();
498        gs.set_dash_pattern(vec![4.0, 2.0], 0.5);
499
500        assert_eq!(gs.dash_pattern.dash_array, vec![4.0, 2.0]);
501        assert_eq!(gs.dash_pattern.dash_phase, 0.5);
502        assert!(!gs.dash_pattern.is_solid());
503    }
504
505    #[test]
506    fn test_set_dash_pattern_back_to_solid() {
507        let mut gs = GraphicsState::default();
508        gs.set_dash_pattern(vec![4.0, 2.0], 0.5);
509        assert!(!gs.dash_pattern.is_solid());
510
511        gs.set_dash_pattern(vec![], 0.0);
512        assert!(gs.dash_pattern.is_solid());
513    }
514
515    // --- ExtGState tests ---
516
517    #[test]
518    fn test_ext_gstate_default_is_all_none() {
519        let ext = ExtGState::default();
520        assert!(ext.line_width.is_none());
521        assert!(ext.dash_pattern.is_none());
522        assert!(ext.stroke_alpha.is_none());
523        assert!(ext.fill_alpha.is_none());
524        assert!(ext.font.is_none());
525    }
526
527    #[test]
528    fn test_apply_ext_gstate_line_width() {
529        let mut gs = GraphicsState::default();
530        assert_eq!(gs.line_width, 1.0);
531
532        let ext = ExtGState {
533            line_width: Some(3.5),
534            ..ExtGState::default()
535        };
536        gs.apply_ext_gstate(&ext);
537
538        assert_eq!(gs.line_width, 3.5);
539        // Other fields unchanged
540        assert!(gs.dash_pattern.is_solid());
541        assert_eq!(gs.stroke_alpha, 1.0);
542        assert_eq!(gs.fill_alpha, 1.0);
543    }
544
545    #[test]
546    fn test_apply_ext_gstate_dash_pattern() {
547        let mut gs = GraphicsState::default();
548        assert!(gs.dash_pattern.is_solid());
549
550        let ext = ExtGState {
551            dash_pattern: Some(DashPattern::new(vec![6.0, 3.0], 0.0)),
552            ..ExtGState::default()
553        };
554        gs.apply_ext_gstate(&ext);
555
556        assert_eq!(gs.dash_pattern.dash_array, vec![6.0, 3.0]);
557        assert_eq!(gs.dash_pattern.dash_phase, 0.0);
558        // Other fields unchanged
559        assert_eq!(gs.line_width, 1.0);
560    }
561
562    #[test]
563    fn test_apply_ext_gstate_stroke_alpha() {
564        let mut gs = GraphicsState::default();
565        assert_eq!(gs.stroke_alpha, 1.0);
566
567        let ext = ExtGState {
568            stroke_alpha: Some(0.5),
569            ..ExtGState::default()
570        };
571        gs.apply_ext_gstate(&ext);
572
573        assert_eq!(gs.stroke_alpha, 0.5);
574        assert_eq!(gs.fill_alpha, 1.0); // unchanged
575    }
576
577    #[test]
578    fn test_apply_ext_gstate_fill_alpha() {
579        let mut gs = GraphicsState::default();
580        assert_eq!(gs.fill_alpha, 1.0);
581
582        let ext = ExtGState {
583            fill_alpha: Some(0.75),
584            ..ExtGState::default()
585        };
586        gs.apply_ext_gstate(&ext);
587
588        assert_eq!(gs.fill_alpha, 0.75);
589        assert_eq!(gs.stroke_alpha, 1.0); // unchanged
590    }
591
592    #[test]
593    fn test_apply_ext_gstate_multiple_fields() {
594        let mut gs = GraphicsState::default();
595
596        let ext = ExtGState {
597            line_width: Some(2.0),
598            dash_pattern: Some(DashPattern::new(vec![1.0, 1.0], 0.0)),
599            stroke_alpha: Some(0.8),
600            fill_alpha: Some(0.6),
601            font: Some(("Helvetica".to_string(), 14.0)),
602        };
603        gs.apply_ext_gstate(&ext);
604
605        assert_eq!(gs.line_width, 2.0);
606        assert_eq!(gs.dash_pattern.dash_array, vec![1.0, 1.0]);
607        assert_eq!(gs.stroke_alpha, 0.8);
608        assert_eq!(gs.fill_alpha, 0.6);
609        // Font is stored in ExtGState but not in GraphicsState directly
610        // (it's a text state concern — callers read ext.font separately)
611    }
612
613    #[test]
614    fn test_apply_ext_gstate_none_fields_preserve_state() {
615        let mut gs = GraphicsState {
616            line_width: 5.0,
617            stroke_alpha: 0.3,
618            fill_alpha: 0.4,
619            dash_pattern: DashPattern::new(vec![2.0], 0.0),
620            ..GraphicsState::default()
621        };
622
623        // Apply empty ExtGState — nothing should change
624        let ext = ExtGState::default();
625        gs.apply_ext_gstate(&ext);
626
627        assert_eq!(gs.line_width, 5.0);
628        assert_eq!(gs.stroke_alpha, 0.3);
629        assert_eq!(gs.fill_alpha, 0.4);
630        assert_eq!(gs.dash_pattern.dash_array, vec![2.0]);
631    }
632
633    #[test]
634    fn test_apply_ext_gstate_sequential() {
635        let mut gs = GraphicsState::default();
636
637        // First ExtGState: set line width
638        let ext1 = ExtGState {
639            line_width: Some(2.0),
640            ..ExtGState::default()
641        };
642        gs.apply_ext_gstate(&ext1);
643        assert_eq!(gs.line_width, 2.0);
644
645        // Second ExtGState: set alpha, line width stays
646        let ext2 = ExtGState {
647            stroke_alpha: Some(0.5),
648            ..ExtGState::default()
649        };
650        gs.apply_ext_gstate(&ext2);
651        assert_eq!(gs.line_width, 2.0); // preserved
652        assert_eq!(gs.stroke_alpha, 0.5);
653    }
654
655    #[test]
656    fn test_ext_gstate_font_override() {
657        let ext = ExtGState {
658            font: Some(("CourierNew".to_string(), 10.0)),
659            ..ExtGState::default()
660        };
661        let (name, size) = ext.font.as_ref().unwrap();
662        assert_eq!(name, "CourierNew");
663        assert_eq!(*size, 10.0);
664    }
665
666    // --- S operator (stroke) ---
667
668    #[test]
669    fn test_stroke_produces_stroke_only() {
670        let mut builder = PathBuilder::new(Ctm::identity());
671        build_triangle(&mut builder);
672
673        let painted = builder.stroke(&default_gs());
674        assert!(painted.stroke);
675        assert!(!painted.fill);
676        assert_eq!(painted.fill_rule, FillRule::NonZeroWinding);
677    }
678
679    #[test]
680    fn test_stroke_captures_path_segments() {
681        let mut builder = PathBuilder::new(Ctm::identity());
682        build_triangle(&mut builder);
683
684        let painted = builder.stroke(&default_gs());
685        assert_eq!(painted.path.segments.len(), 3); // moveto + 2 lineto
686        assert_eq!(
687            painted.path.segments[0],
688            PathSegment::MoveTo(Point::new(0.0, 0.0))
689        );
690    }
691
692    #[test]
693    fn test_stroke_captures_graphics_state() {
694        let mut builder = PathBuilder::new(Ctm::identity());
695        build_triangle(&mut builder);
696
697        let gs = custom_gs();
698        let painted = builder.stroke(&gs);
699        assert_eq!(painted.line_width, 2.5);
700        assert_eq!(painted.stroke_color, Color::Rgb(1.0, 0.0, 0.0));
701        assert_eq!(painted.fill_color, Color::Rgb(0.0, 0.0, 1.0));
702    }
703
704    #[test]
705    fn test_stroke_clears_builder() {
706        let mut builder = PathBuilder::new(Ctm::identity());
707        build_triangle(&mut builder);
708        let _ = builder.stroke(&default_gs());
709
710        assert!(builder.is_empty());
711    }
712
713    // --- Stroke captures dash pattern and alpha ---
714
715    #[test]
716    fn test_stroke_captures_dash_pattern() {
717        let mut builder = PathBuilder::new(Ctm::identity());
718        builder.move_to(0.0, 0.0);
719        builder.line_to(100.0, 0.0);
720
721        let gs = GraphicsState {
722            dash_pattern: DashPattern::new(vec![5.0, 3.0], 1.0),
723            ..GraphicsState::default()
724        };
725        let painted = builder.stroke(&gs);
726        assert_eq!(painted.dash_pattern.dash_array, vec![5.0, 3.0]);
727        assert_eq!(painted.dash_pattern.dash_phase, 1.0);
728    }
729
730    #[test]
731    fn test_stroke_captures_alpha() {
732        let mut builder = PathBuilder::new(Ctm::identity());
733        builder.move_to(0.0, 0.0);
734        builder.line_to(100.0, 0.0);
735
736        let gs = GraphicsState {
737            stroke_alpha: 0.7,
738            fill_alpha: 0.3,
739            ..GraphicsState::default()
740        };
741        let painted = builder.stroke(&gs);
742        assert_eq!(painted.stroke_alpha, 0.7);
743        assert_eq!(painted.fill_alpha, 0.3);
744    }
745
746    #[test]
747    fn test_stroke_default_gs_has_solid_dash_and_full_alpha() {
748        let mut builder = PathBuilder::new(Ctm::identity());
749        builder.move_to(0.0, 0.0);
750        builder.line_to(100.0, 0.0);
751
752        let painted = builder.stroke(&default_gs());
753        assert!(painted.dash_pattern.is_solid());
754        assert_eq!(painted.stroke_alpha, 1.0);
755        assert_eq!(painted.fill_alpha, 1.0);
756    }
757
758    // --- s operator (close + stroke) ---
759
760    #[test]
761    fn test_close_and_stroke_includes_closepath() {
762        let mut builder = PathBuilder::new(Ctm::identity());
763        build_triangle(&mut builder);
764
765        let painted = builder.close_and_stroke(&default_gs());
766        assert!(painted.stroke);
767        assert!(!painted.fill);
768        // Should have: moveto + 2 lineto + closepath = 4 segments
769        assert_eq!(painted.path.segments.len(), 4);
770        assert_eq!(painted.path.segments[3], PathSegment::ClosePath);
771    }
772
773    // --- f/F operator (fill, nonzero winding) ---
774
775    #[test]
776    fn test_fill_produces_fill_only() {
777        let mut builder = PathBuilder::new(Ctm::identity());
778        build_triangle(&mut builder);
779
780        let painted = builder.fill(&default_gs());
781        assert!(!painted.stroke);
782        assert!(painted.fill);
783        assert_eq!(painted.fill_rule, FillRule::NonZeroWinding);
784    }
785
786    #[test]
787    fn test_fill_captures_path() {
788        let mut builder = PathBuilder::new(Ctm::identity());
789        build_rectangle(&mut builder);
790
791        let painted = builder.fill(&default_gs());
792        assert_eq!(painted.path.segments.len(), 5); // moveto + 3 lineto + closepath
793    }
794
795    #[test]
796    fn test_fill_clears_builder() {
797        let mut builder = PathBuilder::new(Ctm::identity());
798        build_triangle(&mut builder);
799        let _ = builder.fill(&default_gs());
800
801        assert!(builder.is_empty());
802    }
803
804    // --- f* operator (fill, even-odd) ---
805
806    #[test]
807    fn test_fill_even_odd_uses_even_odd_rule() {
808        let mut builder = PathBuilder::new(Ctm::identity());
809        build_triangle(&mut builder);
810
811        let painted = builder.fill_even_odd(&default_gs());
812        assert!(!painted.stroke);
813        assert!(painted.fill);
814        assert_eq!(painted.fill_rule, FillRule::EvenOdd);
815    }
816
817    // --- B operator (fill + stroke) ---
818
819    #[test]
820    fn test_fill_and_stroke() {
821        let mut builder = PathBuilder::new(Ctm::identity());
822        build_triangle(&mut builder);
823
824        let painted = builder.fill_and_stroke(&default_gs());
825        assert!(painted.stroke);
826        assert!(painted.fill);
827        assert_eq!(painted.fill_rule, FillRule::NonZeroWinding);
828    }
829
830    #[test]
831    fn test_fill_and_stroke_captures_custom_gs() {
832        let mut builder = PathBuilder::new(Ctm::identity());
833        build_triangle(&mut builder);
834
835        let gs = custom_gs();
836        let painted = builder.fill_and_stroke(&gs);
837        assert_eq!(painted.line_width, 2.5);
838        assert_eq!(painted.stroke_color, Color::Rgb(1.0, 0.0, 0.0));
839        assert_eq!(painted.fill_color, Color::Rgb(0.0, 0.0, 1.0));
840    }
841
842    // --- B* operator (fill even-odd + stroke) ---
843
844    #[test]
845    fn test_fill_even_odd_and_stroke() {
846        let mut builder = PathBuilder::new(Ctm::identity());
847        build_triangle(&mut builder);
848
849        let painted = builder.fill_even_odd_and_stroke(&default_gs());
850        assert!(painted.stroke);
851        assert!(painted.fill);
852        assert_eq!(painted.fill_rule, FillRule::EvenOdd);
853    }
854
855    // --- b operator (close + fill + stroke) ---
856
857    #[test]
858    fn test_close_fill_and_stroke() {
859        let mut builder = PathBuilder::new(Ctm::identity());
860        build_triangle(&mut builder);
861
862        let painted = builder.close_fill_and_stroke(&default_gs());
863        assert!(painted.stroke);
864        assert!(painted.fill);
865        assert_eq!(painted.fill_rule, FillRule::NonZeroWinding);
866        // Should have closepath
867        assert_eq!(painted.path.segments.len(), 4);
868        assert_eq!(painted.path.segments[3], PathSegment::ClosePath);
869    }
870
871    // --- b* operator (close + fill even-odd + stroke) ---
872
873    #[test]
874    fn test_close_fill_even_odd_and_stroke() {
875        let mut builder = PathBuilder::new(Ctm::identity());
876        build_triangle(&mut builder);
877
878        let painted = builder.close_fill_even_odd_and_stroke(&default_gs());
879        assert!(painted.stroke);
880        assert!(painted.fill);
881        assert_eq!(painted.fill_rule, FillRule::EvenOdd);
882        // Should have closepath
883        assert_eq!(painted.path.segments.len(), 4);
884        assert_eq!(painted.path.segments[3], PathSegment::ClosePath);
885    }
886
887    // --- n operator (end path, no painting) ---
888
889    #[test]
890    fn test_end_path_returns_none() {
891        let mut builder = PathBuilder::new(Ctm::identity());
892        build_triangle(&mut builder);
893
894        let result = builder.end_path();
895        assert!(result.is_none());
896    }
897
898    #[test]
899    fn test_end_path_clears_builder() {
900        let mut builder = PathBuilder::new(Ctm::identity());
901        build_triangle(&mut builder);
902        let _ = builder.end_path();
903
904        assert!(builder.is_empty());
905    }
906
907    // --- Sequential painting operations ---
908
909    #[test]
910    fn test_paint_then_build_new_path() {
911        let mut builder = PathBuilder::new(Ctm::identity());
912
913        // First path: stroke a line
914        builder.move_to(0.0, 0.0);
915        builder.line_to(100.0, 0.0);
916        let first = builder.stroke(&default_gs());
917        assert_eq!(first.path.segments.len(), 2);
918
919        // Second path: fill a rectangle
920        build_rectangle(&mut builder);
921        let second = builder.fill(&default_gs());
922        assert_eq!(second.path.segments.len(), 5);
923        assert!(second.fill);
924        assert!(!second.stroke);
925    }
926
927    #[test]
928    fn test_multiple_paints_independent() {
929        let mut builder = PathBuilder::new(Ctm::identity());
930
931        // First paint with one graphics state
932        builder.move_to(0.0, 0.0);
933        builder.line_to(50.0, 50.0);
934        let gs1 = GraphicsState {
935            line_width: 1.0,
936            stroke_color: Color::Rgb(1.0, 0.0, 0.0),
937            fill_color: Color::black(),
938            ..GraphicsState::default()
939        };
940        let first = builder.stroke(&gs1);
941
942        // Second paint with different graphics state
943        builder.move_to(10.0, 10.0);
944        builder.line_to(60.0, 60.0);
945        let gs2 = GraphicsState {
946            line_width: 3.0,
947            stroke_color: Color::Rgb(0.0, 1.0, 0.0),
948            fill_color: Color::black(),
949            ..GraphicsState::default()
950        };
951        let second = builder.stroke(&gs2);
952
953        // Each painted path should have its own state
954        assert_eq!(first.line_width, 1.0);
955        assert_eq!(first.stroke_color, Color::Rgb(1.0, 0.0, 0.0));
956        assert_eq!(second.line_width, 3.0);
957        assert_eq!(second.stroke_color, Color::Rgb(0.0, 1.0, 0.0));
958    }
959
960    // --- Painting with CTM-transformed paths ---
961
962    #[test]
963    fn test_stroke_with_ctm_transformed_path() {
964        let ctm = Ctm::new(2.0, 0.0, 0.0, 2.0, 10.0, 10.0);
965        let mut builder = PathBuilder::new(ctm);
966        builder.move_to(0.0, 0.0);
967        builder.line_to(50.0, 0.0);
968
969        let painted = builder.stroke(&default_gs());
970        // Coordinates should already be CTM-transformed
971        assert_eq!(
972            painted.path.segments[0],
973            PathSegment::MoveTo(Point::new(10.0, 10.0))
974        );
975        assert_eq!(
976            painted.path.segments[1],
977            PathSegment::LineTo(Point::new(110.0, 10.0))
978        );
979    }
980
981    // --- Painting with curves ---
982
983    #[test]
984    fn test_stroke_path_with_curves() {
985        let mut builder = PathBuilder::new(Ctm::identity());
986        builder.move_to(0.0, 0.0);
987        builder.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 0.0);
988
989        let painted = builder.stroke(&default_gs());
990        assert_eq!(painted.path.segments.len(), 2);
991        assert_eq!(
992            painted.path.segments[1],
993            PathSegment::CurveTo {
994                cp1: Point::new(10.0, 20.0),
995                cp2: Point::new(30.0, 40.0),
996                end: Point::new(50.0, 0.0),
997            }
998        );
999        assert!(painted.stroke);
1000    }
1001
1002    #[test]
1003    fn test_fill_path_with_curves() {
1004        let mut builder = PathBuilder::new(Ctm::identity());
1005        builder.move_to(0.0, 0.0);
1006        builder.curve_to(10.0, 50.0, 90.0, 50.0, 100.0, 0.0);
1007        builder.close_path();
1008
1009        let painted = builder.fill(&default_gs());
1010        assert!(painted.fill);
1011        assert!(!painted.stroke);
1012        assert_eq!(painted.path.segments.len(), 3); // moveto + curveto + closepath
1013    }
1014
1015    // --- d operator (inline dash pattern) ---
1016
1017    #[test]
1018    fn test_d_operator_sets_dash() {
1019        let mut gs = GraphicsState::default();
1020        assert!(gs.dash_pattern.is_solid());
1021
1022        // d operator: [3 2] 0 d
1023        gs.set_dash_pattern(vec![3.0, 2.0], 0.0);
1024        assert_eq!(gs.dash_pattern.dash_array, vec![3.0, 2.0]);
1025        assert_eq!(gs.dash_pattern.dash_phase, 0.0);
1026    }
1027
1028    #[test]
1029    fn test_d_operator_with_phase() {
1030        let mut gs = GraphicsState::default();
1031        gs.set_dash_pattern(vec![6.0, 3.0, 1.0, 3.0], 2.0);
1032        assert_eq!(gs.dash_pattern.dash_array, vec![6.0, 3.0, 1.0, 3.0]);
1033        assert_eq!(gs.dash_pattern.dash_phase, 2.0);
1034    }
1035
1036    #[test]
1037    fn test_d_operator_propagates_to_painted_path() {
1038        let mut gs = GraphicsState::default();
1039        gs.set_dash_pattern(vec![4.0, 2.0], 0.0);
1040
1041        let mut builder = PathBuilder::new(Ctm::identity());
1042        builder.move_to(0.0, 0.0);
1043        builder.line_to(100.0, 0.0);
1044
1045        let painted = builder.stroke(&gs);
1046        assert_eq!(painted.dash_pattern.dash_array, vec![4.0, 2.0]);
1047        assert!(!painted.dash_pattern.is_solid());
1048    }
1049
1050    // --- gs operator (ExtGState application) scenarios ---
1051
1052    #[test]
1053    fn test_gs_operator_line_width_propagates_to_paint() {
1054        let mut gs = GraphicsState::default();
1055        let ext = ExtGState {
1056            line_width: Some(4.0),
1057            ..ExtGState::default()
1058        };
1059        gs.apply_ext_gstate(&ext);
1060
1061        let mut builder = PathBuilder::new(Ctm::identity());
1062        builder.move_to(0.0, 0.0);
1063        builder.line_to(100.0, 0.0);
1064
1065        let painted = builder.stroke(&gs);
1066        assert_eq!(painted.line_width, 4.0);
1067    }
1068
1069    #[test]
1070    fn test_gs_operator_dash_propagates_to_paint() {
1071        let mut gs = GraphicsState::default();
1072        let ext = ExtGState {
1073            dash_pattern: Some(DashPattern::new(vec![10.0, 5.0], 0.0)),
1074            ..ExtGState::default()
1075        };
1076        gs.apply_ext_gstate(&ext);
1077
1078        let mut builder = PathBuilder::new(Ctm::identity());
1079        builder.move_to(0.0, 0.0);
1080        builder.line_to(100.0, 0.0);
1081
1082        let painted = builder.stroke(&gs);
1083        assert_eq!(painted.dash_pattern.dash_array, vec![10.0, 5.0]);
1084    }
1085
1086    #[test]
1087    fn test_gs_operator_opacity_propagates_to_paint() {
1088        let mut gs = GraphicsState::default();
1089        let ext = ExtGState {
1090            stroke_alpha: Some(0.5),
1091            fill_alpha: Some(0.25),
1092            ..ExtGState::default()
1093        };
1094        gs.apply_ext_gstate(&ext);
1095
1096        let mut builder = PathBuilder::new(Ctm::identity());
1097        builder.move_to(0.0, 0.0);
1098        builder.line_to(100.0, 100.0);
1099        builder.close_path();
1100
1101        let painted = builder.fill_and_stroke(&gs);
1102        assert_eq!(painted.stroke_alpha, 0.5);
1103        assert_eq!(painted.fill_alpha, 0.25);
1104    }
1105}