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 crate::error::{PdfError, Result};
7use crate::graphics::{LineCap, LineJoin};
8use crate::text::Font;
9use std::collections::HashMap;
10use std::fmt::Write;
11
12/// Rendering intent values according to ISO 32000-1
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum RenderingIntent {
15    /// Absolute colorimetric
16    AbsoluteColorimetric,
17    /// Relative colorimetric
18    RelativeColorimetric,
19    /// Saturation
20    Saturation,
21    /// Perceptual
22    Perceptual,
23}
24
25impl RenderingIntent {
26    /// Get the PDF name for this rendering intent
27    pub fn pdf_name(&self) -> &'static str {
28        match self {
29            RenderingIntent::AbsoluteColorimetric => "AbsoluteColorimetric",
30            RenderingIntent::RelativeColorimetric => "RelativeColorimetric",
31            RenderingIntent::Saturation => "Saturation",
32            RenderingIntent::Perceptual => "Perceptual",
33        }
34    }
35}
36
37/// Blend mode values for transparency
38#[derive(Debug, Clone, PartialEq)]
39pub enum BlendMode {
40    /// Normal blend mode (default)
41    Normal,
42    /// Multiply blend mode
43    Multiply,
44    /// Screen blend mode
45    Screen,
46    /// Overlay blend mode
47    Overlay,
48    /// SoftLight blend mode
49    SoftLight,
50    /// HardLight blend mode
51    HardLight,
52    /// ColorDodge blend mode
53    ColorDodge,
54    /// ColorBurn blend mode
55    ColorBurn,
56    /// Darken blend mode
57    Darken,
58    /// Lighten blend mode
59    Lighten,
60    /// Difference blend mode
61    Difference,
62    /// Exclusion blend mode
63    Exclusion,
64    /// Hue blend mode (PDF 1.4)
65    Hue,
66    /// Saturation blend mode (PDF 1.4)
67    Saturation,
68    /// Color blend mode (PDF 1.4)
69    Color,
70    /// Luminosity blend mode (PDF 1.4)
71    Luminosity,
72}
73
74impl BlendMode {
75    /// Get the PDF name for this blend mode
76    pub fn pdf_name(&self) -> &'static str {
77        match self {
78            BlendMode::Normal => "Normal",
79            BlendMode::Multiply => "Multiply",
80            BlendMode::Screen => "Screen",
81            BlendMode::Overlay => "Overlay",
82            BlendMode::SoftLight => "SoftLight",
83            BlendMode::HardLight => "HardLight",
84            BlendMode::ColorDodge => "ColorDodge",
85            BlendMode::ColorBurn => "ColorBurn",
86            BlendMode::Darken => "Darken",
87            BlendMode::Lighten => "Lighten",
88            BlendMode::Difference => "Difference",
89            BlendMode::Exclusion => "Exclusion",
90            BlendMode::Hue => "Hue",
91            BlendMode::Saturation => "Saturation",
92            BlendMode::Color => "Color",
93            BlendMode::Luminosity => "Luminosity",
94        }
95    }
96}
97
98/// Line dash pattern specification
99#[derive(Debug, Clone, PartialEq)]
100pub struct LineDashPattern {
101    /// Array of dash and gap lengths
102    pub array: Vec<f64>,
103    /// Phase offset
104    pub phase: f64,
105}
106
107impl LineDashPattern {
108    /// Create a new line dash pattern
109    pub fn new(array: Vec<f64>, phase: f64) -> Self {
110        Self { array, phase }
111    }
112
113    /// Create a solid line (no dashes)
114    pub fn solid() -> Self {
115        Self {
116            array: Vec::new(),
117            phase: 0.0,
118        }
119    }
120
121    /// Create a simple dashed line
122    pub fn dashed(dash_length: f64, gap_length: f64) -> Self {
123        Self {
124            array: vec![dash_length, gap_length],
125            phase: 0.0,
126        }
127    }
128
129    /// Create a dotted line
130    pub fn dotted(dot_size: f64, gap_size: f64) -> Self {
131        Self {
132            array: vec![dot_size, gap_size],
133            phase: 0.0,
134        }
135    }
136
137    /// Generate PDF representation of the line dash pattern
138    pub fn to_pdf_string(&self) -> String {
139        if self.array.is_empty() {
140            "[] 0".to_string()
141        } else {
142            let array_str = self
143                .array
144                .iter()
145                .map(|&x| format!("{x:.2}"))
146                .collect::<Vec<_>>()
147                .join(" ");
148            format!("[{array_str}] {:.2}", self.phase)
149        }
150    }
151}
152
153/// Font specification for ExtGState
154#[derive(Debug, Clone, PartialEq)]
155pub struct ExtGStateFont {
156    /// Font
157    pub font: Font,
158    /// Font size
159    pub size: f64,
160}
161
162impl ExtGStateFont {
163    /// Create a new ExtGState font specification
164    pub fn new(font: Font, size: f64) -> Self {
165        Self { font, size }
166    }
167}
168
169/// Transfer function specification (simplified for basic implementation)
170#[derive(Debug, Clone, PartialEq)]
171pub enum TransferFunction {
172    /// Identity transfer function
173    Identity,
174    /// Custom transfer function (placeholder for advanced implementation)
175    Custom(String),
176}
177
178/// Halftone specification (simplified for basic implementation)
179#[derive(Debug, Clone, PartialEq)]
180pub enum Halftone {
181    /// Default halftone
182    Default,
183    /// Custom halftone (placeholder for advanced implementation)
184    Custom(String),
185}
186
187/// Soft mask specification for transparency
188#[derive(Debug, Clone, PartialEq)]
189pub enum SoftMask {
190    /// No soft mask
191    None,
192    /// Custom soft mask (placeholder for advanced implementation)
193    Custom(String),
194}
195
196/// Extended Graphics State Dictionary according to ISO 32000-1 Section 8.4
197#[derive(Debug, Clone)]
198pub struct ExtGState {
199    // Line parameters
200    /// Line width (LW)
201    pub line_width: Option<f64>,
202    /// Line cap style (LC)
203    pub line_cap: Option<LineCap>,
204    /// Line join style (LJ)
205    pub line_join: Option<LineJoin>,
206    /// Miter limit (ML)
207    pub miter_limit: Option<f64>,
208    /// Line dash pattern (D)
209    pub dash_pattern: Option<LineDashPattern>,
210
211    // Rendering intent
212    /// Rendering intent (RI)
213    pub rendering_intent: Option<RenderingIntent>,
214
215    // Overprint control
216    /// Overprint for stroking operations (OP)
217    pub overprint_stroke: Option<bool>,
218    /// Overprint for non-stroking operations (op)
219    pub overprint_fill: Option<bool>,
220    /// Overprint mode (OPM)
221    pub overprint_mode: Option<u8>,
222
223    // Font
224    /// Font and size (Font)
225    pub font: Option<ExtGStateFont>,
226
227    // Color functions (simplified for basic implementation)
228    /// Black generation function (BG)
229    pub black_generation: Option<TransferFunction>,
230    /// Black generation function alternative (BG2)
231    pub black_generation_2: Option<TransferFunction>,
232    /// Undercolor removal function (UCR)
233    pub undercolor_removal: Option<TransferFunction>,
234    /// Undercolor removal function alternative (UCR2)
235    pub undercolor_removal_2: Option<TransferFunction>,
236    /// Transfer function (TR)
237    pub transfer_function: Option<TransferFunction>,
238    /// Transfer function alternative (TR2)
239    pub transfer_function_2: Option<TransferFunction>,
240
241    // Halftone
242    /// Halftone dictionary (HT)
243    pub halftone: Option<Halftone>,
244
245    // Flatness and smoothness
246    /// Flatness tolerance (FL)
247    pub flatness: Option<f64>,
248    /// Smoothness tolerance (SM)
249    pub smoothness: Option<f64>,
250
251    // Additional parameters
252    /// Automatic stroke adjustment (SA)
253    pub stroke_adjustment: Option<bool>,
254
255    // Transparency parameters (PDF 1.4+)
256    /// Blend mode (BM)
257    pub blend_mode: Option<BlendMode>,
258    /// Soft mask (SMask)
259    pub soft_mask: Option<SoftMask>,
260    /// Alpha constant for stroking (CA)
261    pub alpha_stroke: Option<f64>,
262    /// Alpha constant for non-stroking (ca)
263    pub alpha_fill: Option<f64>,
264    /// Alpha source flag (AIS)
265    pub alpha_is_shape: Option<bool>,
266    /// Text knockout flag (TK)
267    pub text_knockout: Option<bool>,
268
269    // PDF 2.0 additions
270    /// Black point compensation (UseBlackPtComp)
271    pub use_black_point_compensation: Option<bool>,
272}
273
274impl Default for ExtGState {
275    fn default() -> Self {
276        Self::new()
277    }
278}
279
280impl ExtGState {
281    /// Create a new empty ExtGState dictionary
282    pub fn new() -> Self {
283        Self {
284            line_width: None,
285            line_cap: None,
286            line_join: None,
287            miter_limit: None,
288            dash_pattern: None,
289            rendering_intent: None,
290            overprint_stroke: None,
291            overprint_fill: None,
292            overprint_mode: None,
293            font: None,
294            black_generation: None,
295            black_generation_2: None,
296            undercolor_removal: None,
297            undercolor_removal_2: None,
298            transfer_function: None,
299            transfer_function_2: None,
300            halftone: None,
301            flatness: None,
302            smoothness: None,
303            stroke_adjustment: None,
304            blend_mode: None,
305            soft_mask: None,
306            alpha_stroke: None,
307            alpha_fill: None,
308            alpha_is_shape: None,
309            text_knockout: None,
310            use_black_point_compensation: None,
311        }
312    }
313
314    // Line parameter setters
315    /// Set line width
316    pub fn with_line_width(mut self, width: f64) -> Self {
317        self.line_width = Some(width.max(0.0));
318        self
319    }
320
321    /// Set line cap style
322    pub fn with_line_cap(mut self, cap: LineCap) -> Self {
323        self.line_cap = Some(cap);
324        self
325    }
326
327    /// Set line join style
328    pub fn with_line_join(mut self, join: LineJoin) -> Self {
329        self.line_join = Some(join);
330        self
331    }
332
333    /// Set miter limit
334    pub fn with_miter_limit(mut self, limit: f64) -> Self {
335        self.miter_limit = Some(limit.max(1.0));
336        self
337    }
338
339    /// Set line dash pattern
340    pub fn with_dash_pattern(mut self, pattern: LineDashPattern) -> Self {
341        self.dash_pattern = Some(pattern);
342        self
343    }
344
345    // Rendering intent setter
346    /// Set rendering intent
347    pub fn with_rendering_intent(mut self, intent: RenderingIntent) -> Self {
348        self.rendering_intent = Some(intent);
349        self
350    }
351
352    // Overprint setters
353    /// Set overprint for stroking operations
354    pub fn with_overprint_stroke(mut self, overprint: bool) -> Self {
355        self.overprint_stroke = Some(overprint);
356        self
357    }
358
359    /// Set overprint for non-stroking operations
360    pub fn with_overprint_fill(mut self, overprint: bool) -> Self {
361        self.overprint_fill = Some(overprint);
362        self
363    }
364
365    /// Set overprint mode
366    pub fn with_overprint_mode(mut self, mode: u8) -> Self {
367        self.overprint_mode = Some(mode);
368        self
369    }
370
371    // Font setter
372    /// Set font and size
373    pub fn with_font(mut self, font: Font, size: f64) -> Self {
374        self.font = Some(ExtGStateFont::new(font, size.max(0.0)));
375        self
376    }
377
378    // Flatness and smoothness setters
379    /// Set flatness tolerance
380    pub fn with_flatness(mut self, flatness: f64) -> Self {
381        self.flatness = Some(flatness.clamp(0.0, 100.0));
382        self
383    }
384
385    /// Set smoothness tolerance
386    pub fn with_smoothness(mut self, smoothness: f64) -> Self {
387        self.smoothness = Some(smoothness.clamp(0.0, 1.0));
388        self
389    }
390
391    /// Set automatic stroke adjustment
392    pub fn with_stroke_adjustment(mut self, adjustment: bool) -> Self {
393        self.stroke_adjustment = Some(adjustment);
394        self
395    }
396
397    // Transparency setters
398    /// Set blend mode
399    pub fn with_blend_mode(mut self, mode: BlendMode) -> Self {
400        self.blend_mode = Some(mode);
401        self
402    }
403
404    /// Set alpha constant for stroking operations
405    pub fn with_alpha_stroke(mut self, alpha: f64) -> Self {
406        self.alpha_stroke = Some(alpha.clamp(0.0, 1.0));
407        self
408    }
409
410    /// Set alpha constant for non-stroking operations
411    pub fn with_alpha_fill(mut self, alpha: f64) -> Self {
412        self.alpha_fill = Some(alpha.clamp(0.0, 1.0));
413        self
414    }
415
416    /// Set alpha constant for both stroking and non-stroking operations
417    pub fn with_alpha(mut self, alpha: f64) -> Self {
418        let clamped = alpha.clamp(0.0, 1.0);
419        self.alpha_stroke = Some(clamped);
420        self.alpha_fill = Some(clamped);
421        self
422    }
423
424    /// Set alpha source flag
425    pub fn with_alpha_is_shape(mut self, is_shape: bool) -> Self {
426        self.alpha_is_shape = Some(is_shape);
427        self
428    }
429
430    /// Set text knockout flag
431    pub fn with_text_knockout(mut self, knockout: bool) -> Self {
432        self.text_knockout = Some(knockout);
433        self
434    }
435
436    /// Set black point compensation (PDF 2.0)
437    pub fn with_black_point_compensation(mut self, use_compensation: bool) -> Self {
438        self.use_black_point_compensation = Some(use_compensation);
439        self
440    }
441
442    /// Check if any transparency parameters are set
443    pub fn uses_transparency(&self) -> bool {
444        self.alpha_stroke.is_some_and(|a| a < 1.0)
445            || self.alpha_fill.is_some_and(|a| a < 1.0)
446            || self.blend_mode.is_some()
447            || self.soft_mask.is_some()
448    }
449
450    /// Generate PDF dictionary representation
451    pub fn to_pdf_dictionary(&self) -> Result<String> {
452        let mut dict = String::from("<< /Type /ExtGState");
453
454        // Line parameters
455        if let Some(width) = self.line_width {
456            write!(&mut dict, " /LW {width:.3}").map_err(|_| {
457                PdfError::InvalidStructure("Failed to write line width".to_string())
458            })?;
459        }
460
461        if let Some(cap) = self.line_cap {
462            write!(&mut dict, " /LC {}", cap as u8)
463                .map_err(|_| PdfError::InvalidStructure("Failed to write line cap".to_string()))?;
464        }
465
466        if let Some(join) = self.line_join {
467            write!(&mut dict, " /LJ {}", join as u8)
468                .map_err(|_| PdfError::InvalidStructure("Failed to write line join".to_string()))?;
469        }
470
471        if let Some(limit) = self.miter_limit {
472            write!(&mut dict, " /ML {limit:.3}").map_err(|_| {
473                PdfError::InvalidStructure("Failed to write miter limit".to_string())
474            })?;
475        }
476
477        if let Some(ref pattern) = self.dash_pattern {
478            write!(&mut dict, " /D {}", pattern.to_pdf_string()).map_err(|_| {
479                PdfError::InvalidStructure("Failed to write dash pattern".to_string())
480            })?;
481        }
482
483        // Rendering intent
484        if let Some(intent) = self.rendering_intent {
485            write!(&mut dict, " /RI /{}", intent.pdf_name()).map_err(|_| {
486                PdfError::InvalidStructure("Failed to write rendering intent".to_string())
487            })?;
488        }
489
490        // Overprint control
491        if let Some(op) = self.overprint_stroke {
492            write!(&mut dict, " /OP {op}").map_err(|_| {
493                PdfError::InvalidStructure("Failed to write overprint stroke".to_string())
494            })?;
495        }
496
497        if let Some(op) = self.overprint_fill {
498            write!(&mut dict, " /op {op}").map_err(|_| {
499                PdfError::InvalidStructure("Failed to write overprint fill".to_string())
500            })?;
501        }
502
503        if let Some(mode) = self.overprint_mode {
504            write!(&mut dict, " /OPM {mode}").map_err(|_| {
505                PdfError::InvalidStructure("Failed to write overprint mode".to_string())
506            })?;
507        }
508
509        // Font
510        if let Some(ref font) = self.font {
511            write!(
512                &mut dict,
513                " /Font [/{} {:.3}]",
514                font.font.pdf_name(),
515                font.size
516            )
517            .map_err(|_| PdfError::InvalidStructure("Failed to write font".to_string()))?;
518        }
519
520        // Flatness and smoothness
521        if let Some(flatness) = self.flatness {
522            write!(&mut dict, " /FL {flatness:.3}")
523                .map_err(|_| PdfError::InvalidStructure("Failed to write flatness".to_string()))?;
524        }
525
526        if let Some(smoothness) = self.smoothness {
527            write!(&mut dict, " /SM {smoothness:.3}").map_err(|_| {
528                PdfError::InvalidStructure("Failed to write smoothness".to_string())
529            })?;
530        }
531
532        // Stroke adjustment
533        if let Some(sa) = self.stroke_adjustment {
534            write!(&mut dict, " /SA {sa}").map_err(|_| {
535                PdfError::InvalidStructure("Failed to write stroke adjustment".to_string())
536            })?;
537        }
538
539        // Transparency parameters
540        if let Some(ref mode) = self.blend_mode {
541            write!(&mut dict, " /BM /{}", mode.pdf_name()).map_err(|_| {
542                PdfError::InvalidStructure("Failed to write blend mode".to_string())
543            })?;
544        }
545
546        if let Some(alpha) = self.alpha_stroke {
547            write!(&mut dict, " /CA {alpha:.3}").map_err(|_| {
548                PdfError::InvalidStructure("Failed to write stroke alpha".to_string())
549            })?;
550        }
551
552        if let Some(alpha) = self.alpha_fill {
553            write!(&mut dict, " /ca {alpha:.3}").map_err(|_| {
554                PdfError::InvalidStructure("Failed to write fill alpha".to_string())
555            })?;
556        }
557
558        if let Some(ais) = self.alpha_is_shape {
559            write!(&mut dict, " /AIS {ais}").map_err(|_| {
560                PdfError::InvalidStructure("Failed to write alpha is shape".to_string())
561            })?;
562        }
563
564        if let Some(tk) = self.text_knockout {
565            write!(&mut dict, " /TK {tk}").map_err(|_| {
566                PdfError::InvalidStructure("Failed to write text knockout".to_string())
567            })?;
568        }
569
570        // PDF 2.0 parameters
571        if let Some(use_comp) = self.use_black_point_compensation {
572            write!(&mut dict, " /UseBlackPtComp {use_comp}").map_err(|_| {
573                PdfError::InvalidStructure("Failed to write black point compensation".to_string())
574            })?;
575        }
576
577        dict.push_str(" >>");
578        Ok(dict)
579    }
580
581    /// Check if the ExtGState is empty (no parameters set)
582    pub fn is_empty(&self) -> bool {
583        self.line_width.is_none()
584            && self.line_cap.is_none()
585            && self.line_join.is_none()
586            && self.miter_limit.is_none()
587            && self.dash_pattern.is_none()
588            && self.rendering_intent.is_none()
589            && self.overprint_stroke.is_none()
590            && self.overprint_fill.is_none()
591            && self.overprint_mode.is_none()
592            && self.font.is_none()
593            && self.flatness.is_none()
594            && self.smoothness.is_none()
595            && self.stroke_adjustment.is_none()
596            && self.blend_mode.is_none()
597            && self.soft_mask.is_none()
598            && self.alpha_stroke.is_none()
599            && self.alpha_fill.is_none()
600            && self.alpha_is_shape.is_none()
601            && self.text_knockout.is_none()
602            && self.use_black_point_compensation.is_none()
603    }
604}
605
606/// ExtGState manager for handling multiple graphics states
607#[derive(Debug, Clone)]
608pub struct ExtGStateManager {
609    states: HashMap<String, ExtGState>,
610    next_id: usize,
611}
612
613impl Default for ExtGStateManager {
614    fn default() -> Self {
615        Self::new()
616    }
617}
618
619impl ExtGStateManager {
620    /// Create a new ExtGState manager
621    pub fn new() -> Self {
622        Self {
623            states: HashMap::new(),
624            next_id: 1,
625        }
626    }
627
628    /// Add an ExtGState and return its name
629    pub fn add_state(&mut self, state: ExtGState) -> Result<String> {
630        if state.is_empty() {
631            return Err(PdfError::InvalidStructure(
632                "ExtGState cannot be empty".to_string(),
633            ));
634        }
635
636        let name = format!("GS{}", self.next_id);
637        self.states.insert(name.clone(), state);
638        self.next_id += 1;
639        Ok(name)
640    }
641
642    /// Get an ExtGState by name
643    pub fn get_state(&self, name: &str) -> Option<&ExtGState> {
644        self.states.get(name)
645    }
646
647    /// Get all states
648    pub fn states(&self) -> &HashMap<String, ExtGState> {
649        &self.states
650    }
651
652    /// Generate ExtGState resource dictionary
653    pub fn to_resource_dictionary(&self) -> Result<String> {
654        if self.states.is_empty() {
655            return Ok(String::new());
656        }
657
658        let mut dict = String::from("/ExtGState <<");
659
660        for (name, state) in &self.states {
661            let state_dict = state.to_pdf_dictionary()?;
662            write!(&mut dict, " /{name} {state_dict}").map_err(|_| {
663                PdfError::InvalidStructure("Failed to write ExtGState resource".to_string())
664            })?;
665        }
666
667        dict.push_str(" >>");
668        Ok(dict)
669    }
670
671    /// Clear all states
672    pub fn clear(&mut self) {
673        self.states.clear();
674        self.next_id = 1;
675    }
676
677    /// Count of registered states
678    pub fn count(&self) -> usize {
679        self.states.len()
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn test_rendering_intent_pdf_names() {
689        assert_eq!(
690            RenderingIntent::AbsoluteColorimetric.pdf_name(),
691            "AbsoluteColorimetric"
692        );
693        assert_eq!(
694            RenderingIntent::RelativeColorimetric.pdf_name(),
695            "RelativeColorimetric"
696        );
697        assert_eq!(RenderingIntent::Saturation.pdf_name(), "Saturation");
698        assert_eq!(RenderingIntent::Perceptual.pdf_name(), "Perceptual");
699    }
700
701    #[test]
702    fn test_blend_mode_pdf_names() {
703        assert_eq!(BlendMode::Normal.pdf_name(), "Normal");
704        assert_eq!(BlendMode::Multiply.pdf_name(), "Multiply");
705        assert_eq!(BlendMode::Screen.pdf_name(), "Screen");
706        assert_eq!(BlendMode::Overlay.pdf_name(), "Overlay");
707    }
708
709    #[test]
710    fn test_line_dash_pattern_creation() {
711        let solid = LineDashPattern::solid();
712        assert!(solid.array.is_empty());
713        assert_eq!(solid.phase, 0.0);
714
715        let dashed = LineDashPattern::dashed(5.0, 3.0);
716        assert_eq!(dashed.array, vec![5.0, 3.0]);
717        assert_eq!(dashed.phase, 0.0);
718
719        let dotted = LineDashPattern::dotted(1.0, 2.0);
720        assert_eq!(dotted.array, vec![1.0, 2.0]);
721    }
722
723    #[test]
724    fn test_line_dash_pattern_pdf_string() {
725        let solid = LineDashPattern::solid();
726        assert_eq!(solid.to_pdf_string(), "[] 0");
727
728        let dashed = LineDashPattern::dashed(5.0, 3.0);
729        assert_eq!(dashed.to_pdf_string(), "[5.00 3.00] 0.00");
730
731        let custom = LineDashPattern::new(vec![10.0, 5.0, 2.0, 5.0], 2.5);
732        assert_eq!(custom.to_pdf_string(), "[10.00 5.00 2.00 5.00] 2.50");
733    }
734
735    #[test]
736    fn test_extgstate_font() {
737        let font = ExtGStateFont::new(Font::Helvetica, 12.0);
738        assert_eq!(font.font, Font::Helvetica);
739        assert_eq!(font.size, 12.0);
740    }
741
742    #[test]
743    fn test_extgstate_creation() {
744        let state = ExtGState::new();
745        assert!(state.is_empty());
746        assert!(!state.uses_transparency());
747    }
748
749    #[test]
750    fn test_extgstate_line_parameters() {
751        let state = ExtGState::new()
752            .with_line_width(2.5)
753            .with_line_cap(LineCap::Round)
754            .with_line_join(LineJoin::Bevel)
755            .with_miter_limit(4.0);
756
757        assert_eq!(state.line_width, Some(2.5));
758        assert_eq!(state.line_cap, Some(LineCap::Round));
759        assert_eq!(state.line_join, Some(LineJoin::Bevel));
760        assert_eq!(state.miter_limit, Some(4.0));
761        assert!(!state.is_empty());
762    }
763
764    #[test]
765    fn test_extgstate_transparency() {
766        let state = ExtGState::new()
767            .with_alpha_stroke(0.8)
768            .with_alpha_fill(0.6)
769            .with_blend_mode(BlendMode::Multiply);
770
771        assert_eq!(state.alpha_stroke, Some(0.8));
772        assert_eq!(state.alpha_fill, Some(0.6));
773        assert_eq!(state.blend_mode, Some(BlendMode::Multiply));
774        assert!(state.uses_transparency());
775    }
776
777    #[test]
778    fn test_extgstate_alpha_clamping() {
779        let state = ExtGState::new()
780            .with_alpha_stroke(1.5) // Should clamp to 1.0
781            .with_alpha_fill(-0.1); // Should clamp to 0.0
782
783        assert_eq!(state.alpha_stroke, Some(1.0));
784        assert_eq!(state.alpha_fill, Some(0.0));
785    }
786
787    #[test]
788    fn test_extgstate_combined_alpha() {
789        let state = ExtGState::new().with_alpha(0.5);
790
791        assert_eq!(state.alpha_stroke, Some(0.5));
792        assert_eq!(state.alpha_fill, Some(0.5));
793    }
794
795    #[test]
796    fn test_extgstate_rendering_intent() {
797        let state = ExtGState::new().with_rendering_intent(RenderingIntent::Perceptual);
798
799        assert_eq!(state.rendering_intent, Some(RenderingIntent::Perceptual));
800    }
801
802    #[test]
803    fn test_extgstate_overprint() {
804        let state = ExtGState::new()
805            .with_overprint_stroke(true)
806            .with_overprint_fill(false)
807            .with_overprint_mode(1);
808
809        assert_eq!(state.overprint_stroke, Some(true));
810        assert_eq!(state.overprint_fill, Some(false));
811        assert_eq!(state.overprint_mode, Some(1));
812    }
813
814    #[test]
815    fn test_extgstate_font_setting() {
816        let state = ExtGState::new().with_font(Font::HelveticaBold, 14.0);
817
818        assert!(state.font.is_some());
819        let font = state.font.unwrap();
820        assert_eq!(font.font, Font::HelveticaBold);
821        assert_eq!(font.size, 14.0);
822    }
823
824    #[test]
825    fn test_extgstate_tolerance_parameters() {
826        let state = ExtGState::new()
827            .with_flatness(1.5)
828            .with_smoothness(0.8)
829            .with_stroke_adjustment(true);
830
831        assert_eq!(state.flatness, Some(1.5));
832        assert_eq!(state.smoothness, Some(0.8));
833        assert_eq!(state.stroke_adjustment, Some(true));
834    }
835
836    #[test]
837    fn test_extgstate_pdf_dictionary_generation() {
838        let state = ExtGState::new()
839            .with_line_width(2.0)
840            .with_line_cap(LineCap::Round)
841            .with_alpha(0.5)
842            .with_blend_mode(BlendMode::Multiply);
843
844        let dict = state.to_pdf_dictionary().unwrap();
845        assert!(dict.contains("/Type /ExtGState"));
846        assert!(dict.contains("/LW 2.000"));
847        assert!(dict.contains("/LC 1"));
848        assert!(dict.contains("/CA 0.500"));
849        assert!(dict.contains("/ca 0.500"));
850        assert!(dict.contains("/BM /Multiply"));
851    }
852
853    #[test]
854    fn test_extgstate_manager_creation() {
855        let manager = ExtGStateManager::new();
856        assert_eq!(manager.count(), 0);
857        assert!(manager.states().is_empty());
858    }
859
860    #[test]
861    fn test_extgstate_manager_add_state() {
862        let mut manager = ExtGStateManager::new();
863        let state = ExtGState::new().with_line_width(2.0);
864
865        let name = manager.add_state(state).unwrap();
866        assert_eq!(name, "GS1");
867        assert_eq!(manager.count(), 1);
868
869        let retrieved = manager.get_state(&name).unwrap();
870        assert_eq!(retrieved.line_width, Some(2.0));
871    }
872
873    #[test]
874    fn test_extgstate_manager_empty_state_rejection() {
875        let mut manager = ExtGStateManager::new();
876        let empty_state = ExtGState::new();
877
878        let result = manager.add_state(empty_state);
879        assert!(result.is_err());
880        assert_eq!(manager.count(), 0);
881    }
882
883    #[test]
884    fn test_extgstate_manager_multiple_states() {
885        let mut manager = ExtGStateManager::new();
886
887        let state1 = ExtGState::new().with_line_width(1.0);
888        let state2 = ExtGState::new().with_alpha(0.5);
889
890        let name1 = manager.add_state(state1).unwrap();
891        let name2 = manager.add_state(state2).unwrap();
892
893        assert_eq!(name1, "GS1");
894        assert_eq!(name2, "GS2");
895        assert_eq!(manager.count(), 2);
896    }
897
898    #[test]
899    fn test_extgstate_manager_resource_dictionary() {
900        let mut manager = ExtGStateManager::new();
901
902        let state = ExtGState::new().with_line_width(2.0);
903        manager.add_state(state).unwrap();
904
905        let dict = manager.to_resource_dictionary().unwrap();
906        assert!(dict.contains("/ExtGState"));
907        assert!(dict.contains("/GS1"));
908        assert!(dict.contains("/LW 2.000"));
909    }
910
911    #[test]
912    fn test_extgstate_manager_clear() {
913        let mut manager = ExtGStateManager::new();
914
915        let state = ExtGState::new().with_line_width(1.0);
916        manager.add_state(state).unwrap();
917        assert_eq!(manager.count(), 1);
918
919        manager.clear();
920        assert_eq!(manager.count(), 0);
921        assert!(manager.states().is_empty());
922    }
923
924    #[test]
925    fn test_extgstate_value_validation() {
926        // Test line width validation (non-negative)
927        let state = ExtGState::new().with_line_width(-1.0);
928        assert_eq!(state.line_width, Some(0.0));
929
930        // Test miter limit validation (>= 1.0)
931        let state = ExtGState::new().with_miter_limit(0.5);
932        assert_eq!(state.miter_limit, Some(1.0));
933
934        // Test flatness validation (0-100)
935        let state = ExtGState::new().with_flatness(150.0);
936        assert_eq!(state.flatness, Some(100.0));
937
938        // Test smoothness validation (0-1)
939        let state = ExtGState::new().with_smoothness(1.5);
940        assert_eq!(state.smoothness, Some(1.0));
941
942        // Test font size validation (non-negative)
943        let state = ExtGState::new().with_font(Font::Helvetica, -5.0);
944        assert_eq!(state.font.unwrap().size, 0.0);
945    }
946
947    #[test]
948    fn test_line_dash_patterns() {
949        let state = ExtGState::new().with_dash_pattern(LineDashPattern::dashed(10.0, 5.0));
950
951        let dict = state.to_pdf_dictionary().unwrap();
952        assert!(dict.contains("/D [10.00 5.00] 0.00"));
953    }
954
955    #[test]
956    fn test_complex_extgstate() {
957        let dash_pattern = LineDashPattern::new(vec![3.0, 2.0, 1.0, 2.0], 1.0);
958
959        let state = ExtGState::new()
960            .with_line_width(1.5)
961            .with_line_cap(LineCap::Square)
962            .with_line_join(LineJoin::Round)
963            .with_miter_limit(10.0)
964            .with_dash_pattern(dash_pattern)
965            .with_rendering_intent(RenderingIntent::Saturation)
966            .with_overprint_stroke(true)
967            .with_overprint_fill(false)
968            .with_font(Font::TimesBold, 18.0)
969            .with_flatness(0.5)
970            .with_smoothness(0.1)
971            .with_stroke_adjustment(false)
972            .with_blend_mode(BlendMode::SoftLight)
973            .with_alpha_stroke(0.8)
974            .with_alpha_fill(0.6)
975            .with_alpha_is_shape(true)
976            .with_text_knockout(false);
977
978        assert!(!state.is_empty());
979        assert!(state.uses_transparency());
980
981        let dict = state.to_pdf_dictionary().unwrap();
982        assert!(dict.contains("/Type /ExtGState"));
983        assert!(dict.contains("/LW 1.500"));
984        assert!(dict.contains("/LC 2"));
985        assert!(dict.contains("/LJ 1"));
986        assert!(dict.contains("/ML 10.000"));
987        assert!(dict.contains("/D [3.00 2.00 1.00 2.00] 1.00"));
988        assert!(dict.contains("/RI /Saturation"));
989        assert!(dict.contains("/OP true"));
990        assert!(dict.contains("/op false"));
991        assert!(dict.contains("/Font [/Times-Bold 18.000]"));
992        assert!(dict.contains("/FL 0.500"));
993        assert!(dict.contains("/SM 0.100"));
994        assert!(dict.contains("/SA false"));
995        assert!(dict.contains("/BM /SoftLight"));
996        assert!(dict.contains("/CA 0.800"));
997        assert!(dict.contains("/ca 0.600"));
998        assert!(dict.contains("/AIS true"));
999        assert!(dict.contains("/TK false"));
1000    }
1001}