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