oxidize_pdf/graphics/
transparency.rs

1//! Transparency Groups implementation for ISO 32000-1:2008 Section 11.4
2//!
3//! Transparency groups allow applying transparency effects to a collection
4//! of objects as a single unit rather than individually.
5
6use super::state::BlendMode;
7use crate::objects::{Dictionary, Object};
8
9/// Transparency group attributes according to ISO 32000-1:2008 Section 11.4.5
10#[derive(Debug, Clone)]
11pub struct TransparencyGroup {
12    /// Whether the group is isolated from its backdrop
13    /// When true, the group is composited against a fully transparent backdrop
14    /// When false, the group inherits the backdrop from its parent
15    pub isolated: bool,
16
17    /// Whether the group is a knockout group
18    /// When true, objects within the group knock out (replace) earlier objects
19    /// When false, objects composite normally with each other
20    pub knockout: bool,
21
22    /// The blend mode to apply to the entire group
23    pub blend_mode: BlendMode,
24
25    /// The opacity to apply to the entire group (0.0 = transparent, 1.0 = opaque)
26    pub opacity: f32,
27
28    /// Optional color space for the group
29    pub color_space: Option<String>,
30}
31
32impl Default for TransparencyGroup {
33    fn default() -> Self {
34        Self {
35            isolated: false,
36            knockout: false,
37            blend_mode: BlendMode::Normal,
38            opacity: 1.0,
39            color_space: None,
40        }
41    }
42}
43
44impl TransparencyGroup {
45    /// Create a new transparency group with default settings
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Create an isolated transparency group
51    pub fn isolated() -> Self {
52        Self {
53            isolated: true,
54            ..Default::default()
55        }
56    }
57
58    /// Create a knockout transparency group
59    pub fn knockout() -> Self {
60        Self {
61            knockout: true,
62            ..Default::default()
63        }
64    }
65
66    /// Set whether the group is isolated
67    pub fn with_isolated(mut self, isolated: bool) -> Self {
68        self.isolated = isolated;
69        self
70    }
71
72    /// Set whether the group is a knockout group
73    pub fn with_knockout(mut self, knockout: bool) -> Self {
74        self.knockout = knockout;
75        self
76    }
77
78    /// Set the blend mode for the group
79    pub fn with_blend_mode(mut self, blend_mode: BlendMode) -> Self {
80        self.blend_mode = blend_mode;
81        self
82    }
83
84    /// Set the opacity for the group
85    pub fn with_opacity(mut self, opacity: f32) -> Self {
86        self.opacity = opacity.clamp(0.0, 1.0);
87        self
88    }
89
90    /// Set the color space for the group
91    pub fn with_color_space(mut self, color_space: impl Into<String>) -> Self {
92        self.color_space = Some(color_space.into());
93        self
94    }
95
96    /// Convert to PDF dictionary representation
97    pub fn to_dict(&self) -> Dictionary {
98        let mut dict = Dictionary::new();
99
100        // Required entries
101        dict.set("Type", Object::Name("Group".into()));
102        dict.set("S", Object::Name("Transparency".into()));
103
104        // Optional entries
105        if self.isolated {
106            dict.set("I", Object::Boolean(true));
107        }
108
109        if self.knockout {
110            dict.set("K", Object::Boolean(true));
111        }
112
113        if let Some(ref cs) = self.color_space {
114            dict.set("CS", Object::Name(cs.clone()));
115        }
116
117        dict
118    }
119}
120
121/// Stack entry for managing nested transparency groups
122#[derive(Debug, Clone)]
123pub(crate) struct TransparencyGroupState {
124    /// The transparency group configuration
125    pub group: TransparencyGroup,
126
127    /// Saved graphics state before entering the group
128    pub saved_state: Vec<u8>,
129
130    /// Content stream for the group
131    #[allow(dead_code)]
132    pub content: Vec<u8>,
133}
134
135impl TransparencyGroupState {
136    /// Create a new transparency group state
137    pub fn new(group: TransparencyGroup) -> Self {
138        Self {
139            group,
140            saved_state: Vec::new(),
141            content: Vec::new(),
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_transparency_group_creation() {
152        let group = TransparencyGroup::new();
153        assert!(!group.isolated);
154        assert!(!group.knockout);
155        assert_eq!(group.opacity, 1.0);
156        assert!(matches!(group.blend_mode, BlendMode::Normal));
157    }
158
159    #[test]
160    fn test_isolated_group() {
161        let group = TransparencyGroup::isolated();
162        assert!(group.isolated);
163        assert!(!group.knockout);
164    }
165
166    #[test]
167    fn test_knockout_group() {
168        let group = TransparencyGroup::knockout();
169        assert!(!group.isolated);
170        assert!(group.knockout);
171    }
172
173    #[test]
174    fn test_group_builder() {
175        let group = TransparencyGroup::new()
176            .with_isolated(true)
177            .with_knockout(true)
178            .with_blend_mode(BlendMode::Multiply)
179            .with_opacity(0.5)
180            .with_color_space("DeviceRGB");
181
182        assert!(group.isolated);
183        assert!(group.knockout);
184        assert_eq!(group.opacity, 0.5);
185        assert!(matches!(group.blend_mode, BlendMode::Multiply));
186        assert_eq!(group.color_space, Some("DeviceRGB".to_string()));
187    }
188
189    #[test]
190    fn test_opacity_clamping() {
191        let group1 = TransparencyGroup::new().with_opacity(1.5);
192        assert_eq!(group1.opacity, 1.0);
193
194        let group2 = TransparencyGroup::new().with_opacity(-0.5);
195        assert_eq!(group2.opacity, 0.0);
196    }
197
198    #[test]
199    fn test_to_dict() {
200        let group = TransparencyGroup::new()
201            .with_isolated(true)
202            .with_knockout(true)
203            .with_color_space("DeviceCMYK");
204
205        let dict = group.to_dict();
206
207        // Check required entries
208        assert_eq!(dict.get("Type"), Some(&Object::Name("Group".into())));
209        assert_eq!(dict.get("S"), Some(&Object::Name("Transparency".into())));
210
211        // Check optional entries
212        assert_eq!(dict.get("I"), Some(&Object::Boolean(true)));
213        assert_eq!(dict.get("K"), Some(&Object::Boolean(true)));
214        assert_eq!(dict.get("CS"), Some(&Object::Name("DeviceCMYK".into())));
215    }
216
217    #[test]
218    fn test_default_dict() {
219        let group = TransparencyGroup::new();
220        let dict = group.to_dict();
221
222        // Should only have required entries for default group
223        assert_eq!(dict.get("Type"), Some(&Object::Name("Group".into())));
224        assert_eq!(dict.get("S"), Some(&Object::Name("Transparency".into())));
225        assert!(dict.get("I").is_none());
226        assert!(dict.get("K").is_none());
227        assert!(dict.get("CS").is_none());
228    }
229}