Skip to main content

fop_render/pdf/
graphics.rs

1//! PDF graphics operations
2//!
3//! Handles drawing operations like borders, rectangles, and lines in PDF.
4
5use fop_layout::area::BorderStyle;
6use fop_types::{Color, Length, Result};
7use std::fmt::Write as FmtWrite;
8
9/// PDF graphics context for drawing operations
10pub struct PdfGraphics {
11    /// Content stream operations
12    operations: String,
13}
14
15impl PdfGraphics {
16    /// Create a new graphics context
17    pub fn new() -> Self {
18        Self {
19            operations: String::new(),
20        }
21    }
22
23    /// Get the content stream
24    pub fn content(&self) -> &str {
25        &self.operations
26    }
27
28    /// Set stroke color (for lines and borders)
29    pub fn set_stroke_color(&mut self, color: Color) -> Result<()> {
30        writeln!(
31            &mut self.operations,
32            "{:.3} {:.3} {:.3} RG",
33            color.r_f32(),
34            color.g_f32(),
35            color.b_f32()
36        )
37        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
38        Ok(())
39    }
40
41    /// Set fill color (for rectangles)
42    pub fn set_fill_color(&mut self, color: Color) -> Result<()> {
43        writeln!(
44            &mut self.operations,
45            "{:.3} {:.3} {:.3} rg",
46            color.r_f32(),
47            color.g_f32(),
48            color.b_f32()
49        )
50        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
51        Ok(())
52    }
53
54    /// Set fill opacity (for fill operations)
55    ///
56    /// Sets the alpha value for fill operations using the /ca operator
57    /// in the graphics state dictionary. This requires ExtGState support.
58    ///
59    /// # Arguments
60    /// * `opacity` - Opacity value from 0.0 (transparent) to 1.0 (opaque)
61    /// * `gs_name` - Name of the graphics state resource (e.g., "gs1")
62    ///
63    /// # PDF Reference
64    /// See PDF specification section 8.4 for graphics state parameters.
65    pub fn set_opacity(&mut self, gs_name: &str) -> Result<()> {
66        writeln!(&mut self.operations, "/{} gs", gs_name)
67            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
68        Ok(())
69    }
70
71    /// Set stroke opacity (for stroke operations)
72    ///
73    /// Sets the alpha value for stroke operations using the /CA operator
74    /// in the graphics state dictionary. This requires ExtGState support.
75    ///
76    /// # Arguments
77    /// * `gs_name` - Name of the graphics state resource (e.g., "gs1")
78    ///
79    /// # PDF Reference
80    /// See PDF specification section 8.4 for graphics state parameters.
81    pub fn set_stroke_opacity(&mut self, gs_name: &str) -> Result<()> {
82        writeln!(&mut self.operations, "/{} gs", gs_name)
83            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
84        Ok(())
85    }
86
87    /// Set line width
88    pub fn set_line_width(&mut self, width: Length) -> Result<()> {
89        writeln!(&mut self.operations, "{:.3} w", width.to_pt())
90            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
91        Ok(())
92    }
93
94    /// Set dash pattern for line drawing
95    ///
96    /// # Arguments
97    /// * `dash_array` - Array of on/off lengths (e.g., &[3.0, 2.0] for dashed)
98    /// * `phase` - Offset into the dash pattern (usually 0)
99    ///
100    /// # Examples
101    /// - Solid: `set_dash_pattern(&[], 0)` or `[]0 d`
102    /// - Dashed: `set_dash_pattern(&[6.0, 3.0], 0)` or `[6 3] 0 d`
103    /// - Dotted: `set_dash_pattern(&[1.0, 2.0], 0)` or `[1 2] 0 d`
104    pub fn set_dash_pattern(&mut self, dash_array: &[f64], phase: f64) -> Result<()> {
105        write!(&mut self.operations, "[")
106            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
107        for (i, &dash) in dash_array.iter().enumerate() {
108            if i > 0 {
109                write!(&mut self.operations, " ")
110                    .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
111            }
112            write!(&mut self.operations, "{:.3}", dash)
113                .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
114        }
115        writeln!(&mut self.operations, "] {:.3} d", phase)
116            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
117        Ok(())
118    }
119
120    /// Draw a rectangle (stroke only)
121    pub fn draw_rectangle(
122        &mut self,
123        x: Length,
124        y: Length,
125        width: Length,
126        height: Length,
127    ) -> Result<()> {
128        writeln!(
129            &mut self.operations,
130            "{:.3} {:.3} {:.3} {:.3} re S",
131            x.to_pt(),
132            y.to_pt(),
133            width.to_pt(),
134            height.to_pt()
135        )
136        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
137        Ok(())
138    }
139
140    /// Fill a rectangle
141    pub fn fill_rectangle(
142        &mut self,
143        x: Length,
144        y: Length,
145        width: Length,
146        height: Length,
147    ) -> Result<()> {
148        self.fill_rectangle_with_radius(x, y, width, height, None)
149    }
150
151    /// Fill a rectangle with optional rounded corners
152    pub fn fill_rectangle_with_radius(
153        &mut self,
154        x: Length,
155        y: Length,
156        width: Length,
157        height: Length,
158        border_radius: Option<[Length; 4]>,
159    ) -> Result<()> {
160        if let Some(radii) = border_radius {
161            // Use rounded rectangle path for filling
162            self.draw_rounded_rectangle(x, y, width, height, radii, true)
163        } else {
164            // Use simple rectangle for filling
165            writeln!(
166                &mut self.operations,
167                "{:.3} {:.3} {:.3} {:.3} re f",
168                x.to_pt(),
169                y.to_pt(),
170                width.to_pt(),
171                height.to_pt()
172            )
173            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
174            Ok(())
175        }
176    }
177
178    /// Draw a rounded rectangle with independent corner radii
179    ///
180    /// Uses Bezier curves to approximate circular arcs for rounded corners.
181    /// The magic number 0.552284749831 is used to approximate a circle with cubic Bezier curves.
182    /// This is derived from 4*(sqrt(2)-1)/3, which minimizes the error between the arc and the curve.
183    ///
184    /// # Arguments
185    /// * `x, y` - Bottom-left corner of the rectangle
186    /// * `width, height` - Dimensions of the rectangle
187    /// * `radii` - Corner radii [top-left, top-right, bottom-right, bottom-left]
188    /// * `fill` - If true, fill the rectangle; if false, stroke it
189    #[allow(clippy::too_many_arguments)]
190    pub fn draw_rounded_rectangle(
191        &mut self,
192        x: Length,
193        y: Length,
194        width: Length,
195        height: Length,
196        radii: [Length; 4],
197        fill: bool,
198    ) -> Result<()> {
199        let x_pt = x.to_pt();
200        let y_pt = y.to_pt();
201        let w_pt = width.to_pt();
202        let h_pt = height.to_pt();
203
204        // Extract corner radii: [top-left, top-right, bottom-right, bottom-left]
205        let [tl, tr, br, bl] = radii;
206        let tl_pt = tl.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
207        let tr_pt = tr.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
208        let br_pt = br.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
209        let bl_pt = bl.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
210
211        // Magic number for Bezier curve approximation of circular arcs
212        // This is 4*(sqrt(2)-1)/3 ≈ 0.552284749831
213        const KAPPA: f64 = 0.552284749831;
214
215        // Start path from bottom-left corner, moving clockwise
216        // Bottom edge, starting after bottom-left corner
217        write!(&mut self.operations, "{:.3} {:.3} m ", x_pt + bl_pt, y_pt)
218            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
219
220        // Bottom edge to bottom-right corner
221        write!(
222            &mut self.operations,
223            "{:.3} {:.3} l ",
224            x_pt + w_pt - br_pt,
225            y_pt
226        )
227        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
228
229        // Bottom-right corner (if radius > 0)
230        if br_pt > 0.0 {
231            write!(
232                &mut self.operations,
233                "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
234                x_pt + w_pt - br_pt + br_pt * KAPPA,
235                y_pt,
236                x_pt + w_pt,
237                y_pt + br_pt - br_pt * KAPPA,
238                x_pt + w_pt,
239                y_pt + br_pt
240            )
241            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
242        }
243
244        // Right edge to top-right corner
245        write!(
246            &mut self.operations,
247            "{:.3} {:.3} l ",
248            x_pt + w_pt,
249            y_pt + h_pt - tr_pt
250        )
251        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
252
253        // Top-right corner (if radius > 0)
254        if tr_pt > 0.0 {
255            write!(
256                &mut self.operations,
257                "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
258                x_pt + w_pt,
259                y_pt + h_pt - tr_pt + tr_pt * KAPPA,
260                x_pt + w_pt - tr_pt + tr_pt * KAPPA,
261                y_pt + h_pt,
262                x_pt + w_pt - tr_pt,
263                y_pt + h_pt
264            )
265            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
266        }
267
268        // Top edge to top-left corner
269        write!(
270            &mut self.operations,
271            "{:.3} {:.3} l ",
272            x_pt + tl_pt,
273            y_pt + h_pt
274        )
275        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
276
277        // Top-left corner (if radius > 0)
278        if tl_pt > 0.0 {
279            write!(
280                &mut self.operations,
281                "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
282                x_pt + tl_pt - tl_pt * KAPPA,
283                y_pt + h_pt,
284                x_pt,
285                y_pt + h_pt - tl_pt + tl_pt * KAPPA,
286                x_pt,
287                y_pt + h_pt - tl_pt
288            )
289            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
290        }
291
292        // Left edge back to bottom-left corner
293        write!(&mut self.operations, "{:.3} {:.3} l ", x_pt, y_pt + bl_pt)
294            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
295
296        // Bottom-left corner (if radius > 0)
297        if bl_pt > 0.0 {
298            write!(
299                &mut self.operations,
300                "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
301                x_pt,
302                y_pt + bl_pt - bl_pt * KAPPA,
303                x_pt + bl_pt - bl_pt * KAPPA,
304                y_pt,
305                x_pt + bl_pt,
306                y_pt
307            )
308            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
309        }
310
311        // Close path and fill or stroke
312        if fill {
313            writeln!(&mut self.operations, "f")
314                .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
315        } else {
316            writeln!(&mut self.operations, "S")
317                .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
318        }
319
320        Ok(())
321    }
322
323    /// Draw a line
324    pub fn draw_line(&mut self, x1: Length, y1: Length, x2: Length, y2: Length) -> Result<()> {
325        writeln!(
326            &mut self.operations,
327            "{:.3} {:.3} m {:.3} {:.3} l S",
328            x1.to_pt(),
329            y1.to_pt(),
330            x2.to_pt(),
331            y2.to_pt()
332        )
333        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
334        Ok(())
335    }
336
337    /// Draw borders (all four sides)
338    #[allow(clippy::too_many_arguments)]
339    pub fn draw_borders(
340        &mut self,
341        x: Length,
342        y: Length,
343        width: Length,
344        height: Length,
345        border_widths: [Length; 4], // top, right, bottom, left
346        border_colors: [Color; 4],
347        border_styles: [BorderStyle; 4],
348    ) -> Result<()> {
349        self.draw_borders_with_radius(
350            x,
351            y,
352            width,
353            height,
354            border_widths,
355            border_colors,
356            border_styles,
357            None,
358        )
359    }
360
361    /// Draw borders with optional rounded corners
362    #[allow(clippy::too_many_arguments)]
363    pub fn draw_borders_with_radius(
364        &mut self,
365        x: Length,
366        y: Length,
367        width: Length,
368        height: Length,
369        border_widths: [Length; 4], // top, right, bottom, left
370        border_colors: [Color; 4],
371        border_styles: [BorderStyle; 4],
372        border_radius: Option<[Length; 4]>, // top-left, top-right, bottom-right, bottom-left
373    ) -> Result<()> {
374        let [top_width, right_width, bottom_width, left_width] = border_widths;
375        let [top_color, right_color, bottom_color, left_color] = border_colors;
376        let [top_style, right_style, bottom_style, left_style] = border_styles;
377
378        // If we have rounded corners and uniform border properties, use draw_rounded_rectangle
379        if let Some(radii) = border_radius {
380            // Check if all borders have the same color, width, and style
381            let uniform_color =
382                top_color == right_color && top_color == bottom_color && top_color == left_color;
383            let uniform_width =
384                top_width == right_width && top_width == bottom_width && top_width == left_width;
385            let uniform_style =
386                top_style == right_style && top_style == bottom_style && top_style == left_style;
387
388            if uniform_color
389                && uniform_width
390                && uniform_style
391                && top_width > Length::ZERO
392                && !matches!(top_style, BorderStyle::None | BorderStyle::Hidden)
393            {
394                // Use the optimized rounded rectangle path
395                self.set_stroke_color(top_color)?;
396                self.set_line_width(top_width)?;
397                self.apply_border_style(top_style)?;
398                self.draw_rounded_rectangle(x, y, width, height, radii, false)?;
399                // Reset to solid
400                self.set_dash_pattern(&[], 0.0)?;
401                return Ok(());
402            }
403        }
404
405        // Fall back to drawing individual border sides
406        // For now, we draw straight lines even with border-radius
407        // A full implementation would draw arcs for each corner
408        // Top border
409        if top_width > Length::ZERO && !matches!(top_style, BorderStyle::None | BorderStyle::Hidden)
410        {
411            self.set_stroke_color(top_color)?;
412            self.set_line_width(top_width)?;
413            self.apply_border_style(top_style)?;
414            let y_top = y + height;
415            self.draw_line(x, y_top, x + width, y_top)?;
416            // Reset to solid for next border
417            self.set_dash_pattern(&[], 0.0)?;
418        }
419
420        // Right border
421        if right_width > Length::ZERO
422            && !matches!(right_style, BorderStyle::None | BorderStyle::Hidden)
423        {
424            self.set_stroke_color(right_color)?;
425            self.set_line_width(right_width)?;
426            self.apply_border_style(right_style)?;
427            let x_right = x + width;
428            self.draw_line(x_right, y, x_right, y + height)?;
429            // Reset to solid for next border
430            self.set_dash_pattern(&[], 0.0)?;
431        }
432
433        // Bottom border
434        if bottom_width > Length::ZERO
435            && !matches!(bottom_style, BorderStyle::None | BorderStyle::Hidden)
436        {
437            self.set_stroke_color(bottom_color)?;
438            self.set_line_width(bottom_width)?;
439            self.apply_border_style(bottom_style)?;
440            self.draw_line(x, y, x + width, y)?;
441            // Reset to solid for next border
442            self.set_dash_pattern(&[], 0.0)?;
443        }
444
445        // Left border
446        if left_width > Length::ZERO
447            && !matches!(left_style, BorderStyle::None | BorderStyle::Hidden)
448        {
449            self.set_stroke_color(left_color)?;
450            self.set_line_width(left_width)?;
451            self.apply_border_style(left_style)?;
452            self.draw_line(x, y, x, y + height)?;
453            // Reset to solid for next border
454            self.set_dash_pattern(&[], 0.0)?;
455        }
456
457        Ok(())
458    }
459
460    /// Apply border style by setting appropriate dash pattern
461    fn apply_border_style(&mut self, style: BorderStyle) -> Result<()> {
462        match style {
463            BorderStyle::Solid => self.set_dash_pattern(&[], 0.0),
464            BorderStyle::Dashed => self.set_dash_pattern(&[6.0, 3.0], 0.0),
465            BorderStyle::Dotted => self.set_dash_pattern(&[1.0, 2.0], 0.0),
466            // For now, treat other styles as solid (double, groove, ridge, inset, outset)
467            // These would require more complex rendering
468            _ => self.set_dash_pattern(&[], 0.0),
469        }
470    }
471
472    /// Save graphics state
473    pub fn save_state(&mut self) -> Result<()> {
474        writeln!(&mut self.operations, "q")
475            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
476        Ok(())
477    }
478
479    /// Restore graphics state
480    pub fn restore_state(&mut self) -> Result<()> {
481        writeln!(&mut self.operations, "Q")
482            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
483        Ok(())
484    }
485
486    /// Save graphics state and set clipping path
487    ///
488    /// This method saves the current graphics state and establishes a rectangular
489    /// clipping path. Content drawn after this call will be clipped to the specified
490    /// rectangle until restore_clip_state() is called.
491    ///
492    /// PDF operators used:
493    /// - q: Save graphics state
494    /// - re: Rectangle path
495    /// - W: Set clipping path (intersect with current path)
496    /// - n: End path without stroking or filling
497    ///
498    /// # Arguments
499    /// * `x, y` - Bottom-left corner of clipping rectangle (PDF coordinates)
500    /// * `width, height` - Dimensions of clipping rectangle
501    ///
502    /// # PDF Reference
503    /// See PDF specification section 8.5 for clipping path details.
504    pub fn save_clip_state(
505        &mut self,
506        x: Length,
507        y: Length,
508        width: Length,
509        height: Length,
510    ) -> Result<()> {
511        // Save graphics state
512        writeln!(&mut self.operations, "q")
513            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
514
515        // Define rectangle path and set as clipping path
516        writeln!(
517            &mut self.operations,
518            "{:.3} {:.3} {:.3} {:.3} re W n",
519            x.to_pt(),
520            y.to_pt(),
521            width.to_pt(),
522            height.to_pt()
523        )
524        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
525
526        Ok(())
527    }
528
529    /// Restore graphics state after clipping
530    ///
531    /// This restores the graphics state that was saved by save_clip_state(),
532    /// removing the clipping path.
533    ///
534    /// PDF operator used:
535    /// - Q: Restore graphics state
536    pub fn restore_clip_state(&mut self) -> Result<()> {
537        self.restore_state()
538    }
539
540    /// Fill a rectangle with a gradient
541    ///
542    /// Uses PDF shading pattern to fill the specified rectangle with a gradient.
543    ///
544    /// # Arguments
545    /// * `x, y` - Bottom-left corner of the rectangle
546    /// * `width, height` - Dimensions of the rectangle
547    /// * `gradient_index` - Index of the gradient shading resource (Sh0, Sh1, etc.)
548    ///
549    /// # PDF Reference
550    /// See PDF specification section 8.7 for shading patterns.
551    /// Uses the /sh operator to paint with shading pattern.
552    pub fn fill_gradient(
553        &mut self,
554        x: Length,
555        y: Length,
556        width: Length,
557        height: Length,
558        gradient_index: usize,
559    ) -> Result<()> {
560        // Save graphics state
561        writeln!(&mut self.operations, "q")
562            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
563
564        // Set up transformation matrix to map gradient to rectangle
565        // We need to translate and scale the coordinate system
566        writeln!(
567            &mut self.operations,
568            "{:.3} 0 0 {:.3} {:.3} {:.3} cm",
569            width.to_pt() / 100.0,  // Scale from 0-100 to width
570            height.to_pt() / 100.0, // Scale from 0-100 to height
571            x.to_pt(),
572            y.to_pt()
573        )
574        .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
575
576        // Paint with shading pattern
577        writeln!(&mut self.operations, "/Sh{} sh", gradient_index)
578            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
579
580        // Restore graphics state
581        writeln!(&mut self.operations, "Q")
582            .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
583
584        Ok(())
585    }
586
587    /// Fill a rectangle with a gradient and optional rounded corners
588    pub fn fill_gradient_with_radius(
589        &mut self,
590        x: Length,
591        y: Length,
592        width: Length,
593        height: Length,
594        gradient_index: usize,
595        border_radius: Option<[Length; 4]>,
596    ) -> Result<()> {
597        if border_radius.is_some() {
598            // For rounded corners, we need to clip with a rounded rectangle path first
599            // Save state
600            self.save_state()?;
601
602            // Create rounded rectangle path for clipping
603            if let Some(radii) = border_radius {
604                self.draw_rounded_rectangle(x, y, width, height, radii, false)?;
605                // Use as clipping path (W n)
606                writeln!(&mut self.operations, "W n")
607                    .map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
608            }
609
610            // Now paint the gradient
611            self.fill_gradient(x, y, width, height, gradient_index)?;
612
613            // Restore state (removes clipping)
614            self.restore_state()?;
615        } else {
616            // No rounding, just fill
617            self.fill_gradient(x, y, width, height, gradient_index)?;
618        }
619
620        Ok(())
621    }
622}
623
624impl Default for PdfGraphics {
625    fn default() -> Self {
626        Self::new()
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633
634    #[test]
635    fn test_graphics_creation() {
636        let graphics = PdfGraphics::new();
637        assert_eq!(graphics.content(), "");
638    }
639
640    #[test]
641    fn test_set_stroke_color() {
642        let mut graphics = PdfGraphics::new();
643        graphics
644            .set_stroke_color(Color::RED)
645            .expect("test: should succeed");
646
647        assert!(graphics.content().contains("1.000 0.000 0.000 RG"));
648    }
649
650    #[test]
651    fn test_set_fill_color() {
652        let mut graphics = PdfGraphics::new();
653        graphics
654            .set_fill_color(Color::BLUE)
655            .expect("test: should succeed");
656
657        assert!(graphics.content().contains("0.000 0.000 1.000 rg"));
658    }
659
660    #[test]
661    fn test_set_line_width() {
662        let mut graphics = PdfGraphics::new();
663        graphics
664            .set_line_width(Length::from_pt(2.0))
665            .expect("test: should succeed");
666
667        assert!(graphics.content().contains("2.000 w"));
668    }
669
670    #[test]
671    fn test_draw_rectangle() {
672        let mut graphics = PdfGraphics::new();
673        graphics
674            .draw_rectangle(
675                Length::from_pt(10.0),
676                Length::from_pt(20.0),
677                Length::from_pt(100.0),
678                Length::from_pt(50.0),
679            )
680            .expect("test: should succeed");
681
682        assert!(graphics
683            .content()
684            .contains("10.000 20.000 100.000 50.000 re S"));
685    }
686
687    #[test]
688    fn test_fill_rectangle() {
689        let mut graphics = PdfGraphics::new();
690        graphics
691            .fill_rectangle(
692                Length::from_pt(0.0),
693                Length::from_pt(0.0),
694                Length::from_pt(50.0),
695                Length::from_pt(50.0),
696            )
697            .expect("test: should succeed");
698
699        assert!(graphics.content().contains("re f"));
700    }
701
702    #[test]
703    fn test_draw_line() {
704        let mut graphics = PdfGraphics::new();
705        graphics
706            .draw_line(
707                Length::from_pt(0.0),
708                Length::from_pt(0.0),
709                Length::from_pt(100.0),
710                Length::from_pt(100.0),
711            )
712            .expect("test: should succeed");
713
714        assert!(graphics.content().contains("m"));
715        assert!(graphics.content().contains("l S"));
716    }
717
718    #[test]
719    fn test_draw_borders() {
720        let mut graphics = PdfGraphics::new();
721        graphics
722            .draw_borders(
723                Length::from_pt(10.0),
724                Length::from_pt(10.0),
725                Length::from_pt(100.0),
726                Length::from_pt(50.0),
727                [Length::from_pt(1.0); 4],
728                [Color::BLACK; 4],
729                [BorderStyle::Solid; 4],
730            )
731            .expect("test: should succeed");
732
733        // Should contain line drawing operations
734        assert!(graphics.content().contains("m"));
735        assert!(graphics.content().contains("l S"));
736    }
737
738    #[test]
739    fn test_save_restore_state() {
740        let mut graphics = PdfGraphics::new();
741        graphics.save_state().expect("test: should succeed");
742        graphics.restore_state().expect("test: should succeed");
743
744        assert!(graphics.content().contains("q"));
745        assert!(graphics.content().contains("Q"));
746    }
747
748    #[test]
749    fn test_set_dash_pattern() {
750        let mut graphics = PdfGraphics::new();
751
752        // Solid (no dash)
753        graphics
754            .set_dash_pattern(&[], 0.0)
755            .expect("test: should succeed");
756        assert!(graphics.content().contains("[] 0.000 d"));
757
758        // Dashed
759        let mut graphics2 = PdfGraphics::new();
760        graphics2
761            .set_dash_pattern(&[6.0, 3.0], 0.0)
762            .expect("test: should succeed");
763        assert!(graphics2.content().contains("[6.000 3.000] 0.000 d"));
764
765        // Dotted
766        let mut graphics3 = PdfGraphics::new();
767        graphics3
768            .set_dash_pattern(&[1.0, 2.0], 0.0)
769            .expect("test: should succeed");
770        assert!(graphics3.content().contains("[1.000 2.000] 0.000 d"));
771    }
772
773    #[test]
774    fn test_border_styles() {
775        let mut graphics = PdfGraphics::new();
776
777        // Test dashed border
778        graphics
779            .draw_borders(
780                Length::from_pt(10.0),
781                Length::from_pt(10.0),
782                Length::from_pt(100.0),
783                Length::from_pt(50.0),
784                [Length::from_pt(2.0); 4],
785                [Color::RED; 4],
786                [BorderStyle::Dashed; 4],
787            )
788            .expect("test: should succeed");
789
790        assert!(graphics.content().contains("[6.000 3.000] 0.000 d"));
791        assert!(graphics.content().contains("1.000 0.000 0.000 RG")); // Red color
792    }
793}
794
795#[cfg(test)]
796mod tests_extended {
797    use super::*;
798
799    // ── Color tests ──────────────────────────────────────────────────────────
800
801    #[test]
802    fn test_set_stroke_color_green() {
803        let mut g = PdfGraphics::new();
804        g.set_stroke_color(Color::GREEN)
805            .expect("test: should succeed");
806        assert!(g.content().contains("0.000 1.000 0.000 RG"));
807    }
808
809    #[test]
810    fn test_set_stroke_color_black() {
811        let mut g = PdfGraphics::new();
812        g.set_stroke_color(Color::BLACK)
813            .expect("test: should succeed");
814        assert!(g.content().contains("0.000 0.000 0.000 RG"));
815    }
816
817    #[test]
818    fn test_set_stroke_color_white() {
819        let mut g = PdfGraphics::new();
820        g.set_stroke_color(Color::WHITE)
821            .expect("test: should succeed");
822        assert!(g.content().contains("1.000 1.000 1.000 RG"));
823    }
824
825    #[test]
826    fn test_set_fill_color_red() {
827        let mut g = PdfGraphics::new();
828        g.set_fill_color(Color::RED).expect("test: should succeed");
829        assert!(g.content().contains("1.000 0.000 0.000 rg"));
830    }
831
832    #[test]
833    fn test_set_fill_color_green() {
834        let mut g = PdfGraphics::new();
835        g.set_fill_color(Color::GREEN)
836            .expect("test: should succeed");
837        assert!(g.content().contains("0.000 1.000 0.000 rg"));
838    }
839
840    #[test]
841    fn test_set_fill_color_custom_rgb() {
842        let mut g = PdfGraphics::new();
843        // rgb(128, 64, 32) → f32 ≈ 0.502, 0.251, 0.125
844        g.set_fill_color(Color::rgb(128, 64, 32))
845            .expect("test: should succeed");
846        let content = g.content().to_string();
847        assert!(content.contains("rg"), "fill operator missing: {}", content);
848        // Verify it does NOT contain the uppercase stroke operator
849        assert!(!content.contains("RG"), "should use lowercase rg not RG");
850    }
851
852    #[test]
853    fn test_stroke_uses_rg_uppercase_operator() {
854        let mut g = PdfGraphics::new();
855        g.set_stroke_color(Color::BLUE)
856            .expect("test: should succeed");
857        // PDF stroke color operator is RG (uppercase)
858        assert!(g.content().contains("RG"));
859        // Must not accidentally use fill operator
860        assert!(!g.content().contains(" rg"));
861    }
862
863    #[test]
864    fn test_fill_uses_rg_lowercase_operator() {
865        let mut g = PdfGraphics::new();
866        g.set_fill_color(Color::BLUE).expect("test: should succeed");
867        // PDF fill color operator is rg (lowercase)
868        assert!(g.content().contains(" rg") || g.content().ends_with("rg\n"));
869        // Must not accidentally use stroke operator
870        assert!(!g.content().contains("RG"));
871    }
872
873    // ── Line width tests ─────────────────────────────────────────────────────
874
875    #[test]
876    fn test_line_width_zero() {
877        let mut g = PdfGraphics::new();
878        g.set_line_width(Length::ZERO)
879            .expect("test: should succeed");
880        assert!(g.content().contains("0.000 w"));
881    }
882
883    #[test]
884    fn test_line_width_fractional() {
885        let mut g = PdfGraphics::new();
886        g.set_line_width(Length::from_pt(0.5))
887            .expect("test: should succeed");
888        assert!(g.content().contains("0.500 w"));
889    }
890
891    #[test]
892    fn test_line_width_large() {
893        let mut g = PdfGraphics::new();
894        g.set_line_width(Length::from_pt(10.0))
895            .expect("test: should succeed");
896        assert!(g.content().contains("10.000 w"));
897    }
898
899    // ── Path construction operator tests ─────────────────────────────────────
900
901    #[test]
902    fn test_draw_line_uses_m_operator() {
903        let mut g = PdfGraphics::new();
904        g.draw_line(
905            Length::from_pt(5.0),
906            Length::from_pt(10.0),
907            Length::from_pt(50.0),
908            Length::from_pt(100.0),
909        )
910        .expect("test: should succeed");
911        let c = g.content();
912        // moveto
913        assert!(c.contains("5.000 10.000 m"), "expected 'm' operator: {}", c);
914        // lineto
915        assert!(
916            c.contains("50.000 100.000 l"),
917            "expected 'l' operator: {}",
918            c
919        );
920        // stroke
921        assert!(c.contains("S"), "expected 'S' operator: {}", c);
922    }
923
924    #[test]
925    fn test_draw_rectangle_uses_re_operator() {
926        let mut g = PdfGraphics::new();
927        g.draw_rectangle(
928            Length::from_pt(1.0),
929            Length::from_pt(2.0),
930            Length::from_pt(30.0),
931            Length::from_pt(40.0),
932        )
933        .expect("test: should succeed");
934        let c = g.content();
935        // `re` appended by `S` (stroke)
936        assert!(c.contains("re S"), "expected 're S': {}", c);
937        assert!(c.contains("1.000 2.000 30.000 40.000"));
938    }
939
940    #[test]
941    fn test_fill_rectangle_uses_re_f_operator() {
942        let mut g = PdfGraphics::new();
943        g.fill_rectangle(
944            Length::from_pt(3.0),
945            Length::from_pt(4.0),
946            Length::from_pt(60.0),
947            Length::from_pt(80.0),
948        )
949        .expect("test: should succeed");
950        let c = g.content();
951        // `re` appended by `f` (fill)
952        assert!(c.contains("re f"), "expected 're f': {}", c);
953        assert!(c.contains("3.000 4.000 60.000 80.000"));
954    }
955
956    // ── Path painting operator tests ─────────────────────────────────────────
957
958    #[test]
959    fn test_draw_line_stroke_operator_s() {
960        let mut g = PdfGraphics::new();
961        g.draw_line(
962            Length::from_pt(0.0),
963            Length::from_pt(0.0),
964            Length::from_pt(100.0),
965            Length::from_pt(0.0),
966        )
967        .expect("test: should succeed");
968        // S = stroke path
969        assert!(g.content().contains("l S"), "stroke 'S' missing");
970    }
971
972    #[test]
973    fn test_fill_rectangle_f_operator() {
974        let mut g = PdfGraphics::new();
975        g.fill_rectangle(
976            Length::ZERO,
977            Length::ZERO,
978            Length::from_pt(50.0),
979            Length::from_pt(50.0),
980        )
981        .expect("test: should succeed");
982        // f = fill path
983        assert!(g.content().contains("re f"));
984    }
985
986    // ── Graphics state save/restore ──────────────────────────────────────────
987
988    #[test]
989    fn test_save_state_q_operator() {
990        let mut g = PdfGraphics::new();
991        g.save_state().expect("test: should succeed");
992        assert!(g.content().contains("q\n"), "q operator missing");
993    }
994
995    #[test]
996    fn test_restore_state_q_operator() {
997        let mut g = PdfGraphics::new();
998        g.restore_state().expect("test: should succeed");
999        assert!(g.content().contains("Q\n"), "Q operator missing");
1000    }
1001
1002    #[test]
1003    fn test_save_restore_nesting() {
1004        let mut g = PdfGraphics::new();
1005        g.save_state().expect("test: should succeed");
1006        g.save_state().expect("test: should succeed");
1007        g.restore_state().expect("test: should succeed");
1008        g.restore_state().expect("test: should succeed");
1009        let c = g.content();
1010        assert_eq!(c.matches("q\n").count(), 2);
1011        assert_eq!(c.matches("Q\n").count(), 2);
1012    }
1013
1014    // ── Dash pattern tests ───────────────────────────────────────────────────
1015
1016    #[test]
1017    fn test_dash_pattern_single_value() {
1018        let mut g = PdfGraphics::new();
1019        g.set_dash_pattern(&[3.0], 0.0)
1020            .expect("test: should succeed");
1021        assert!(g.content().contains("[3.000] 0.000 d"));
1022    }
1023
1024    #[test]
1025    fn test_dash_pattern_with_phase() {
1026        let mut g = PdfGraphics::new();
1027        g.set_dash_pattern(&[4.0, 2.0], 1.0)
1028            .expect("test: should succeed");
1029        assert!(g.content().contains("[4.000 2.000] 1.000 d"));
1030    }
1031
1032    #[test]
1033    fn test_dash_pattern_reset_to_solid() {
1034        let mut g = PdfGraphics::new();
1035        g.set_dash_pattern(&[6.0, 3.0], 0.0)
1036            .expect("test: should succeed");
1037        g.set_dash_pattern(&[], 0.0).expect("test: should succeed");
1038        let c = g.content();
1039        assert!(c.contains("[] 0.000 d"), "solid reset missing: {}", c);
1040    }
1041
1042    // ── Clipping path tests ──────────────────────────────────────────────────
1043
1044    #[test]
1045    fn test_save_clip_state_uses_w_n() {
1046        let mut g = PdfGraphics::new();
1047        g.save_clip_state(
1048            Length::from_pt(10.0),
1049            Length::from_pt(20.0),
1050            Length::from_pt(100.0),
1051            Length::from_pt(50.0),
1052        )
1053        .expect("test: should succeed");
1054        let c = g.content();
1055        // q saves state
1056        assert!(c.contains("q\n"), "q missing: {}", c);
1057        // W sets clipping path, n ends path
1058        assert!(c.contains("re W n"), "clipping 'W n' missing: {}", c);
1059        // Rectangle coordinates present
1060        assert!(c.contains("10.000 20.000 100.000 50.000"), "coords missing");
1061    }
1062
1063    #[test]
1064    fn test_restore_clip_state_uses_q_operator() {
1065        let mut g = PdfGraphics::new();
1066        g.save_clip_state(
1067            Length::ZERO,
1068            Length::ZERO,
1069            Length::from_pt(200.0),
1070            Length::from_pt(100.0),
1071        )
1072        .expect("test: should succeed");
1073        g.restore_clip_state().expect("test: should succeed");
1074        let c = g.content();
1075        assert!(
1076            c.contains("Q\n"),
1077            "Q operator missing after restore_clip_state"
1078        );
1079    }
1080
1081    // ── CTM / transformation matrix tests ───────────────────────────────────
1082
1083    #[test]
1084    fn test_fill_gradient_cm_operator() {
1085        let mut g = PdfGraphics::new();
1086        g.fill_gradient(
1087            Length::from_pt(10.0),
1088            Length::from_pt(20.0),
1089            Length::from_pt(100.0),
1090            Length::from_pt(50.0),
1091            0,
1092        )
1093        .expect("test: should succeed");
1094        let c = g.content();
1095        // cm = current transformation matrix
1096        assert!(c.contains("cm"), "cm operator missing: {}", c);
1097        // Shading operator
1098        assert!(c.contains("/Sh0 sh"), "shading ref missing: {}", c);
1099        // State saved and restored around gradient
1100        assert!(c.contains("q\n"), "q missing");
1101        assert!(c.contains("Q\n"), "Q missing");
1102    }
1103
1104    #[test]
1105    fn test_fill_gradient_index_increments() {
1106        let mut g = PdfGraphics::new();
1107        g.fill_gradient(
1108            Length::ZERO,
1109            Length::ZERO,
1110            Length::from_pt(50.0),
1111            Length::from_pt(50.0),
1112            1,
1113        )
1114        .expect("test: should succeed");
1115        assert!(g.content().contains("/Sh1 sh"));
1116    }
1117
1118    // ── Opacity graphics state operator ─────────────────────────────────────
1119
1120    #[test]
1121    fn test_set_opacity_gs_operator() {
1122        let mut g = PdfGraphics::new();
1123        g.set_opacity("gs1").expect("test: should succeed");
1124        assert!(g.content().contains("/gs1 gs"));
1125    }
1126
1127    #[test]
1128    fn test_set_stroke_opacity_gs_operator() {
1129        let mut g = PdfGraphics::new();
1130        g.set_stroke_opacity("gs2").expect("test: should succeed");
1131        assert!(g.content().contains("/gs2 gs"));
1132    }
1133
1134    // ── Border style tests ───────────────────────────────────────────────────
1135
1136    #[test]
1137    fn test_draw_borders_dotted() {
1138        let mut g = PdfGraphics::new();
1139        g.draw_borders(
1140            Length::from_pt(5.0),
1141            Length::from_pt(5.0),
1142            Length::from_pt(80.0),
1143            Length::from_pt(40.0),
1144            [Length::from_pt(1.0); 4],
1145            [Color::BLACK; 4],
1146            [BorderStyle::Dotted; 4],
1147        )
1148        .expect("test: should succeed");
1149        // Dotted → [1.000 2.000] dash pattern
1150        assert!(
1151            g.content().contains("[1.000 2.000]"),
1152            "dotted dash pattern missing"
1153        );
1154    }
1155
1156    #[test]
1157    fn test_draw_borders_none_produces_no_lines() {
1158        let mut g = PdfGraphics::new();
1159        g.draw_borders(
1160            Length::from_pt(5.0),
1161            Length::from_pt(5.0),
1162            Length::from_pt(80.0),
1163            Length::from_pt(40.0),
1164            [Length::from_pt(1.0); 4],
1165            [Color::BLACK; 4],
1166            [BorderStyle::None; 4],
1167        )
1168        .expect("test: should succeed");
1169        // No-draw borders should produce empty or no stroke operations
1170        let c = g.content();
1171        assert!(
1172            !c.contains("l S"),
1173            "none border style should not produce 'l S': {}",
1174            c
1175        );
1176    }
1177
1178    #[test]
1179    fn test_draw_borders_zero_width_produces_no_lines() {
1180        let mut g = PdfGraphics::new();
1181        g.draw_borders(
1182            Length::from_pt(0.0),
1183            Length::from_pt(0.0),
1184            Length::from_pt(100.0),
1185            Length::from_pt(50.0),
1186            [Length::ZERO; 4],
1187            [Color::BLACK; 4],
1188            [BorderStyle::Solid; 4],
1189        )
1190        .expect("test: should succeed");
1191        // Zero-width borders should not produce stroke operations
1192        assert!(!g.content().contains("l S"), "zero-width should not stroke");
1193    }
1194
1195    // ── Rounded rectangle tests ──────────────────────────────────────────────
1196
1197    #[test]
1198    fn test_fill_rectangle_with_radius_uses_bezier() {
1199        let mut g = PdfGraphics::new();
1200        let radii = [Length::from_pt(5.0); 4];
1201        g.fill_rectangle_with_radius(
1202            Length::from_pt(10.0),
1203            Length::from_pt(10.0),
1204            Length::from_pt(100.0),
1205            Length::from_pt(50.0),
1206            Some(radii),
1207        )
1208        .expect("test: should succeed");
1209        let c = g.content();
1210        // Bezier curves use the `c` operator
1211        assert!(c.contains(" c "), "Bezier 'c' operator missing: {}", c);
1212        // Closed with fill
1213        assert!(c.contains("f\n"), "fill 'f' missing");
1214    }
1215
1216    #[test]
1217    fn test_fill_rectangle_no_radius_uses_simple_re() {
1218        let mut g = PdfGraphics::new();
1219        g.fill_rectangle_with_radius(
1220            Length::from_pt(10.0),
1221            Length::from_pt(10.0),
1222            Length::from_pt(100.0),
1223            Length::from_pt(50.0),
1224            None,
1225        )
1226        .expect("test: should succeed");
1227        let c = g.content();
1228        // Simple rectangle uses `re f`
1229        assert!(c.contains("re f"), "expected 're f': {}", c);
1230    }
1231
1232    // ── Default trait test ───────────────────────────────────────────────────
1233
1234    #[test]
1235    fn test_default_creates_empty_graphics() {
1236        let g = PdfGraphics::default();
1237        assert_eq!(g.content(), "");
1238    }
1239}