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