oxidize_pdf/graphics/
mod.rs

1mod color;
2mod color_profiles;
3mod path;
4mod patterns;
5mod pdf_image;
6mod shadings;
7mod state;
8
9pub use color::Color;
10pub use color_profiles::{IccColorSpace, IccProfile, IccProfileManager, StandardIccProfile};
11pub use path::{LineCap, LineJoin, PathBuilder};
12pub use patterns::{
13    PaintType, PatternGraphicsContext, PatternManager, PatternMatrix, PatternType, TilingPattern,
14    TilingType,
15};
16pub use pdf_image::{ColorSpace as ImageColorSpace, Image, ImageFormat};
17pub use shadings::{
18    AxialShading, ColorStop, FunctionBasedShading, Point, RadialShading, ShadingDefinition,
19    ShadingManager, ShadingPattern, ShadingType,
20};
21pub use state::{
22    BlendMode, ExtGState, ExtGStateFont, ExtGStateManager, Halftone, LineDashPattern,
23    RenderingIntent, SoftMask, TransferFunction,
24};
25
26use crate::error::Result;
27use crate::text::{ColumnContent, ColumnLayout, Font, ListElement, Table};
28use std::fmt::Write;
29
30#[derive(Clone)]
31pub struct GraphicsContext {
32    operations: String,
33    current_color: Color,
34    stroke_color: Color,
35    line_width: f64,
36    fill_opacity: f64,
37    stroke_opacity: f64,
38    // Extended Graphics State support
39    extgstate_manager: ExtGStateManager,
40    pending_extgstate: Option<ExtGState>,
41    current_dash_pattern: Option<LineDashPattern>,
42    current_miter_limit: f64,
43    current_line_cap: LineCap,
44    current_line_join: LineJoin,
45    current_rendering_intent: RenderingIntent,
46    current_flatness: f64,
47    current_smoothness: f64,
48}
49
50impl Default for GraphicsContext {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl GraphicsContext {
57    pub fn new() -> Self {
58        Self {
59            operations: String::new(),
60            current_color: Color::black(),
61            stroke_color: Color::black(),
62            line_width: 1.0,
63            fill_opacity: 1.0,
64            stroke_opacity: 1.0,
65            // Extended Graphics State defaults
66            extgstate_manager: ExtGStateManager::new(),
67            pending_extgstate: None,
68            current_dash_pattern: None,
69            current_miter_limit: 10.0,
70            current_line_cap: LineCap::Butt,
71            current_line_join: LineJoin::Miter,
72            current_rendering_intent: RenderingIntent::RelativeColorimetric,
73            current_flatness: 1.0,
74            current_smoothness: 0.0,
75        }
76    }
77
78    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
79        writeln!(&mut self.operations, "{x:.2} {y:.2} m").unwrap();
80        self
81    }
82
83    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
84        writeln!(&mut self.operations, "{x:.2} {y:.2} l").unwrap();
85        self
86    }
87
88    pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
89        writeln!(
90            &mut self.operations,
91            "{x1:.2} {y1:.2} {x2:.2} {y2:.2} {x3:.2} {y3:.2} c"
92        )
93        .unwrap();
94        self
95    }
96
97    pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
98        writeln!(
99            &mut self.operations,
100            "{x:.2} {y:.2} {width:.2} {height:.2} re"
101        )
102        .unwrap();
103        self
104    }
105
106    pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
107        let k = 0.552284749831;
108        let r = radius;
109
110        self.move_to(cx + r, cy);
111        self.curve_to(cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r);
112        self.curve_to(cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy);
113        self.curve_to(cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r);
114        self.curve_to(cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy);
115        self.close_path()
116    }
117
118    pub fn close_path(&mut self) -> &mut Self {
119        self.operations.push_str("h\n");
120        self
121    }
122
123    pub fn stroke(&mut self) -> &mut Self {
124        self.apply_pending_extgstate().unwrap_or_default();
125        self.apply_stroke_color();
126        self.operations.push_str("S\n");
127        self
128    }
129
130    pub fn fill(&mut self) -> &mut Self {
131        self.apply_pending_extgstate().unwrap_or_default();
132        self.apply_fill_color();
133        self.operations.push_str("f\n");
134        self
135    }
136
137    pub fn fill_stroke(&mut self) -> &mut Self {
138        self.apply_pending_extgstate().unwrap_or_default();
139        self.apply_fill_color();
140        self.apply_stroke_color();
141        self.operations.push_str("B\n");
142        self
143    }
144
145    pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
146        self.stroke_color = color;
147        self
148    }
149
150    pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
151        self.current_color = color;
152        self
153    }
154
155    pub fn set_line_width(&mut self, width: f64) -> &mut Self {
156        self.line_width = width;
157        writeln!(&mut self.operations, "{width:.2} w").unwrap();
158        self
159    }
160
161    pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
162        self.current_line_cap = cap;
163        writeln!(&mut self.operations, "{} J", cap as u8).unwrap();
164        self
165    }
166
167    pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
168        self.current_line_join = join;
169        writeln!(&mut self.operations, "{} j", join as u8).unwrap();
170        self
171    }
172
173    /// Set the opacity for both fill and stroke operations (0.0 to 1.0)
174    pub fn set_opacity(&mut self, opacity: f64) -> &mut Self {
175        let opacity = opacity.clamp(0.0, 1.0);
176        self.fill_opacity = opacity;
177        self.stroke_opacity = opacity;
178        self
179    }
180
181    /// Set the fill opacity (0.0 to 1.0)
182    pub fn set_fill_opacity(&mut self, opacity: f64) -> &mut Self {
183        self.fill_opacity = opacity.clamp(0.0, 1.0);
184        self
185    }
186
187    /// Set the stroke opacity (0.0 to 1.0)
188    pub fn set_stroke_opacity(&mut self, opacity: f64) -> &mut Self {
189        self.stroke_opacity = opacity.clamp(0.0, 1.0);
190        self
191    }
192
193    pub fn save_state(&mut self) -> &mut Self {
194        self.operations.push_str("q\n");
195        self
196    }
197
198    pub fn restore_state(&mut self) -> &mut Self {
199        self.operations.push_str("Q\n");
200        self
201    }
202
203    pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
204        writeln!(&mut self.operations, "1 0 0 1 {tx:.2} {ty:.2} cm").unwrap();
205        self
206    }
207
208    pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
209        writeln!(&mut self.operations, "{sx:.2} 0 0 {sy:.2} 0 0 cm").unwrap();
210        self
211    }
212
213    pub fn rotate(&mut self, angle: f64) -> &mut Self {
214        let cos = angle.cos();
215        let sin = angle.sin();
216        writeln!(
217            &mut self.operations,
218            "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
219            cos, sin, -sin, cos
220        )
221        .unwrap();
222        self
223    }
224
225    pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
226        writeln!(
227            &mut self.operations,
228            "{a:.2} {b:.2} {c:.2} {d:.2} {e:.2} {f:.2} cm"
229        )
230        .unwrap();
231        self
232    }
233
234    pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
235        self.rect(x, y, width, height)
236    }
237
238    pub fn draw_image(
239        &mut self,
240        image_name: &str,
241        x: f64,
242        y: f64,
243        width: f64,
244        height: f64,
245    ) -> &mut Self {
246        // Save graphics state
247        self.save_state();
248
249        // Set up transformation matrix for image placement
250        // PDF coordinate system has origin at bottom-left, so we need to translate and scale
251        writeln!(
252            &mut self.operations,
253            "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
254        )
255        .unwrap();
256
257        // Draw the image XObject
258        writeln!(&mut self.operations, "/{image_name} Do").unwrap();
259
260        // Restore graphics state
261        self.restore_state();
262
263        self
264    }
265
266    fn apply_stroke_color(&mut self) {
267        match self.stroke_color {
268            Color::Rgb(r, g, b) => {
269                writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} RG").unwrap();
270            }
271            Color::Gray(g) => {
272                writeln!(&mut self.operations, "{g:.3} G").unwrap();
273            }
274            Color::Cmyk(c, m, y, k) => {
275                writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} K").unwrap();
276            }
277        }
278    }
279
280    fn apply_fill_color(&mut self) {
281        match self.current_color {
282            Color::Rgb(r, g, b) => {
283                writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} rg").unwrap();
284            }
285            Color::Gray(g) => {
286                writeln!(&mut self.operations, "{g:.3} g").unwrap();
287            }
288            Color::Cmyk(c, m, y, k) => {
289                writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} k").unwrap();
290            }
291        }
292    }
293
294    pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
295        Ok(self.operations.as_bytes().to_vec())
296    }
297
298    /// Check if transparency is used (opacity != 1.0)
299    pub fn uses_transparency(&self) -> bool {
300        self.fill_opacity < 1.0 || self.stroke_opacity < 1.0
301    }
302
303    /// Generate the graphics state dictionary for transparency
304    pub fn generate_graphics_state_dict(&self) -> Option<String> {
305        if !self.uses_transparency() {
306            return None;
307        }
308
309        let mut dict = String::from("<< /Type /ExtGState");
310
311        if self.fill_opacity < 1.0 {
312            write!(&mut dict, " /ca {:.3}", self.fill_opacity).unwrap();
313        }
314
315        if self.stroke_opacity < 1.0 {
316            write!(&mut dict, " /CA {:.3}", self.stroke_opacity).unwrap();
317        }
318
319        dict.push_str(" >>");
320        Some(dict)
321    }
322
323    /// Get the current fill color
324    pub fn fill_color(&self) -> Color {
325        self.current_color
326    }
327
328    /// Get the current stroke color
329    pub fn stroke_color(&self) -> Color {
330        self.stroke_color
331    }
332
333    /// Get the current line width
334    pub fn line_width(&self) -> f64 {
335        self.line_width
336    }
337
338    /// Get the current fill opacity
339    pub fn fill_opacity(&self) -> f64 {
340        self.fill_opacity
341    }
342
343    /// Get the current stroke opacity
344    pub fn stroke_opacity(&self) -> f64 {
345        self.stroke_opacity
346    }
347
348    /// Get the operations string
349    pub fn operations(&self) -> &str {
350        &self.operations
351    }
352
353    /// Clear all operations
354    pub fn clear(&mut self) {
355        self.operations.clear();
356    }
357
358    /// Begin a text object
359    pub fn begin_text(&mut self) -> &mut Self {
360        self.operations.push_str("BT\n");
361        self
362    }
363
364    /// End a text object
365    pub fn end_text(&mut self) -> &mut Self {
366        self.operations.push_str("ET\n");
367        self
368    }
369
370    /// Set font and size
371    pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
372        writeln!(&mut self.operations, "/{} {} Tf", font.pdf_name(), size).unwrap();
373        self
374    }
375
376    /// Set text position
377    pub fn set_text_position(&mut self, x: f64, y: f64) -> &mut Self {
378        writeln!(&mut self.operations, "{x:.2} {y:.2} Td").unwrap();
379        self
380    }
381
382    /// Show text
383    pub fn show_text(&mut self, text: &str) -> Result<&mut Self> {
384        // Escape special characters in PDF string
385        self.operations.push('(');
386        for ch in text.chars() {
387            match ch {
388                '(' => self.operations.push_str("\\("),
389                ')' => self.operations.push_str("\\)"),
390                '\\' => self.operations.push_str("\\\\"),
391                '\n' => self.operations.push_str("\\n"),
392                '\r' => self.operations.push_str("\\r"),
393                '\t' => self.operations.push_str("\\t"),
394                _ => self.operations.push(ch),
395            }
396        }
397        self.operations.push_str(") Tj\n");
398        Ok(self)
399    }
400
401    /// Render a table
402    pub fn render_table(&mut self, table: &Table) -> Result<()> {
403        table.render(self)
404    }
405
406    /// Render a list
407    pub fn render_list(&mut self, list: &ListElement) -> Result<()> {
408        match list {
409            ListElement::Ordered(ordered) => ordered.render(self),
410            ListElement::Unordered(unordered) => unordered.render(self),
411        }
412    }
413
414    /// Render column layout
415    pub fn render_column_layout(
416        &mut self,
417        layout: &ColumnLayout,
418        content: &ColumnContent,
419        x: f64,
420        y: f64,
421        height: f64,
422    ) -> Result<()> {
423        layout.render(self, content, x, y, height)
424    }
425
426    // Extended Graphics State methods
427
428    /// Set line dash pattern
429    pub fn set_line_dash_pattern(&mut self, pattern: LineDashPattern) -> &mut Self {
430        self.current_dash_pattern = Some(pattern.clone());
431        writeln!(&mut self.operations, "{} d", pattern.to_pdf_string()).unwrap();
432        self
433    }
434
435    /// Set line dash pattern to solid (no dashes)
436    pub fn set_line_solid(&mut self) -> &mut Self {
437        self.current_dash_pattern = None;
438        self.operations.push_str("[] 0 d\n");
439        self
440    }
441
442    /// Set miter limit
443    pub fn set_miter_limit(&mut self, limit: f64) -> &mut Self {
444        self.current_miter_limit = limit.max(1.0);
445        writeln!(&mut self.operations, "{:.2} M", self.current_miter_limit).unwrap();
446        self
447    }
448
449    /// Set rendering intent
450    pub fn set_rendering_intent(&mut self, intent: RenderingIntent) -> &mut Self {
451        self.current_rendering_intent = intent;
452        writeln!(&mut self.operations, "/{} ri", intent.pdf_name()).unwrap();
453        self
454    }
455
456    /// Set flatness tolerance
457    pub fn set_flatness(&mut self, flatness: f64) -> &mut Self {
458        self.current_flatness = flatness.clamp(0.0, 100.0);
459        writeln!(&mut self.operations, "{:.2} i", self.current_flatness).unwrap();
460        self
461    }
462
463    /// Apply an ExtGState dictionary immediately
464    pub fn apply_extgstate(&mut self, state: ExtGState) -> Result<&mut Self> {
465        let state_name = self.extgstate_manager.add_state(state)?;
466        writeln!(&mut self.operations, "/{state_name} gs").unwrap();
467        Ok(self)
468    }
469
470    /// Store an ExtGState to be applied before the next drawing operation
471    fn set_pending_extgstate(&mut self, state: ExtGState) {
472        self.pending_extgstate = Some(state);
473    }
474
475    /// Apply any pending ExtGState before drawing
476    fn apply_pending_extgstate(&mut self) -> Result<()> {
477        if let Some(state) = self.pending_extgstate.take() {
478            let state_name = self.extgstate_manager.add_state(state)?;
479            writeln!(&mut self.operations, "/{state_name} gs").unwrap();
480        }
481        Ok(())
482    }
483
484    /// Create and apply a custom ExtGState
485    pub fn with_extgstate<F>(&mut self, builder: F) -> Result<&mut Self>
486    where
487        F: FnOnce(ExtGState) -> ExtGState,
488    {
489        let state = builder(ExtGState::new());
490        self.apply_extgstate(state)
491    }
492
493    /// Set blend mode for transparency
494    pub fn set_blend_mode(&mut self, mode: BlendMode) -> Result<&mut Self> {
495        let state = ExtGState::new().with_blend_mode(mode);
496        self.apply_extgstate(state)
497    }
498
499    /// Set alpha for both stroke and fill operations
500    pub fn set_alpha(&mut self, alpha: f64) -> Result<&mut Self> {
501        let state = ExtGState::new().with_alpha(alpha);
502        self.set_pending_extgstate(state);
503        Ok(self)
504    }
505
506    /// Set alpha for stroke operations only
507    pub fn set_alpha_stroke(&mut self, alpha: f64) -> Result<&mut Self> {
508        let state = ExtGState::new().with_alpha_stroke(alpha);
509        self.set_pending_extgstate(state);
510        Ok(self)
511    }
512
513    /// Set alpha for fill operations only
514    pub fn set_alpha_fill(&mut self, alpha: f64) -> Result<&mut Self> {
515        let state = ExtGState::new().with_alpha_fill(alpha);
516        self.set_pending_extgstate(state);
517        Ok(self)
518    }
519
520    /// Set overprint for stroke operations
521    pub fn set_overprint_stroke(&mut self, overprint: bool) -> Result<&mut Self> {
522        let state = ExtGState::new().with_overprint_stroke(overprint);
523        self.apply_extgstate(state)
524    }
525
526    /// Set overprint for fill operations
527    pub fn set_overprint_fill(&mut self, overprint: bool) -> Result<&mut Self> {
528        let state = ExtGState::new().with_overprint_fill(overprint);
529        self.apply_extgstate(state)
530    }
531
532    /// Set stroke adjustment
533    pub fn set_stroke_adjustment(&mut self, adjustment: bool) -> Result<&mut Self> {
534        let state = ExtGState::new().with_stroke_adjustment(adjustment);
535        self.apply_extgstate(state)
536    }
537
538    /// Set smoothness tolerance
539    pub fn set_smoothness(&mut self, smoothness: f64) -> Result<&mut Self> {
540        self.current_smoothness = smoothness.clamp(0.0, 1.0);
541        let state = ExtGState::new().with_smoothness(self.current_smoothness);
542        self.apply_extgstate(state)
543    }
544
545    // Getters for extended graphics state
546
547    /// Get current line dash pattern
548    pub fn line_dash_pattern(&self) -> Option<&LineDashPattern> {
549        self.current_dash_pattern.as_ref()
550    }
551
552    /// Get current miter limit
553    pub fn miter_limit(&self) -> f64 {
554        self.current_miter_limit
555    }
556
557    /// Get current line cap
558    pub fn line_cap(&self) -> LineCap {
559        self.current_line_cap
560    }
561
562    /// Get current line join
563    pub fn line_join(&self) -> LineJoin {
564        self.current_line_join
565    }
566
567    /// Get current rendering intent
568    pub fn rendering_intent(&self) -> RenderingIntent {
569        self.current_rendering_intent
570    }
571
572    /// Get current flatness tolerance
573    pub fn flatness(&self) -> f64 {
574        self.current_flatness
575    }
576
577    /// Get current smoothness tolerance
578    pub fn smoothness(&self) -> f64 {
579        self.current_smoothness
580    }
581
582    /// Get the ExtGState manager (for advanced usage)
583    pub fn extgstate_manager(&self) -> &ExtGStateManager {
584        &self.extgstate_manager
585    }
586
587    /// Get mutable ExtGState manager (for advanced usage)
588    pub fn extgstate_manager_mut(&mut self) -> &mut ExtGStateManager {
589        &mut self.extgstate_manager
590    }
591
592    /// Generate ExtGState resource dictionary for PDF
593    pub fn generate_extgstate_resources(&self) -> Result<String> {
594        self.extgstate_manager.to_resource_dictionary()
595    }
596
597    /// Check if any extended graphics states are defined
598    pub fn has_extgstates(&self) -> bool {
599        self.extgstate_manager.count() > 0
600    }
601
602    /// Add a command to the operations
603    pub fn add_command(&mut self, command: &str) {
604        self.operations.push_str(command);
605        self.operations.push('\n');
606    }
607
608    /// Create clipping path from current path using non-zero winding rule
609    pub fn clip(&mut self) -> &mut Self {
610        self.operations.push_str("W\n");
611        self
612    }
613
614    /// Create clipping path from current path using even-odd rule
615    pub fn clip_even_odd(&mut self) -> &mut Self {
616        self.operations.push_str("W*\n");
617        self
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    #[test]
626    fn test_graphics_context_new() {
627        let ctx = GraphicsContext::new();
628        assert_eq!(ctx.fill_color(), Color::black());
629        assert_eq!(ctx.stroke_color(), Color::black());
630        assert_eq!(ctx.line_width(), 1.0);
631        assert_eq!(ctx.fill_opacity(), 1.0);
632        assert_eq!(ctx.stroke_opacity(), 1.0);
633        assert!(ctx.operations().is_empty());
634    }
635
636    #[test]
637    fn test_graphics_context_default() {
638        let ctx = GraphicsContext::default();
639        assert_eq!(ctx.fill_color(), Color::black());
640        assert_eq!(ctx.stroke_color(), Color::black());
641        assert_eq!(ctx.line_width(), 1.0);
642    }
643
644    #[test]
645    fn test_move_to() {
646        let mut ctx = GraphicsContext::new();
647        ctx.move_to(10.0, 20.0);
648        assert!(ctx.operations().contains("10.00 20.00 m\n"));
649    }
650
651    #[test]
652    fn test_line_to() {
653        let mut ctx = GraphicsContext::new();
654        ctx.line_to(30.0, 40.0);
655        assert!(ctx.operations().contains("30.00 40.00 l\n"));
656    }
657
658    #[test]
659    fn test_curve_to() {
660        let mut ctx = GraphicsContext::new();
661        ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
662        assert!(ctx
663            .operations()
664            .contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
665    }
666
667    #[test]
668    fn test_rect() {
669        let mut ctx = GraphicsContext::new();
670        ctx.rect(10.0, 20.0, 100.0, 50.0);
671        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
672    }
673
674    #[test]
675    fn test_rectangle_alias() {
676        let mut ctx = GraphicsContext::new();
677        ctx.rectangle(10.0, 20.0, 100.0, 50.0);
678        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
679    }
680
681    #[test]
682    fn test_circle() {
683        let mut ctx = GraphicsContext::new();
684        ctx.circle(50.0, 50.0, 25.0);
685
686        let ops = ctx.operations();
687        // Check that it starts with move to radius point
688        assert!(ops.contains("75.00 50.00 m\n"));
689        // Check that it contains curve operations
690        assert!(ops.contains(" c\n"));
691        // Check that it closes the path
692        assert!(ops.contains("h\n"));
693    }
694
695    #[test]
696    fn test_close_path() {
697        let mut ctx = GraphicsContext::new();
698        ctx.close_path();
699        assert!(ctx.operations().contains("h\n"));
700    }
701
702    #[test]
703    fn test_stroke() {
704        let mut ctx = GraphicsContext::new();
705        ctx.set_stroke_color(Color::red());
706        ctx.rect(0.0, 0.0, 10.0, 10.0);
707        ctx.stroke();
708
709        let ops = ctx.operations();
710        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
711        assert!(ops.contains("S\n"));
712    }
713
714    #[test]
715    fn test_fill() {
716        let mut ctx = GraphicsContext::new();
717        ctx.set_fill_color(Color::blue());
718        ctx.rect(0.0, 0.0, 10.0, 10.0);
719        ctx.fill();
720
721        let ops = ctx.operations();
722        assert!(ops.contains("0.000 0.000 1.000 rg\n"));
723        assert!(ops.contains("f\n"));
724    }
725
726    #[test]
727    fn test_fill_stroke() {
728        let mut ctx = GraphicsContext::new();
729        ctx.set_fill_color(Color::green());
730        ctx.set_stroke_color(Color::red());
731        ctx.rect(0.0, 0.0, 10.0, 10.0);
732        ctx.fill_stroke();
733
734        let ops = ctx.operations();
735        assert!(ops.contains("0.000 1.000 0.000 rg\n"));
736        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
737        assert!(ops.contains("B\n"));
738    }
739
740    #[test]
741    fn test_set_stroke_color() {
742        let mut ctx = GraphicsContext::new();
743        ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
744        assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
745    }
746
747    #[test]
748    fn test_set_fill_color() {
749        let mut ctx = GraphicsContext::new();
750        ctx.set_fill_color(Color::gray(0.5));
751        assert_eq!(ctx.fill_color(), Color::Gray(0.5));
752    }
753
754    #[test]
755    fn test_set_line_width() {
756        let mut ctx = GraphicsContext::new();
757        ctx.set_line_width(2.5);
758        assert_eq!(ctx.line_width(), 2.5);
759        assert!(ctx.operations().contains("2.50 w\n"));
760    }
761
762    #[test]
763    fn test_set_line_cap() {
764        let mut ctx = GraphicsContext::new();
765        ctx.set_line_cap(LineCap::Round);
766        assert!(ctx.operations().contains("1 J\n"));
767
768        ctx.set_line_cap(LineCap::Butt);
769        assert!(ctx.operations().contains("0 J\n"));
770
771        ctx.set_line_cap(LineCap::Square);
772        assert!(ctx.operations().contains("2 J\n"));
773    }
774
775    #[test]
776    fn test_set_line_join() {
777        let mut ctx = GraphicsContext::new();
778        ctx.set_line_join(LineJoin::Round);
779        assert!(ctx.operations().contains("1 j\n"));
780
781        ctx.set_line_join(LineJoin::Miter);
782        assert!(ctx.operations().contains("0 j\n"));
783
784        ctx.set_line_join(LineJoin::Bevel);
785        assert!(ctx.operations().contains("2 j\n"));
786    }
787
788    #[test]
789    fn test_save_restore_state() {
790        let mut ctx = GraphicsContext::new();
791        ctx.save_state();
792        assert!(ctx.operations().contains("q\n"));
793
794        ctx.restore_state();
795        assert!(ctx.operations().contains("Q\n"));
796    }
797
798    #[test]
799    fn test_translate() {
800        let mut ctx = GraphicsContext::new();
801        ctx.translate(50.0, 100.0);
802        assert!(ctx.operations().contains("1 0 0 1 50.00 100.00 cm\n"));
803    }
804
805    #[test]
806    fn test_scale() {
807        let mut ctx = GraphicsContext::new();
808        ctx.scale(2.0, 3.0);
809        assert!(ctx.operations().contains("2.00 0 0 3.00 0 0 cm\n"));
810    }
811
812    #[test]
813    fn test_rotate() {
814        let mut ctx = GraphicsContext::new();
815        let angle = std::f64::consts::PI / 4.0; // 45 degrees
816        ctx.rotate(angle);
817
818        let ops = ctx.operations();
819        assert!(ops.contains(" cm\n"));
820        // Should contain cos and sin values
821        assert!(ops.contains("0.707107")); // Approximate cos(45°)
822    }
823
824    #[test]
825    fn test_transform() {
826        let mut ctx = GraphicsContext::new();
827        ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
828        assert!(ctx
829            .operations()
830            .contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
831    }
832
833    #[test]
834    fn test_draw_image() {
835        let mut ctx = GraphicsContext::new();
836        ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
837
838        let ops = ctx.operations();
839        assert!(ops.contains("q\n")); // Save state
840        assert!(ops.contains("100.00 0 0 150.00 10.00 20.00 cm\n")); // Transform
841        assert!(ops.contains("/Image1 Do\n")); // Draw image
842        assert!(ops.contains("Q\n")); // Restore state
843    }
844
845    #[test]
846    fn test_gray_color_operations() {
847        let mut ctx = GraphicsContext::new();
848        ctx.set_stroke_color(Color::gray(0.5));
849        ctx.set_fill_color(Color::gray(0.7));
850        ctx.stroke();
851        ctx.fill();
852
853        let ops = ctx.operations();
854        assert!(ops.contains("0.500 G\n")); // Stroke gray
855        assert!(ops.contains("0.700 g\n")); // Fill gray
856    }
857
858    #[test]
859    fn test_cmyk_color_operations() {
860        let mut ctx = GraphicsContext::new();
861        ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
862        ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
863        ctx.stroke();
864        ctx.fill();
865
866        let ops = ctx.operations();
867        assert!(ops.contains("0.100 0.200 0.300 0.400 K\n")); // Stroke CMYK
868        assert!(ops.contains("0.500 0.600 0.700 0.800 k\n")); // Fill CMYK
869    }
870
871    #[test]
872    fn test_method_chaining() {
873        let mut ctx = GraphicsContext::new();
874        ctx.move_to(0.0, 0.0)
875            .line_to(10.0, 0.0)
876            .line_to(10.0, 10.0)
877            .line_to(0.0, 10.0)
878            .close_path()
879            .set_fill_color(Color::red())
880            .fill();
881
882        let ops = ctx.operations();
883        assert!(ops.contains("0.00 0.00 m\n"));
884        assert!(ops.contains("10.00 0.00 l\n"));
885        assert!(ops.contains("10.00 10.00 l\n"));
886        assert!(ops.contains("0.00 10.00 l\n"));
887        assert!(ops.contains("h\n"));
888        assert!(ops.contains("f\n"));
889    }
890
891    #[test]
892    fn test_generate_operations() {
893        let mut ctx = GraphicsContext::new();
894        ctx.rect(0.0, 0.0, 10.0, 10.0);
895
896        let result = ctx.generate_operations();
897        assert!(result.is_ok());
898        let bytes = result.unwrap();
899        let ops_string = String::from_utf8(bytes).unwrap();
900        assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
901    }
902
903    #[test]
904    fn test_clear_operations() {
905        let mut ctx = GraphicsContext::new();
906        ctx.rect(0.0, 0.0, 10.0, 10.0);
907        assert!(!ctx.operations().is_empty());
908
909        ctx.clear();
910        assert!(ctx.operations().is_empty());
911    }
912
913    #[test]
914    fn test_complex_path() {
915        let mut ctx = GraphicsContext::new();
916        ctx.save_state()
917            .translate(100.0, 100.0)
918            .rotate(std::f64::consts::PI / 6.0)
919            .scale(2.0, 2.0)
920            .set_line_width(2.0)
921            .set_stroke_color(Color::blue())
922            .move_to(0.0, 0.0)
923            .line_to(50.0, 0.0)
924            .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
925            .close_path()
926            .stroke()
927            .restore_state();
928
929        let ops = ctx.operations();
930        assert!(ops.contains("q\n"));
931        assert!(ops.contains("cm\n"));
932        assert!(ops.contains("2.00 w\n"));
933        assert!(ops.contains("0.000 0.000 1.000 RG\n"));
934        assert!(ops.contains("S\n"));
935        assert!(ops.contains("Q\n"));
936    }
937
938    #[test]
939    fn test_graphics_context_clone() {
940        let mut ctx = GraphicsContext::new();
941        ctx.set_fill_color(Color::red());
942        ctx.set_stroke_color(Color::blue());
943        ctx.set_line_width(3.0);
944        ctx.set_opacity(0.5);
945        ctx.rect(0.0, 0.0, 10.0, 10.0);
946
947        let ctx_clone = ctx.clone();
948        assert_eq!(ctx_clone.fill_color(), Color::red());
949        assert_eq!(ctx_clone.stroke_color(), Color::blue());
950        assert_eq!(ctx_clone.line_width(), 3.0);
951        assert_eq!(ctx_clone.fill_opacity(), 0.5);
952        assert_eq!(ctx_clone.stroke_opacity(), 0.5);
953        assert_eq!(ctx_clone.operations(), ctx.operations());
954    }
955
956    #[test]
957    fn test_set_opacity() {
958        let mut ctx = GraphicsContext::new();
959
960        // Test setting opacity
961        ctx.set_opacity(0.5);
962        assert_eq!(ctx.fill_opacity(), 0.5);
963        assert_eq!(ctx.stroke_opacity(), 0.5);
964
965        // Test clamping to valid range
966        ctx.set_opacity(1.5);
967        assert_eq!(ctx.fill_opacity(), 1.0);
968        assert_eq!(ctx.stroke_opacity(), 1.0);
969
970        ctx.set_opacity(-0.5);
971        assert_eq!(ctx.fill_opacity(), 0.0);
972        assert_eq!(ctx.stroke_opacity(), 0.0);
973    }
974
975    #[test]
976    fn test_set_fill_opacity() {
977        let mut ctx = GraphicsContext::new();
978
979        ctx.set_fill_opacity(0.3);
980        assert_eq!(ctx.fill_opacity(), 0.3);
981        assert_eq!(ctx.stroke_opacity(), 1.0); // Should not affect stroke
982
983        // Test clamping
984        ctx.set_fill_opacity(2.0);
985        assert_eq!(ctx.fill_opacity(), 1.0);
986    }
987
988    #[test]
989    fn test_set_stroke_opacity() {
990        let mut ctx = GraphicsContext::new();
991
992        ctx.set_stroke_opacity(0.7);
993        assert_eq!(ctx.stroke_opacity(), 0.7);
994        assert_eq!(ctx.fill_opacity(), 1.0); // Should not affect fill
995
996        // Test clamping
997        ctx.set_stroke_opacity(-1.0);
998        assert_eq!(ctx.stroke_opacity(), 0.0);
999    }
1000
1001    #[test]
1002    fn test_uses_transparency() {
1003        let mut ctx = GraphicsContext::new();
1004
1005        // Initially no transparency
1006        assert!(!ctx.uses_transparency());
1007
1008        // With fill transparency
1009        ctx.set_fill_opacity(0.5);
1010        assert!(ctx.uses_transparency());
1011
1012        // Reset and test stroke transparency
1013        ctx.set_fill_opacity(1.0);
1014        assert!(!ctx.uses_transparency());
1015        ctx.set_stroke_opacity(0.8);
1016        assert!(ctx.uses_transparency());
1017
1018        // Both transparent
1019        ctx.set_fill_opacity(0.5);
1020        assert!(ctx.uses_transparency());
1021    }
1022
1023    #[test]
1024    fn test_generate_graphics_state_dict() {
1025        let mut ctx = GraphicsContext::new();
1026
1027        // No transparency
1028        assert_eq!(ctx.generate_graphics_state_dict(), None);
1029
1030        // Fill opacity only
1031        ctx.set_fill_opacity(0.5);
1032        let dict = ctx.generate_graphics_state_dict().unwrap();
1033        assert!(dict.contains("/Type /ExtGState"));
1034        assert!(dict.contains("/ca 0.500"));
1035        assert!(!dict.contains("/CA"));
1036
1037        // Stroke opacity only
1038        ctx.set_fill_opacity(1.0);
1039        ctx.set_stroke_opacity(0.75);
1040        let dict = ctx.generate_graphics_state_dict().unwrap();
1041        assert!(dict.contains("/Type /ExtGState"));
1042        assert!(dict.contains("/CA 0.750"));
1043        assert!(!dict.contains("/ca"));
1044
1045        // Both opacities
1046        ctx.set_fill_opacity(0.25);
1047        let dict = ctx.generate_graphics_state_dict().unwrap();
1048        assert!(dict.contains("/Type /ExtGState"));
1049        assert!(dict.contains("/ca 0.250"));
1050        assert!(dict.contains("/CA 0.750"));
1051    }
1052
1053    #[test]
1054    fn test_opacity_with_graphics_operations() {
1055        let mut ctx = GraphicsContext::new();
1056
1057        ctx.set_fill_color(Color::red())
1058            .set_opacity(0.5)
1059            .rect(10.0, 10.0, 100.0, 100.0)
1060            .fill();
1061
1062        assert_eq!(ctx.fill_opacity(), 0.5);
1063        assert_eq!(ctx.stroke_opacity(), 0.5);
1064
1065        let ops = ctx.operations();
1066        assert!(ops.contains("10.00 10.00 100.00 100.00 re"));
1067        assert!(ops.contains("1.000 0.000 0.000 rg")); // Red color
1068        assert!(ops.contains("f")); // Fill
1069    }
1070
1071    #[test]
1072    fn test_begin_end_text() {
1073        let mut ctx = GraphicsContext::new();
1074        ctx.begin_text();
1075        assert!(ctx.operations().contains("BT\n"));
1076
1077        ctx.end_text();
1078        assert!(ctx.operations().contains("ET\n"));
1079    }
1080
1081    #[test]
1082    fn test_set_font() {
1083        let mut ctx = GraphicsContext::new();
1084        ctx.set_font(Font::Helvetica, 12.0);
1085        assert!(ctx.operations().contains("/Helvetica 12 Tf\n"));
1086
1087        ctx.set_font(Font::TimesBold, 14.5);
1088        assert!(ctx.operations().contains("/Times-Bold 14.5 Tf\n"));
1089    }
1090
1091    #[test]
1092    fn test_set_text_position() {
1093        let mut ctx = GraphicsContext::new();
1094        ctx.set_text_position(100.0, 200.0);
1095        assert!(ctx.operations().contains("100.00 200.00 Td\n"));
1096    }
1097
1098    #[test]
1099    fn test_show_text() {
1100        let mut ctx = GraphicsContext::new();
1101        ctx.show_text("Hello World").unwrap();
1102        assert!(ctx.operations().contains("(Hello World) Tj\n"));
1103    }
1104
1105    #[test]
1106    fn test_show_text_with_escaping() {
1107        let mut ctx = GraphicsContext::new();
1108        ctx.show_text("Test (parentheses)").unwrap();
1109        assert!(ctx.operations().contains("(Test \\(parentheses\\)) Tj\n"));
1110
1111        ctx.clear();
1112        ctx.show_text("Back\\slash").unwrap();
1113        assert!(ctx.operations().contains("(Back\\\\slash) Tj\n"));
1114
1115        ctx.clear();
1116        ctx.show_text("Line\nBreak").unwrap();
1117        assert!(ctx.operations().contains("(Line\\nBreak) Tj\n"));
1118    }
1119
1120    #[test]
1121    fn test_text_operations_chaining() {
1122        let mut ctx = GraphicsContext::new();
1123        ctx.begin_text()
1124            .set_font(Font::Courier, 10.0)
1125            .set_text_position(50.0, 100.0)
1126            .show_text("Test")
1127            .unwrap()
1128            .end_text();
1129
1130        let ops = ctx.operations();
1131        assert!(ops.contains("BT\n"));
1132        assert!(ops.contains("/Courier 10 Tf\n"));
1133        assert!(ops.contains("50.00 100.00 Td\n"));
1134        assert!(ops.contains("(Test) Tj\n"));
1135        assert!(ops.contains("ET\n"));
1136    }
1137
1138    #[test]
1139    fn test_clip() {
1140        let mut ctx = GraphicsContext::new();
1141        ctx.clip();
1142        assert!(ctx.operations().contains("W\n"));
1143    }
1144
1145    #[test]
1146    fn test_clip_even_odd() {
1147        let mut ctx = GraphicsContext::new();
1148        ctx.clip_even_odd();
1149        assert!(ctx.operations().contains("W*\n"));
1150    }
1151
1152    #[test]
1153    fn test_clipping_with_path() {
1154        let mut ctx = GraphicsContext::new();
1155
1156        // Create a rectangular clipping path
1157        ctx.rect(10.0, 10.0, 100.0, 50.0).clip();
1158
1159        let ops = ctx.operations();
1160        assert!(ops.contains("10.00 10.00 100.00 50.00 re\n"));
1161        assert!(ops.contains("W\n"));
1162    }
1163
1164    #[test]
1165    fn test_clipping_even_odd_with_path() {
1166        let mut ctx = GraphicsContext::new();
1167
1168        // Create a complex path and clip with even-odd rule
1169        ctx.move_to(0.0, 0.0)
1170            .line_to(100.0, 0.0)
1171            .line_to(100.0, 100.0)
1172            .line_to(0.0, 100.0)
1173            .close_path()
1174            .clip_even_odd();
1175
1176        let ops = ctx.operations();
1177        assert!(ops.contains("0.00 0.00 m\n"));
1178        assert!(ops.contains("100.00 0.00 l\n"));
1179        assert!(ops.contains("100.00 100.00 l\n"));
1180        assert!(ops.contains("0.00 100.00 l\n"));
1181        assert!(ops.contains("h\n"));
1182        assert!(ops.contains("W*\n"));
1183    }
1184
1185    #[test]
1186    fn test_clipping_chaining() {
1187        let mut ctx = GraphicsContext::new();
1188
1189        // Test method chaining with clipping
1190        ctx.save_state()
1191            .rect(20.0, 20.0, 60.0, 60.0)
1192            .clip()
1193            .set_fill_color(Color::red())
1194            .rect(0.0, 0.0, 100.0, 100.0)
1195            .fill()
1196            .restore_state();
1197
1198        let ops = ctx.operations();
1199        assert!(ops.contains("q\n"));
1200        assert!(ops.contains("20.00 20.00 60.00 60.00 re\n"));
1201        assert!(ops.contains("W\n"));
1202        assert!(ops.contains("1.000 0.000 0.000 rg\n"));
1203        assert!(ops.contains("0.00 0.00 100.00 100.00 re\n"));
1204        assert!(ops.contains("f\n"));
1205        assert!(ops.contains("Q\n"));
1206    }
1207
1208    #[test]
1209    fn test_multiple_clipping_regions() {
1210        let mut ctx = GraphicsContext::new();
1211
1212        // Test nested clipping regions
1213        ctx.save_state()
1214            .rect(0.0, 0.0, 200.0, 200.0)
1215            .clip()
1216            .save_state()
1217            .circle(100.0, 100.0, 50.0)
1218            .clip_even_odd()
1219            .set_fill_color(Color::blue())
1220            .rect(50.0, 50.0, 100.0, 100.0)
1221            .fill()
1222            .restore_state()
1223            .restore_state();
1224
1225        let ops = ctx.operations();
1226        // Check for nested save/restore states
1227        let q_count = ops.matches("q\n").count();
1228        let q_restore_count = ops.matches("Q\n").count();
1229        assert_eq!(q_count, 2);
1230        assert_eq!(q_restore_count, 2);
1231
1232        // Check for both clipping operations
1233        assert!(ops.contains("W\n"));
1234        assert!(ops.contains("W*\n"));
1235    }
1236}