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