Skip to main content

oxidize_pdf/graphics/
mod.rs

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