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