Skip to main content

oxidize_pdf/graphics/
state.rs

1//! Extended Graphics State Dictionary support according to ISO 32000-1 Section 8.4
2//!
3//! This module provides comprehensive support for PDF Extended Graphics State (ExtGState)
4//! dictionary parameters as specified in ISO 32000-1:2008.
5
6use super::soft_mask::SoftMask;
7use crate::error::{PdfError, Result};
8use crate::graphics::{LineCap, LineJoin};
9use crate::text::Font;
10use std::collections::HashMap;
11use std::fmt::Write;
12
13/// Rendering intent values according to ISO 32000-1
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum RenderingIntent {
16    /// Absolute colorimetric
17    AbsoluteColorimetric,
18    /// Relative colorimetric
19    RelativeColorimetric,
20    /// Saturation
21    Saturation,
22    /// Perceptual
23    Perceptual,
24}
25
26impl RenderingIntent {
27    /// Get the PDF name for this rendering intent
28    pub fn pdf_name(&self) -> &'static str {
29        match self {
30            RenderingIntent::AbsoluteColorimetric => "AbsoluteColorimetric",
31            RenderingIntent::RelativeColorimetric => "RelativeColorimetric",
32            RenderingIntent::Saturation => "Saturation",
33            RenderingIntent::Perceptual => "Perceptual",
34        }
35    }
36}
37
38/// Blend mode values for transparency
39#[derive(Debug, Clone, PartialEq)]
40pub enum BlendMode {
41    /// Normal blend mode (default)
42    Normal,
43    /// Multiply blend mode
44    Multiply,
45    /// Screen blend mode
46    Screen,
47    /// Overlay blend mode
48    Overlay,
49    /// SoftLight blend mode
50    SoftLight,
51    /// HardLight blend mode
52    HardLight,
53    /// ColorDodge blend mode
54    ColorDodge,
55    /// ColorBurn blend mode
56    ColorBurn,
57    /// Darken blend mode
58    Darken,
59    /// Lighten blend mode
60    Lighten,
61    /// Difference blend mode
62    Difference,
63    /// Exclusion blend mode
64    Exclusion,
65    /// Hue blend mode (PDF 1.4)
66    Hue,
67    /// Saturation blend mode (PDF 1.4)
68    Saturation,
69    /// Color blend mode (PDF 1.4)
70    Color,
71    /// Luminosity blend mode (PDF 1.4)
72    Luminosity,
73}
74
75impl BlendMode {
76    /// Get the PDF name for this blend mode
77    pub fn pdf_name(&self) -> &'static str {
78        match self {
79            BlendMode::Normal => "Normal",
80            BlendMode::Multiply => "Multiply",
81            BlendMode::Screen => "Screen",
82            BlendMode::Overlay => "Overlay",
83            BlendMode::SoftLight => "SoftLight",
84            BlendMode::HardLight => "HardLight",
85            BlendMode::ColorDodge => "ColorDodge",
86            BlendMode::ColorBurn => "ColorBurn",
87            BlendMode::Darken => "Darken",
88            BlendMode::Lighten => "Lighten",
89            BlendMode::Difference => "Difference",
90            BlendMode::Exclusion => "Exclusion",
91            BlendMode::Hue => "Hue",
92            BlendMode::Saturation => "Saturation",
93            BlendMode::Color => "Color",
94            BlendMode::Luminosity => "Luminosity",
95        }
96    }
97}
98
99/// Line dash pattern specification
100#[derive(Debug, Clone, PartialEq)]
101pub struct LineDashPattern {
102    /// Array of dash and gap lengths
103    pub array: Vec<f64>,
104    /// Phase offset
105    pub phase: f64,
106}
107
108impl LineDashPattern {
109    /// Create a new line dash pattern
110    pub fn new(array: Vec<f64>, phase: f64) -> Self {
111        Self { array, phase }
112    }
113
114    /// Create a solid line (no dashes)
115    pub fn solid() -> Self {
116        Self {
117            array: Vec::new(),
118            phase: 0.0,
119        }
120    }
121
122    /// Create a simple dashed line
123    pub fn dashed(dash_length: f64, gap_length: f64) -> Self {
124        Self {
125            array: vec![dash_length, gap_length],
126            phase: 0.0,
127        }
128    }
129
130    /// Create a dotted line
131    pub fn dotted(dot_size: f64, gap_size: f64) -> Self {
132        Self {
133            array: vec![dot_size, gap_size],
134            phase: 0.0,
135        }
136    }
137
138    /// Generate PDF representation of the line dash pattern.
139    ///
140    /// Non-finite components (NaN / ±inf) are clamped to `0.0` via the
141    /// shared `finite_or_zero` helper (issue #220 / Phase 5 of the
142    /// v2.7.0 IR refactor) so the emitted `d` operator never contains
143    /// invalid PDF numeric tokens per ISO 32000-1 §7.3.3.
144    pub fn to_pdf_string(&self) -> String {
145        use crate::graphics::color::finite_or_zero;
146        if self.array.is_empty() {
147            "[] 0".to_string()
148        } else {
149            let array_str = self
150                .array
151                .iter()
152                .map(|&x| format!("{:.2}", finite_or_zero(x)))
153                .collect::<Vec<_>>()
154                .join(" ");
155            format!("[{array_str}] {:.2}", finite_or_zero(self.phase))
156        }
157    }
158}
159
160/// Font specification for ExtGState
161#[derive(Debug, Clone, PartialEq)]
162pub struct ExtGStateFont {
163    /// Font
164    pub font: Font,
165    /// Font size
166    pub size: f64,
167}
168
169impl ExtGStateFont {
170    /// Create a new ExtGState font specification
171    pub fn new(font: Font, size: f64) -> Self {
172        Self { font, size }
173    }
174}
175
176/// Transfer function specification according to ISO 32000-1
177#[derive(Debug, Clone, PartialEq)]
178#[allow(clippy::large_enum_variant)]
179pub enum TransferFunction {
180    /// Identity transfer function (no transformation)
181    Identity,
182    /// Single transfer function for all components
183    Single(TransferFunctionData),
184    /// Separate transfer functions for each color component (C, M, Y, K or R, G, B)
185    Separate {
186        /// Function for first component (Cyan or Red)
187        c_or_r: TransferFunctionData,
188        /// Function for second component (Magenta or Green)
189        m_or_g: TransferFunctionData,
190        /// Function for third component (Yellow or Blue)
191        y_or_b: TransferFunctionData,
192        /// Function for fourth component (Black) - optional for RGB
193        k: Option<TransferFunctionData>,
194    },
195}
196
197/// Data for a single transfer function
198#[derive(Debug, Clone, PartialEq)]
199pub struct TransferFunctionData {
200    /// Function type (0, 2, 3, or 4)
201    pub function_type: u32,
202    /// Domain of the function
203    pub domain: Vec<f64>,
204    /// Range of the function
205    pub range: Vec<f64>,
206    /// Function-specific parameters
207    pub params: TransferFunctionParams,
208}
209
210/// Parameters for different transfer function types
211#[derive(Debug, Clone, PartialEq)]
212pub enum TransferFunctionParams {
213    /// Type 0: Sampled function
214    Sampled {
215        /// Sample values
216        samples: Vec<f64>,
217        /// Number of samples in each dimension
218        size: Vec<u32>,
219        /// Bits per sample
220        bits_per_sample: u32,
221    },
222    /// Type 2: Exponential interpolation
223    Exponential {
224        /// C0 values
225        c0: Vec<f64>,
226        /// C1 values
227        c1: Vec<f64>,
228        /// Exponent
229        n: f64,
230    },
231    /// Type 3: Stitching function
232    Stitching {
233        /// Functions to stitch together
234        functions: Vec<TransferFunctionData>,
235        /// Bounds for stitching
236        bounds: Vec<f64>,
237        /// Encode values
238        encode: Vec<f64>,
239    },
240    /// Type 4: PostScript calculator function
241    PostScript {
242        /// PostScript code
243        code: String,
244    },
245}
246
247impl TransferFunction {
248    /// Create an identity transfer function
249    pub fn identity() -> Self {
250        TransferFunction::Identity
251    }
252
253    /// Create a gamma correction transfer function
254    pub fn gamma(gamma_value: f64) -> Self {
255        TransferFunction::Single(TransferFunctionData {
256            function_type: 2,
257            domain: vec![0.0, 1.0],
258            range: vec![0.0, 1.0],
259            params: TransferFunctionParams::Exponential {
260                c0: vec![0.0],
261                c1: vec![1.0],
262                n: gamma_value,
263            },
264        })
265    }
266
267    /// Create a linear transfer function with slope and intercept
268    pub fn linear(slope: f64, intercept: f64) -> Self {
269        TransferFunction::Single(TransferFunctionData {
270            function_type: 2,
271            domain: vec![0.0, 1.0],
272            range: vec![0.0, 1.0],
273            params: TransferFunctionParams::Exponential {
274                c0: vec![intercept],
275                c1: vec![slope + intercept],
276                n: 1.0,
277            },
278        })
279    }
280
281    /// Convert transfer function to PDF representation
282    pub fn to_pdf_string(&self) -> String {
283        match self {
284            TransferFunction::Identity => "/Identity".to_string(),
285            TransferFunction::Single(data) => data.to_pdf_string(),
286            TransferFunction::Separate {
287                c_or_r,
288                m_or_g,
289                y_or_b,
290                k,
291            } => {
292                let mut result = String::from("[");
293                result.push_str(&c_or_r.to_pdf_string());
294                result.push(' ');
295                result.push_str(&m_or_g.to_pdf_string());
296                result.push(' ');
297                result.push_str(&y_or_b.to_pdf_string());
298                if let Some(k_func) = k {
299                    result.push(' ');
300                    result.push_str(&k_func.to_pdf_string());
301                }
302                result.push(']');
303                result
304            }
305        }
306    }
307}
308
309impl TransferFunctionData {
310    /// Convert transfer function data to PDF representation
311    pub fn to_pdf_string(&self) -> String {
312        let mut dict = String::from("<<");
313
314        // Function type
315        dict.push_str(&format!(" /FunctionType {}", self.function_type));
316
317        // Domain
318        dict.push_str(" /Domain [");
319        for (i, val) in self.domain.iter().enumerate() {
320            if i > 0 {
321                dict.push(' ');
322            }
323            dict.push_str(&format!("{:.3}", val));
324        }
325        dict.push(']');
326
327        // Range
328        dict.push_str(" /Range [");
329        for (i, val) in self.range.iter().enumerate() {
330            if i > 0 {
331                dict.push(' ');
332            }
333            dict.push_str(&format!("{:.3}", val));
334        }
335        dict.push(']');
336
337        // Function-specific parameters
338        match &self.params {
339            TransferFunctionParams::Exponential { c0, c1, n } => {
340                // Type 2: Exponential interpolation function
341                dict.push_str(" /C0 [");
342                for (i, val) in c0.iter().enumerate() {
343                    if i > 0 {
344                        dict.push(' ');
345                    }
346                    dict.push_str(&format!("{:.3}", val));
347                }
348                dict.push_str("] /C1 [");
349                for (i, val) in c1.iter().enumerate() {
350                    if i > 0 {
351                        dict.push(' ');
352                    }
353                    dict.push_str(&format!("{:.3}", val));
354                }
355                dict.push_str(&format!("] /N {:.3}", n));
356            }
357            TransferFunctionParams::Sampled {
358                size,
359                bits_per_sample,
360                samples,
361                ..
362            } => {
363                // Type 0: Sampled function
364                dict.push_str(" /Size [");
365                for (i, val) in size.iter().enumerate() {
366                    if i > 0 {
367                        dict.push(' ');
368                    }
369                    dict.push_str(&format!("{}", val));
370                }
371                dict.push_str(&format!("] /BitsPerSample {}", bits_per_sample));
372                // Samples would be encoded as a stream
373                dict.push_str(" /Length ");
374                dict.push_str(&format!("{}", samples.len()));
375            }
376            TransferFunctionParams::Stitching {
377                bounds,
378                encode,
379                functions,
380            } => {
381                // Type 3: Stitching function
382                dict.push_str(" /Bounds [");
383                for (i, val) in bounds.iter().enumerate() {
384                    if i > 0 {
385                        dict.push(' ');
386                    }
387                    dict.push_str(&format!("{:.3}", val));
388                }
389                dict.push_str("] /Encode [");
390                for (i, val) in encode.iter().enumerate() {
391                    if i > 0 {
392                        dict.push(' ');
393                    }
394                    dict.push_str(&format!("{:.3}", val));
395                }
396                dict.push_str("] /Functions [");
397                for (i, func) in functions.iter().enumerate() {
398                    if i > 0 {
399                        dict.push(' ');
400                    }
401                    dict.push_str(&func.to_pdf_string());
402                }
403                dict.push(']');
404            }
405            TransferFunctionParams::PostScript { code } => {
406                // Type 4: PostScript calculator function
407                dict.push_str(&format!(
408                    " /Length {} stream\n{}\nendstream",
409                    code.len(),
410                    code
411                ));
412            }
413        }
414
415        dict.push_str(" >>");
416        dict
417    }
418}
419
420/// Halftone specification according to ISO 32000-1
421#[derive(Debug, Clone, PartialEq)]
422pub enum Halftone {
423    /// Default halftone
424    Default,
425    /// Type 1: Simple halftone
426    Type1 {
427        /// Halftone frequency
428        frequency: f64,
429        /// Halftone angle in degrees
430        angle: f64,
431        /// Spot function name
432        spot_function: SpotFunction,
433    },
434    /// Type 5: Halftone with multiple colorants
435    Type5 {
436        /// Halftone for each colorant
437        colorants: HashMap<String, HalftoneColorant>,
438        /// Default halftone
439        default: Box<Halftone>,
440    },
441    /// Type 6: Threshold array
442    Type6 {
443        /// Width of threshold array
444        width: u32,
445        /// Height of threshold array
446        height: u32,
447        /// Threshold values
448        thresholds: Vec<u8>,
449    },
450    /// Type 10: Stochastic (FM) screening
451    Type10 {
452        /// Halftone frequency
453        frequency: f64,
454    },
455    /// Type 16: Multiple threshold arrays
456    Type16 {
457        /// Width of threshold arrays
458        width: u32,
459        /// Height of threshold arrays  
460        height: u32,
461        /// Multiple threshold arrays
462        thresholds: Vec<Vec<u8>>,
463    },
464}
465
466/// Spot function for halftone screening
467#[derive(Debug, Clone, PartialEq)]
468pub enum SpotFunction {
469    /// Simple dot
470    SimpleDot,
471    /// Inverted simple dot
472    InvertedSimpleDot,
473    /// Round dot
474    Round,
475    /// Inverted round dot
476    InvertedRound,
477    /// Ellipse
478    Ellipse,
479    /// Square
480    Square,
481    /// Cross
482    Cross,
483    /// Diamond
484    Diamond,
485    /// Line
486    Line,
487    /// Custom spot function
488    Custom(String),
489}
490
491impl SpotFunction {
492    /// Get the PDF name for this spot function
493    pub fn pdf_name(&self) -> String {
494        match self {
495            SpotFunction::SimpleDot => "SimpleDot".to_string(),
496            SpotFunction::InvertedSimpleDot => "InvertedSimpleDot".to_string(),
497            SpotFunction::Round => "Round".to_string(),
498            SpotFunction::InvertedRound => "InvertedRound".to_string(),
499            SpotFunction::Ellipse => "Ellipse".to_string(),
500            SpotFunction::Square => "Square".to_string(),
501            SpotFunction::Cross => "Cross".to_string(),
502            SpotFunction::Diamond => "Diamond".to_string(),
503            SpotFunction::Line => "Line".to_string(),
504            SpotFunction::Custom(name) => name.clone(),
505        }
506    }
507}
508
509/// Halftone specification for a single colorant
510#[derive(Debug, Clone, PartialEq)]
511pub struct HalftoneColorant {
512    /// Halftone frequency
513    pub frequency: f64,
514    /// Halftone angle in degrees
515    pub angle: f64,
516    /// Spot function
517    pub spot_function: SpotFunction,
518}
519
520/// Extended Graphics State Dictionary according to ISO 32000-1 Section 8.4
521#[derive(Debug, Clone)]
522pub struct ExtGState {
523    // Line parameters
524    /// Line width (LW)
525    pub line_width: Option<f64>,
526    /// Line cap style (LC)
527    pub line_cap: Option<LineCap>,
528    /// Line join style (LJ)
529    pub line_join: Option<LineJoin>,
530    /// Miter limit (ML)
531    pub miter_limit: Option<f64>,
532    /// Line dash pattern (D)
533    pub dash_pattern: Option<LineDashPattern>,
534
535    // Rendering intent
536    /// Rendering intent (RI)
537    pub rendering_intent: Option<RenderingIntent>,
538
539    // Overprint control
540    /// Overprint for stroking operations (OP)
541    pub overprint_stroke: Option<bool>,
542    /// Overprint for non-stroking operations (op)
543    pub overprint_fill: Option<bool>,
544    /// Overprint mode (OPM)
545    pub overprint_mode: Option<u8>,
546
547    // Font
548    /// Font and size (Font)
549    pub font: Option<ExtGStateFont>,
550
551    // Color functions (simplified for basic implementation)
552    /// Black generation function (BG)
553    pub black_generation: Option<TransferFunction>,
554    /// Black generation function alternative (BG2)
555    pub black_generation_2: Option<TransferFunction>,
556    /// Undercolor removal function (UCR)
557    pub undercolor_removal: Option<TransferFunction>,
558    /// Undercolor removal function alternative (UCR2)
559    pub undercolor_removal_2: Option<TransferFunction>,
560    /// Transfer function (TR)
561    pub transfer_function: Option<TransferFunction>,
562    /// Transfer function alternative (TR2)
563    pub transfer_function_2: Option<TransferFunction>,
564
565    // Halftone
566    /// Halftone dictionary (HT)
567    pub halftone: Option<Halftone>,
568
569    // Flatness and smoothness
570    /// Flatness tolerance (FL)
571    pub flatness: Option<f64>,
572    /// Smoothness tolerance (SM)
573    pub smoothness: Option<f64>,
574
575    // Additional parameters
576    /// Automatic stroke adjustment (SA)
577    pub stroke_adjustment: Option<bool>,
578
579    // Transparency parameters (PDF 1.4+)
580    /// Blend mode (BM)
581    pub blend_mode: Option<BlendMode>,
582    /// Soft mask (SMask)
583    pub soft_mask: Option<SoftMask>,
584    /// Alpha constant for stroking (CA)
585    pub alpha_stroke: Option<f64>,
586    /// Alpha constant for non-stroking (ca)
587    pub alpha_fill: Option<f64>,
588    /// Alpha source flag (AIS)
589    pub alpha_is_shape: Option<bool>,
590    /// Text knockout flag (TK)
591    pub text_knockout: Option<bool>,
592
593    // PDF 2.0 additions
594    /// Black point compensation (UseBlackPtComp)
595    pub use_black_point_compensation: Option<bool>,
596}
597
598impl Default for ExtGState {
599    fn default() -> Self {
600        Self::new()
601    }
602}
603
604impl ExtGState {
605    /// Create a new empty ExtGState dictionary
606    pub fn new() -> Self {
607        Self {
608            line_width: None,
609            line_cap: None,
610            line_join: None,
611            miter_limit: None,
612            dash_pattern: None,
613            rendering_intent: None,
614            overprint_stroke: None,
615            overprint_fill: None,
616            overprint_mode: None,
617            font: None,
618            black_generation: None,
619            black_generation_2: None,
620            undercolor_removal: None,
621            undercolor_removal_2: None,
622            transfer_function: None,
623            transfer_function_2: None,
624            halftone: None,
625            flatness: None,
626            smoothness: None,
627            stroke_adjustment: None,
628            blend_mode: None,
629            soft_mask: None,
630            alpha_stroke: None,
631            alpha_fill: None,
632            alpha_is_shape: None,
633            text_knockout: None,
634            use_black_point_compensation: None,
635        }
636    }
637
638    // Line parameter setters
639    /// Set line width
640    pub fn with_line_width(mut self, width: f64) -> Self {
641        self.line_width = Some(width.max(0.0));
642        self
643    }
644
645    /// Set line cap style
646    pub fn with_line_cap(mut self, cap: LineCap) -> Self {
647        self.line_cap = Some(cap);
648        self
649    }
650
651    /// Set line join style
652    pub fn with_line_join(mut self, join: LineJoin) -> Self {
653        self.line_join = Some(join);
654        self
655    }
656
657    /// Set miter limit
658    pub fn with_miter_limit(mut self, limit: f64) -> Self {
659        self.miter_limit = Some(limit.max(1.0));
660        self
661    }
662
663    /// Set line dash pattern
664    pub fn with_dash_pattern(mut self, pattern: LineDashPattern) -> Self {
665        self.dash_pattern = Some(pattern);
666        self
667    }
668
669    // Rendering intent setter
670    /// Set rendering intent
671    pub fn with_rendering_intent(mut self, intent: RenderingIntent) -> Self {
672        self.rendering_intent = Some(intent);
673        self
674    }
675
676    // Overprint setters
677    /// Set overprint for stroking operations
678    pub fn with_overprint_stroke(mut self, overprint: bool) -> Self {
679        self.overprint_stroke = Some(overprint);
680        self
681    }
682
683    /// Set overprint for non-stroking operations
684    pub fn with_overprint_fill(mut self, overprint: bool) -> Self {
685        self.overprint_fill = Some(overprint);
686        self
687    }
688
689    /// Set overprint mode
690    pub fn with_overprint_mode(mut self, mode: u8) -> Self {
691        self.overprint_mode = Some(mode);
692        self
693    }
694
695    // Font setter
696    /// Set font and size
697    pub fn with_font(mut self, font: Font, size: f64) -> Self {
698        self.font = Some(ExtGStateFont::new(font, size.max(0.0)));
699        self
700    }
701
702    // Flatness and smoothness setters
703    /// Set flatness tolerance
704    pub fn with_flatness(mut self, flatness: f64) -> Self {
705        self.flatness = Some(flatness.clamp(0.0, 100.0));
706        self
707    }
708
709    /// Set smoothness tolerance
710    pub fn with_smoothness(mut self, smoothness: f64) -> Self {
711        self.smoothness = Some(smoothness.clamp(0.0, 1.0));
712        self
713    }
714
715    /// Set automatic stroke adjustment
716    pub fn with_stroke_adjustment(mut self, adjustment: bool) -> Self {
717        self.stroke_adjustment = Some(adjustment);
718        self
719    }
720
721    // Transparency setters
722    /// Set blend mode
723    pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
724        self.blend_mode = Some(mode);
725        self
726    }
727
728    /// Set alpha constant for stroking operations
729    pub fn with_alpha_stroke(mut self, alpha: f64) -> Self {
730        self.alpha_stroke = Some(alpha.clamp(0.0, 1.0));
731        self
732    }
733
734    /// Set alpha constant for non-stroking operations
735    pub fn with_alpha_fill(mut self, alpha: f64) -> Self {
736        self.alpha_fill = Some(alpha.clamp(0.0, 1.0));
737        self
738    }
739
740    /// Set alpha constant for both stroking and non-stroking operations
741    pub fn with_alpha(mut self, alpha: f64) -> Self {
742        let clamped = alpha.clamp(0.0, 1.0);
743        self.alpha_stroke = Some(clamped);
744        self.alpha_fill = Some(clamped);
745        self
746    }
747
748    /// Set alpha source flag
749    pub fn with_alpha_is_shape(mut self, is_shape: bool) -> Self {
750        self.alpha_is_shape = Some(is_shape);
751        self
752    }
753
754    /// Set text knockout flag
755    pub fn with_text_knockout(mut self, knockout: bool) -> Self {
756        self.text_knockout = Some(knockout);
757        self
758    }
759
760    /// Set soft mask for transparency
761    pub fn set_soft_mask(&mut self, mask: SoftMask) {
762        self.soft_mask = Some(mask);
763    }
764
765    /// Set soft mask with a named XObject
766    pub fn set_soft_mask_name(&mut self, name: String) {
767        self.soft_mask = Some(SoftMask::luminosity(name));
768    }
769
770    /// Remove soft mask (set to None)
771    pub fn set_soft_mask_none(&mut self) {
772        self.soft_mask = Some(SoftMask::none());
773    }
774
775    /// Set black point compensation (PDF 2.0)
776    pub fn with_black_point_compensation(mut self, use_compensation: bool) -> Self {
777        self.use_black_point_compensation = Some(use_compensation);
778        self
779    }
780
781    // Transfer function setters
782    /// Set transfer function for output device gamma correction
783    pub fn with_transfer_function(mut self, func: TransferFunction) -> Self {
784        self.transfer_function = Some(func);
785        self
786    }
787
788    /// Set gamma correction transfer function
789    pub fn with_gamma_correction(mut self, gamma: f64) -> Self {
790        self.transfer_function = Some(TransferFunction::gamma(gamma));
791        self
792    }
793
794    /// Set linear transfer function with slope and intercept
795    pub fn with_linear_transfer(mut self, slope: f64, intercept: f64) -> Self {
796        self.transfer_function = Some(TransferFunction::linear(slope, intercept));
797        self
798    }
799
800    /// Set alternative transfer function (TR2)
801    pub fn with_transfer_function_2(mut self, func: TransferFunction) -> Self {
802        self.transfer_function_2 = Some(func);
803        self
804    }
805
806    /// Set black generation function
807    pub fn with_black_generation(mut self, func: TransferFunction) -> Self {
808        self.black_generation = Some(func);
809        self
810    }
811
812    /// Set undercolor removal function
813    pub fn with_undercolor_removal(mut self, func: TransferFunction) -> Self {
814        self.undercolor_removal = Some(func);
815        self
816    }
817
818    /// Check if any transparency parameters are set
819    pub fn uses_transparency(&self) -> bool {
820        self.alpha_stroke.is_some_and(|a| a < 1.0)
821            || self.alpha_fill.is_some_and(|a| a < 1.0)
822            || self.blend_mode.is_some()
823            || self.soft_mask.is_some()
824    }
825
826    /// Generate PDF dictionary representation
827    pub fn to_pdf_dictionary(&self) -> Result<String> {
828        let mut dict = String::from("<< /Type /ExtGState");
829
830        // Line parameters
831        if let Some(width) = self.line_width {
832            write!(&mut dict, " /LW {width:.3}").map_err(|_| {
833                PdfError::InvalidStructure("Failed to write line width".to_string())
834            })?;
835        }
836
837        if let Some(cap) = self.line_cap {
838            write!(&mut dict, " /LC {}", cap as u8)
839                .map_err(|_| PdfError::InvalidStructure("Failed to write line cap".to_string()))?;
840        }
841
842        if let Some(join) = self.line_join {
843            write!(&mut dict, " /LJ {}", join as u8)
844                .map_err(|_| PdfError::InvalidStructure("Failed to write line join".to_string()))?;
845        }
846
847        if let Some(limit) = self.miter_limit {
848            write!(&mut dict, " /ML {limit:.3}").map_err(|_| {
849                PdfError::InvalidStructure("Failed to write miter limit".to_string())
850            })?;
851        }
852
853        if let Some(ref pattern) = self.dash_pattern {
854            write!(&mut dict, " /D {}", pattern.to_pdf_string()).map_err(|_| {
855                PdfError::InvalidStructure("Failed to write dash pattern".to_string())
856            })?;
857        }
858
859        // Rendering intent
860        if let Some(intent) = self.rendering_intent {
861            write!(&mut dict, " /RI /{}", intent.pdf_name()).map_err(|_| {
862                PdfError::InvalidStructure("Failed to write rendering intent".to_string())
863            })?;
864        }
865
866        // Overprint control
867        if let Some(op) = self.overprint_stroke {
868            write!(&mut dict, " /OP {op}").map_err(|_| {
869                PdfError::InvalidStructure("Failed to write overprint stroke".to_string())
870            })?;
871        }
872
873        if let Some(op) = self.overprint_fill {
874            write!(&mut dict, " /op {op}").map_err(|_| {
875                PdfError::InvalidStructure("Failed to write overprint fill".to_string())
876            })?;
877        }
878
879        if let Some(mode) = self.overprint_mode {
880            write!(&mut dict, " /OPM {mode}").map_err(|_| {
881                PdfError::InvalidStructure("Failed to write overprint mode".to_string())
882            })?;
883        }
884
885        // Font
886        if let Some(ref font) = self.font {
887            write!(
888                &mut dict,
889                " /Font [/{} {:.3}]",
890                font.font.pdf_name(),
891                font.size
892            )
893            .map_err(|_| PdfError::InvalidStructure("Failed to write font".to_string()))?;
894        }
895
896        // Flatness and smoothness
897        if let Some(flatness) = self.flatness {
898            write!(&mut dict, " /FL {flatness:.3}")
899                .map_err(|_| PdfError::InvalidStructure("Failed to write flatness".to_string()))?;
900        }
901
902        if let Some(smoothness) = self.smoothness {
903            write!(&mut dict, " /SM {smoothness:.3}").map_err(|_| {
904                PdfError::InvalidStructure("Failed to write smoothness".to_string())
905            })?;
906        }
907
908        // Stroke adjustment
909        if let Some(sa) = self.stroke_adjustment {
910            write!(&mut dict, " /SA {sa}").map_err(|_| {
911                PdfError::InvalidStructure("Failed to write stroke adjustment".to_string())
912            })?;
913        }
914
915        // Transparency parameters
916        if let Some(ref mode) = self.blend_mode {
917            write!(&mut dict, " /BM /{}", mode.pdf_name()).map_err(|_| {
918                PdfError::InvalidStructure("Failed to write blend mode".to_string())
919            })?;
920        }
921
922        if let Some(ref mask) = self.soft_mask {
923            if mask.is_none() {
924                write!(&mut dict, " /SMask /None").map_err(|_| {
925                    PdfError::InvalidStructure("Failed to write soft mask".to_string())
926                })?;
927            } else {
928                // In a full implementation, this would write the soft mask dictionary
929                // For now, we write a reference
930                write!(&mut dict, " /SMask {}", mask.to_pdf_string()).map_err(|_| {
931                    PdfError::InvalidStructure("Failed to write soft mask".to_string())
932                })?;
933            }
934        }
935
936        if let Some(alpha) = self.alpha_stroke {
937            write!(&mut dict, " /CA {alpha:.3}").map_err(|_| {
938                PdfError::InvalidStructure("Failed to write stroke alpha".to_string())
939            })?;
940        }
941
942        if let Some(alpha) = self.alpha_fill {
943            write!(&mut dict, " /ca {alpha:.3}").map_err(|_| {
944                PdfError::InvalidStructure("Failed to write fill alpha".to_string())
945            })?;
946        }
947
948        if let Some(ais) = self.alpha_is_shape {
949            write!(&mut dict, " /AIS {ais}").map_err(|_| {
950                PdfError::InvalidStructure("Failed to write alpha is shape".to_string())
951            })?;
952        }
953
954        if let Some(tk) = self.text_knockout {
955            write!(&mut dict, " /TK {tk}").map_err(|_| {
956                PdfError::InvalidStructure("Failed to write text knockout".to_string())
957            })?;
958        }
959
960        // Transfer functions
961        if let Some(ref tf) = self.transfer_function {
962            write!(&mut dict, " /TR {}", tf.to_pdf_string()).map_err(|_| {
963                PdfError::InvalidStructure("Failed to write transfer function".to_string())
964            })?;
965        }
966
967        if let Some(ref tf) = self.transfer_function_2 {
968            write!(&mut dict, " /TR2 {}", tf.to_pdf_string()).map_err(|_| {
969                PdfError::InvalidStructure("Failed to write transfer function 2".to_string())
970            })?;
971        }
972
973        if let Some(ref bg) = self.black_generation {
974            write!(&mut dict, " /BG {}", bg.to_pdf_string()).map_err(|_| {
975                PdfError::InvalidStructure("Failed to write black generation".to_string())
976            })?;
977        }
978
979        if let Some(ref bg) = self.black_generation_2 {
980            write!(&mut dict, " /BG2 {}", bg.to_pdf_string()).map_err(|_| {
981                PdfError::InvalidStructure("Failed to write black generation 2".to_string())
982            })?;
983        }
984
985        if let Some(ref ucr) = self.undercolor_removal {
986            write!(&mut dict, " /UCR {}", ucr.to_pdf_string()).map_err(|_| {
987                PdfError::InvalidStructure("Failed to write undercolor removal".to_string())
988            })?;
989        }
990
991        if let Some(ref ucr) = self.undercolor_removal_2 {
992            write!(&mut dict, " /UCR2 {}", ucr.to_pdf_string()).map_err(|_| {
993                PdfError::InvalidStructure("Failed to write undercolor removal 2".to_string())
994            })?;
995        }
996
997        // PDF 2.0 parameters
998        if let Some(use_comp) = self.use_black_point_compensation {
999            write!(&mut dict, " /UseBlackPtComp {use_comp}").map_err(|_| {
1000                PdfError::InvalidStructure("Failed to write black point compensation".to_string())
1001            })?;
1002        }
1003
1004        dict.push_str(" >>");
1005        Ok(dict)
1006    }
1007
1008    /// Check if the ExtGState is empty (no parameters set)
1009    pub fn is_empty(&self) -> bool {
1010        self.line_width.is_none()
1011            && self.line_cap.is_none()
1012            && self.line_join.is_none()
1013            && self.miter_limit.is_none()
1014            && self.dash_pattern.is_none()
1015            && self.rendering_intent.is_none()
1016            && self.overprint_stroke.is_none()
1017            && self.overprint_fill.is_none()
1018            && self.overprint_mode.is_none()
1019            && self.font.is_none()
1020            && self.flatness.is_none()
1021            && self.smoothness.is_none()
1022            && self.stroke_adjustment.is_none()
1023            && self.blend_mode.is_none()
1024            && self.soft_mask.is_none()
1025            && self.alpha_stroke.is_none()
1026            && self.alpha_fill.is_none()
1027            && self.alpha_is_shape.is_none()
1028            && self.text_knockout.is_none()
1029            && self.transfer_function.is_none()
1030            && self.transfer_function_2.is_none()
1031            && self.black_generation.is_none()
1032            && self.black_generation_2.is_none()
1033            && self.undercolor_removal.is_none()
1034            && self.undercolor_removal_2.is_none()
1035            && self.use_black_point_compensation.is_none()
1036    }
1037
1038    /// Convert to Dictionary object for PDF writer
1039    pub fn to_dict(&self) -> crate::objects::Dictionary {
1040        use crate::objects::{Dictionary, Object};
1041
1042        let mut dict = Dictionary::new();
1043        dict.set("Type", Object::Name("ExtGState".to_string()));
1044
1045        // Line parameters
1046        if let Some(width) = self.line_width {
1047            dict.set("LW", Object::Real(width));
1048        }
1049
1050        if let Some(cap) = self.line_cap {
1051            dict.set("LC", Object::Integer(cap as i64));
1052        }
1053
1054        if let Some(join) = self.line_join {
1055            dict.set("LJ", Object::Integer(join as i64));
1056        }
1057
1058        if let Some(limit) = self.miter_limit {
1059            dict.set("ML", Object::Real(limit));
1060        }
1061
1062        // Transparency parameters
1063        if let Some(mode) = &self.blend_mode {
1064            dict.set("BM", Object::Name(mode.pdf_name().to_string()));
1065        }
1066
1067        if let Some(alpha) = self.alpha_stroke {
1068            dict.set("CA", Object::Real(alpha));
1069        }
1070
1071        if let Some(alpha) = self.alpha_fill {
1072            dict.set("ca", Object::Real(alpha));
1073        }
1074
1075        if let Some(ais) = self.alpha_is_shape {
1076            dict.set("AIS", Object::Boolean(ais));
1077        }
1078
1079        if let Some(tk) = self.text_knockout {
1080            dict.set("TK", Object::Boolean(tk));
1081        }
1082
1083        // Other parameters
1084        if let Some(intent) = &self.rendering_intent {
1085            dict.set("RI", Object::Name(intent.pdf_name().to_string()));
1086        }
1087
1088        if let Some(op) = self.overprint_stroke {
1089            dict.set("OP", Object::Boolean(op));
1090        }
1091
1092        if let Some(op) = self.overprint_fill {
1093            dict.set("op", Object::Boolean(op));
1094        }
1095
1096        if let Some(mode) = self.overprint_mode {
1097            dict.set("OPM", Object::Integer(mode as i64));
1098        }
1099
1100        if let Some(flatness) = self.flatness {
1101            dict.set("FL", Object::Real(flatness));
1102        }
1103
1104        if let Some(smoothness) = self.smoothness {
1105            dict.set("SM", Object::Real(smoothness));
1106        }
1107
1108        if let Some(sa) = self.stroke_adjustment {
1109            dict.set("SA", Object::Boolean(sa));
1110        }
1111
1112        dict
1113    }
1114}
1115
1116/// ExtGState manager for handling multiple graphics states
1117#[derive(Debug, Clone)]
1118pub struct ExtGStateManager {
1119    states: HashMap<String, ExtGState>,
1120    next_id: usize,
1121}
1122
1123impl Default for ExtGStateManager {
1124    fn default() -> Self {
1125        Self::new()
1126    }
1127}
1128
1129impl ExtGStateManager {
1130    /// Create a new ExtGState manager
1131    pub fn new() -> Self {
1132        Self {
1133            states: HashMap::new(),
1134            next_id: 1,
1135        }
1136    }
1137
1138    /// Add an ExtGState and return its name
1139    pub fn add_state(&mut self, state: ExtGState) -> Result<String> {
1140        if state.is_empty() {
1141            return Err(PdfError::InvalidStructure(
1142                "ExtGState cannot be empty".to_string(),
1143            ));
1144        }
1145
1146        let name = format!("GS{}", self.next_id);
1147        self.states.insert(name.clone(), state);
1148        self.next_id += 1;
1149        Ok(name)
1150    }
1151
1152    /// Get an ExtGState by name
1153    pub fn get_state(&self, name: &str) -> Option<&ExtGState> {
1154        self.states.get(name)
1155    }
1156
1157    /// Get all states
1158    pub fn states(&self) -> &HashMap<String, ExtGState> {
1159        &self.states
1160    }
1161
1162    /// Generate ExtGState resource dictionary
1163    pub fn to_resource_dictionary(&self) -> Result<String> {
1164        if self.states.is_empty() {
1165            return Ok(String::new());
1166        }
1167
1168        let mut dict = String::from("/ExtGState <<");
1169
1170        for (name, state) in &self.states {
1171            let state_dict = state.to_pdf_dictionary()?;
1172            write!(&mut dict, " /{name} {state_dict}").map_err(|_| {
1173                PdfError::InvalidStructure("Failed to write ExtGState resource".to_string())
1174            })?;
1175        }
1176
1177        dict.push_str(" >>");
1178        Ok(dict)
1179    }
1180
1181    /// Clear all states
1182    pub fn clear(&mut self) {
1183        self.states.clear();
1184        self.next_id = 1;
1185    }
1186
1187    /// Count of registered states
1188    pub fn count(&self) -> usize {
1189        self.states.len()
1190    }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195    use super::*;
1196
1197    #[test]
1198    fn test_rendering_intent_pdf_names() {
1199        assert_eq!(
1200            RenderingIntent::AbsoluteColorimetric.pdf_name(),
1201            "AbsoluteColorimetric"
1202        );
1203        assert_eq!(
1204            RenderingIntent::RelativeColorimetric.pdf_name(),
1205            "RelativeColorimetric"
1206        );
1207        assert_eq!(RenderingIntent::Saturation.pdf_name(), "Saturation");
1208        assert_eq!(RenderingIntent::Perceptual.pdf_name(), "Perceptual");
1209    }
1210
1211    #[test]
1212    fn test_blend_mode_pdf_names() {
1213        assert_eq!(BlendMode::Normal.pdf_name(), "Normal");
1214        assert_eq!(BlendMode::Multiply.pdf_name(), "Multiply");
1215        assert_eq!(BlendMode::Screen.pdf_name(), "Screen");
1216        assert_eq!(BlendMode::Overlay.pdf_name(), "Overlay");
1217    }
1218
1219    #[test]
1220    fn test_line_dash_pattern_creation() {
1221        let solid = LineDashPattern::solid();
1222        assert!(solid.array.is_empty());
1223        assert_eq!(solid.phase, 0.0);
1224
1225        let dashed = LineDashPattern::dashed(5.0, 3.0);
1226        assert_eq!(dashed.array, vec![5.0, 3.0]);
1227        assert_eq!(dashed.phase, 0.0);
1228
1229        let dotted = LineDashPattern::dotted(1.0, 2.0);
1230        assert_eq!(dotted.array, vec![1.0, 2.0]);
1231    }
1232
1233    #[test]
1234    fn test_line_dash_pattern_pdf_string() {
1235        let solid = LineDashPattern::solid();
1236        assert_eq!(solid.to_pdf_string(), "[] 0");
1237
1238        let dashed = LineDashPattern::dashed(5.0, 3.0);
1239        assert_eq!(dashed.to_pdf_string(), "[5.00 3.00] 0.00");
1240
1241        let custom = LineDashPattern::new(vec![10.0, 5.0, 2.0, 5.0], 2.5);
1242        assert_eq!(custom.to_pdf_string(), "[10.00 5.00 2.00 5.00] 2.50");
1243    }
1244
1245    #[test]
1246    fn test_extgstate_font() {
1247        let font = ExtGStateFont::new(Font::Helvetica, 12.0);
1248        assert_eq!(font.font, Font::Helvetica);
1249        assert_eq!(font.size, 12.0);
1250    }
1251
1252    #[test]
1253    fn test_extgstate_creation() {
1254        let state = ExtGState::new();
1255        assert!(state.is_empty());
1256        assert!(!state.uses_transparency());
1257    }
1258
1259    #[test]
1260    fn test_extgstate_line_parameters() {
1261        let state = ExtGState::new()
1262            .with_line_width(2.5)
1263            .with_line_cap(LineCap::Round)
1264            .with_line_join(LineJoin::Bevel)
1265            .with_miter_limit(4.0);
1266
1267        assert_eq!(state.line_width, Some(2.5));
1268        assert_eq!(state.line_cap, Some(LineCap::Round));
1269        assert_eq!(state.line_join, Some(LineJoin::Bevel));
1270        assert_eq!(state.miter_limit, Some(4.0));
1271        assert!(!state.is_empty());
1272    }
1273
1274    #[test]
1275    fn test_extgstate_transparency() {
1276        let state = ExtGState::new()
1277            .with_alpha_stroke(0.8)
1278            .with_alpha_fill(0.6)
1279            .with_blend_mode(BlendMode::Multiply);
1280
1281        assert_eq!(state.alpha_stroke, Some(0.8));
1282        assert_eq!(state.alpha_fill, Some(0.6));
1283        assert_eq!(state.blend_mode, Some(BlendMode::Multiply));
1284        assert!(state.uses_transparency());
1285    }
1286
1287    #[test]
1288    fn test_extgstate_alpha_clamping() {
1289        let state = ExtGState::new()
1290            .with_alpha_stroke(1.5) // Should clamp to 1.0
1291            .with_alpha_fill(-0.1); // Should clamp to 0.0
1292
1293        assert_eq!(state.alpha_stroke, Some(1.0));
1294        assert_eq!(state.alpha_fill, Some(0.0));
1295    }
1296
1297    #[test]
1298    fn test_extgstate_combined_alpha() {
1299        let state = ExtGState::new().with_alpha(0.5);
1300
1301        assert_eq!(state.alpha_stroke, Some(0.5));
1302        assert_eq!(state.alpha_fill, Some(0.5));
1303    }
1304
1305    #[test]
1306    fn test_extgstate_rendering_intent() {
1307        let state = ExtGState::new().with_rendering_intent(RenderingIntent::Perceptual);
1308
1309        assert_eq!(state.rendering_intent, Some(RenderingIntent::Perceptual));
1310    }
1311
1312    #[test]
1313    fn test_extgstate_overprint() {
1314        let state = ExtGState::new()
1315            .with_overprint_stroke(true)
1316            .with_overprint_fill(false)
1317            .with_overprint_mode(1);
1318
1319        assert_eq!(state.overprint_stroke, Some(true));
1320        assert_eq!(state.overprint_fill, Some(false));
1321        assert_eq!(state.overprint_mode, Some(1));
1322    }
1323
1324    #[test]
1325    fn test_extgstate_font_setting() {
1326        let state = ExtGState::new().with_font(Font::HelveticaBold, 14.0);
1327
1328        assert!(state.font.is_some());
1329        let font = state.font.unwrap();
1330        assert_eq!(font.font, Font::HelveticaBold);
1331        assert_eq!(font.size, 14.0);
1332    }
1333
1334    #[test]
1335    fn test_extgstate_tolerance_parameters() {
1336        let state = ExtGState::new()
1337            .with_flatness(1.5)
1338            .with_smoothness(0.8)
1339            .with_stroke_adjustment(true);
1340
1341        assert_eq!(state.flatness, Some(1.5));
1342        assert_eq!(state.smoothness, Some(0.8));
1343        assert_eq!(state.stroke_adjustment, Some(true));
1344    }
1345
1346    #[test]
1347    fn test_extgstate_pdf_dictionary_generation() {
1348        let state = ExtGState::new()
1349            .with_line_width(2.0)
1350            .with_line_cap(LineCap::Round)
1351            .with_alpha(0.5)
1352            .with_blend_mode(BlendMode::Multiply);
1353
1354        let dict = state.to_pdf_dictionary().unwrap();
1355        assert!(dict.contains("/Type /ExtGState"));
1356        assert!(dict.contains("/LW 2.000"));
1357        assert!(dict.contains("/LC 1"));
1358        assert!(dict.contains("/CA 0.500"));
1359        assert!(dict.contains("/ca 0.500"));
1360        assert!(dict.contains("/BM /Multiply"));
1361    }
1362
1363    #[test]
1364    fn test_extgstate_manager_creation() {
1365        let manager = ExtGStateManager::new();
1366        assert_eq!(manager.count(), 0);
1367        assert!(manager.states().is_empty());
1368    }
1369
1370    #[test]
1371    fn test_extgstate_manager_add_state() {
1372        let mut manager = ExtGStateManager::new();
1373        let state = ExtGState::new().with_line_width(2.0);
1374
1375        let name = manager.add_state(state).unwrap();
1376        assert_eq!(name, "GS1");
1377        assert_eq!(manager.count(), 1);
1378
1379        let retrieved = manager.get_state(&name).unwrap();
1380        assert_eq!(retrieved.line_width, Some(2.0));
1381    }
1382
1383    #[test]
1384    fn test_extgstate_manager_empty_state_rejection() {
1385        let mut manager = ExtGStateManager::new();
1386        let empty_state = ExtGState::new();
1387
1388        let result = manager.add_state(empty_state);
1389        assert!(result.is_err());
1390        assert_eq!(manager.count(), 0);
1391    }
1392
1393    #[test]
1394    fn test_extgstate_manager_multiple_states() {
1395        let mut manager = ExtGStateManager::new();
1396
1397        let state1 = ExtGState::new().with_line_width(1.0);
1398        let state2 = ExtGState::new().with_alpha(0.5);
1399
1400        let name1 = manager.add_state(state1).unwrap();
1401        let name2 = manager.add_state(state2).unwrap();
1402
1403        assert_eq!(name1, "GS1");
1404        assert_eq!(name2, "GS2");
1405        assert_eq!(manager.count(), 2);
1406    }
1407
1408    #[test]
1409    fn test_extgstate_manager_resource_dictionary() {
1410        let mut manager = ExtGStateManager::new();
1411
1412        let state = ExtGState::new().with_line_width(2.0);
1413        manager.add_state(state).unwrap();
1414
1415        let dict = manager.to_resource_dictionary().unwrap();
1416        assert!(dict.contains("/ExtGState"));
1417        assert!(dict.contains("/GS1"));
1418        assert!(dict.contains("/LW 2.000"));
1419    }
1420
1421    #[test]
1422    fn test_extgstate_manager_clear() {
1423        let mut manager = ExtGStateManager::new();
1424
1425        let state = ExtGState::new().with_line_width(1.0);
1426        manager.add_state(state).unwrap();
1427        assert_eq!(manager.count(), 1);
1428
1429        manager.clear();
1430        assert_eq!(manager.count(), 0);
1431        assert!(manager.states().is_empty());
1432    }
1433
1434    #[test]
1435    fn test_extgstate_value_validation() {
1436        // Test line width validation (non-negative)
1437        let state = ExtGState::new().with_line_width(-1.0);
1438        assert_eq!(state.line_width, Some(0.0));
1439
1440        // Test miter limit validation (>= 1.0)
1441        let state = ExtGState::new().with_miter_limit(0.5);
1442        assert_eq!(state.miter_limit, Some(1.0));
1443
1444        // Test flatness validation (0-100)
1445        let state = ExtGState::new().with_flatness(150.0);
1446        assert_eq!(state.flatness, Some(100.0));
1447
1448        // Test smoothness validation (0-1)
1449        let state = ExtGState::new().with_smoothness(1.5);
1450        assert_eq!(state.smoothness, Some(1.0));
1451
1452        // Test font size validation (non-negative)
1453        let state = ExtGState::new().with_font(Font::Helvetica, -5.0);
1454        assert_eq!(state.font.unwrap().size, 0.0);
1455    }
1456
1457    #[test]
1458    fn test_line_dash_patterns() {
1459        let state = ExtGState::new().with_dash_pattern(LineDashPattern::dashed(10.0, 5.0));
1460
1461        let dict = state.to_pdf_dictionary().unwrap();
1462        assert!(dict.contains("/D [10.00 5.00] 0.00"));
1463    }
1464
1465    #[test]
1466    fn test_complex_extgstate() {
1467        let dash_pattern = LineDashPattern::new(vec![3.0, 2.0, 1.0, 2.0], 1.0);
1468
1469        let state = ExtGState::new()
1470            .with_line_width(1.5)
1471            .with_line_cap(LineCap::Square)
1472            .with_line_join(LineJoin::Round)
1473            .with_miter_limit(10.0)
1474            .with_dash_pattern(dash_pattern)
1475            .with_rendering_intent(RenderingIntent::Saturation)
1476            .with_overprint_stroke(true)
1477            .with_overprint_fill(false)
1478            .with_font(Font::TimesBold, 18.0)
1479            .with_flatness(0.5)
1480            .with_smoothness(0.1)
1481            .with_stroke_adjustment(false)
1482            .with_blend_mode(BlendMode::SoftLight)
1483            .with_alpha_stroke(0.8)
1484            .with_alpha_fill(0.6)
1485            .with_alpha_is_shape(true)
1486            .with_text_knockout(false);
1487
1488        assert!(!state.is_empty());
1489        assert!(state.uses_transparency());
1490
1491        let dict = state.to_pdf_dictionary().unwrap();
1492        assert!(dict.contains("/Type /ExtGState"));
1493        assert!(dict.contains("/LW 1.500"));
1494        assert!(dict.contains("/LC 2"));
1495        assert!(dict.contains("/LJ 1"));
1496        assert!(dict.contains("/ML 10.000"));
1497        assert!(dict.contains("/D [3.00 2.00 1.00 2.00] 1.00"));
1498        assert!(dict.contains("/RI /Saturation"));
1499        assert!(dict.contains("/OP true"));
1500        assert!(dict.contains("/op false"));
1501        assert!(dict.contains("/Font [/Times-Bold 18.000]"));
1502        assert!(dict.contains("/FL 0.500"));
1503        assert!(dict.contains("/SM 0.100"));
1504        assert!(dict.contains("/SA false"));
1505        assert!(dict.contains("/BM /SoftLight"));
1506        assert!(dict.contains("/CA 0.800"));
1507        assert!(dict.contains("/ca 0.600"));
1508        assert!(dict.contains("/AIS true"));
1509        assert!(dict.contains("/TK false"));
1510    }
1511
1512    #[test]
1513    fn test_transfer_function_identity() {
1514        let tf = TransferFunction::identity();
1515        assert_eq!(tf.to_pdf_string(), "/Identity");
1516    }
1517
1518    #[test]
1519    fn test_transfer_function_gamma() {
1520        let tf = TransferFunction::gamma(2.2);
1521        let pdf = tf.to_pdf_string();
1522        assert!(pdf.contains("/FunctionType 2"));
1523        assert!(pdf.contains("/N 2.200"));
1524        assert!(pdf.contains("/Domain [0.000 1.000]"));
1525        assert!(pdf.contains("/Range [0.000 1.000]"));
1526        assert!(pdf.contains("/C0 [0.000]"));
1527        assert!(pdf.contains("/C1 [1.000]"));
1528    }
1529
1530    #[test]
1531    fn test_transfer_function_linear() {
1532        let tf = TransferFunction::linear(0.8, 0.1);
1533        let pdf = tf.to_pdf_string();
1534        assert!(pdf.contains("/FunctionType 2"));
1535        assert!(pdf.contains("/N 1.000"));
1536        assert!(pdf.contains("/C0 [0.100]")); // intercept
1537        assert!(pdf.contains("/C1 [0.900]")); // slope + intercept
1538    }
1539
1540    #[test]
1541    fn test_extgstate_with_transfer_functions() {
1542        let state = ExtGState::new()
1543            .with_gamma_correction(1.8)
1544            .with_transfer_function_2(TransferFunction::identity())
1545            .with_black_generation(TransferFunction::linear(1.0, 0.0))
1546            .with_undercolor_removal(TransferFunction::gamma(2.2));
1547
1548        assert!(!state.is_empty());
1549
1550        let dict = state.to_pdf_dictionary().unwrap();
1551        assert!(dict.contains("/TR"));
1552        assert!(dict.contains("/TR2 /Identity"));
1553        assert!(dict.contains("/BG"));
1554        assert!(dict.contains("/UCR"));
1555        assert!(dict.contains("/N 1.800")); // gamma value for TR
1556        assert!(dict.contains("/N 2.200")); // gamma value for UCR
1557    }
1558
1559    #[test]
1560    fn test_transfer_function_separate() {
1561        let c_func = TransferFunctionData {
1562            function_type: 2,
1563            domain: vec![0.0, 1.0],
1564            range: vec![0.0, 1.0],
1565            params: TransferFunctionParams::Exponential {
1566                c0: vec![0.0],
1567                c1: vec![1.0],
1568                n: 1.5,
1569            },
1570        };
1571
1572        let m_func = c_func.clone();
1573        let y_func = c_func.clone();
1574        let k_func = Some(TransferFunctionData {
1575            function_type: 2,
1576            domain: vec![0.0, 1.0],
1577            range: vec![0.0, 1.0],
1578            params: TransferFunctionParams::Exponential {
1579                c0: vec![0.1],
1580                c1: vec![0.9],
1581                n: 2.0,
1582            },
1583        });
1584
1585        let tf = TransferFunction::Separate {
1586            c_or_r: c_func,
1587            m_or_g: m_func,
1588            y_or_b: y_func,
1589            k: k_func,
1590        };
1591
1592        let pdf = tf.to_pdf_string();
1593        assert!(pdf.starts_with('['));
1594        assert!(pdf.ends_with(']'));
1595        assert!(pdf.contains("/FunctionType 2"));
1596        // Should have 4 functions for CMYK
1597        assert_eq!(pdf.matches("/FunctionType 2").count(), 4);
1598    }
1599
1600    /// RED for Phase 5 of the v2.7.0 IR refactor: with the legacy
1601    /// `format!("{x:.2}")` emission, a non-finite component in the
1602    /// dash array (e.g. an attacker-controlled value bypassing the
1603    /// public constructors) propagates `NaN` / `inf` into a `d`
1604    /// operator, which is invalid per ISO 32000-1 §7.3.3. Once
1605    /// `to_pdf_string` routes through `finite_or_zero`, non-finite
1606    /// values are clamped to `0.00` and the assertion below passes.
1607    #[test]
1608    fn nan_dash_array_component_sanitised_at_emission() {
1609        let pattern = LineDashPattern {
1610            array: vec![5.0, f64::NAN, 3.0],
1611            phase: 0.0,
1612        };
1613        let s = pattern.to_pdf_string();
1614        assert!(
1615            !s.contains("NaN") && !s.contains("inf"),
1616            "non-finite tokens must not appear in `d` operator, got: {s:?}"
1617        );
1618        assert_eq!(
1619            s, "[5.00 0.00 3.00] 0.00",
1620            "NaN dash component must clamp to 0.00, got: {s:?}"
1621        );
1622    }
1623
1624    #[test]
1625    fn pos_inf_dash_phase_sanitised_at_emission() {
1626        let pattern = LineDashPattern {
1627            array: vec![5.0, 3.0],
1628            phase: f64::INFINITY,
1629        };
1630        let s = pattern.to_pdf_string();
1631        assert!(
1632            !s.contains("inf") && !s.contains("NaN"),
1633            "non-finite phase must not appear in `d` operator, got: {s:?}"
1634        );
1635        assert_eq!(
1636            s, "[5.00 3.00] 0.00",
1637            "+inf phase must clamp to 0.00, got: {s:?}"
1638        );
1639    }
1640}