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    fn set_pending_extgstate(&mut self, state: ExtGState) {
860        self.pending_extgstate = Some(state);
861    }
862
863    /// Apply any pending ExtGState before drawing
864    fn apply_pending_extgstate(&mut self) -> Result<()> {
865        if let Some(state) = self.pending_extgstate.take() {
866            let state_name = self.extgstate_manager.add_state(state)?;
867            writeln!(&mut self.operations, "/{state_name} gs")
868                .expect("Writing to string should never fail");
869        }
870        Ok(())
871    }
872
873    /// Create and apply a custom ExtGState
874    pub fn with_extgstate<F>(&mut self, builder: F) -> Result<&mut Self>
875    where
876        F: FnOnce(ExtGState) -> ExtGState,
877    {
878        let state = builder(ExtGState::new());
879        self.apply_extgstate(state)
880    }
881
882    /// Set blend mode for transparency
883    pub fn set_blend_mode(&mut self, mode: BlendMode) -> Result<&mut Self> {
884        let state = ExtGState::new().with_blend_mode(mode);
885        self.apply_extgstate(state)
886    }
887
888    /// Set alpha for both stroke and fill operations
889    pub fn set_alpha(&mut self, alpha: f64) -> Result<&mut Self> {
890        let state = ExtGState::new().with_alpha(alpha);
891        self.set_pending_extgstate(state);
892        Ok(self)
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.set_pending_extgstate(state);
899        Ok(self)
900    }
901
902    /// Set alpha for fill operations only
903    pub fn set_alpha_fill(&mut self, alpha: f64) -> Result<&mut Self> {
904        let state = ExtGState::new().with_alpha_fill(alpha);
905        self.set_pending_extgstate(state);
906        Ok(self)
907    }
908
909    /// Set overprint for stroke operations
910    pub fn set_overprint_stroke(&mut self, overprint: bool) -> Result<&mut Self> {
911        let state = ExtGState::new().with_overprint_stroke(overprint);
912        self.apply_extgstate(state)
913    }
914
915    /// Set overprint for fill operations
916    pub fn set_overprint_fill(&mut self, overprint: bool) -> Result<&mut Self> {
917        let state = ExtGState::new().with_overprint_fill(overprint);
918        self.apply_extgstate(state)
919    }
920
921    /// Set stroke adjustment
922    pub fn set_stroke_adjustment(&mut self, adjustment: bool) -> Result<&mut Self> {
923        let state = ExtGState::new().with_stroke_adjustment(adjustment);
924        self.apply_extgstate(state)
925    }
926
927    /// Set smoothness tolerance
928    pub fn set_smoothness(&mut self, smoothness: f64) -> Result<&mut Self> {
929        self.current_smoothness = smoothness.clamp(0.0, 1.0);
930        let state = ExtGState::new().with_smoothness(self.current_smoothness);
931        self.apply_extgstate(state)
932    }
933
934    // Getters for extended graphics state
935
936    /// Get current line dash pattern
937    pub fn line_dash_pattern(&self) -> Option<&LineDashPattern> {
938        self.current_dash_pattern.as_ref()
939    }
940
941    /// Get current miter limit
942    pub fn miter_limit(&self) -> f64 {
943        self.current_miter_limit
944    }
945
946    /// Get current line cap
947    pub fn line_cap(&self) -> LineCap {
948        self.current_line_cap
949    }
950
951    /// Get current line join
952    pub fn line_join(&self) -> LineJoin {
953        self.current_line_join
954    }
955
956    /// Get current rendering intent
957    pub fn rendering_intent(&self) -> RenderingIntent {
958        self.current_rendering_intent
959    }
960
961    /// Get current flatness tolerance
962    pub fn flatness(&self) -> f64 {
963        self.current_flatness
964    }
965
966    /// Get current smoothness tolerance
967    pub fn smoothness(&self) -> f64 {
968        self.current_smoothness
969    }
970
971    /// Get the ExtGState manager (for advanced usage)
972    pub fn extgstate_manager(&self) -> &ExtGStateManager {
973        &self.extgstate_manager
974    }
975
976    /// Get mutable ExtGState manager (for advanced usage)
977    pub fn extgstate_manager_mut(&mut self) -> &mut ExtGStateManager {
978        &mut self.extgstate_manager
979    }
980
981    /// Generate ExtGState resource dictionary for PDF
982    pub fn generate_extgstate_resources(&self) -> Result<String> {
983        self.extgstate_manager.to_resource_dictionary()
984    }
985
986    /// Check if any extended graphics states are defined
987    pub fn has_extgstates(&self) -> bool {
988        self.extgstate_manager.count() > 0
989    }
990
991    /// Add a command to the operations
992    pub fn add_command(&mut self, command: &str) {
993        self.operations.push_str(command);
994        self.operations.push('\n');
995    }
996
997    /// Create clipping path from current path using non-zero winding rule
998    pub fn clip(&mut self) -> &mut Self {
999        self.operations.push_str("W\n");
1000        self
1001    }
1002
1003    /// Create clipping path from current path using even-odd rule
1004    pub fn clip_even_odd(&mut self) -> &mut Self {
1005        self.operations.push_str("W*\n");
1006        self
1007    }
1008
1009    /// Create clipping path and stroke it
1010    pub fn clip_stroke(&mut self) -> &mut Self {
1011        self.apply_stroke_color();
1012        self.operations.push_str("W S\n");
1013        self
1014    }
1015
1016    /// Set a custom clipping path
1017    pub fn set_clipping_path(&mut self, path: ClippingPath) -> Result<&mut Self> {
1018        let ops = path.to_pdf_operations()?;
1019        self.operations.push_str(&ops);
1020        self.clipping_region.set_clip(path);
1021        Ok(self)
1022    }
1023
1024    /// Clear the current clipping path
1025    pub fn clear_clipping(&mut self) -> &mut Self {
1026        self.clipping_region.clear_clip();
1027        self
1028    }
1029
1030    /// Save the current clipping state (called automatically by save_state)
1031    fn save_clipping_state(&mut self) {
1032        self.clipping_region.save();
1033    }
1034
1035    /// Restore the previous clipping state (called automatically by restore_state)
1036    fn restore_clipping_state(&mut self) {
1037        self.clipping_region.restore();
1038    }
1039
1040    /// Create a rectangular clipping region
1041    pub fn clip_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> Result<&mut Self> {
1042        let path = ClippingPath::rect(x, y, width, height);
1043        self.set_clipping_path(path)
1044    }
1045
1046    /// Create a circular clipping region
1047    pub fn clip_circle(&mut self, cx: f64, cy: f64, radius: f64) -> Result<&mut Self> {
1048        let path = ClippingPath::circle(cx, cy, radius);
1049        self.set_clipping_path(path)
1050    }
1051
1052    /// Create an elliptical clipping region
1053    pub fn clip_ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) -> Result<&mut Self> {
1054        let path = ClippingPath::ellipse(cx, cy, rx, ry);
1055        self.set_clipping_path(path)
1056    }
1057
1058    /// Check if a clipping path is active
1059    pub fn has_clipping(&self) -> bool {
1060        self.clipping_region.has_clip()
1061    }
1062
1063    /// Get the current clipping path
1064    pub fn clipping_path(&self) -> Option<&ClippingPath> {
1065        self.clipping_region.current()
1066    }
1067
1068    /// Set the font manager for custom fonts
1069    pub fn set_font_manager(&mut self, font_manager: Arc<FontManager>) -> &mut Self {
1070        self.font_manager = Some(font_manager);
1071        self
1072    }
1073
1074    /// Set the current font to a custom font
1075    pub fn set_custom_font(&mut self, font_name: &str, size: f64) -> &mut Self {
1076        self.current_font_name = Some(font_name.to_string());
1077        self.current_font_size = size;
1078
1079        // Try to get the glyph mapping from the font manager
1080        if let Some(ref font_manager) = self.font_manager {
1081            if let Some(mapping) = font_manager.get_font_glyph_mapping(font_name) {
1082                self.glyph_mapping = Some(mapping);
1083            }
1084        }
1085
1086        self
1087    }
1088
1089    /// Set the glyph mapping for Unicode fonts (Unicode -> GlyphID)
1090    pub fn set_glyph_mapping(&mut self, mapping: HashMap<u32, u16>) -> &mut Self {
1091        self.glyph_mapping = Some(mapping);
1092        self
1093    }
1094
1095    /// Draw text at the specified position with automatic encoding detection
1096    pub fn draw_text(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1097        // Track used characters for font subsetting
1098        self.used_characters.extend(text.chars());
1099
1100        // Check if we're using a custom font (which will be Type0/Unicode)
1101        // Custom fonts are those that are not the standard PDF fonts (Helvetica, Times, etc.)
1102        let using_custom_font = if let Some(ref font_name) = self.current_font_name {
1103            // If font name doesn't start with standard PDF font names, it's custom
1104            !matches!(
1105                font_name.as_str(),
1106                "Helvetica"
1107                    | "Times"
1108                    | "Courier"
1109                    | "Symbol"
1110                    | "ZapfDingbats"
1111                    | "Helvetica-Bold"
1112                    | "Helvetica-Oblique"
1113                    | "Helvetica-BoldOblique"
1114                    | "Times-Roman"
1115                    | "Times-Bold"
1116                    | "Times-Italic"
1117                    | "Times-BoldItalic"
1118                    | "Courier-Bold"
1119                    | "Courier-Oblique"
1120                    | "Courier-BoldOblique"
1121            )
1122        } else {
1123            false
1124        };
1125
1126        // Detect if text needs Unicode encoding
1127        let needs_unicode = text.chars().any(|c| c as u32 > 255) || using_custom_font;
1128
1129        // Use appropriate encoding based on content and font type
1130        if needs_unicode {
1131            self.draw_with_unicode_encoding(text, x, y)
1132        } else {
1133            self.draw_with_simple_encoding(text, x, y)
1134        }
1135    }
1136
1137    /// Internal: Draw text with simple encoding (WinAnsiEncoding for standard fonts)
1138    fn draw_with_simple_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1139        // Check if text contains characters outside Latin-1
1140        let has_unicode = text.chars().any(|c| c as u32 > 255);
1141
1142        if has_unicode {
1143            // Warning: Text contains Unicode characters but no Unicode font is set
1144            eprintln!("Warning: Text contains Unicode characters but using Latin-1 font. Characters will be replaced with '?'");
1145        }
1146
1147        // Begin text object
1148        self.operations.push_str("BT\n");
1149
1150        // Set font if available
1151        if let Some(font_name) = &self.current_font_name {
1152            writeln!(
1153                &mut self.operations,
1154                "/{} {} Tf",
1155                font_name, self.current_font_size
1156            )
1157            .expect("Writing to string should never fail");
1158        } else {
1159            writeln!(
1160                &mut self.operations,
1161                "/Helvetica {} Tf",
1162                self.current_font_size
1163            )
1164            .expect("Writing to string should never fail");
1165        }
1166
1167        // Set text position
1168        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1169            .expect("Writing to string should never fail");
1170
1171        // Use parentheses encoding for Latin-1 text (standard PDF fonts use WinAnsiEncoding)
1172        // This allows proper rendering of accented characters
1173        self.operations.push('(');
1174        for ch in text.chars() {
1175            let code = ch as u32;
1176            if code <= 127 {
1177                // ASCII characters - handle special characters that need escaping
1178                match ch {
1179                    '(' => self.operations.push_str("\\("),
1180                    ')' => self.operations.push_str("\\)"),
1181                    '\\' => self.operations.push_str("\\\\"),
1182                    '\n' => self.operations.push_str("\\n"),
1183                    '\r' => self.operations.push_str("\\r"),
1184                    '\t' => self.operations.push_str("\\t"),
1185                    _ => self.operations.push(ch),
1186                }
1187            } else if code <= 255 {
1188                // Latin-1 characters (128-255)
1189                // For WinAnsiEncoding, we can use octal notation for high-bit characters
1190                write!(&mut self.operations, "\\{:03o}", code)
1191                    .expect("Writing to string should never fail");
1192            } else {
1193                // Characters outside Latin-1 - replace with '?'
1194                self.operations.push('?');
1195            }
1196        }
1197        self.operations.push_str(") Tj\n");
1198
1199        // End text object
1200        self.operations.push_str("ET\n");
1201
1202        Ok(self)
1203    }
1204
1205    /// Internal: Draw text with Unicode encoding (Type0/CID)
1206    fn draw_with_unicode_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1207        // Begin text object
1208        self.operations.push_str("BT\n");
1209
1210        // Set font - ensure it's a Type0 font for Unicode
1211        if let Some(font_name) = &self.current_font_name {
1212            // The font should be converted to Type0 by FontManager if needed
1213            writeln!(
1214                &mut self.operations,
1215                "/{} {} Tf",
1216                font_name, self.current_font_size
1217            )
1218            .expect("Writing to string should never fail");
1219        } else {
1220            writeln!(
1221                &mut self.operations,
1222                "/Helvetica {} Tf",
1223                self.current_font_size
1224            )
1225            .expect("Writing to string should never fail");
1226        }
1227
1228        // Set text position
1229        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1230            .expect("Writing to string should never fail");
1231
1232        // IMPORTANT: For Type0 fonts with Identity-H encoding, we write CIDs (Character IDs),
1233        // NOT GlyphIDs. The CIDToGIDMap in the font handles the CID -> GlyphID conversion.
1234        // In our case, we use Unicode code points as CIDs.
1235        self.operations.push('<');
1236
1237        for ch in text.chars() {
1238            let code = ch as u32;
1239
1240            // For Type0 fonts with Identity-H encoding, write the Unicode code point as CID
1241            // The CIDToGIDMap will handle the conversion to the actual glyph ID
1242            if code <= 0xFFFF {
1243                // Write the Unicode code point as a 2-byte hex value (CID)
1244                write!(&mut self.operations, "{:04X}", code)
1245                    .expect("Writing to string should never fail");
1246            } else {
1247                // Characters outside BMP - use replacement character
1248                // Most PDF viewers don't handle supplementary planes well
1249                write!(&mut self.operations, "FFFD").expect("Writing to string should never fail");
1250                // Unicode replacement character
1251            }
1252        }
1253        self.operations.push_str("> Tj\n");
1254
1255        // End text object
1256        self.operations.push_str("ET\n");
1257
1258        Ok(self)
1259    }
1260
1261    /// Legacy: Draw text with hex encoding (kept for compatibility)
1262    #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1263    pub fn draw_text_hex(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1264        // Begin text object
1265        self.operations.push_str("BT\n");
1266
1267        // Set font if available
1268        if let Some(font_name) = &self.current_font_name {
1269            writeln!(
1270                &mut self.operations,
1271                "/{} {} Tf",
1272                font_name, self.current_font_size
1273            )
1274            .expect("Writing to string should never fail");
1275        } else {
1276            // Fallback to Helvetica if no font is set
1277            writeln!(
1278                &mut self.operations,
1279                "/Helvetica {} Tf",
1280                self.current_font_size
1281            )
1282            .expect("Writing to string should never fail");
1283        }
1284
1285        // Set text position
1286        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1287            .expect("Writing to string should never fail");
1288
1289        // Encode text as hex string
1290        // For TrueType fonts with Identity-H encoding, we need UTF-16BE
1291        // But we'll use single-byte encoding for now to fix spacing
1292        self.operations.push('<');
1293        for ch in text.chars() {
1294            if ch as u32 <= 255 {
1295                // For characters in the Latin-1 range, use single byte
1296                write!(&mut self.operations, "{:02X}", ch as u8)
1297                    .expect("Writing to string should never fail");
1298            } else {
1299                // For characters outside Latin-1, we need proper glyph mapping
1300                // For now, use a placeholder
1301                write!(&mut self.operations, "3F").expect("Writing to string should never fail");
1302                // '?' character
1303            }
1304        }
1305        self.operations.push_str("> Tj\n");
1306
1307        // End text object
1308        self.operations.push_str("ET\n");
1309
1310        Ok(self)
1311    }
1312
1313    /// Legacy: Draw text with Type0 font encoding (kept for compatibility)
1314    #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1315    pub fn draw_text_cid(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1316        use crate::fonts::needs_type0_font;
1317
1318        // Begin text object
1319        self.operations.push_str("BT\n");
1320
1321        // Set font if available
1322        if let Some(font_name) = &self.current_font_name {
1323            writeln!(
1324                &mut self.operations,
1325                "/{} {} Tf",
1326                font_name, self.current_font_size
1327            )
1328            .expect("Writing to string should never fail");
1329        } else {
1330            writeln!(
1331                &mut self.operations,
1332                "/Helvetica {} Tf",
1333                self.current_font_size
1334            )
1335            .expect("Writing to string should never fail");
1336        }
1337
1338        // Set text position
1339        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1340            .expect("Writing to string should never fail");
1341
1342        // Check if text needs Type0 encoding
1343        if needs_type0_font(text) {
1344            // Use 2-byte hex encoding for CIDs with identity mapping
1345            self.operations.push('<');
1346            for ch in text.chars() {
1347                let code = ch as u32;
1348
1349                // Handle all Unicode characters
1350                if code <= 0xFFFF {
1351                    // Direct identity mapping for BMP characters
1352                    write!(&mut self.operations, "{:04X}", code)
1353                        .expect("Writing to string should never fail");
1354                } else if code <= 0x10FFFF {
1355                    // For characters outside BMP - use surrogate pairs
1356                    let code = code - 0x10000;
1357                    let high = ((code >> 10) & 0x3FF) + 0xD800;
1358                    let low = (code & 0x3FF) + 0xDC00;
1359                    write!(&mut self.operations, "{:04X}{:04X}", high, low)
1360                        .expect("Writing to string should never fail");
1361                } else {
1362                    // Invalid Unicode - use replacement character
1363                    write!(&mut self.operations, "FFFD")
1364                        .expect("Writing to string should never fail");
1365                }
1366            }
1367            self.operations.push_str("> Tj\n");
1368        } else {
1369            // Use regular single-byte encoding for Latin-1
1370            self.operations.push('<');
1371            for ch in text.chars() {
1372                if ch as u32 <= 255 {
1373                    write!(&mut self.operations, "{:02X}", ch as u8)
1374                        .expect("Writing to string should never fail");
1375                } else {
1376                    write!(&mut self.operations, "3F")
1377                        .expect("Writing to string should never fail");
1378                }
1379            }
1380            self.operations.push_str("> Tj\n");
1381        }
1382
1383        // End text object
1384        self.operations.push_str("ET\n");
1385        Ok(self)
1386    }
1387
1388    /// Legacy: Draw text with UTF-16BE encoding (kept for compatibility)
1389    #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1390    pub fn draw_text_unicode(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1391        // Begin text object
1392        self.operations.push_str("BT\n");
1393
1394        // Set font if available
1395        if let Some(font_name) = &self.current_font_name {
1396            writeln!(
1397                &mut self.operations,
1398                "/{} {} Tf",
1399                font_name, self.current_font_size
1400            )
1401            .expect("Writing to string should never fail");
1402        } else {
1403            // Fallback to Helvetica if no font is set
1404            writeln!(
1405                &mut self.operations,
1406                "/Helvetica {} Tf",
1407                self.current_font_size
1408            )
1409            .expect("Writing to string should never fail");
1410        }
1411
1412        // Set text position
1413        writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1414            .expect("Writing to string should never fail");
1415
1416        // Encode text as UTF-16BE hex string
1417        self.operations.push('<');
1418        let mut utf16_buffer = [0u16; 2];
1419        for ch in text.chars() {
1420            let encoded = ch.encode_utf16(&mut utf16_buffer);
1421            for unit in encoded {
1422                // Write UTF-16BE (big-endian)
1423                write!(&mut self.operations, "{:04X}", unit)
1424                    .expect("Writing to string should never fail");
1425            }
1426        }
1427        self.operations.push_str("> Tj\n");
1428
1429        // End text object
1430        self.operations.push_str("ET\n");
1431
1432        Ok(self)
1433    }
1434
1435    /// Get the characters used in this graphics context
1436    pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
1437        if self.used_characters.is_empty() {
1438            None
1439        } else {
1440            Some(self.used_characters.clone())
1441        }
1442    }
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447    use super::*;
1448
1449    #[test]
1450    fn test_graphics_context_new() {
1451        let ctx = GraphicsContext::new();
1452        assert_eq!(ctx.fill_color(), Color::black());
1453        assert_eq!(ctx.stroke_color(), Color::black());
1454        assert_eq!(ctx.line_width(), 1.0);
1455        assert_eq!(ctx.fill_opacity(), 1.0);
1456        assert_eq!(ctx.stroke_opacity(), 1.0);
1457        assert!(ctx.operations().is_empty());
1458    }
1459
1460    #[test]
1461    fn test_graphics_context_default() {
1462        let ctx = GraphicsContext::default();
1463        assert_eq!(ctx.fill_color(), Color::black());
1464        assert_eq!(ctx.stroke_color(), Color::black());
1465        assert_eq!(ctx.line_width(), 1.0);
1466    }
1467
1468    #[test]
1469    fn test_move_to() {
1470        let mut ctx = GraphicsContext::new();
1471        ctx.move_to(10.0, 20.0);
1472        assert!(ctx.operations().contains("10.00 20.00 m\n"));
1473    }
1474
1475    #[test]
1476    fn test_line_to() {
1477        let mut ctx = GraphicsContext::new();
1478        ctx.line_to(30.0, 40.0);
1479        assert!(ctx.operations().contains("30.00 40.00 l\n"));
1480    }
1481
1482    #[test]
1483    fn test_curve_to() {
1484        let mut ctx = GraphicsContext::new();
1485        ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
1486        assert!(ctx
1487            .operations()
1488            .contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
1489    }
1490
1491    #[test]
1492    fn test_rect() {
1493        let mut ctx = GraphicsContext::new();
1494        ctx.rect(10.0, 20.0, 100.0, 50.0);
1495        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1496    }
1497
1498    #[test]
1499    fn test_rectangle_alias() {
1500        let mut ctx = GraphicsContext::new();
1501        ctx.rectangle(10.0, 20.0, 100.0, 50.0);
1502        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1503    }
1504
1505    #[test]
1506    fn test_circle() {
1507        let mut ctx = GraphicsContext::new();
1508        ctx.circle(50.0, 50.0, 25.0);
1509
1510        let ops = ctx.operations();
1511        // Check that it starts with move to radius point
1512        assert!(ops.contains("75.00 50.00 m\n"));
1513        // Check that it contains curve operations
1514        assert!(ops.contains(" c\n"));
1515        // Check that it closes the path
1516        assert!(ops.contains("h\n"));
1517    }
1518
1519    #[test]
1520    fn test_close_path() {
1521        let mut ctx = GraphicsContext::new();
1522        ctx.close_path();
1523        assert!(ctx.operations().contains("h\n"));
1524    }
1525
1526    #[test]
1527    fn test_stroke() {
1528        let mut ctx = GraphicsContext::new();
1529        ctx.set_stroke_color(Color::red());
1530        ctx.rect(0.0, 0.0, 10.0, 10.0);
1531        ctx.stroke();
1532
1533        let ops = ctx.operations();
1534        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1535        assert!(ops.contains("S\n"));
1536    }
1537
1538    #[test]
1539    fn test_fill() {
1540        let mut ctx = GraphicsContext::new();
1541        ctx.set_fill_color(Color::blue());
1542        ctx.rect(0.0, 0.0, 10.0, 10.0);
1543        ctx.fill();
1544
1545        let ops = ctx.operations();
1546        assert!(ops.contains("0.000 0.000 1.000 rg\n"));
1547        assert!(ops.contains("f\n"));
1548    }
1549
1550    #[test]
1551    fn test_fill_stroke() {
1552        let mut ctx = GraphicsContext::new();
1553        ctx.set_fill_color(Color::green());
1554        ctx.set_stroke_color(Color::red());
1555        ctx.rect(0.0, 0.0, 10.0, 10.0);
1556        ctx.fill_stroke();
1557
1558        let ops = ctx.operations();
1559        assert!(ops.contains("0.000 1.000 0.000 rg\n"));
1560        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1561        assert!(ops.contains("B\n"));
1562    }
1563
1564    #[test]
1565    fn test_set_stroke_color() {
1566        let mut ctx = GraphicsContext::new();
1567        ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
1568        assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
1569    }
1570
1571    #[test]
1572    fn test_set_fill_color() {
1573        let mut ctx = GraphicsContext::new();
1574        ctx.set_fill_color(Color::gray(0.5));
1575        assert_eq!(ctx.fill_color(), Color::Gray(0.5));
1576    }
1577
1578    #[test]
1579    fn test_set_line_width() {
1580        let mut ctx = GraphicsContext::new();
1581        ctx.set_line_width(2.5);
1582        assert_eq!(ctx.line_width(), 2.5);
1583        assert!(ctx.operations().contains("2.50 w\n"));
1584    }
1585
1586    #[test]
1587    fn test_set_line_cap() {
1588        let mut ctx = GraphicsContext::new();
1589        ctx.set_line_cap(LineCap::Round);
1590        assert!(ctx.operations().contains("1 J\n"));
1591
1592        ctx.set_line_cap(LineCap::Butt);
1593        assert!(ctx.operations().contains("0 J\n"));
1594
1595        ctx.set_line_cap(LineCap::Square);
1596        assert!(ctx.operations().contains("2 J\n"));
1597    }
1598
1599    #[test]
1600    fn test_set_line_join() {
1601        let mut ctx = GraphicsContext::new();
1602        ctx.set_line_join(LineJoin::Round);
1603        assert!(ctx.operations().contains("1 j\n"));
1604
1605        ctx.set_line_join(LineJoin::Miter);
1606        assert!(ctx.operations().contains("0 j\n"));
1607
1608        ctx.set_line_join(LineJoin::Bevel);
1609        assert!(ctx.operations().contains("2 j\n"));
1610    }
1611
1612    #[test]
1613    fn test_save_restore_state() {
1614        let mut ctx = GraphicsContext::new();
1615        ctx.save_state();
1616        assert!(ctx.operations().contains("q\n"));
1617
1618        ctx.restore_state();
1619        assert!(ctx.operations().contains("Q\n"));
1620    }
1621
1622    #[test]
1623    fn test_translate() {
1624        let mut ctx = GraphicsContext::new();
1625        ctx.translate(50.0, 100.0);
1626        assert!(ctx.operations().contains("1 0 0 1 50.00 100.00 cm\n"));
1627    }
1628
1629    #[test]
1630    fn test_scale() {
1631        let mut ctx = GraphicsContext::new();
1632        ctx.scale(2.0, 3.0);
1633        assert!(ctx.operations().contains("2.00 0 0 3.00 0 0 cm\n"));
1634    }
1635
1636    #[test]
1637    fn test_rotate() {
1638        let mut ctx = GraphicsContext::new();
1639        let angle = std::f64::consts::PI / 4.0; // 45 degrees
1640        ctx.rotate(angle);
1641
1642        let ops = ctx.operations();
1643        assert!(ops.contains(" cm\n"));
1644        // Should contain cos and sin values
1645        assert!(ops.contains("0.707107")); // Approximate cos(45°)
1646    }
1647
1648    #[test]
1649    fn test_transform() {
1650        let mut ctx = GraphicsContext::new();
1651        ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
1652        assert!(ctx
1653            .operations()
1654            .contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
1655    }
1656
1657    #[test]
1658    fn test_draw_image() {
1659        let mut ctx = GraphicsContext::new();
1660        ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
1661
1662        let ops = ctx.operations();
1663        assert!(ops.contains("q\n")); // Save state
1664        assert!(ops.contains("100.00 0 0 150.00 10.00 20.00 cm\n")); // Transform
1665        assert!(ops.contains("/Image1 Do\n")); // Draw image
1666        assert!(ops.contains("Q\n")); // Restore state
1667    }
1668
1669    #[test]
1670    fn test_gray_color_operations() {
1671        let mut ctx = GraphicsContext::new();
1672        ctx.set_stroke_color(Color::gray(0.5));
1673        ctx.set_fill_color(Color::gray(0.7));
1674        ctx.stroke();
1675        ctx.fill();
1676
1677        let ops = ctx.operations();
1678        assert!(ops.contains("0.500 G\n")); // Stroke gray
1679        assert!(ops.contains("0.700 g\n")); // Fill gray
1680    }
1681
1682    #[test]
1683    fn test_cmyk_color_operations() {
1684        let mut ctx = GraphicsContext::new();
1685        ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
1686        ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
1687        ctx.stroke();
1688        ctx.fill();
1689
1690        let ops = ctx.operations();
1691        assert!(ops.contains("0.100 0.200 0.300 0.400 K\n")); // Stroke CMYK
1692        assert!(ops.contains("0.500 0.600 0.700 0.800 k\n")); // Fill CMYK
1693    }
1694
1695    #[test]
1696    fn test_method_chaining() {
1697        let mut ctx = GraphicsContext::new();
1698        ctx.move_to(0.0, 0.0)
1699            .line_to(10.0, 0.0)
1700            .line_to(10.0, 10.0)
1701            .line_to(0.0, 10.0)
1702            .close_path()
1703            .set_fill_color(Color::red())
1704            .fill();
1705
1706        let ops = ctx.operations();
1707        assert!(ops.contains("0.00 0.00 m\n"));
1708        assert!(ops.contains("10.00 0.00 l\n"));
1709        assert!(ops.contains("10.00 10.00 l\n"));
1710        assert!(ops.contains("0.00 10.00 l\n"));
1711        assert!(ops.contains("h\n"));
1712        assert!(ops.contains("f\n"));
1713    }
1714
1715    #[test]
1716    fn test_generate_operations() {
1717        let mut ctx = GraphicsContext::new();
1718        ctx.rect(0.0, 0.0, 10.0, 10.0);
1719
1720        let result = ctx.generate_operations();
1721        assert!(result.is_ok());
1722        let bytes = result.expect("Writing to string should never fail");
1723        let ops_string = String::from_utf8(bytes).expect("Writing to string should never fail");
1724        assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
1725    }
1726
1727    #[test]
1728    fn test_clear_operations() {
1729        let mut ctx = GraphicsContext::new();
1730        ctx.rect(0.0, 0.0, 10.0, 10.0);
1731        assert!(!ctx.operations().is_empty());
1732
1733        ctx.clear();
1734        assert!(ctx.operations().is_empty());
1735    }
1736
1737    #[test]
1738    fn test_complex_path() {
1739        let mut ctx = GraphicsContext::new();
1740        ctx.save_state()
1741            .translate(100.0, 100.0)
1742            .rotate(std::f64::consts::PI / 6.0)
1743            .scale(2.0, 2.0)
1744            .set_line_width(2.0)
1745            .set_stroke_color(Color::blue())
1746            .move_to(0.0, 0.0)
1747            .line_to(50.0, 0.0)
1748            .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
1749            .close_path()
1750            .stroke()
1751            .restore_state();
1752
1753        let ops = ctx.operations();
1754        assert!(ops.contains("q\n"));
1755        assert!(ops.contains("cm\n"));
1756        assert!(ops.contains("2.00 w\n"));
1757        assert!(ops.contains("0.000 0.000 1.000 RG\n"));
1758        assert!(ops.contains("S\n"));
1759        assert!(ops.contains("Q\n"));
1760    }
1761
1762    #[test]
1763    fn test_graphics_context_clone() {
1764        let mut ctx = GraphicsContext::new();
1765        ctx.set_fill_color(Color::red());
1766        ctx.set_stroke_color(Color::blue());
1767        ctx.set_line_width(3.0);
1768        ctx.set_opacity(0.5);
1769        ctx.rect(0.0, 0.0, 10.0, 10.0);
1770
1771        let ctx_clone = ctx.clone();
1772        assert_eq!(ctx_clone.fill_color(), Color::red());
1773        assert_eq!(ctx_clone.stroke_color(), Color::blue());
1774        assert_eq!(ctx_clone.line_width(), 3.0);
1775        assert_eq!(ctx_clone.fill_opacity(), 0.5);
1776        assert_eq!(ctx_clone.stroke_opacity(), 0.5);
1777        assert_eq!(ctx_clone.operations(), ctx.operations());
1778    }
1779
1780    #[test]
1781    fn test_set_opacity() {
1782        let mut ctx = GraphicsContext::new();
1783
1784        // Test setting opacity
1785        ctx.set_opacity(0.5);
1786        assert_eq!(ctx.fill_opacity(), 0.5);
1787        assert_eq!(ctx.stroke_opacity(), 0.5);
1788
1789        // Test clamping to valid range
1790        ctx.set_opacity(1.5);
1791        assert_eq!(ctx.fill_opacity(), 1.0);
1792        assert_eq!(ctx.stroke_opacity(), 1.0);
1793
1794        ctx.set_opacity(-0.5);
1795        assert_eq!(ctx.fill_opacity(), 0.0);
1796        assert_eq!(ctx.stroke_opacity(), 0.0);
1797    }
1798
1799    #[test]
1800    fn test_set_fill_opacity() {
1801        let mut ctx = GraphicsContext::new();
1802
1803        ctx.set_fill_opacity(0.3);
1804        assert_eq!(ctx.fill_opacity(), 0.3);
1805        assert_eq!(ctx.stroke_opacity(), 1.0); // Should not affect stroke
1806
1807        // Test clamping
1808        ctx.set_fill_opacity(2.0);
1809        assert_eq!(ctx.fill_opacity(), 1.0);
1810    }
1811
1812    #[test]
1813    fn test_set_stroke_opacity() {
1814        let mut ctx = GraphicsContext::new();
1815
1816        ctx.set_stroke_opacity(0.7);
1817        assert_eq!(ctx.stroke_opacity(), 0.7);
1818        assert_eq!(ctx.fill_opacity(), 1.0); // Should not affect fill
1819
1820        // Test clamping
1821        ctx.set_stroke_opacity(-1.0);
1822        assert_eq!(ctx.stroke_opacity(), 0.0);
1823    }
1824
1825    #[test]
1826    fn test_uses_transparency() {
1827        let mut ctx = GraphicsContext::new();
1828
1829        // Initially no transparency
1830        assert!(!ctx.uses_transparency());
1831
1832        // With fill transparency
1833        ctx.set_fill_opacity(0.5);
1834        assert!(ctx.uses_transparency());
1835
1836        // Reset and test stroke transparency
1837        ctx.set_fill_opacity(1.0);
1838        assert!(!ctx.uses_transparency());
1839        ctx.set_stroke_opacity(0.8);
1840        assert!(ctx.uses_transparency());
1841
1842        // Both transparent
1843        ctx.set_fill_opacity(0.5);
1844        assert!(ctx.uses_transparency());
1845    }
1846
1847    #[test]
1848    fn test_generate_graphics_state_dict() {
1849        let mut ctx = GraphicsContext::new();
1850
1851        // No transparency
1852        assert_eq!(ctx.generate_graphics_state_dict(), None);
1853
1854        // Fill opacity only
1855        ctx.set_fill_opacity(0.5);
1856        let dict = ctx
1857            .generate_graphics_state_dict()
1858            .expect("Writing to string should never fail");
1859        assert!(dict.contains("/Type /ExtGState"));
1860        assert!(dict.contains("/ca 0.500"));
1861        assert!(!dict.contains("/CA"));
1862
1863        // Stroke opacity only
1864        ctx.set_fill_opacity(1.0);
1865        ctx.set_stroke_opacity(0.75);
1866        let dict = ctx
1867            .generate_graphics_state_dict()
1868            .expect("Writing to string should never fail");
1869        assert!(dict.contains("/Type /ExtGState"));
1870        assert!(dict.contains("/CA 0.750"));
1871        assert!(!dict.contains("/ca"));
1872
1873        // Both opacities
1874        ctx.set_fill_opacity(0.25);
1875        let dict = ctx
1876            .generate_graphics_state_dict()
1877            .expect("Writing to string should never fail");
1878        assert!(dict.contains("/Type /ExtGState"));
1879        assert!(dict.contains("/ca 0.250"));
1880        assert!(dict.contains("/CA 0.750"));
1881    }
1882
1883    #[test]
1884    fn test_opacity_with_graphics_operations() {
1885        let mut ctx = GraphicsContext::new();
1886
1887        ctx.set_fill_color(Color::red())
1888            .set_opacity(0.5)
1889            .rect(10.0, 10.0, 100.0, 100.0)
1890            .fill();
1891
1892        assert_eq!(ctx.fill_opacity(), 0.5);
1893        assert_eq!(ctx.stroke_opacity(), 0.5);
1894
1895        let ops = ctx.operations();
1896        assert!(ops.contains("10.00 10.00 100.00 100.00 re"));
1897        assert!(ops.contains("1.000 0.000 0.000 rg")); // Red color
1898        assert!(ops.contains("f")); // Fill
1899    }
1900
1901    #[test]
1902    fn test_begin_end_text() {
1903        let mut ctx = GraphicsContext::new();
1904        ctx.begin_text();
1905        assert!(ctx.operations().contains("BT\n"));
1906
1907        ctx.end_text();
1908        assert!(ctx.operations().contains("ET\n"));
1909    }
1910
1911    #[test]
1912    fn test_set_font() {
1913        let mut ctx = GraphicsContext::new();
1914        ctx.set_font(Font::Helvetica, 12.0);
1915        assert!(ctx.operations().contains("/Helvetica 12 Tf\n"));
1916
1917        ctx.set_font(Font::TimesBold, 14.5);
1918        assert!(ctx.operations().contains("/Times-Bold 14.5 Tf\n"));
1919    }
1920
1921    #[test]
1922    fn test_set_text_position() {
1923        let mut ctx = GraphicsContext::new();
1924        ctx.set_text_position(100.0, 200.0);
1925        assert!(ctx.operations().contains("100.00 200.00 Td\n"));
1926    }
1927
1928    #[test]
1929    fn test_show_text() {
1930        let mut ctx = GraphicsContext::new();
1931        ctx.show_text("Hello World")
1932            .expect("Writing to string should never fail");
1933        assert!(ctx.operations().contains("(Hello World) Tj\n"));
1934    }
1935
1936    #[test]
1937    fn test_show_text_with_escaping() {
1938        let mut ctx = GraphicsContext::new();
1939        ctx.show_text("Test (parentheses)")
1940            .expect("Writing to string should never fail");
1941        assert!(ctx.operations().contains("(Test \\(parentheses\\)) Tj\n"));
1942
1943        ctx.clear();
1944        ctx.show_text("Back\\slash")
1945            .expect("Writing to string should never fail");
1946        assert!(ctx.operations().contains("(Back\\\\slash) Tj\n"));
1947
1948        ctx.clear();
1949        ctx.show_text("Line\nBreak")
1950            .expect("Writing to string should never fail");
1951        assert!(ctx.operations().contains("(Line\\nBreak) Tj\n"));
1952    }
1953
1954    #[test]
1955    fn test_text_operations_chaining() {
1956        let mut ctx = GraphicsContext::new();
1957        ctx.begin_text()
1958            .set_font(Font::Courier, 10.0)
1959            .set_text_position(50.0, 100.0)
1960            .show_text("Test")
1961            .unwrap()
1962            .end_text();
1963
1964        let ops = ctx.operations();
1965        assert!(ops.contains("BT\n"));
1966        assert!(ops.contains("/Courier 10 Tf\n"));
1967        assert!(ops.contains("50.00 100.00 Td\n"));
1968        assert!(ops.contains("(Test) Tj\n"));
1969        assert!(ops.contains("ET\n"));
1970    }
1971
1972    #[test]
1973    fn test_clip() {
1974        let mut ctx = GraphicsContext::new();
1975        ctx.clip();
1976        assert!(ctx.operations().contains("W\n"));
1977    }
1978
1979    #[test]
1980    fn test_clip_even_odd() {
1981        let mut ctx = GraphicsContext::new();
1982        ctx.clip_even_odd();
1983        assert!(ctx.operations().contains("W*\n"));
1984    }
1985
1986    #[test]
1987    fn test_clipping_with_path() {
1988        let mut ctx = GraphicsContext::new();
1989
1990        // Create a rectangular clipping path
1991        ctx.rect(10.0, 10.0, 100.0, 50.0).clip();
1992
1993        let ops = ctx.operations();
1994        assert!(ops.contains("10.00 10.00 100.00 50.00 re\n"));
1995        assert!(ops.contains("W\n"));
1996    }
1997
1998    #[test]
1999    fn test_clipping_even_odd_with_path() {
2000        let mut ctx = GraphicsContext::new();
2001
2002        // Create a complex path and clip with even-odd rule
2003        ctx.move_to(0.0, 0.0)
2004            .line_to(100.0, 0.0)
2005            .line_to(100.0, 100.0)
2006            .line_to(0.0, 100.0)
2007            .close_path()
2008            .clip_even_odd();
2009
2010        let ops = ctx.operations();
2011        assert!(ops.contains("0.00 0.00 m\n"));
2012        assert!(ops.contains("100.00 0.00 l\n"));
2013        assert!(ops.contains("100.00 100.00 l\n"));
2014        assert!(ops.contains("0.00 100.00 l\n"));
2015        assert!(ops.contains("h\n"));
2016        assert!(ops.contains("W*\n"));
2017    }
2018
2019    #[test]
2020    fn test_clipping_chaining() {
2021        let mut ctx = GraphicsContext::new();
2022
2023        // Test method chaining with clipping
2024        ctx.save_state()
2025            .rect(20.0, 20.0, 60.0, 60.0)
2026            .clip()
2027            .set_fill_color(Color::red())
2028            .rect(0.0, 0.0, 100.0, 100.0)
2029            .fill()
2030            .restore_state();
2031
2032        let ops = ctx.operations();
2033        assert!(ops.contains("q\n"));
2034        assert!(ops.contains("20.00 20.00 60.00 60.00 re\n"));
2035        assert!(ops.contains("W\n"));
2036        assert!(ops.contains("1.000 0.000 0.000 rg\n"));
2037        assert!(ops.contains("0.00 0.00 100.00 100.00 re\n"));
2038        assert!(ops.contains("f\n"));
2039        assert!(ops.contains("Q\n"));
2040    }
2041
2042    #[test]
2043    fn test_multiple_clipping_regions() {
2044        let mut ctx = GraphicsContext::new();
2045
2046        // Test nested clipping regions
2047        ctx.save_state()
2048            .rect(0.0, 0.0, 200.0, 200.0)
2049            .clip()
2050            .save_state()
2051            .circle(100.0, 100.0, 50.0)
2052            .clip_even_odd()
2053            .set_fill_color(Color::blue())
2054            .rect(50.0, 50.0, 100.0, 100.0)
2055            .fill()
2056            .restore_state()
2057            .restore_state();
2058
2059        let ops = ctx.operations();
2060        // Check for nested save/restore states
2061        let q_count = ops.matches("q\n").count();
2062        let q_restore_count = ops.matches("Q\n").count();
2063        assert_eq!(q_count, 2);
2064        assert_eq!(q_restore_count, 2);
2065
2066        // Check for both clipping operations
2067        assert!(ops.contains("W\n"));
2068        assert!(ops.contains("W*\n"));
2069    }
2070
2071    // ============= Additional Critical Method Tests =============
2072
2073    #[test]
2074    fn test_move_to_and_line_to() {
2075        let mut ctx = GraphicsContext::new();
2076        ctx.move_to(100.0, 200.0).line_to(300.0, 400.0).stroke();
2077
2078        let ops = ctx
2079            .generate_operations()
2080            .expect("Writing to string should never fail");
2081        let ops_str = String::from_utf8_lossy(&ops);
2082        assert!(ops_str.contains("100.00 200.00 m"));
2083        assert!(ops_str.contains("300.00 400.00 l"));
2084        assert!(ops_str.contains("S"));
2085    }
2086
2087    #[test]
2088    fn test_bezier_curve() {
2089        let mut ctx = GraphicsContext::new();
2090        ctx.move_to(0.0, 0.0)
2091            .curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0)
2092            .stroke();
2093
2094        let ops = ctx
2095            .generate_operations()
2096            .expect("Writing to string should never fail");
2097        let ops_str = String::from_utf8_lossy(&ops);
2098        assert!(ops_str.contains("0.00 0.00 m"));
2099        assert!(ops_str.contains("10.00 20.00 30.00 40.00 50.00 60.00 c"));
2100        assert!(ops_str.contains("S"));
2101    }
2102
2103    #[test]
2104    fn test_circle_path() {
2105        let mut ctx = GraphicsContext::new();
2106        ctx.circle(100.0, 100.0, 50.0).fill();
2107
2108        let ops = ctx
2109            .generate_operations()
2110            .expect("Writing to string should never fail");
2111        let ops_str = String::from_utf8_lossy(&ops);
2112        // Circle should use bezier curves (c operator)
2113        assert!(ops_str.contains(" c"));
2114        assert!(ops_str.contains("f"));
2115    }
2116
2117    #[test]
2118    fn test_path_closing() {
2119        let mut ctx = GraphicsContext::new();
2120        ctx.move_to(0.0, 0.0)
2121            .line_to(100.0, 0.0)
2122            .line_to(100.0, 100.0)
2123            .close_path()
2124            .stroke();
2125
2126        let ops = ctx
2127            .generate_operations()
2128            .expect("Writing to string should never fail");
2129        let ops_str = String::from_utf8_lossy(&ops);
2130        assert!(ops_str.contains("h")); // close path operator
2131        assert!(ops_str.contains("S"));
2132    }
2133
2134    #[test]
2135    fn test_fill_and_stroke() {
2136        let mut ctx = GraphicsContext::new();
2137        ctx.rect(10.0, 10.0, 50.0, 50.0).fill_stroke();
2138
2139        let ops = ctx
2140            .generate_operations()
2141            .expect("Writing to string should never fail");
2142        let ops_str = String::from_utf8_lossy(&ops);
2143        assert!(ops_str.contains("10.00 10.00 50.00 50.00 re"));
2144        assert!(ops_str.contains("B")); // fill and stroke operator
2145    }
2146
2147    #[test]
2148    fn test_color_settings() {
2149        let mut ctx = GraphicsContext::new();
2150        ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2151            .set_stroke_color(Color::rgb(0.0, 1.0, 0.0))
2152            .rect(10.0, 10.0, 50.0, 50.0)
2153            .fill_stroke(); // This will write the colors
2154
2155        assert_eq!(ctx.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2156        assert_eq!(ctx.stroke_color(), Color::rgb(0.0, 1.0, 0.0));
2157
2158        let ops = ctx
2159            .generate_operations()
2160            .expect("Writing to string should never fail");
2161        let ops_str = String::from_utf8_lossy(&ops);
2162        assert!(ops_str.contains("1.000 0.000 0.000 rg")); // red fill
2163        assert!(ops_str.contains("0.000 1.000 0.000 RG")); // green stroke
2164    }
2165
2166    #[test]
2167    fn test_line_styles() {
2168        let mut ctx = GraphicsContext::new();
2169        ctx.set_line_width(2.5)
2170            .set_line_cap(LineCap::Round)
2171            .set_line_join(LineJoin::Bevel);
2172
2173        assert_eq!(ctx.line_width(), 2.5);
2174
2175        let ops = ctx
2176            .generate_operations()
2177            .expect("Writing to string should never fail");
2178        let ops_str = String::from_utf8_lossy(&ops);
2179        assert!(ops_str.contains("2.50 w")); // line width
2180        assert!(ops_str.contains("1 J")); // round line cap
2181        assert!(ops_str.contains("2 j")); // bevel line join
2182    }
2183
2184    #[test]
2185    fn test_opacity_settings() {
2186        let mut ctx = GraphicsContext::new();
2187        ctx.set_opacity(0.5);
2188
2189        assert_eq!(ctx.fill_opacity(), 0.5);
2190        assert_eq!(ctx.stroke_opacity(), 0.5);
2191        assert!(ctx.uses_transparency());
2192
2193        ctx.set_fill_opacity(0.7).set_stroke_opacity(0.3);
2194
2195        assert_eq!(ctx.fill_opacity(), 0.7);
2196        assert_eq!(ctx.stroke_opacity(), 0.3);
2197    }
2198
2199    #[test]
2200    fn test_state_save_restore() {
2201        let mut ctx = GraphicsContext::new();
2202        ctx.save_state()
2203            .set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2204            .restore_state();
2205
2206        let ops = ctx
2207            .generate_operations()
2208            .expect("Writing to string should never fail");
2209        let ops_str = String::from_utf8_lossy(&ops);
2210        assert!(ops_str.contains("q")); // save state
2211        assert!(ops_str.contains("Q")); // restore state
2212    }
2213
2214    #[test]
2215    fn test_transformations() {
2216        let mut ctx = GraphicsContext::new();
2217        ctx.translate(100.0, 200.0).scale(2.0, 3.0).rotate(45.0);
2218
2219        let ops = ctx
2220            .generate_operations()
2221            .expect("Writing to string should never fail");
2222        let ops_str = String::from_utf8_lossy(&ops);
2223        assert!(ops_str.contains("1 0 0 1 100.00 200.00 cm")); // translate
2224        assert!(ops_str.contains("2.00 0 0 3.00 0 0 cm")); // scale
2225        assert!(ops_str.contains("cm")); // rotate matrix
2226    }
2227
2228    #[test]
2229    fn test_custom_transform() {
2230        let mut ctx = GraphicsContext::new();
2231        ctx.transform(1.0, 0.5, 0.5, 1.0, 10.0, 20.0);
2232
2233        let ops = ctx
2234            .generate_operations()
2235            .expect("Writing to string should never fail");
2236        let ops_str = String::from_utf8_lossy(&ops);
2237        assert!(ops_str.contains("1.00 0.50 0.50 1.00 10.00 20.00 cm"));
2238    }
2239
2240    #[test]
2241    fn test_rectangle_path() {
2242        let mut ctx = GraphicsContext::new();
2243        ctx.rectangle(25.0, 25.0, 150.0, 100.0).stroke();
2244
2245        let ops = ctx
2246            .generate_operations()
2247            .expect("Writing to string should never fail");
2248        let ops_str = String::from_utf8_lossy(&ops);
2249        assert!(ops_str.contains("25.00 25.00 150.00 100.00 re"));
2250        assert!(ops_str.contains("S"));
2251    }
2252
2253    #[test]
2254    fn test_empty_operations() {
2255        let ctx = GraphicsContext::new();
2256        let ops = ctx
2257            .generate_operations()
2258            .expect("Writing to string should never fail");
2259        assert!(ops.is_empty());
2260    }
2261
2262    #[test]
2263    fn test_complex_path_operations() {
2264        let mut ctx = GraphicsContext::new();
2265        ctx.move_to(50.0, 50.0)
2266            .line_to(100.0, 50.0)
2267            .curve_to(125.0, 50.0, 150.0, 75.0, 150.0, 100.0)
2268            .line_to(150.0, 150.0)
2269            .close_path()
2270            .fill();
2271
2272        let ops = ctx
2273            .generate_operations()
2274            .expect("Writing to string should never fail");
2275        let ops_str = String::from_utf8_lossy(&ops);
2276        assert!(ops_str.contains("50.00 50.00 m"));
2277        assert!(ops_str.contains("100.00 50.00 l"));
2278        assert!(ops_str.contains("125.00 50.00 150.00 75.00 150.00 100.00 c"));
2279        assert!(ops_str.contains("150.00 150.00 l"));
2280        assert!(ops_str.contains("h"));
2281        assert!(ops_str.contains("f"));
2282    }
2283
2284    #[test]
2285    fn test_graphics_state_dict_generation() {
2286        let mut ctx = GraphicsContext::new();
2287
2288        // Without transparency, should return None
2289        assert!(ctx.generate_graphics_state_dict().is_none());
2290
2291        // With transparency, should generate dict
2292        ctx.set_opacity(0.5);
2293        let dict = ctx.generate_graphics_state_dict();
2294        assert!(dict.is_some());
2295        let dict_str = dict.expect("Writing to string should never fail");
2296        assert!(dict_str.contains("/ca 0.5"));
2297        assert!(dict_str.contains("/CA 0.5"));
2298    }
2299
2300    #[test]
2301    fn test_line_dash_pattern() {
2302        let mut ctx = GraphicsContext::new();
2303        let pattern = LineDashPattern {
2304            array: vec![3.0, 2.0],
2305            phase: 0.0,
2306        };
2307        ctx.set_line_dash_pattern(pattern);
2308
2309        let ops = ctx
2310            .generate_operations()
2311            .expect("Writing to string should never fail");
2312        let ops_str = String::from_utf8_lossy(&ops);
2313        assert!(ops_str.contains("[3.00 2.00] 0.00 d"));
2314    }
2315
2316    #[test]
2317    fn test_miter_limit_setting() {
2318        let mut ctx = GraphicsContext::new();
2319        ctx.set_miter_limit(4.0);
2320
2321        let ops = ctx
2322            .generate_operations()
2323            .expect("Writing to string should never fail");
2324        let ops_str = String::from_utf8_lossy(&ops);
2325        assert!(ops_str.contains("4.00 M"));
2326    }
2327
2328    #[test]
2329    fn test_line_cap_styles() {
2330        let mut ctx = GraphicsContext::new();
2331
2332        ctx.set_line_cap(LineCap::Butt);
2333        let ops = ctx
2334            .generate_operations()
2335            .expect("Writing to string should never fail");
2336        let ops_str = String::from_utf8_lossy(&ops);
2337        assert!(ops_str.contains("0 J"));
2338
2339        let mut ctx = GraphicsContext::new();
2340        ctx.set_line_cap(LineCap::Round);
2341        let ops = ctx
2342            .generate_operations()
2343            .expect("Writing to string should never fail");
2344        let ops_str = String::from_utf8_lossy(&ops);
2345        assert!(ops_str.contains("1 J"));
2346
2347        let mut ctx = GraphicsContext::new();
2348        ctx.set_line_cap(LineCap::Square);
2349        let ops = ctx
2350            .generate_operations()
2351            .expect("Writing to string should never fail");
2352        let ops_str = String::from_utf8_lossy(&ops);
2353        assert!(ops_str.contains("2 J"));
2354    }
2355
2356    #[test]
2357    fn test_transparency_groups() {
2358        let mut ctx = GraphicsContext::new();
2359
2360        // Test basic transparency group
2361        let group = TransparencyGroup::new()
2362            .with_isolated(true)
2363            .with_opacity(0.5);
2364
2365        ctx.begin_transparency_group(group);
2366        assert!(ctx.in_transparency_group());
2367
2368        // Draw something in the group
2369        ctx.rect(10.0, 10.0, 100.0, 100.0);
2370        ctx.fill();
2371
2372        ctx.end_transparency_group();
2373        assert!(!ctx.in_transparency_group());
2374
2375        // Check that operations contain transparency markers
2376        let ops = ctx.operations();
2377        assert!(ops.contains("% Begin Transparency Group"));
2378        assert!(ops.contains("% End Transparency Group"));
2379    }
2380
2381    #[test]
2382    fn test_nested_transparency_groups() {
2383        let mut ctx = GraphicsContext::new();
2384
2385        // First group
2386        let group1 = TransparencyGroup::isolated().with_opacity(0.8);
2387        ctx.begin_transparency_group(group1);
2388        assert!(ctx.in_transparency_group());
2389
2390        // Nested group
2391        let group2 = TransparencyGroup::knockout().with_blend_mode(BlendMode::Multiply);
2392        ctx.begin_transparency_group(group2);
2393
2394        // Draw in nested group
2395        ctx.circle(50.0, 50.0, 25.0);
2396        ctx.fill();
2397
2398        // End nested group
2399        ctx.end_transparency_group();
2400        assert!(ctx.in_transparency_group()); // Still in first group
2401
2402        // End first group
2403        ctx.end_transparency_group();
2404        assert!(!ctx.in_transparency_group());
2405    }
2406
2407    #[test]
2408    fn test_line_join_styles() {
2409        let mut ctx = GraphicsContext::new();
2410
2411        ctx.set_line_join(LineJoin::Miter);
2412        let ops = ctx
2413            .generate_operations()
2414            .expect("Writing to string should never fail");
2415        let ops_str = String::from_utf8_lossy(&ops);
2416        assert!(ops_str.contains("0 j"));
2417
2418        let mut ctx = GraphicsContext::new();
2419        ctx.set_line_join(LineJoin::Round);
2420        let ops = ctx
2421            .generate_operations()
2422            .expect("Writing to string should never fail");
2423        let ops_str = String::from_utf8_lossy(&ops);
2424        assert!(ops_str.contains("1 j"));
2425
2426        let mut ctx = GraphicsContext::new();
2427        ctx.set_line_join(LineJoin::Bevel);
2428        let ops = ctx
2429            .generate_operations()
2430            .expect("Writing to string should never fail");
2431        let ops_str = String::from_utf8_lossy(&ops);
2432        assert!(ops_str.contains("2 j"));
2433    }
2434
2435    #[test]
2436    fn test_rendering_intent() {
2437        let mut ctx = GraphicsContext::new();
2438
2439        ctx.set_rendering_intent(RenderingIntent::AbsoluteColorimetric);
2440        assert_eq!(
2441            ctx.rendering_intent(),
2442            RenderingIntent::AbsoluteColorimetric
2443        );
2444
2445        ctx.set_rendering_intent(RenderingIntent::Perceptual);
2446        assert_eq!(ctx.rendering_intent(), RenderingIntent::Perceptual);
2447
2448        ctx.set_rendering_intent(RenderingIntent::Saturation);
2449        assert_eq!(ctx.rendering_intent(), RenderingIntent::Saturation);
2450    }
2451
2452    #[test]
2453    fn test_flatness_tolerance() {
2454        let mut ctx = GraphicsContext::new();
2455
2456        ctx.set_flatness(0.5);
2457        assert_eq!(ctx.flatness(), 0.5);
2458
2459        let ops = ctx
2460            .generate_operations()
2461            .expect("Writing to string should never fail");
2462        let ops_str = String::from_utf8_lossy(&ops);
2463        assert!(ops_str.contains("0.50 i"));
2464    }
2465
2466    #[test]
2467    fn test_smoothness_tolerance() {
2468        let mut ctx = GraphicsContext::new();
2469
2470        let _ = ctx.set_smoothness(0.1);
2471        assert_eq!(ctx.smoothness(), 0.1);
2472    }
2473
2474    #[test]
2475    fn test_bezier_curves() {
2476        let mut ctx = GraphicsContext::new();
2477
2478        // Cubic Bezier
2479        ctx.move_to(10.0, 10.0);
2480        ctx.curve_to(20.0, 10.0, 30.0, 20.0, 30.0, 30.0);
2481
2482        let ops = ctx
2483            .generate_operations()
2484            .expect("Writing to string should never fail");
2485        let ops_str = String::from_utf8_lossy(&ops);
2486        assert!(ops_str.contains("10.00 10.00 m"));
2487        assert!(ops_str.contains("c")); // cubic curve
2488    }
2489
2490    #[test]
2491    fn test_clipping_path() {
2492        let mut ctx = GraphicsContext::new();
2493
2494        ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2495        ctx.clip();
2496
2497        let ops = ctx
2498            .generate_operations()
2499            .expect("Writing to string should never fail");
2500        let ops_str = String::from_utf8_lossy(&ops);
2501        assert!(ops_str.contains("W"));
2502    }
2503
2504    #[test]
2505    fn test_even_odd_clipping() {
2506        let mut ctx = GraphicsContext::new();
2507
2508        ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2509        ctx.clip_even_odd();
2510
2511        let ops = ctx
2512            .generate_operations()
2513            .expect("Writing to string should never fail");
2514        let ops_str = String::from_utf8_lossy(&ops);
2515        assert!(ops_str.contains("W*"));
2516    }
2517
2518    #[test]
2519    fn test_color_creation() {
2520        // Test color creation methods
2521        let gray = Color::gray(0.5);
2522        assert_eq!(gray, Color::Gray(0.5));
2523
2524        let rgb = Color::rgb(0.2, 0.4, 0.6);
2525        assert_eq!(rgb, Color::Rgb(0.2, 0.4, 0.6));
2526
2527        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
2528        assert_eq!(cmyk, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
2529
2530        // Test predefined colors
2531        assert_eq!(Color::black(), Color::Gray(0.0));
2532        assert_eq!(Color::white(), Color::Gray(1.0));
2533        assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
2534    }
2535
2536    #[test]
2537    fn test_extended_graphics_state() {
2538        let ctx = GraphicsContext::new();
2539
2540        // Test that we can create and use an extended graphics state
2541        let _extgstate = ExtGState::new();
2542
2543        // We should be able to create the state without errors
2544        assert!(ctx.generate_operations().is_ok());
2545    }
2546
2547    #[test]
2548    fn test_path_construction_methods() {
2549        let mut ctx = GraphicsContext::new();
2550
2551        // Test basic path construction methods that exist
2552        ctx.move_to(10.0, 10.0);
2553        ctx.line_to(20.0, 20.0);
2554        ctx.curve_to(30.0, 30.0, 40.0, 40.0, 50.0, 50.0);
2555        ctx.rect(60.0, 60.0, 30.0, 30.0);
2556        ctx.circle(100.0, 100.0, 25.0);
2557        ctx.close_path();
2558
2559        let ops = ctx
2560            .generate_operations()
2561            .expect("Writing to string should never fail");
2562        assert!(!ops.is_empty());
2563    }
2564
2565    #[test]
2566    fn test_graphics_context_clone_advanced() {
2567        let mut ctx = GraphicsContext::new();
2568        ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
2569        ctx.set_line_width(5.0);
2570
2571        let cloned = ctx.clone();
2572        assert_eq!(cloned.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2573        assert_eq!(cloned.line_width(), 5.0);
2574    }
2575
2576    #[test]
2577    fn test_basic_drawing_operations() {
2578        let mut ctx = GraphicsContext::new();
2579
2580        // Test that we can at least create a basic drawing
2581        ctx.move_to(50.0, 50.0);
2582        ctx.line_to(100.0, 100.0);
2583        ctx.stroke();
2584
2585        let ops = ctx
2586            .generate_operations()
2587            .expect("Writing to string should never fail");
2588        let ops_str = String::from_utf8_lossy(&ops);
2589        assert!(ops_str.contains("m")); // move
2590        assert!(ops_str.contains("l")); // line
2591        assert!(ops_str.contains("S")); // stroke
2592    }
2593
2594    #[test]
2595    fn test_graphics_state_stack() {
2596        let mut ctx = GraphicsContext::new();
2597
2598        // Initial state
2599        ctx.set_fill_color(Color::black());
2600
2601        // Save and change
2602        ctx.save_state();
2603        ctx.set_fill_color(Color::red());
2604        assert_eq!(ctx.fill_color(), Color::red());
2605
2606        // Save again and change
2607        ctx.save_state();
2608        ctx.set_fill_color(Color::blue());
2609        assert_eq!(ctx.fill_color(), Color::blue());
2610
2611        // Restore once
2612        ctx.restore_state();
2613        assert_eq!(ctx.fill_color(), Color::red());
2614
2615        // Restore again
2616        ctx.restore_state();
2617        assert_eq!(ctx.fill_color(), Color::black());
2618    }
2619
2620    #[test]
2621    fn test_word_spacing() {
2622        let mut ctx = GraphicsContext::new();
2623        ctx.set_word_spacing(2.5);
2624
2625        let ops = ctx.generate_operations().unwrap();
2626        let ops_str = String::from_utf8_lossy(&ops);
2627        assert!(ops_str.contains("2.50 Tw"));
2628    }
2629
2630    #[test]
2631    fn test_character_spacing() {
2632        let mut ctx = GraphicsContext::new();
2633        ctx.set_character_spacing(1.0);
2634
2635        let ops = ctx.generate_operations().unwrap();
2636        let ops_str = String::from_utf8_lossy(&ops);
2637        assert!(ops_str.contains("1.00 Tc"));
2638    }
2639
2640    #[test]
2641    fn test_justified_text() {
2642        let mut ctx = GraphicsContext::new();
2643        ctx.begin_text();
2644        ctx.set_text_position(100.0, 200.0);
2645        ctx.show_justified_text("Hello world from PDF", 200.0)
2646            .unwrap();
2647        ctx.end_text();
2648
2649        let ops = ctx.generate_operations().unwrap();
2650        let ops_str = String::from_utf8_lossy(&ops);
2651
2652        // Should contain text operations
2653        assert!(ops_str.contains("BT")); // Begin text
2654        assert!(ops_str.contains("ET")); // End text
2655        assert!(ops_str.contains("100.00 200.00 Td")); // Text position
2656        assert!(ops_str.contains("(Hello world from PDF) Tj")); // Show text
2657
2658        // Should contain word spacing operations
2659        assert!(ops_str.contains("Tw")); // Word spacing
2660    }
2661
2662    #[test]
2663    fn test_justified_text_single_word() {
2664        let mut ctx = GraphicsContext::new();
2665        ctx.begin_text();
2666        ctx.show_justified_text("Hello", 200.0).unwrap();
2667        ctx.end_text();
2668
2669        let ops = ctx.generate_operations().unwrap();
2670        let ops_str = String::from_utf8_lossy(&ops);
2671
2672        // Single word should just use normal text display
2673        assert!(ops_str.contains("(Hello) Tj"));
2674        // Should not contain word spacing since there's only one word
2675        assert_eq!(ops_str.matches("Tw").count(), 0);
2676    }
2677
2678    #[test]
2679    fn test_text_width_estimation() {
2680        let ctx = GraphicsContext::new();
2681        let width = ctx.estimate_text_width_simple("Hello");
2682
2683        // Should return reasonable estimation based on font size and character count
2684        assert!(width > 0.0);
2685        assert_eq!(width, 5.0 * 12.0 * 0.6); // 5 chars * 12pt font * 0.6 factor
2686    }
2687}