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