oxidize_pdf/graphics/
mod.rs

1pub mod calibrated_color;
2pub mod clipping;
3mod color;
4mod color_profiles;
5pub mod devicen_color;
6pub mod form_xobject;
7mod indexed_color;
8pub mod lab_color;
9mod path;
10mod patterns;
11mod pdf_image;
12mod png_decoder;
13pub mod separation_color;
14mod shadings;
15pub mod soft_mask;
16pub mod state;
17pub mod transparency;
18
19pub use calibrated_color::{CalGrayColorSpace, CalRgbColorSpace, CalibratedColor};
20pub use clipping::{ClippingPath, ClippingRegion};
21pub use color::Color;
22pub use color_profiles::{IccColorSpace, IccProfile, IccProfileManager, StandardIccProfile};
23pub use devicen_color::{
24    AlternateColorSpace as DeviceNAlternateColorSpace, ColorantDefinition, ColorantType,
25    DeviceNAttributes, DeviceNColorSpace, LinearTransform, SampledFunction, TintTransformFunction,
26};
27pub use form_xobject::{
28    FormTemplates, FormXObject, FormXObjectBuilder, FormXObjectManager,
29    TransparencyGroup as FormTransparencyGroup,
30};
31pub use indexed_color::{BaseColorSpace, ColorLookupTable, IndexedColorManager, IndexedColorSpace};
32pub use lab_color::{LabColor, LabColorSpace};
33pub use path::{LineCap, LineJoin, PathBuilder, PathCommand, WindingRule};
34pub use patterns::{
35    PaintType, PatternGraphicsContext, PatternManager, PatternMatrix, PatternType, TilingPattern,
36    TilingType,
37};
38pub use pdf_image::{ColorSpace, Image, ImageFormat, MaskType};
39pub use separation_color::{
40    AlternateColorSpace, SeparationColor, SeparationColorSpace, SpotColors, TintTransform,
41};
42pub use shadings::{
43    AxialShading, ColorStop, FunctionBasedShading, Point, RadialShading, ShadingDefinition,
44    ShadingManager, ShadingPattern, ShadingType,
45};
46pub use soft_mask::{SoftMask, SoftMaskState, SoftMaskType};
47pub use state::{
48    BlendMode, ExtGState, ExtGStateFont, ExtGStateManager, Halftone, LineDashPattern,
49    RenderingIntent, TransferFunction,
50};
51pub use transparency::TransparencyGroup;
52use transparency::TransparencyGroupState;
53
54use crate::error::Result;
55use crate::text::{ColumnContent, ColumnLayout, Font, FontManager, ListElement, Table};
56use std::collections::{HashMap, HashSet};
57use std::fmt::Write;
58use std::sync::Arc;
59
60#[derive(Clone)]
61pub struct GraphicsContext {
62    operations: String,
63    current_color: Color,
64    stroke_color: Color,
65    line_width: f64,
66    fill_opacity: f64,
67    stroke_opacity: f64,
68    // Extended Graphics State support
69    extgstate_manager: ExtGStateManager,
70    pending_extgstate: Option<ExtGState>,
71    current_dash_pattern: Option<LineDashPattern>,
72    current_miter_limit: f64,
73    current_line_cap: LineCap,
74    current_line_join: LineJoin,
75    current_rendering_intent: RenderingIntent,
76    current_flatness: f64,
77    current_smoothness: f64,
78    // Clipping support
79    clipping_region: ClippingRegion,
80    // Font management
81    font_manager: Option<Arc<FontManager>>,
82    // State stack for save/restore
83    state_stack: Vec<(Color, Color)>,
84    current_font_name: Option<String>,
85    current_font_size: f64,
86    // Character tracking for font subsetting
87    used_characters: HashSet<char>,
88    // Glyph mapping for Unicode fonts (Unicode code point -> Glyph ID)
89    glyph_mapping: Option<HashMap<u32, u16>>,
90    // Transparency group stack for nested groups
91    transparency_stack: Vec<TransparencyGroupState>,
92}
93
94impl Default for GraphicsContext {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl GraphicsContext {
101    pub fn new() -> Self {
102        Self {
103            operations: String::new(),
104            current_color: Color::black(),
105            stroke_color: Color::black(),
106            line_width: 1.0,
107            fill_opacity: 1.0,
108            stroke_opacity: 1.0,
109            // Extended Graphics State defaults
110            extgstate_manager: ExtGStateManager::new(),
111            pending_extgstate: None,
112            current_dash_pattern: None,
113            current_miter_limit: 10.0,
114            current_line_cap: LineCap::Butt,
115            current_line_join: LineJoin::Miter,
116            current_rendering_intent: RenderingIntent::RelativeColorimetric,
117            current_flatness: 1.0,
118            current_smoothness: 0.0,
119            // Clipping defaults
120            clipping_region: ClippingRegion::new(),
121            // Font defaults
122            font_manager: None,
123            state_stack: Vec::new(),
124            current_font_name: None,
125            current_font_size: 12.0,
126            used_characters: HashSet::new(),
127            glyph_mapping: None,
128            transparency_stack: Vec::new(),
129        }
130    }
131
132    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
133        writeln!(&mut self.operations, "{x:.2} {y:.2} m")
134            .expect("Writing to string should never fail");
135        self
136    }
137
138    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
139        writeln!(&mut self.operations, "{x:.2} {y:.2} l")
140            .expect("Writing to string should never fail");
141        self
142    }
143
144    pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
145        writeln!(
146            &mut self.operations,
147            "{x1:.2} {y1:.2} {x2:.2} {y2:.2} {x3:.2} {y3:.2} c"
148        )
149        .expect("Writing to string should never fail");
150        self
151    }
152
153    pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
154        writeln!(
155            &mut self.operations,
156            "{x:.2} {y:.2} {width:.2} {height:.2} re"
157        )
158        .expect("Writing to string should never fail");
159        self
160    }
161
162    pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
163        let k = 0.552284749831;
164        let r = radius;
165
166        self.move_to(cx + r, cy);
167        self.curve_to(cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r);
168        self.curve_to(cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy);
169        self.curve_to(cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r);
170        self.curve_to(cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy);
171        self.close_path()
172    }
173
174    pub fn close_path(&mut self) -> &mut Self {
175        self.operations.push_str("h\n");
176        self
177    }
178
179    pub fn stroke(&mut self) -> &mut Self {
180        self.apply_pending_extgstate().unwrap_or_default();
181        self.apply_stroke_color();
182        self.operations.push_str("S\n");
183        self
184    }
185
186    pub fn fill(&mut self) -> &mut Self {
187        self.apply_pending_extgstate().unwrap_or_default();
188        self.apply_fill_color();
189        self.operations.push_str("f\n");
190        self
191    }
192
193    pub fn fill_stroke(&mut self) -> &mut Self {
194        self.apply_pending_extgstate().unwrap_or_default();
195        self.apply_fill_color();
196        self.apply_stroke_color();
197        self.operations.push_str("B\n");
198        self
199    }
200
201    pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
202        self.stroke_color = color;
203        self
204    }
205
206    pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
207        self.current_color = color;
208        self
209    }
210
211    /// Set fill color using calibrated color space
212    pub fn set_fill_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
213        // Generate a unique color space name
214        let cs_name = match &color {
215            CalibratedColor::Gray(_, _) => "CalGray1",
216            CalibratedColor::Rgb(_, _) => "CalRGB1",
217        };
218
219        // Set the color space (this would need to be registered in the PDF resources)
220        writeln!(&mut self.operations, "/{} cs", cs_name)
221            .expect("Writing to string should never fail");
222
223        // Set color values
224        let values = color.values();
225        for value in &values {
226            write!(&mut self.operations, "{:.4} ", value)
227                .expect("Writing to string should never fail");
228        }
229        writeln!(&mut self.operations, "sc").expect("Writing to string should never fail");
230
231        self
232    }
233
234    /// Set stroke color using calibrated color space
235    pub fn set_stroke_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
236        // Generate a unique color space name
237        let cs_name = match &color {
238            CalibratedColor::Gray(_, _) => "CalGray1",
239            CalibratedColor::Rgb(_, _) => "CalRGB1",
240        };
241
242        // Set the color space (this would need to be registered in the PDF resources)
243        writeln!(&mut self.operations, "/{} CS", cs_name)
244            .expect("Writing to string should never fail");
245
246        // Set color values
247        let values = color.values();
248        for value in &values {
249            write!(&mut self.operations, "{:.4} ", value)
250                .expect("Writing to string should never fail");
251        }
252        writeln!(&mut self.operations, "SC").expect("Writing to string should never fail");
253
254        self
255    }
256
257    /// Set fill color using Lab color space
258    pub fn set_fill_color_lab(&mut self, color: LabColor) -> &mut Self {
259        // Set the color space (this would need to be registered in the PDF resources)
260        writeln!(&mut self.operations, "/Lab1 cs").expect("Writing to string should never fail");
261
262        // Set color values (normalized for PDF)
263        let values = color.values();
264        for value in &values {
265            write!(&mut self.operations, "{:.4} ", value)
266                .expect("Writing to string should never fail");
267        }
268        writeln!(&mut self.operations, "sc").expect("Writing to string should never fail");
269
270        self
271    }
272
273    /// Set stroke color using Lab color space
274    pub fn set_stroke_color_lab(&mut self, color: LabColor) -> &mut Self {
275        // Set the color space (this would need to be registered in the PDF resources)
276        writeln!(&mut self.operations, "/Lab1 CS").expect("Writing to string should never fail");
277
278        // Set color values (normalized for PDF)
279        let values = color.values();
280        for value in &values {
281            write!(&mut self.operations, "{:.4} ", value)
282                .expect("Writing to string should never fail");
283        }
284        writeln!(&mut self.operations, "SC").expect("Writing to string should never fail");
285
286        self
287    }
288
289    pub fn set_line_width(&mut self, width: f64) -> &mut Self {
290        self.line_width = width;
291        writeln!(&mut self.operations, "{width:.2} w")
292            .expect("Writing to string should never fail");
293        self
294    }
295
296    pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
297        self.current_line_cap = cap;
298        writeln!(&mut self.operations, "{} J", cap as u8)
299            .expect("Writing to string should never fail");
300        self
301    }
302
303    pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
304        self.current_line_join = join;
305        writeln!(&mut self.operations, "{} j", join as u8)
306            .expect("Writing to string should never fail");
307        self
308    }
309
310    /// Set the opacity for both fill and stroke operations (0.0 to 1.0)
311    pub fn set_opacity(&mut self, opacity: f64) -> &mut Self {
312        let opacity = opacity.clamp(0.0, 1.0);
313        self.fill_opacity = opacity;
314        self.stroke_opacity = opacity;
315
316        // Create pending ExtGState if opacity is not 1.0
317        if opacity < 1.0 {
318            let mut state = ExtGState::new();
319            state.alpha_fill = Some(opacity);
320            state.alpha_stroke = Some(opacity);
321            self.pending_extgstate = Some(state);
322        }
323
324        self
325    }
326
327    /// Set the fill opacity (0.0 to 1.0)
328    pub fn set_fill_opacity(&mut self, opacity: f64) -> &mut Self {
329        self.fill_opacity = opacity.clamp(0.0, 1.0);
330
331        // Update or create pending ExtGState
332        if opacity < 1.0 {
333            if let Some(ref mut state) = self.pending_extgstate {
334                state.alpha_fill = Some(opacity);
335            } else {
336                let mut state = ExtGState::new();
337                state.alpha_fill = Some(opacity);
338                self.pending_extgstate = Some(state);
339            }
340        }
341
342        self
343    }
344
345    /// Set the stroke opacity (0.0 to 1.0)
346    pub fn set_stroke_opacity(&mut self, opacity: f64) -> &mut Self {
347        self.stroke_opacity = opacity.clamp(0.0, 1.0);
348
349        // Update or create pending ExtGState
350        if opacity < 1.0 {
351            if let Some(ref mut state) = self.pending_extgstate {
352                state.alpha_stroke = Some(opacity);
353            } else {
354                let mut state = ExtGState::new();
355                state.alpha_stroke = Some(opacity);
356                self.pending_extgstate = Some(state);
357            }
358        }
359
360        self
361    }
362
363    pub fn save_state(&mut self) -> &mut Self {
364        self.operations.push_str("q\n");
365        self.save_clipping_state();
366        // Save color state
367        self.state_stack
368            .push((self.current_color, self.stroke_color));
369        self
370    }
371
372    pub fn restore_state(&mut self) -> &mut Self {
373        self.operations.push_str("Q\n");
374        self.restore_clipping_state();
375        // Restore color state
376        if let Some((fill, stroke)) = self.state_stack.pop() {
377            self.current_color = fill;
378            self.stroke_color = stroke;
379        }
380        self
381    }
382
383    /// Begin a transparency group
384    /// ISO 32000-1:2008 Section 11.4
385    pub fn begin_transparency_group(&mut self, group: TransparencyGroup) -> &mut Self {
386        // Save current state
387        self.save_state();
388
389        // Mark beginning of transparency group with special comment
390        writeln!(&mut self.operations, "% Begin Transparency Group")
391            .expect("Writing to string should never fail");
392
393        // Apply group settings via ExtGState
394        let mut extgstate = ExtGState::new();
395        extgstate = extgstate.with_blend_mode(group.blend_mode.clone());
396        extgstate.alpha_fill = Some(group.opacity as f64);
397        extgstate.alpha_stroke = Some(group.opacity as f64);
398
399        // Apply the ExtGState
400        self.pending_extgstate = Some(extgstate);
401        let _ = self.apply_pending_extgstate();
402
403        // Create group state and push to stack
404        let mut group_state = TransparencyGroupState::new(group);
405        // Save current operations state
406        group_state.saved_state = self.operations.as_bytes().to_vec();
407        self.transparency_stack.push(group_state);
408
409        self
410    }
411
412    /// End a transparency group
413    pub fn end_transparency_group(&mut self) -> &mut Self {
414        if let Some(_group_state) = self.transparency_stack.pop() {
415            // Mark end of transparency group
416            writeln!(&mut self.operations, "% End Transparency Group")
417                .expect("Writing to string should never fail");
418
419            // Restore state
420            self.restore_state();
421        }
422        self
423    }
424
425    /// Check if we're currently inside a transparency group
426    pub fn in_transparency_group(&self) -> bool {
427        !self.transparency_stack.is_empty()
428    }
429
430    /// Get the current transparency group (if any)
431    pub fn current_transparency_group(&self) -> Option<&TransparencyGroup> {
432        self.transparency_stack.last().map(|state| &state.group)
433    }
434
435    pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
436        writeln!(&mut self.operations, "1 0 0 1 {tx:.2} {ty:.2} cm")
437            .expect("Writing to string should never fail");
438        self
439    }
440
441    pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
442        writeln!(&mut self.operations, "{sx:.2} 0 0 {sy:.2} 0 0 cm")
443            .expect("Writing to string should never fail");
444        self
445    }
446
447    pub fn rotate(&mut self, angle: f64) -> &mut Self {
448        let cos = angle.cos();
449        let sin = angle.sin();
450        writeln!(
451            &mut self.operations,
452            "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
453            cos, sin, -sin, cos
454        )
455        .expect("Writing to string should never fail");
456        self
457    }
458
459    pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
460        writeln!(
461            &mut self.operations,
462            "{a:.2} {b:.2} {c:.2} {d:.2} {e:.2} {f:.2} cm"
463        )
464        .expect("Writing to string should never fail");
465        self
466    }
467
468    pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
469        self.rect(x, y, width, height)
470    }
471
472    pub fn draw_image(
473        &mut self,
474        image_name: &str,
475        x: f64,
476        y: f64,
477        width: f64,
478        height: f64,
479    ) -> &mut Self {
480        // Save graphics state
481        self.save_state();
482
483        // Set up transformation matrix for image placement
484        // PDF coordinate system has origin at bottom-left, so we need to translate and scale
485        writeln!(
486            &mut self.operations,
487            "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
488        )
489        .expect("Writing to string should never fail");
490
491        // Draw the image XObject
492        writeln!(&mut self.operations, "/{image_name} Do")
493            .expect("Writing to string should never fail");
494
495        // Restore graphics state
496        self.restore_state();
497
498        self
499    }
500
501    /// Draw an image with transparency support (soft mask)
502    /// This method handles images with alpha channels or soft masks
503    pub fn draw_image_with_transparency(
504        &mut self,
505        image_name: &str,
506        x: f64,
507        y: f64,
508        width: f64,
509        height: f64,
510        mask_name: Option<&str>,
511    ) -> &mut Self {
512        // Save graphics state
513        self.save_state();
514
515        // If we have a mask, we need to set up an ExtGState with SMask
516        if let Some(mask) = mask_name {
517            // Create an ExtGState for the soft mask
518            let mut extgstate = ExtGState::new();
519            extgstate.set_soft_mask_name(mask.to_string());
520
521            // Register and apply the ExtGState
522            let gs_name = self
523                .extgstate_manager
524                .add_state(extgstate)
525                .unwrap_or_else(|_| "GS1".to_string());
526            writeln!(&mut self.operations, "/{} gs", gs_name)
527                .expect("Writing to string should never fail");
528        }
529
530        // Set up transformation matrix for image placement
531        writeln!(
532            &mut self.operations,
533            "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
534        )
535        .expect("Writing to string should never fail");
536
537        // Draw the image XObject
538        writeln!(&mut self.operations, "/{image_name} Do")
539            .expect("Writing to string should never fail");
540
541        // If we had a mask, reset the soft mask to None
542        if mask_name.is_some() {
543            // Create an ExtGState that removes the soft mask
544            let mut reset_extgstate = ExtGState::new();
545            reset_extgstate.set_soft_mask_none();
546
547            let gs_name = self
548                .extgstate_manager
549                .add_state(reset_extgstate)
550                .unwrap_or_else(|_| "GS2".to_string());
551            writeln!(&mut self.operations, "/{} gs", gs_name)
552                .expect("Writing to string should never fail");
553        }
554
555        // Restore graphics state
556        self.restore_state();
557
558        self
559    }
560
561    fn apply_stroke_color(&mut self) {
562        match self.stroke_color {
563            Color::Rgb(r, g, b) => {
564                writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} RG")
565                    .expect("Writing to string should never fail");
566            }
567            Color::Gray(g) => {
568                writeln!(&mut self.operations, "{g:.3} G")
569                    .expect("Writing to string should never fail");
570            }
571            Color::Cmyk(c, m, y, k) => {
572                writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} K")
573                    .expect("Writing to string should never fail");
574            }
575        }
576    }
577
578    fn apply_fill_color(&mut self) {
579        match self.current_color {
580            Color::Rgb(r, g, b) => {
581                writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} rg")
582                    .expect("Writing to string should never fail");
583            }
584            Color::Gray(g) => {
585                writeln!(&mut self.operations, "{g:.3} g")
586                    .expect("Writing to string should never fail");
587            }
588            Color::Cmyk(c, m, y, k) => {
589                writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} k")
590                    .expect("Writing to string should never fail");
591            }
592        }
593    }
594
595    pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
596        Ok(self.operations.as_bytes().to_vec())
597    }
598
599    /// Check if transparency is used (opacity != 1.0)
600    pub fn uses_transparency(&self) -> bool {
601        self.fill_opacity < 1.0 || self.stroke_opacity < 1.0
602    }
603
604    /// Generate the graphics state dictionary for transparency
605    pub fn generate_graphics_state_dict(&self) -> Option<String> {
606        if !self.uses_transparency() {
607            return None;
608        }
609
610        let mut dict = String::from("<< /Type /ExtGState");
611
612        if self.fill_opacity < 1.0 {
613            write!(&mut dict, " /ca {:.3}", self.fill_opacity)
614                .expect("Writing to string should never fail");
615        }
616
617        if self.stroke_opacity < 1.0 {
618            write!(&mut dict, " /CA {:.3}", self.stroke_opacity)
619                .expect("Writing to string should never fail");
620        }
621
622        dict.push_str(" >>");
623        Some(dict)
624    }
625
626    /// Get the current fill color
627    pub fn fill_color(&self) -> Color {
628        self.current_color
629    }
630
631    /// Get the current stroke color
632    pub fn stroke_color(&self) -> Color {
633        self.stroke_color
634    }
635
636    /// Get the current line width
637    pub fn line_width(&self) -> f64 {
638        self.line_width
639    }
640
641    /// Get the current fill opacity
642    pub fn fill_opacity(&self) -> f64 {
643        self.fill_opacity
644    }
645
646    /// Get the current stroke opacity
647    pub fn stroke_opacity(&self) -> f64 {
648        self.stroke_opacity
649    }
650
651    /// Get the operations string
652    pub fn operations(&self) -> &str {
653        &self.operations
654    }
655
656    /// Get the operations string (alias for testing)
657    pub fn get_operations(&self) -> &str {
658        &self.operations
659    }
660
661    /// Clear all operations
662    pub fn clear(&mut self) {
663        self.operations.clear();
664    }
665
666    /// Begin a text object
667    pub fn begin_text(&mut self) -> &mut Self {
668        self.operations.push_str("BT\n");
669        self
670    }
671
672    /// End a text object
673    pub fn end_text(&mut self) -> &mut Self {
674        self.operations.push_str("ET\n");
675        self
676    }
677
678    /// Set font and size
679    pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
680        writeln!(&mut self.operations, "/{} {} Tf", font.pdf_name(), size)
681            .expect("Writing to string should never fail");
682
683        // Track font name and size for Unicode detection and proper font handling
684        match &font {
685            Font::Custom(name) => {
686                self.current_font_name = Some(name.clone());
687                self.current_font_size = size;
688            }
689            _ => {
690                self.current_font_name = Some(font.pdf_name());
691                self.current_font_size = size;
692            }
693        }
694
695        self
696    }
697
698    /// Set text position
699    pub fn set_text_position(&mut self, x: f64, y: f64) -> &mut Self {
700        writeln!(&mut self.operations, "{x:.2} {y:.2} Td")
701            .expect("Writing to string should never fail");
702        self
703    }
704
705    /// Show text
706    pub fn show_text(&mut self, text: &str) -> Result<&mut Self> {
707        // Escape special characters in PDF string
708        self.operations.push('(');
709        for ch in text.chars() {
710            match ch {
711                '(' => self.operations.push_str("\\("),
712                ')' => self.operations.push_str("\\)"),
713                '\\' => self.operations.push_str("\\\\"),
714                '\n' => self.operations.push_str("\\n"),
715                '\r' => self.operations.push_str("\\r"),
716                '\t' => self.operations.push_str("\\t"),
717                _ => self.operations.push(ch),
718            }
719        }
720        self.operations.push_str(") Tj\n");
721        Ok(self)
722    }
723
724    /// Set word spacing for text justification
725    pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
726        writeln!(&mut self.operations, "{spacing:.2} Tw")
727            .expect("Writing to string should never fail");
728        self
729    }
730
731    /// Set character spacing
732    pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
733        writeln!(&mut self.operations, "{spacing:.2} Tc")
734            .expect("Writing to string should never fail");
735        self
736    }
737
738    /// Show justified text with automatic word spacing calculation
739    pub fn show_justified_text(&mut self, text: &str, target_width: f64) -> Result<&mut Self> {
740        // Split text into words
741        let words: Vec<&str> = text.split_whitespace().collect();
742        if words.len() <= 1 {
743            // Can't justify single word or empty text
744            return self.show_text(text);
745        }
746
747        // Calculate natural width of text without extra spacing
748        let text_without_spaces = words.join("");
749        let natural_text_width = self.estimate_text_width_simple(&text_without_spaces);
750        let space_width = self.estimate_text_width_simple(" ");
751        let natural_width = natural_text_width + (space_width * (words.len() - 1) as f64);
752
753        // Calculate extra spacing needed per word gap
754        let extra_space_needed = target_width - natural_width;
755        let word_gaps = (words.len() - 1) as f64;
756
757        if word_gaps > 0.0 && extra_space_needed > 0.0 {
758            let extra_word_spacing = extra_space_needed / word_gaps;
759
760            // Set word spacing
761            self.set_word_spacing(extra_word_spacing);
762
763            // Show text (spaces will be expanded automatically)
764            self.show_text(text)?;
765
766            // Reset word spacing to default
767            self.set_word_spacing(0.0);
768        } else {
769            // Fallback to normal text display
770            self.show_text(text)?;
771        }
772
773        Ok(self)
774    }
775
776    /// Simple text width estimation (placeholder implementation)
777    fn estimate_text_width_simple(&self, text: &str) -> f64 {
778        // This is a simplified estimation. In a full implementation,
779        // you would use actual font metrics.
780        let font_size = self.current_font_size;
781        text.len() as f64 * font_size * 0.6 // Approximate width factor
782    }
783
784    /// Render a table
785    pub fn render_table(&mut self, table: &Table) -> Result<()> {
786        table.render(self)
787    }
788
789    /// Render a list
790    pub fn render_list(&mut self, list: &ListElement) -> Result<()> {
791        match list {
792            ListElement::Ordered(ordered) => ordered.render(self),
793            ListElement::Unordered(unordered) => unordered.render(self),
794        }
795    }
796
797    /// Render column layout
798    pub fn render_column_layout(
799        &mut self,
800        layout: &ColumnLayout,
801        content: &ColumnContent,
802        x: f64,
803        y: f64,
804        height: f64,
805    ) -> Result<()> {
806        layout.render(self, content, x, y, height)
807    }
808
809    // Extended Graphics State methods
810
811    /// Set line dash pattern
812    pub fn set_line_dash_pattern(&mut self, pattern: LineDashPattern) -> &mut Self {
813        self.current_dash_pattern = Some(pattern.clone());
814        writeln!(&mut self.operations, "{} d", pattern.to_pdf_string())
815            .expect("Writing to string should never fail");
816        self
817    }
818
819    /// Set line dash pattern to solid (no dashes)
820    pub fn set_line_solid(&mut self) -> &mut Self {
821        self.current_dash_pattern = None;
822        self.operations.push_str("[] 0 d\n");
823        self
824    }
825
826    /// Set miter limit
827    pub fn set_miter_limit(&mut self, limit: f64) -> &mut Self {
828        self.current_miter_limit = limit.max(1.0);
829        writeln!(&mut self.operations, "{:.2} M", self.current_miter_limit)
830            .expect("Writing to string should never fail");
831        self
832    }
833
834    /// Set rendering intent
835    pub fn set_rendering_intent(&mut self, intent: RenderingIntent) -> &mut Self {
836        self.current_rendering_intent = intent;
837        writeln!(&mut self.operations, "/{} ri", intent.pdf_name())
838            .expect("Writing to string should never fail");
839        self
840    }
841
842    /// Set flatness tolerance
843    pub fn set_flatness(&mut self, flatness: f64) -> &mut Self {
844        self.current_flatness = flatness.clamp(0.0, 100.0);
845        writeln!(&mut self.operations, "{:.2} i", self.current_flatness)
846            .expect("Writing to string should never fail");
847        self
848    }
849
850    /// Apply an ExtGState dictionary immediately
851    pub fn apply_extgstate(&mut self, state: ExtGState) -> Result<&mut Self> {
852        let state_name = self.extgstate_manager.add_state(state)?;
853        writeln!(&mut self.operations, "/{state_name} gs")
854            .expect("Writing to string should never fail");
855        Ok(self)
856    }
857
858    /// Store an ExtGState to be applied before the next drawing operation
859    #[allow(dead_code)]
860    fn set_pending_extgstate(&mut self, state: ExtGState) {
861        self.pending_extgstate = Some(state);
862    }
863
864    /// Apply any pending ExtGState before drawing
865    fn apply_pending_extgstate(&mut self) -> Result<()> {
866        if let Some(state) = self.pending_extgstate.take() {
867            let state_name = self.extgstate_manager.add_state(state)?;
868            writeln!(&mut self.operations, "/{state_name} gs")
869                .expect("Writing to string should never fail");
870        }
871        Ok(())
872    }
873
874    /// Create and apply a custom ExtGState
875    pub fn with_extgstate<F>(&mut self, builder: F) -> Result<&mut Self>
876    where
877        F: FnOnce(ExtGState) -> ExtGState,
878    {
879        let state = builder(ExtGState::new());
880        self.apply_extgstate(state)
881    }
882
883    /// Set blend mode for transparency
884    pub fn set_blend_mode(&mut self, mode: BlendMode) -> Result<&mut Self> {
885        let state = ExtGState::new().with_blend_mode(mode);
886        self.apply_extgstate(state)
887    }
888
889    /// Set alpha for both stroke and fill operations
890    pub fn set_alpha(&mut self, alpha: f64) -> Result<&mut Self> {
891        let state = ExtGState::new().with_alpha(alpha);
892        self.apply_extgstate(state)
893    }
894
895    /// Set alpha for stroke operations only
896    pub fn set_alpha_stroke(&mut self, alpha: f64) -> Result<&mut Self> {
897        let state = ExtGState::new().with_alpha_stroke(alpha);
898        self.apply_extgstate(state)
899    }
900
901    /// Set alpha for fill operations only
902    pub fn set_alpha_fill(&mut self, alpha: f64) -> Result<&mut Self> {
903        let state = ExtGState::new().with_alpha_fill(alpha);
904        self.apply_extgstate(state)
905    }
906
907    /// Set overprint for stroke operations
908    pub fn set_overprint_stroke(&mut self, overprint: bool) -> Result<&mut Self> {
909        let state = ExtGState::new().with_overprint_stroke(overprint);
910        self.apply_extgstate(state)
911    }
912
913    /// Set overprint for fill operations
914    pub fn set_overprint_fill(&mut self, overprint: bool) -> Result<&mut Self> {
915        let state = ExtGState::new().with_overprint_fill(overprint);
916        self.apply_extgstate(state)
917    }
918
919    /// Set stroke adjustment
920    pub fn set_stroke_adjustment(&mut self, adjustment: bool) -> Result<&mut Self> {
921        let state = ExtGState::new().with_stroke_adjustment(adjustment);
922        self.apply_extgstate(state)
923    }
924
925    /// Set smoothness tolerance
926    pub fn set_smoothness(&mut self, smoothness: f64) -> Result<&mut Self> {
927        self.current_smoothness = smoothness.clamp(0.0, 1.0);
928        let state = ExtGState::new().with_smoothness(self.current_smoothness);
929        self.apply_extgstate(state)
930    }
931
932    // Getters for extended graphics state
933
934    /// Get current line dash pattern
935    pub fn line_dash_pattern(&self) -> Option<&LineDashPattern> {
936        self.current_dash_pattern.as_ref()
937    }
938
939    /// Get current miter limit
940    pub fn miter_limit(&self) -> f64 {
941        self.current_miter_limit
942    }
943
944    /// Get current line cap
945    pub fn line_cap(&self) -> LineCap {
946        self.current_line_cap
947    }
948
949    /// Get current line join
950    pub fn line_join(&self) -> LineJoin {
951        self.current_line_join
952    }
953
954    /// Get current rendering intent
955    pub fn rendering_intent(&self) -> RenderingIntent {
956        self.current_rendering_intent
957    }
958
959    /// Get current flatness tolerance
960    pub fn flatness(&self) -> f64 {
961        self.current_flatness
962    }
963
964    /// Get current smoothness tolerance
965    pub fn smoothness(&self) -> f64 {
966        self.current_smoothness
967    }
968
969    /// Get the ExtGState manager (for advanced usage)
970    pub fn extgstate_manager(&self) -> &ExtGStateManager {
971        &self.extgstate_manager
972    }
973
974    /// Get mutable ExtGState manager (for advanced usage)
975    pub fn extgstate_manager_mut(&mut self) -> &mut ExtGStateManager {
976        &mut self.extgstate_manager
977    }
978
979    /// Generate ExtGState resource dictionary for PDF
980    pub fn generate_extgstate_resources(&self) -> Result<String> {
981        self.extgstate_manager.to_resource_dictionary()
982    }
983
984    /// Check if any extended graphics states are defined
985    pub fn has_extgstates(&self) -> bool {
986        self.extgstate_manager.count() > 0
987    }
988
989    /// Add a command to the operations
990    pub fn add_command(&mut self, command: &str) {
991        self.operations.push_str(command);
992        self.operations.push('\n');
993    }
994
995    /// Create clipping path from current path using non-zero winding rule
996    pub fn clip(&mut self) -> &mut Self {
997        self.operations.push_str("W\n");
998        self
999    }
1000
1001    /// Create clipping path from current path using even-odd rule
1002    pub fn clip_even_odd(&mut self) -> &mut Self {
1003        self.operations.push_str("W*\n");
1004        self
1005    }
1006
1007    /// Create clipping path and stroke it
1008    pub fn clip_stroke(&mut self) -> &mut Self {
1009        self.apply_stroke_color();
1010        self.operations.push_str("W S\n");
1011        self
1012    }
1013
1014    /// Set a custom clipping path
1015    pub fn set_clipping_path(&mut self, path: ClippingPath) -> Result<&mut Self> {
1016        let ops = path.to_pdf_operations()?;
1017        self.operations.push_str(&ops);
1018        self.clipping_region.set_clip(path);
1019        Ok(self)
1020    }
1021
1022    /// Clear the current clipping path
1023    pub fn clear_clipping(&mut self) -> &mut Self {
1024        self.clipping_region.clear_clip();
1025        self
1026    }
1027
1028    /// Save the current clipping state (called automatically by save_state)
1029    fn save_clipping_state(&mut self) {
1030        self.clipping_region.save();
1031    }
1032
1033    /// Restore the previous clipping state (called automatically by restore_state)
1034    fn restore_clipping_state(&mut self) {
1035        self.clipping_region.restore();
1036    }
1037
1038    /// Create a rectangular clipping region
1039    pub fn clip_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> Result<&mut Self> {
1040        let path = ClippingPath::rect(x, y, width, height);
1041        self.set_clipping_path(path)
1042    }
1043
1044    /// Create a circular clipping region
1045    pub fn clip_circle(&mut self, cx: f64, cy: f64, radius: f64) -> Result<&mut Self> {
1046        let path = ClippingPath::circle(cx, cy, radius);
1047        self.set_clipping_path(path)
1048    }
1049
1050    /// Create an elliptical clipping region
1051    pub fn clip_ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) -> Result<&mut Self> {
1052        let path = ClippingPath::ellipse(cx, cy, rx, ry);
1053        self.set_clipping_path(path)
1054    }
1055
1056    /// Check if a clipping path is active
1057    pub fn has_clipping(&self) -> bool {
1058        self.clipping_region.has_clip()
1059    }
1060
1061    /// Get the current clipping path
1062    pub fn clipping_path(&self) -> Option<&ClippingPath> {
1063        self.clipping_region.current()
1064    }
1065
1066    /// Set the font manager for custom fonts
1067    pub fn set_font_manager(&mut self, font_manager: Arc<FontManager>) -> &mut Self {
1068        self.font_manager = Some(font_manager);
1069        self
1070    }
1071
1072    /// Set the current font to a custom font
1073    pub fn set_custom_font(&mut self, font_name: &str, size: f64) -> &mut Self {
1074        self.current_font_name = Some(font_name.to_string());
1075        self.current_font_size = size;
1076
1077        // Try to get the glyph mapping from the font manager
1078        if let Some(ref font_manager) = self.font_manager {
1079            if let Some(mapping) = font_manager.get_font_glyph_mapping(font_name) {
1080                self.glyph_mapping = Some(mapping);
1081            }
1082        }
1083
1084        self
1085    }
1086
1087    /// Set the glyph mapping for Unicode fonts (Unicode -> GlyphID)
1088    pub fn set_glyph_mapping(&mut self, mapping: HashMap<u32, u16>) -> &mut Self {
1089        self.glyph_mapping = Some(mapping);
1090        self
1091    }
1092
1093    /// Draw text at the specified position with automatic encoding detection
1094    pub fn draw_text(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1095        // Track used characters for font subsetting
1096        self.used_characters.extend(text.chars());
1097
1098        // Check if we're using a custom font (which will be Type0/Unicode)
1099        // Custom fonts are those that are not the standard PDF fonts (Helvetica, Times, etc.)
1100        let using_custom_font = if let Some(ref font_name) = self.current_font_name {
1101            // If font name doesn't start with standard PDF font names, it's custom
1102            !matches!(
1103                font_name.as_str(),
1104                "Helvetica"
1105                    | "Times"
1106                    | "Courier"
1107                    | "Symbol"
1108                    | "ZapfDingbats"
1109                    | "Helvetica-Bold"
1110                    | "Helvetica-Oblique"
1111                    | "Helvetica-BoldOblique"
1112                    | "Times-Roman"
1113                    | "Times-Bold"
1114                    | "Times-Italic"
1115                    | "Times-BoldItalic"
1116                    | "Courier-Bold"
1117                    | "Courier-Oblique"
1118                    | "Courier-BoldOblique"
1119            )
1120        } else {
1121            false
1122        };
1123
1124        // Detect if text needs Unicode encoding
1125        let needs_unicode = text.chars().any(|c| c as u32 > 255) || using_custom_font;
1126
1127        // Use appropriate encoding based on content and font type
1128        if needs_unicode {
1129            self.draw_with_unicode_encoding(text, x, y)
1130        } else {
1131            self.draw_with_simple_encoding(text, x, y)
1132        }
1133    }
1134
1135    /// Internal: Draw text with simple encoding (WinAnsiEncoding for standard fonts)
1136    fn draw_with_simple_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1137        // Check if text contains characters outside Latin-1
1138        let has_unicode = text.chars().any(|c| c as u32 > 255);
1139
1140        if has_unicode {
1141            // Warning: Text contains Unicode characters but no Unicode font is set
1142            eprintln!("Warning: Text contains Unicode characters but using Latin-1 font. Characters will be replaced with '?'");
1143        }
1144
1145        // Begin text object
1146        self.operations.push_str("BT\n");
1147
1148        // Set font if available
1149        if let Some(font_name) = &self.current_font_name {
1150            writeln!(
1151                &mut self.operations,
1152                "/{} {} Tf",
1153                font_name, self.current_font_size
1154            )
1155            .expect("Writing to string should never fail");
1156        } else {
1157            writeln!(
1158                &mut self.operations,
1159                "/Helvetica {} Tf",
1160                self.current_font_size
1161            )
1162            .expect("Writing to string should never fail");
1163        }
1164
1165        // Set text position
1166        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1167            .expect("Writing to string should never fail");
1168
1169        // Use parentheses encoding for Latin-1 text (standard PDF fonts use WinAnsiEncoding)
1170        // This allows proper rendering of accented characters
1171        self.operations.push('(');
1172        for ch in text.chars() {
1173            let code = ch as u32;
1174            if code <= 127 {
1175                // ASCII characters - handle special characters that need escaping
1176                match ch {
1177                    '(' => self.operations.push_str("\\("),
1178                    ')' => self.operations.push_str("\\)"),
1179                    '\\' => self.operations.push_str("\\\\"),
1180                    '\n' => self.operations.push_str("\\n"),
1181                    '\r' => self.operations.push_str("\\r"),
1182                    '\t' => self.operations.push_str("\\t"),
1183                    _ => self.operations.push(ch),
1184                }
1185            } else if code <= 255 {
1186                // Latin-1 characters (128-255)
1187                // For WinAnsiEncoding, we can use octal notation for high-bit characters
1188                write!(&mut self.operations, "\\{:03o}", code)
1189                    .expect("Writing to string should never fail");
1190            } else {
1191                // Characters outside Latin-1 - replace with '?'
1192                self.operations.push('?');
1193            }
1194        }
1195        self.operations.push_str(") Tj\n");
1196
1197        // End text object
1198        self.operations.push_str("ET\n");
1199
1200        Ok(self)
1201    }
1202
1203    /// Internal: Draw text with Unicode encoding (Type0/CID)
1204    fn draw_with_unicode_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1205        // Begin text object
1206        self.operations.push_str("BT\n");
1207
1208        // Set font - ensure it's a Type0 font for Unicode
1209        if let Some(font_name) = &self.current_font_name {
1210            // The font should be converted to Type0 by FontManager if needed
1211            writeln!(
1212                &mut self.operations,
1213                "/{} {} Tf",
1214                font_name, self.current_font_size
1215            )
1216            .expect("Writing to string should never fail");
1217        } else {
1218            writeln!(
1219                &mut self.operations,
1220                "/Helvetica {} Tf",
1221                self.current_font_size
1222            )
1223            .expect("Writing to string should never fail");
1224        }
1225
1226        // Set text position
1227        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1228            .expect("Writing to string should never fail");
1229
1230        // IMPORTANT: For Type0 fonts with Identity-H encoding, we write CIDs (Character IDs),
1231        // NOT GlyphIDs. The CIDToGIDMap in the font handles the CID -> GlyphID conversion.
1232        // In our case, we use Unicode code points as CIDs.
1233        self.operations.push('<');
1234
1235        for ch in text.chars() {
1236            let code = ch as u32;
1237
1238            // For Type0 fonts with Identity-H encoding, write the Unicode code point as CID
1239            // The CIDToGIDMap will handle the conversion to the actual glyph ID
1240            if code <= 0xFFFF {
1241                // Write the Unicode code point as a 2-byte hex value (CID)
1242                write!(&mut self.operations, "{:04X}", code)
1243                    .expect("Writing to string should never fail");
1244            } else {
1245                // Characters outside BMP - use replacement character
1246                // Most PDF viewers don't handle supplementary planes well
1247                write!(&mut self.operations, "FFFD").expect("Writing to string should never fail");
1248                // Unicode replacement character
1249            }
1250        }
1251        self.operations.push_str("> Tj\n");
1252
1253        // End text object
1254        self.operations.push_str("ET\n");
1255
1256        Ok(self)
1257    }
1258
1259    /// Legacy: Draw text with hex encoding (kept for compatibility)
1260    #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1261    pub fn draw_text_hex(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1262        // Begin text object
1263        self.operations.push_str("BT\n");
1264
1265        // Set font if available
1266        if let Some(font_name) = &self.current_font_name {
1267            writeln!(
1268                &mut self.operations,
1269                "/{} {} Tf",
1270                font_name, self.current_font_size
1271            )
1272            .expect("Writing to string should never fail");
1273        } else {
1274            // Fallback to Helvetica if no font is set
1275            writeln!(
1276                &mut self.operations,
1277                "/Helvetica {} Tf",
1278                self.current_font_size
1279            )
1280            .expect("Writing to string should never fail");
1281        }
1282
1283        // Set text position
1284        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1285            .expect("Writing to string should never fail");
1286
1287        // Encode text as hex string
1288        // For TrueType fonts with Identity-H encoding, we need UTF-16BE
1289        // But we'll use single-byte encoding for now to fix spacing
1290        self.operations.push('<');
1291        for ch in text.chars() {
1292            if ch as u32 <= 255 {
1293                // For characters in the Latin-1 range, use single byte
1294                write!(&mut self.operations, "{:02X}", ch as u8)
1295                    .expect("Writing to string should never fail");
1296            } else {
1297                // For characters outside Latin-1, we need proper glyph mapping
1298                // For now, use a placeholder
1299                write!(&mut self.operations, "3F").expect("Writing to string should never fail");
1300                // '?' character
1301            }
1302        }
1303        self.operations.push_str("> Tj\n");
1304
1305        // End text object
1306        self.operations.push_str("ET\n");
1307
1308        Ok(self)
1309    }
1310
1311    /// Legacy: Draw text with Type0 font encoding (kept for compatibility)
1312    #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1313    pub fn draw_text_cid(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1314        use crate::fonts::needs_type0_font;
1315
1316        // Begin text object
1317        self.operations.push_str("BT\n");
1318
1319        // Set font if available
1320        if let Some(font_name) = &self.current_font_name {
1321            writeln!(
1322                &mut self.operations,
1323                "/{} {} Tf",
1324                font_name, self.current_font_size
1325            )
1326            .expect("Writing to string should never fail");
1327        } else {
1328            writeln!(
1329                &mut self.operations,
1330                "/Helvetica {} Tf",
1331                self.current_font_size
1332            )
1333            .expect("Writing to string should never fail");
1334        }
1335
1336        // Set text position
1337        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1338            .expect("Writing to string should never fail");
1339
1340        // Check if text needs Type0 encoding
1341        if needs_type0_font(text) {
1342            // Use 2-byte hex encoding for CIDs with identity mapping
1343            self.operations.push('<');
1344            for ch in text.chars() {
1345                let code = ch as u32;
1346
1347                // Handle all Unicode characters
1348                if code <= 0xFFFF {
1349                    // Direct identity mapping for BMP characters
1350                    write!(&mut self.operations, "{:04X}", code)
1351                        .expect("Writing to string should never fail");
1352                } else if code <= 0x10FFFF {
1353                    // For characters outside BMP - use surrogate pairs
1354                    let code = code - 0x10000;
1355                    let high = ((code >> 10) & 0x3FF) + 0xD800;
1356                    let low = (code & 0x3FF) + 0xDC00;
1357                    write!(&mut self.operations, "{:04X}{:04X}", high, low)
1358                        .expect("Writing to string should never fail");
1359                } else {
1360                    // Invalid Unicode - use replacement character
1361                    write!(&mut self.operations, "FFFD")
1362                        .expect("Writing to string should never fail");
1363                }
1364            }
1365            self.operations.push_str("> Tj\n");
1366        } else {
1367            // Use regular single-byte encoding for Latin-1
1368            self.operations.push('<');
1369            for ch in text.chars() {
1370                if ch as u32 <= 255 {
1371                    write!(&mut self.operations, "{:02X}", ch as u8)
1372                        .expect("Writing to string should never fail");
1373                } else {
1374                    write!(&mut self.operations, "3F")
1375                        .expect("Writing to string should never fail");
1376                }
1377            }
1378            self.operations.push_str("> Tj\n");
1379        }
1380
1381        // End text object
1382        self.operations.push_str("ET\n");
1383        Ok(self)
1384    }
1385
1386    /// Legacy: Draw text with UTF-16BE encoding (kept for compatibility)
1387    #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1388    pub fn draw_text_unicode(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1389        // Begin text object
1390        self.operations.push_str("BT\n");
1391
1392        // Set font if available
1393        if let Some(font_name) = &self.current_font_name {
1394            writeln!(
1395                &mut self.operations,
1396                "/{} {} Tf",
1397                font_name, self.current_font_size
1398            )
1399            .expect("Writing to string should never fail");
1400        } else {
1401            // Fallback to Helvetica if no font is set
1402            writeln!(
1403                &mut self.operations,
1404                "/Helvetica {} Tf",
1405                self.current_font_size
1406            )
1407            .expect("Writing to string should never fail");
1408        }
1409
1410        // Set text position
1411        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1412            .expect("Writing to string should never fail");
1413
1414        // Encode text as UTF-16BE hex string
1415        self.operations.push('<');
1416        let mut utf16_buffer = [0u16; 2];
1417        for ch in text.chars() {
1418            let encoded = ch.encode_utf16(&mut utf16_buffer);
1419            for unit in encoded {
1420                // Write UTF-16BE (big-endian)
1421                write!(&mut self.operations, "{:04X}", unit)
1422                    .expect("Writing to string should never fail");
1423            }
1424        }
1425        self.operations.push_str("> Tj\n");
1426
1427        // End text object
1428        self.operations.push_str("ET\n");
1429
1430        Ok(self)
1431    }
1432
1433    /// Get the characters used in this graphics context
1434    pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
1435        if self.used_characters.is_empty() {
1436            None
1437        } else {
1438            Some(self.used_characters.clone())
1439        }
1440    }
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445    use super::*;
1446
1447    #[test]
1448    fn test_graphics_context_new() {
1449        let ctx = GraphicsContext::new();
1450        assert_eq!(ctx.fill_color(), Color::black());
1451        assert_eq!(ctx.stroke_color(), Color::black());
1452        assert_eq!(ctx.line_width(), 1.0);
1453        assert_eq!(ctx.fill_opacity(), 1.0);
1454        assert_eq!(ctx.stroke_opacity(), 1.0);
1455        assert!(ctx.operations().is_empty());
1456    }
1457
1458    #[test]
1459    fn test_graphics_context_default() {
1460        let ctx = GraphicsContext::default();
1461        assert_eq!(ctx.fill_color(), Color::black());
1462        assert_eq!(ctx.stroke_color(), Color::black());
1463        assert_eq!(ctx.line_width(), 1.0);
1464    }
1465
1466    #[test]
1467    fn test_move_to() {
1468        let mut ctx = GraphicsContext::new();
1469        ctx.move_to(10.0, 20.0);
1470        assert!(ctx.operations().contains("10.00 20.00 m\n"));
1471    }
1472
1473    #[test]
1474    fn test_line_to() {
1475        let mut ctx = GraphicsContext::new();
1476        ctx.line_to(30.0, 40.0);
1477        assert!(ctx.operations().contains("30.00 40.00 l\n"));
1478    }
1479
1480    #[test]
1481    fn test_curve_to() {
1482        let mut ctx = GraphicsContext::new();
1483        ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
1484        assert!(ctx
1485            .operations()
1486            .contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
1487    }
1488
1489    #[test]
1490    fn test_rect() {
1491        let mut ctx = GraphicsContext::new();
1492        ctx.rect(10.0, 20.0, 100.0, 50.0);
1493        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1494    }
1495
1496    #[test]
1497    fn test_rectangle_alias() {
1498        let mut ctx = GraphicsContext::new();
1499        ctx.rectangle(10.0, 20.0, 100.0, 50.0);
1500        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1501    }
1502
1503    #[test]
1504    fn test_circle() {
1505        let mut ctx = GraphicsContext::new();
1506        ctx.circle(50.0, 50.0, 25.0);
1507
1508        let ops = ctx.operations();
1509        // Check that it starts with move to radius point
1510        assert!(ops.contains("75.00 50.00 m\n"));
1511        // Check that it contains curve operations
1512        assert!(ops.contains(" c\n"));
1513        // Check that it closes the path
1514        assert!(ops.contains("h\n"));
1515    }
1516
1517    #[test]
1518    fn test_close_path() {
1519        let mut ctx = GraphicsContext::new();
1520        ctx.close_path();
1521        assert!(ctx.operations().contains("h\n"));
1522    }
1523
1524    #[test]
1525    fn test_stroke() {
1526        let mut ctx = GraphicsContext::new();
1527        ctx.set_stroke_color(Color::red());
1528        ctx.rect(0.0, 0.0, 10.0, 10.0);
1529        ctx.stroke();
1530
1531        let ops = ctx.operations();
1532        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1533        assert!(ops.contains("S\n"));
1534    }
1535
1536    #[test]
1537    fn test_fill() {
1538        let mut ctx = GraphicsContext::new();
1539        ctx.set_fill_color(Color::blue());
1540        ctx.rect(0.0, 0.0, 10.0, 10.0);
1541        ctx.fill();
1542
1543        let ops = ctx.operations();
1544        assert!(ops.contains("0.000 0.000 1.000 rg\n"));
1545        assert!(ops.contains("f\n"));
1546    }
1547
1548    #[test]
1549    fn test_fill_stroke() {
1550        let mut ctx = GraphicsContext::new();
1551        ctx.set_fill_color(Color::green());
1552        ctx.set_stroke_color(Color::red());
1553        ctx.rect(0.0, 0.0, 10.0, 10.0);
1554        ctx.fill_stroke();
1555
1556        let ops = ctx.operations();
1557        assert!(ops.contains("0.000 1.000 0.000 rg\n"));
1558        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1559        assert!(ops.contains("B\n"));
1560    }
1561
1562    #[test]
1563    fn test_set_stroke_color() {
1564        let mut ctx = GraphicsContext::new();
1565        ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
1566        assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
1567    }
1568
1569    #[test]
1570    fn test_set_fill_color() {
1571        let mut ctx = GraphicsContext::new();
1572        ctx.set_fill_color(Color::gray(0.5));
1573        assert_eq!(ctx.fill_color(), Color::Gray(0.5));
1574    }
1575
1576    #[test]
1577    fn test_set_line_width() {
1578        let mut ctx = GraphicsContext::new();
1579        ctx.set_line_width(2.5);
1580        assert_eq!(ctx.line_width(), 2.5);
1581        assert!(ctx.operations().contains("2.50 w\n"));
1582    }
1583
1584    #[test]
1585    fn test_set_line_cap() {
1586        let mut ctx = GraphicsContext::new();
1587        ctx.set_line_cap(LineCap::Round);
1588        assert!(ctx.operations().contains("1 J\n"));
1589
1590        ctx.set_line_cap(LineCap::Butt);
1591        assert!(ctx.operations().contains("0 J\n"));
1592
1593        ctx.set_line_cap(LineCap::Square);
1594        assert!(ctx.operations().contains("2 J\n"));
1595    }
1596
1597    #[test]
1598    fn test_set_line_join() {
1599        let mut ctx = GraphicsContext::new();
1600        ctx.set_line_join(LineJoin::Round);
1601        assert!(ctx.operations().contains("1 j\n"));
1602
1603        ctx.set_line_join(LineJoin::Miter);
1604        assert!(ctx.operations().contains("0 j\n"));
1605
1606        ctx.set_line_join(LineJoin::Bevel);
1607        assert!(ctx.operations().contains("2 j\n"));
1608    }
1609
1610    #[test]
1611    fn test_save_restore_state() {
1612        let mut ctx = GraphicsContext::new();
1613        ctx.save_state();
1614        assert!(ctx.operations().contains("q\n"));
1615
1616        ctx.restore_state();
1617        assert!(ctx.operations().contains("Q\n"));
1618    }
1619
1620    #[test]
1621    fn test_translate() {
1622        let mut ctx = GraphicsContext::new();
1623        ctx.translate(50.0, 100.0);
1624        assert!(ctx.operations().contains("1 0 0 1 50.00 100.00 cm\n"));
1625    }
1626
1627    #[test]
1628    fn test_scale() {
1629        let mut ctx = GraphicsContext::new();
1630        ctx.scale(2.0, 3.0);
1631        assert!(ctx.operations().contains("2.00 0 0 3.00 0 0 cm\n"));
1632    }
1633
1634    #[test]
1635    fn test_rotate() {
1636        let mut ctx = GraphicsContext::new();
1637        let angle = std::f64::consts::PI / 4.0; // 45 degrees
1638        ctx.rotate(angle);
1639
1640        let ops = ctx.operations();
1641        assert!(ops.contains(" cm\n"));
1642        // Should contain cos and sin values
1643        assert!(ops.contains("0.707107")); // Approximate cos(45°)
1644    }
1645
1646    #[test]
1647    fn test_transform() {
1648        let mut ctx = GraphicsContext::new();
1649        ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
1650        assert!(ctx
1651            .operations()
1652            .contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
1653    }
1654
1655    #[test]
1656    fn test_draw_image() {
1657        let mut ctx = GraphicsContext::new();
1658        ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
1659
1660        let ops = ctx.operations();
1661        assert!(ops.contains("q\n")); // Save state
1662        assert!(ops.contains("100.00 0 0 150.00 10.00 20.00 cm\n")); // Transform
1663        assert!(ops.contains("/Image1 Do\n")); // Draw image
1664        assert!(ops.contains("Q\n")); // Restore state
1665    }
1666
1667    #[test]
1668    fn test_gray_color_operations() {
1669        let mut ctx = GraphicsContext::new();
1670        ctx.set_stroke_color(Color::gray(0.5));
1671        ctx.set_fill_color(Color::gray(0.7));
1672        ctx.stroke();
1673        ctx.fill();
1674
1675        let ops = ctx.operations();
1676        assert!(ops.contains("0.500 G\n")); // Stroke gray
1677        assert!(ops.contains("0.700 g\n")); // Fill gray
1678    }
1679
1680    #[test]
1681    fn test_cmyk_color_operations() {
1682        let mut ctx = GraphicsContext::new();
1683        ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
1684        ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
1685        ctx.stroke();
1686        ctx.fill();
1687
1688        let ops = ctx.operations();
1689        assert!(ops.contains("0.100 0.200 0.300 0.400 K\n")); // Stroke CMYK
1690        assert!(ops.contains("0.500 0.600 0.700 0.800 k\n")); // Fill CMYK
1691    }
1692
1693    #[test]
1694    fn test_method_chaining() {
1695        let mut ctx = GraphicsContext::new();
1696        ctx.move_to(0.0, 0.0)
1697            .line_to(10.0, 0.0)
1698            .line_to(10.0, 10.0)
1699            .line_to(0.0, 10.0)
1700            .close_path()
1701            .set_fill_color(Color::red())
1702            .fill();
1703
1704        let ops = ctx.operations();
1705        assert!(ops.contains("0.00 0.00 m\n"));
1706        assert!(ops.contains("10.00 0.00 l\n"));
1707        assert!(ops.contains("10.00 10.00 l\n"));
1708        assert!(ops.contains("0.00 10.00 l\n"));
1709        assert!(ops.contains("h\n"));
1710        assert!(ops.contains("f\n"));
1711    }
1712
1713    #[test]
1714    fn test_generate_operations() {
1715        let mut ctx = GraphicsContext::new();
1716        ctx.rect(0.0, 0.0, 10.0, 10.0);
1717
1718        let result = ctx.generate_operations();
1719        assert!(result.is_ok());
1720        let bytes = result.expect("Writing to string should never fail");
1721        let ops_string = String::from_utf8(bytes).expect("Writing to string should never fail");
1722        assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
1723    }
1724
1725    #[test]
1726    fn test_clear_operations() {
1727        let mut ctx = GraphicsContext::new();
1728        ctx.rect(0.0, 0.0, 10.0, 10.0);
1729        assert!(!ctx.operations().is_empty());
1730
1731        ctx.clear();
1732        assert!(ctx.operations().is_empty());
1733    }
1734
1735    #[test]
1736    fn test_complex_path() {
1737        let mut ctx = GraphicsContext::new();
1738        ctx.save_state()
1739            .translate(100.0, 100.0)
1740            .rotate(std::f64::consts::PI / 6.0)
1741            .scale(2.0, 2.0)
1742            .set_line_width(2.0)
1743            .set_stroke_color(Color::blue())
1744            .move_to(0.0, 0.0)
1745            .line_to(50.0, 0.0)
1746            .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
1747            .close_path()
1748            .stroke()
1749            .restore_state();
1750
1751        let ops = ctx.operations();
1752        assert!(ops.contains("q\n"));
1753        assert!(ops.contains("cm\n"));
1754        assert!(ops.contains("2.00 w\n"));
1755        assert!(ops.contains("0.000 0.000 1.000 RG\n"));
1756        assert!(ops.contains("S\n"));
1757        assert!(ops.contains("Q\n"));
1758    }
1759
1760    #[test]
1761    fn test_graphics_context_clone() {
1762        let mut ctx = GraphicsContext::new();
1763        ctx.set_fill_color(Color::red());
1764        ctx.set_stroke_color(Color::blue());
1765        ctx.set_line_width(3.0);
1766        ctx.set_opacity(0.5);
1767        ctx.rect(0.0, 0.0, 10.0, 10.0);
1768
1769        let ctx_clone = ctx.clone();
1770        assert_eq!(ctx_clone.fill_color(), Color::red());
1771        assert_eq!(ctx_clone.stroke_color(), Color::blue());
1772        assert_eq!(ctx_clone.line_width(), 3.0);
1773        assert_eq!(ctx_clone.fill_opacity(), 0.5);
1774        assert_eq!(ctx_clone.stroke_opacity(), 0.5);
1775        assert_eq!(ctx_clone.operations(), ctx.operations());
1776    }
1777
1778    #[test]
1779    fn test_set_opacity() {
1780        let mut ctx = GraphicsContext::new();
1781
1782        // Test setting opacity
1783        ctx.set_opacity(0.5);
1784        assert_eq!(ctx.fill_opacity(), 0.5);
1785        assert_eq!(ctx.stroke_opacity(), 0.5);
1786
1787        // Test clamping to valid range
1788        ctx.set_opacity(1.5);
1789        assert_eq!(ctx.fill_opacity(), 1.0);
1790        assert_eq!(ctx.stroke_opacity(), 1.0);
1791
1792        ctx.set_opacity(-0.5);
1793        assert_eq!(ctx.fill_opacity(), 0.0);
1794        assert_eq!(ctx.stroke_opacity(), 0.0);
1795    }
1796
1797    #[test]
1798    fn test_set_fill_opacity() {
1799        let mut ctx = GraphicsContext::new();
1800
1801        ctx.set_fill_opacity(0.3);
1802        assert_eq!(ctx.fill_opacity(), 0.3);
1803        assert_eq!(ctx.stroke_opacity(), 1.0); // Should not affect stroke
1804
1805        // Test clamping
1806        ctx.set_fill_opacity(2.0);
1807        assert_eq!(ctx.fill_opacity(), 1.0);
1808    }
1809
1810    #[test]
1811    fn test_set_stroke_opacity() {
1812        let mut ctx = GraphicsContext::new();
1813
1814        ctx.set_stroke_opacity(0.7);
1815        assert_eq!(ctx.stroke_opacity(), 0.7);
1816        assert_eq!(ctx.fill_opacity(), 1.0); // Should not affect fill
1817
1818        // Test clamping
1819        ctx.set_stroke_opacity(-1.0);
1820        assert_eq!(ctx.stroke_opacity(), 0.0);
1821    }
1822
1823    #[test]
1824    fn test_uses_transparency() {
1825        let mut ctx = GraphicsContext::new();
1826
1827        // Initially no transparency
1828        assert!(!ctx.uses_transparency());
1829
1830        // With fill transparency
1831        ctx.set_fill_opacity(0.5);
1832        assert!(ctx.uses_transparency());
1833
1834        // Reset and test stroke transparency
1835        ctx.set_fill_opacity(1.0);
1836        assert!(!ctx.uses_transparency());
1837        ctx.set_stroke_opacity(0.8);
1838        assert!(ctx.uses_transparency());
1839
1840        // Both transparent
1841        ctx.set_fill_opacity(0.5);
1842        assert!(ctx.uses_transparency());
1843    }
1844
1845    #[test]
1846    fn test_generate_graphics_state_dict() {
1847        let mut ctx = GraphicsContext::new();
1848
1849        // No transparency
1850        assert_eq!(ctx.generate_graphics_state_dict(), None);
1851
1852        // Fill opacity only
1853        ctx.set_fill_opacity(0.5);
1854        let dict = ctx
1855            .generate_graphics_state_dict()
1856            .expect("Writing to string should never fail");
1857        assert!(dict.contains("/Type /ExtGState"));
1858        assert!(dict.contains("/ca 0.500"));
1859        assert!(!dict.contains("/CA"));
1860
1861        // Stroke opacity only
1862        ctx.set_fill_opacity(1.0);
1863        ctx.set_stroke_opacity(0.75);
1864        let dict = ctx
1865            .generate_graphics_state_dict()
1866            .expect("Writing to string should never fail");
1867        assert!(dict.contains("/Type /ExtGState"));
1868        assert!(dict.contains("/CA 0.750"));
1869        assert!(!dict.contains("/ca"));
1870
1871        // Both opacities
1872        ctx.set_fill_opacity(0.25);
1873        let dict = ctx
1874            .generate_graphics_state_dict()
1875            .expect("Writing to string should never fail");
1876        assert!(dict.contains("/Type /ExtGState"));
1877        assert!(dict.contains("/ca 0.250"));
1878        assert!(dict.contains("/CA 0.750"));
1879    }
1880
1881    #[test]
1882    fn test_opacity_with_graphics_operations() {
1883        let mut ctx = GraphicsContext::new();
1884
1885        ctx.set_fill_color(Color::red())
1886            .set_opacity(0.5)
1887            .rect(10.0, 10.0, 100.0, 100.0)
1888            .fill();
1889
1890        assert_eq!(ctx.fill_opacity(), 0.5);
1891        assert_eq!(ctx.stroke_opacity(), 0.5);
1892
1893        let ops = ctx.operations();
1894        assert!(ops.contains("10.00 10.00 100.00 100.00 re"));
1895        assert!(ops.contains("1.000 0.000 0.000 rg")); // Red color
1896        assert!(ops.contains("f")); // Fill
1897    }
1898
1899    #[test]
1900    fn test_begin_end_text() {
1901        let mut ctx = GraphicsContext::new();
1902        ctx.begin_text();
1903        assert!(ctx.operations().contains("BT\n"));
1904
1905        ctx.end_text();
1906        assert!(ctx.operations().contains("ET\n"));
1907    }
1908
1909    #[test]
1910    fn test_set_font() {
1911        let mut ctx = GraphicsContext::new();
1912        ctx.set_font(Font::Helvetica, 12.0);
1913        assert!(ctx.operations().contains("/Helvetica 12 Tf\n"));
1914
1915        ctx.set_font(Font::TimesBold, 14.5);
1916        assert!(ctx.operations().contains("/Times-Bold 14.5 Tf\n"));
1917    }
1918
1919    #[test]
1920    fn test_set_text_position() {
1921        let mut ctx = GraphicsContext::new();
1922        ctx.set_text_position(100.0, 200.0);
1923        assert!(ctx.operations().contains("100.00 200.00 Td\n"));
1924    }
1925
1926    #[test]
1927    fn test_show_text() {
1928        let mut ctx = GraphicsContext::new();
1929        ctx.show_text("Hello World")
1930            .expect("Writing to string should never fail");
1931        assert!(ctx.operations().contains("(Hello World) Tj\n"));
1932    }
1933
1934    #[test]
1935    fn test_show_text_with_escaping() {
1936        let mut ctx = GraphicsContext::new();
1937        ctx.show_text("Test (parentheses)")
1938            .expect("Writing to string should never fail");
1939        assert!(ctx.operations().contains("(Test \\(parentheses\\)) Tj\n"));
1940
1941        ctx.clear();
1942        ctx.show_text("Back\\slash")
1943            .expect("Writing to string should never fail");
1944        assert!(ctx.operations().contains("(Back\\\\slash) Tj\n"));
1945
1946        ctx.clear();
1947        ctx.show_text("Line\nBreak")
1948            .expect("Writing to string should never fail");
1949        assert!(ctx.operations().contains("(Line\\nBreak) Tj\n"));
1950    }
1951
1952    #[test]
1953    fn test_text_operations_chaining() {
1954        let mut ctx = GraphicsContext::new();
1955        ctx.begin_text()
1956            .set_font(Font::Courier, 10.0)
1957            .set_text_position(50.0, 100.0)
1958            .show_text("Test")
1959            .unwrap()
1960            .end_text();
1961
1962        let ops = ctx.operations();
1963        assert!(ops.contains("BT\n"));
1964        assert!(ops.contains("/Courier 10 Tf\n"));
1965        assert!(ops.contains("50.00 100.00 Td\n"));
1966        assert!(ops.contains("(Test) Tj\n"));
1967        assert!(ops.contains("ET\n"));
1968    }
1969
1970    #[test]
1971    fn test_clip() {
1972        let mut ctx = GraphicsContext::new();
1973        ctx.clip();
1974        assert!(ctx.operations().contains("W\n"));
1975    }
1976
1977    #[test]
1978    fn test_clip_even_odd() {
1979        let mut ctx = GraphicsContext::new();
1980        ctx.clip_even_odd();
1981        assert!(ctx.operations().contains("W*\n"));
1982    }
1983
1984    #[test]
1985    fn test_clipping_with_path() {
1986        let mut ctx = GraphicsContext::new();
1987
1988        // Create a rectangular clipping path
1989        ctx.rect(10.0, 10.0, 100.0, 50.0).clip();
1990
1991        let ops = ctx.operations();
1992        assert!(ops.contains("10.00 10.00 100.00 50.00 re\n"));
1993        assert!(ops.contains("W\n"));
1994    }
1995
1996    #[test]
1997    fn test_clipping_even_odd_with_path() {
1998        let mut ctx = GraphicsContext::new();
1999
2000        // Create a complex path and clip with even-odd rule
2001        ctx.move_to(0.0, 0.0)
2002            .line_to(100.0, 0.0)
2003            .line_to(100.0, 100.0)
2004            .line_to(0.0, 100.0)
2005            .close_path()
2006            .clip_even_odd();
2007
2008        let ops = ctx.operations();
2009        assert!(ops.contains("0.00 0.00 m\n"));
2010        assert!(ops.contains("100.00 0.00 l\n"));
2011        assert!(ops.contains("100.00 100.00 l\n"));
2012        assert!(ops.contains("0.00 100.00 l\n"));
2013        assert!(ops.contains("h\n"));
2014        assert!(ops.contains("W*\n"));
2015    }
2016
2017    #[test]
2018    fn test_clipping_chaining() {
2019        let mut ctx = GraphicsContext::new();
2020
2021        // Test method chaining with clipping
2022        ctx.save_state()
2023            .rect(20.0, 20.0, 60.0, 60.0)
2024            .clip()
2025            .set_fill_color(Color::red())
2026            .rect(0.0, 0.0, 100.0, 100.0)
2027            .fill()
2028            .restore_state();
2029
2030        let ops = ctx.operations();
2031        assert!(ops.contains("q\n"));
2032        assert!(ops.contains("20.00 20.00 60.00 60.00 re\n"));
2033        assert!(ops.contains("W\n"));
2034        assert!(ops.contains("1.000 0.000 0.000 rg\n"));
2035        assert!(ops.contains("0.00 0.00 100.00 100.00 re\n"));
2036        assert!(ops.contains("f\n"));
2037        assert!(ops.contains("Q\n"));
2038    }
2039
2040    #[test]
2041    fn test_multiple_clipping_regions() {
2042        let mut ctx = GraphicsContext::new();
2043
2044        // Test nested clipping regions
2045        ctx.save_state()
2046            .rect(0.0, 0.0, 200.0, 200.0)
2047            .clip()
2048            .save_state()
2049            .circle(100.0, 100.0, 50.0)
2050            .clip_even_odd()
2051            .set_fill_color(Color::blue())
2052            .rect(50.0, 50.0, 100.0, 100.0)
2053            .fill()
2054            .restore_state()
2055            .restore_state();
2056
2057        let ops = ctx.operations();
2058        // Check for nested save/restore states
2059        let q_count = ops.matches("q\n").count();
2060        let q_restore_count = ops.matches("Q\n").count();
2061        assert_eq!(q_count, 2);
2062        assert_eq!(q_restore_count, 2);
2063
2064        // Check for both clipping operations
2065        assert!(ops.contains("W\n"));
2066        assert!(ops.contains("W*\n"));
2067    }
2068
2069    // ============= Additional Critical Method Tests =============
2070
2071    #[test]
2072    fn test_move_to_and_line_to() {
2073        let mut ctx = GraphicsContext::new();
2074        ctx.move_to(100.0, 200.0).line_to(300.0, 400.0).stroke();
2075
2076        let ops = ctx
2077            .generate_operations()
2078            .expect("Writing to string should never fail");
2079        let ops_str = String::from_utf8_lossy(&ops);
2080        assert!(ops_str.contains("100.00 200.00 m"));
2081        assert!(ops_str.contains("300.00 400.00 l"));
2082        assert!(ops_str.contains("S"));
2083    }
2084
2085    #[test]
2086    fn test_bezier_curve() {
2087        let mut ctx = GraphicsContext::new();
2088        ctx.move_to(0.0, 0.0)
2089            .curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0)
2090            .stroke();
2091
2092        let ops = ctx
2093            .generate_operations()
2094            .expect("Writing to string should never fail");
2095        let ops_str = String::from_utf8_lossy(&ops);
2096        assert!(ops_str.contains("0.00 0.00 m"));
2097        assert!(ops_str.contains("10.00 20.00 30.00 40.00 50.00 60.00 c"));
2098        assert!(ops_str.contains("S"));
2099    }
2100
2101    #[test]
2102    fn test_circle_path() {
2103        let mut ctx = GraphicsContext::new();
2104        ctx.circle(100.0, 100.0, 50.0).fill();
2105
2106        let ops = ctx
2107            .generate_operations()
2108            .expect("Writing to string should never fail");
2109        let ops_str = String::from_utf8_lossy(&ops);
2110        // Circle should use bezier curves (c operator)
2111        assert!(ops_str.contains(" c"));
2112        assert!(ops_str.contains("f"));
2113    }
2114
2115    #[test]
2116    fn test_path_closing() {
2117        let mut ctx = GraphicsContext::new();
2118        ctx.move_to(0.0, 0.0)
2119            .line_to(100.0, 0.0)
2120            .line_to(100.0, 100.0)
2121            .close_path()
2122            .stroke();
2123
2124        let ops = ctx
2125            .generate_operations()
2126            .expect("Writing to string should never fail");
2127        let ops_str = String::from_utf8_lossy(&ops);
2128        assert!(ops_str.contains("h")); // close path operator
2129        assert!(ops_str.contains("S"));
2130    }
2131
2132    #[test]
2133    fn test_fill_and_stroke() {
2134        let mut ctx = GraphicsContext::new();
2135        ctx.rect(10.0, 10.0, 50.0, 50.0).fill_stroke();
2136
2137        let ops = ctx
2138            .generate_operations()
2139            .expect("Writing to string should never fail");
2140        let ops_str = String::from_utf8_lossy(&ops);
2141        assert!(ops_str.contains("10.00 10.00 50.00 50.00 re"));
2142        assert!(ops_str.contains("B")); // fill and stroke operator
2143    }
2144
2145    #[test]
2146    fn test_color_settings() {
2147        let mut ctx = GraphicsContext::new();
2148        ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2149            .set_stroke_color(Color::rgb(0.0, 1.0, 0.0))
2150            .rect(10.0, 10.0, 50.0, 50.0)
2151            .fill_stroke(); // This will write the colors
2152
2153        assert_eq!(ctx.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2154        assert_eq!(ctx.stroke_color(), Color::rgb(0.0, 1.0, 0.0));
2155
2156        let ops = ctx
2157            .generate_operations()
2158            .expect("Writing to string should never fail");
2159        let ops_str = String::from_utf8_lossy(&ops);
2160        assert!(ops_str.contains("1.000 0.000 0.000 rg")); // red fill
2161        assert!(ops_str.contains("0.000 1.000 0.000 RG")); // green stroke
2162    }
2163
2164    #[test]
2165    fn test_line_styles() {
2166        let mut ctx = GraphicsContext::new();
2167        ctx.set_line_width(2.5)
2168            .set_line_cap(LineCap::Round)
2169            .set_line_join(LineJoin::Bevel);
2170
2171        assert_eq!(ctx.line_width(), 2.5);
2172
2173        let ops = ctx
2174            .generate_operations()
2175            .expect("Writing to string should never fail");
2176        let ops_str = String::from_utf8_lossy(&ops);
2177        assert!(ops_str.contains("2.50 w")); // line width
2178        assert!(ops_str.contains("1 J")); // round line cap
2179        assert!(ops_str.contains("2 j")); // bevel line join
2180    }
2181
2182    #[test]
2183    fn test_opacity_settings() {
2184        let mut ctx = GraphicsContext::new();
2185        ctx.set_opacity(0.5);
2186
2187        assert_eq!(ctx.fill_opacity(), 0.5);
2188        assert_eq!(ctx.stroke_opacity(), 0.5);
2189        assert!(ctx.uses_transparency());
2190
2191        ctx.set_fill_opacity(0.7).set_stroke_opacity(0.3);
2192
2193        assert_eq!(ctx.fill_opacity(), 0.7);
2194        assert_eq!(ctx.stroke_opacity(), 0.3);
2195    }
2196
2197    #[test]
2198    fn test_state_save_restore() {
2199        let mut ctx = GraphicsContext::new();
2200        ctx.save_state()
2201            .set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2202            .restore_state();
2203
2204        let ops = ctx
2205            .generate_operations()
2206            .expect("Writing to string should never fail");
2207        let ops_str = String::from_utf8_lossy(&ops);
2208        assert!(ops_str.contains("q")); // save state
2209        assert!(ops_str.contains("Q")); // restore state
2210    }
2211
2212    #[test]
2213    fn test_transformations() {
2214        let mut ctx = GraphicsContext::new();
2215        ctx.translate(100.0, 200.0).scale(2.0, 3.0).rotate(45.0);
2216
2217        let ops = ctx
2218            .generate_operations()
2219            .expect("Writing to string should never fail");
2220        let ops_str = String::from_utf8_lossy(&ops);
2221        assert!(ops_str.contains("1 0 0 1 100.00 200.00 cm")); // translate
2222        assert!(ops_str.contains("2.00 0 0 3.00 0 0 cm")); // scale
2223        assert!(ops_str.contains("cm")); // rotate matrix
2224    }
2225
2226    #[test]
2227    fn test_custom_transform() {
2228        let mut ctx = GraphicsContext::new();
2229        ctx.transform(1.0, 0.5, 0.5, 1.0, 10.0, 20.0);
2230
2231        let ops = ctx
2232            .generate_operations()
2233            .expect("Writing to string should never fail");
2234        let ops_str = String::from_utf8_lossy(&ops);
2235        assert!(ops_str.contains("1.00 0.50 0.50 1.00 10.00 20.00 cm"));
2236    }
2237
2238    #[test]
2239    fn test_rectangle_path() {
2240        let mut ctx = GraphicsContext::new();
2241        ctx.rectangle(25.0, 25.0, 150.0, 100.0).stroke();
2242
2243        let ops = ctx
2244            .generate_operations()
2245            .expect("Writing to string should never fail");
2246        let ops_str = String::from_utf8_lossy(&ops);
2247        assert!(ops_str.contains("25.00 25.00 150.00 100.00 re"));
2248        assert!(ops_str.contains("S"));
2249    }
2250
2251    #[test]
2252    fn test_empty_operations() {
2253        let ctx = GraphicsContext::new();
2254        let ops = ctx
2255            .generate_operations()
2256            .expect("Writing to string should never fail");
2257        assert!(ops.is_empty());
2258    }
2259
2260    #[test]
2261    fn test_complex_path_operations() {
2262        let mut ctx = GraphicsContext::new();
2263        ctx.move_to(50.0, 50.0)
2264            .line_to(100.0, 50.0)
2265            .curve_to(125.0, 50.0, 150.0, 75.0, 150.0, 100.0)
2266            .line_to(150.0, 150.0)
2267            .close_path()
2268            .fill();
2269
2270        let ops = ctx
2271            .generate_operations()
2272            .expect("Writing to string should never fail");
2273        let ops_str = String::from_utf8_lossy(&ops);
2274        assert!(ops_str.contains("50.00 50.00 m"));
2275        assert!(ops_str.contains("100.00 50.00 l"));
2276        assert!(ops_str.contains("125.00 50.00 150.00 75.00 150.00 100.00 c"));
2277        assert!(ops_str.contains("150.00 150.00 l"));
2278        assert!(ops_str.contains("h"));
2279        assert!(ops_str.contains("f"));
2280    }
2281
2282    #[test]
2283    fn test_graphics_state_dict_generation() {
2284        let mut ctx = GraphicsContext::new();
2285
2286        // Without transparency, should return None
2287        assert!(ctx.generate_graphics_state_dict().is_none());
2288
2289        // With transparency, should generate dict
2290        ctx.set_opacity(0.5);
2291        let dict = ctx.generate_graphics_state_dict();
2292        assert!(dict.is_some());
2293        let dict_str = dict.expect("Writing to string should never fail");
2294        assert!(dict_str.contains("/ca 0.5"));
2295        assert!(dict_str.contains("/CA 0.5"));
2296    }
2297
2298    #[test]
2299    fn test_line_dash_pattern() {
2300        let mut ctx = GraphicsContext::new();
2301        let pattern = LineDashPattern {
2302            array: vec![3.0, 2.0],
2303            phase: 0.0,
2304        };
2305        ctx.set_line_dash_pattern(pattern);
2306
2307        let ops = ctx
2308            .generate_operations()
2309            .expect("Writing to string should never fail");
2310        let ops_str = String::from_utf8_lossy(&ops);
2311        assert!(ops_str.contains("[3.00 2.00] 0.00 d"));
2312    }
2313
2314    #[test]
2315    fn test_miter_limit_setting() {
2316        let mut ctx = GraphicsContext::new();
2317        ctx.set_miter_limit(4.0);
2318
2319        let ops = ctx
2320            .generate_operations()
2321            .expect("Writing to string should never fail");
2322        let ops_str = String::from_utf8_lossy(&ops);
2323        assert!(ops_str.contains("4.00 M"));
2324    }
2325
2326    #[test]
2327    fn test_line_cap_styles() {
2328        let mut ctx = GraphicsContext::new();
2329
2330        ctx.set_line_cap(LineCap::Butt);
2331        let ops = ctx
2332            .generate_operations()
2333            .expect("Writing to string should never fail");
2334        let ops_str = String::from_utf8_lossy(&ops);
2335        assert!(ops_str.contains("0 J"));
2336
2337        let mut ctx = GraphicsContext::new();
2338        ctx.set_line_cap(LineCap::Round);
2339        let ops = ctx
2340            .generate_operations()
2341            .expect("Writing to string should never fail");
2342        let ops_str = String::from_utf8_lossy(&ops);
2343        assert!(ops_str.contains("1 J"));
2344
2345        let mut ctx = GraphicsContext::new();
2346        ctx.set_line_cap(LineCap::Square);
2347        let ops = ctx
2348            .generate_operations()
2349            .expect("Writing to string should never fail");
2350        let ops_str = String::from_utf8_lossy(&ops);
2351        assert!(ops_str.contains("2 J"));
2352    }
2353
2354    #[test]
2355    fn test_transparency_groups() {
2356        let mut ctx = GraphicsContext::new();
2357
2358        // Test basic transparency group
2359        let group = TransparencyGroup::new()
2360            .with_isolated(true)
2361            .with_opacity(0.5);
2362
2363        ctx.begin_transparency_group(group);
2364        assert!(ctx.in_transparency_group());
2365
2366        // Draw something in the group
2367        ctx.rect(10.0, 10.0, 100.0, 100.0);
2368        ctx.fill();
2369
2370        ctx.end_transparency_group();
2371        assert!(!ctx.in_transparency_group());
2372
2373        // Check that operations contain transparency markers
2374        let ops = ctx.operations();
2375        assert!(ops.contains("% Begin Transparency Group"));
2376        assert!(ops.contains("% End Transparency Group"));
2377    }
2378
2379    #[test]
2380    fn test_nested_transparency_groups() {
2381        let mut ctx = GraphicsContext::new();
2382
2383        // First group
2384        let group1 = TransparencyGroup::isolated().with_opacity(0.8);
2385        ctx.begin_transparency_group(group1);
2386        assert!(ctx.in_transparency_group());
2387
2388        // Nested group
2389        let group2 = TransparencyGroup::knockout().with_blend_mode(BlendMode::Multiply);
2390        ctx.begin_transparency_group(group2);
2391
2392        // Draw in nested group
2393        ctx.circle(50.0, 50.0, 25.0);
2394        ctx.fill();
2395
2396        // End nested group
2397        ctx.end_transparency_group();
2398        assert!(ctx.in_transparency_group()); // Still in first group
2399
2400        // End first group
2401        ctx.end_transparency_group();
2402        assert!(!ctx.in_transparency_group());
2403    }
2404
2405    #[test]
2406    fn test_line_join_styles() {
2407        let mut ctx = GraphicsContext::new();
2408
2409        ctx.set_line_join(LineJoin::Miter);
2410        let ops = ctx
2411            .generate_operations()
2412            .expect("Writing to string should never fail");
2413        let ops_str = String::from_utf8_lossy(&ops);
2414        assert!(ops_str.contains("0 j"));
2415
2416        let mut ctx = GraphicsContext::new();
2417        ctx.set_line_join(LineJoin::Round);
2418        let ops = ctx
2419            .generate_operations()
2420            .expect("Writing to string should never fail");
2421        let ops_str = String::from_utf8_lossy(&ops);
2422        assert!(ops_str.contains("1 j"));
2423
2424        let mut ctx = GraphicsContext::new();
2425        ctx.set_line_join(LineJoin::Bevel);
2426        let ops = ctx
2427            .generate_operations()
2428            .expect("Writing to string should never fail");
2429        let ops_str = String::from_utf8_lossy(&ops);
2430        assert!(ops_str.contains("2 j"));
2431    }
2432
2433    #[test]
2434    fn test_rendering_intent() {
2435        let mut ctx = GraphicsContext::new();
2436
2437        ctx.set_rendering_intent(RenderingIntent::AbsoluteColorimetric);
2438        assert_eq!(
2439            ctx.rendering_intent(),
2440            RenderingIntent::AbsoluteColorimetric
2441        );
2442
2443        ctx.set_rendering_intent(RenderingIntent::Perceptual);
2444        assert_eq!(ctx.rendering_intent(), RenderingIntent::Perceptual);
2445
2446        ctx.set_rendering_intent(RenderingIntent::Saturation);
2447        assert_eq!(ctx.rendering_intent(), RenderingIntent::Saturation);
2448    }
2449
2450    #[test]
2451    fn test_flatness_tolerance() {
2452        let mut ctx = GraphicsContext::new();
2453
2454        ctx.set_flatness(0.5);
2455        assert_eq!(ctx.flatness(), 0.5);
2456
2457        let ops = ctx
2458            .generate_operations()
2459            .expect("Writing to string should never fail");
2460        let ops_str = String::from_utf8_lossy(&ops);
2461        assert!(ops_str.contains("0.50 i"));
2462    }
2463
2464    #[test]
2465    fn test_smoothness_tolerance() {
2466        let mut ctx = GraphicsContext::new();
2467
2468        let _ = ctx.set_smoothness(0.1);
2469        assert_eq!(ctx.smoothness(), 0.1);
2470    }
2471
2472    #[test]
2473    fn test_bezier_curves() {
2474        let mut ctx = GraphicsContext::new();
2475
2476        // Cubic Bezier
2477        ctx.move_to(10.0, 10.0);
2478        ctx.curve_to(20.0, 10.0, 30.0, 20.0, 30.0, 30.0);
2479
2480        let ops = ctx
2481            .generate_operations()
2482            .expect("Writing to string should never fail");
2483        let ops_str = String::from_utf8_lossy(&ops);
2484        assert!(ops_str.contains("10.00 10.00 m"));
2485        assert!(ops_str.contains("c")); // cubic curve
2486    }
2487
2488    #[test]
2489    fn test_clipping_path() {
2490        let mut ctx = GraphicsContext::new();
2491
2492        ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2493        ctx.clip();
2494
2495        let ops = ctx
2496            .generate_operations()
2497            .expect("Writing to string should never fail");
2498        let ops_str = String::from_utf8_lossy(&ops);
2499        assert!(ops_str.contains("W"));
2500    }
2501
2502    #[test]
2503    fn test_even_odd_clipping() {
2504        let mut ctx = GraphicsContext::new();
2505
2506        ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2507        ctx.clip_even_odd();
2508
2509        let ops = ctx
2510            .generate_operations()
2511            .expect("Writing to string should never fail");
2512        let ops_str = String::from_utf8_lossy(&ops);
2513        assert!(ops_str.contains("W*"));
2514    }
2515
2516    #[test]
2517    fn test_color_creation() {
2518        // Test color creation methods
2519        let gray = Color::gray(0.5);
2520        assert_eq!(gray, Color::Gray(0.5));
2521
2522        let rgb = Color::rgb(0.2, 0.4, 0.6);
2523        assert_eq!(rgb, Color::Rgb(0.2, 0.4, 0.6));
2524
2525        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
2526        assert_eq!(cmyk, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
2527
2528        // Test predefined colors
2529        assert_eq!(Color::black(), Color::Gray(0.0));
2530        assert_eq!(Color::white(), Color::Gray(1.0));
2531        assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
2532    }
2533
2534    #[test]
2535    fn test_extended_graphics_state() {
2536        let ctx = GraphicsContext::new();
2537
2538        // Test that we can create and use an extended graphics state
2539        let _extgstate = ExtGState::new();
2540
2541        // We should be able to create the state without errors
2542        assert!(ctx.generate_operations().is_ok());
2543    }
2544
2545    #[test]
2546    fn test_path_construction_methods() {
2547        let mut ctx = GraphicsContext::new();
2548
2549        // Test basic path construction methods that exist
2550        ctx.move_to(10.0, 10.0);
2551        ctx.line_to(20.0, 20.0);
2552        ctx.curve_to(30.0, 30.0, 40.0, 40.0, 50.0, 50.0);
2553        ctx.rect(60.0, 60.0, 30.0, 30.0);
2554        ctx.circle(100.0, 100.0, 25.0);
2555        ctx.close_path();
2556
2557        let ops = ctx
2558            .generate_operations()
2559            .expect("Writing to string should never fail");
2560        assert!(!ops.is_empty());
2561    }
2562
2563    #[test]
2564    fn test_graphics_context_clone_advanced() {
2565        let mut ctx = GraphicsContext::new();
2566        ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
2567        ctx.set_line_width(5.0);
2568
2569        let cloned = ctx.clone();
2570        assert_eq!(cloned.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2571        assert_eq!(cloned.line_width(), 5.0);
2572    }
2573
2574    #[test]
2575    fn test_basic_drawing_operations() {
2576        let mut ctx = GraphicsContext::new();
2577
2578        // Test that we can at least create a basic drawing
2579        ctx.move_to(50.0, 50.0);
2580        ctx.line_to(100.0, 100.0);
2581        ctx.stroke();
2582
2583        let ops = ctx
2584            .generate_operations()
2585            .expect("Writing to string should never fail");
2586        let ops_str = String::from_utf8_lossy(&ops);
2587        assert!(ops_str.contains("m")); // move
2588        assert!(ops_str.contains("l")); // line
2589        assert!(ops_str.contains("S")); // stroke
2590    }
2591
2592    #[test]
2593    fn test_graphics_state_stack() {
2594        let mut ctx = GraphicsContext::new();
2595
2596        // Initial state
2597        ctx.set_fill_color(Color::black());
2598
2599        // Save and change
2600        ctx.save_state();
2601        ctx.set_fill_color(Color::red());
2602        assert_eq!(ctx.fill_color(), Color::red());
2603
2604        // Save again and change
2605        ctx.save_state();
2606        ctx.set_fill_color(Color::blue());
2607        assert_eq!(ctx.fill_color(), Color::blue());
2608
2609        // Restore once
2610        ctx.restore_state();
2611        assert_eq!(ctx.fill_color(), Color::red());
2612
2613        // Restore again
2614        ctx.restore_state();
2615        assert_eq!(ctx.fill_color(), Color::black());
2616    }
2617
2618    #[test]
2619    fn test_word_spacing() {
2620        let mut ctx = GraphicsContext::new();
2621        ctx.set_word_spacing(2.5);
2622
2623        let ops = ctx.generate_operations().unwrap();
2624        let ops_str = String::from_utf8_lossy(&ops);
2625        assert!(ops_str.contains("2.50 Tw"));
2626    }
2627
2628    #[test]
2629    fn test_character_spacing() {
2630        let mut ctx = GraphicsContext::new();
2631        ctx.set_character_spacing(1.0);
2632
2633        let ops = ctx.generate_operations().unwrap();
2634        let ops_str = String::from_utf8_lossy(&ops);
2635        assert!(ops_str.contains("1.00 Tc"));
2636    }
2637
2638    #[test]
2639    fn test_justified_text() {
2640        let mut ctx = GraphicsContext::new();
2641        ctx.begin_text();
2642        ctx.set_text_position(100.0, 200.0);
2643        ctx.show_justified_text("Hello world from PDF", 200.0)
2644            .unwrap();
2645        ctx.end_text();
2646
2647        let ops = ctx.generate_operations().unwrap();
2648        let ops_str = String::from_utf8_lossy(&ops);
2649
2650        // Should contain text operations
2651        assert!(ops_str.contains("BT")); // Begin text
2652        assert!(ops_str.contains("ET")); // End text
2653        assert!(ops_str.contains("100.00 200.00 Td")); // Text position
2654        assert!(ops_str.contains("(Hello world from PDF) Tj")); // Show text
2655
2656        // Should contain word spacing operations
2657        assert!(ops_str.contains("Tw")); // Word spacing
2658    }
2659
2660    #[test]
2661    fn test_justified_text_single_word() {
2662        let mut ctx = GraphicsContext::new();
2663        ctx.begin_text();
2664        ctx.show_justified_text("Hello", 200.0).unwrap();
2665        ctx.end_text();
2666
2667        let ops = ctx.generate_operations().unwrap();
2668        let ops_str = String::from_utf8_lossy(&ops);
2669
2670        // Single word should just use normal text display
2671        assert!(ops_str.contains("(Hello) Tj"));
2672        // Should not contain word spacing since there's only one word
2673        assert_eq!(ops_str.matches("Tw").count(), 0);
2674    }
2675
2676    #[test]
2677    fn test_text_width_estimation() {
2678        let ctx = GraphicsContext::new();
2679        let width = ctx.estimate_text_width_simple("Hello");
2680
2681        // Should return reasonable estimation based on font size and character count
2682        assert!(width > 0.0);
2683        assert_eq!(width, 5.0 * 12.0 * 0.6); // 5 chars * 12pt font * 0.6 factor
2684    }
2685
2686    #[test]
2687    fn test_set_alpha_methods() {
2688        let mut ctx = GraphicsContext::new();
2689
2690        // Test that set_alpha methods don't panic and return correctly
2691        assert!(ctx.set_alpha(0.5).is_ok());
2692        assert!(ctx.set_alpha_fill(0.3).is_ok());
2693        assert!(ctx.set_alpha_stroke(0.7).is_ok());
2694
2695        // Test edge cases - should handle clamping in ExtGState
2696        assert!(ctx.set_alpha(1.5).is_ok()); // Should not panic
2697        assert!(ctx.set_alpha(-0.2).is_ok()); // Should not panic
2698        assert!(ctx.set_alpha_fill(2.0).is_ok()); // Should not panic
2699        assert!(ctx.set_alpha_stroke(-1.0).is_ok()); // Should not panic
2700
2701        // Test that methods return self for chaining
2702        let result = ctx
2703            .set_alpha(0.5)
2704            .and_then(|c| c.set_alpha_fill(0.3))
2705            .and_then(|c| c.set_alpha_stroke(0.7));
2706        assert!(result.is_ok());
2707    }
2708
2709    #[test]
2710    fn test_alpha_methods_generate_extgstate() {
2711        let mut ctx = GraphicsContext::new();
2712
2713        // Set some transparency
2714        ctx.set_alpha(0.5).unwrap();
2715
2716        // Draw something to trigger ExtGState generation
2717        ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
2718
2719        let ops = ctx.generate_operations().unwrap();
2720        let ops_str = String::from_utf8_lossy(&ops);
2721
2722        // Should contain ExtGState reference
2723        assert!(ops_str.contains("/GS")); // ExtGState name
2724        assert!(ops_str.contains(" gs\n")); // ExtGState operator
2725
2726        // Test separate alpha settings
2727        ctx.clear();
2728        ctx.set_alpha_fill(0.3).unwrap();
2729        ctx.set_alpha_stroke(0.8).unwrap();
2730        ctx.rect(20.0, 20.0, 60.0, 60.0).fill_stroke();
2731
2732        let ops2 = ctx.generate_operations().unwrap();
2733        let ops_str2 = String::from_utf8_lossy(&ops2);
2734
2735        // Should contain multiple ExtGState references
2736        assert!(ops_str2.contains("/GS")); // ExtGState names
2737        assert!(ops_str2.contains(" gs\n")); // ExtGState operators
2738    }
2739}