Skip to main content

oxidize_pdf/graphics/
mod.rs

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