Skip to main content

oxidize_pdf/graphics/
soft_mask.rs

1//! Soft Mask support for PDF graphics according to ISO 32000-1 Section 11.6
2//!
3//! Soft masks allow specifying transparency using luminosity or alpha values
4//! from another graphics object, enabling complex transparency effects.
5
6use crate::error::Result;
7use crate::objects::{Dictionary, Object};
8use std::fmt;
9
10/// Soft mask type according to ISO 32000-1
11#[derive(Debug, Clone, PartialEq)]
12pub enum SoftMaskType {
13    /// Alpha channel soft mask
14    Alpha,
15    /// Luminosity soft mask
16    Luminosity,
17}
18
19impl SoftMaskType {
20    /// Get PDF name for this soft mask type
21    pub fn pdf_name(&self) -> &'static str {
22        match self {
23            SoftMaskType::Alpha => "Alpha",
24            SoftMaskType::Luminosity => "Luminosity",
25        }
26    }
27}
28
29/// Transfer function for soft mask
30#[derive(Debug, Clone)]
31pub enum TransferFunction {
32    /// Identity function (no transformation)
33    Identity,
34    /// Custom function name
35    Custom(String),
36    /// Function array for complex transformations
37    FunctionArray(Vec<f64>),
38}
39
40impl TransferFunction {
41    /// Convert to PDF representation
42    pub fn to_pdf_object(&self) -> Object {
43        match self {
44            TransferFunction::Identity => Object::Name("Identity".to_string()),
45            TransferFunction::Custom(name) => Object::Name(name.clone()),
46            TransferFunction::FunctionArray(values) => {
47                Object::Array(values.iter().map(|&v| Object::Real(v)).collect())
48            }
49        }
50    }
51}
52
53/// Soft mask configuration
54#[derive(Debug, Clone)]
55pub struct SoftMask {
56    /// Type of soft mask (Alpha or Luminosity)
57    pub mask_type: SoftMaskType,
58
59    /// Reference to the transparency group XObject
60    pub group_ref: Option<String>,
61
62    /// Background color to use with the soft mask
63    pub background_color: Option<Vec<f64>>,
64
65    /// Transfer function for the soft mask
66    pub transfer_function: Option<TransferFunction>,
67
68    /// Bounding box for the soft mask effect
69    pub bbox: Option<[f64; 4]>,
70}
71
72impl Default for SoftMask {
73    fn default() -> Self {
74        Self::none()
75    }
76}
77
78impl SoftMask {
79    /// Create a soft mask that disables masking (None)
80    pub fn none() -> Self {
81        Self {
82            mask_type: SoftMaskType::Alpha,
83            group_ref: None,
84            background_color: None,
85            transfer_function: None,
86            bbox: None,
87        }
88    }
89
90    /// Create an alpha soft mask
91    pub fn alpha(group_ref: String) -> Self {
92        Self {
93            mask_type: SoftMaskType::Alpha,
94            group_ref: Some(group_ref),
95            background_color: None,
96            transfer_function: None,
97            bbox: None,
98        }
99    }
100
101    /// Create a luminosity soft mask
102    pub fn luminosity(group_ref: String) -> Self {
103        Self {
104            mask_type: SoftMaskType::Luminosity,
105            group_ref: Some(group_ref),
106            background_color: None,
107            transfer_function: None,
108            bbox: None,
109        }
110    }
111
112    /// Set background color for the soft mask
113    pub fn with_background_color(mut self, color: Vec<f64>) -> Self {
114        self.background_color = Some(color);
115        self
116    }
117
118    /// Set transfer function for the soft mask
119    pub fn with_transfer_function(mut self, func: TransferFunction) -> Self {
120        self.transfer_function = Some(func);
121        self
122    }
123
124    /// Set bounding box for the soft mask
125    pub fn with_bbox(mut self, bbox: [f64; 4]) -> Self {
126        self.bbox = Some(bbox);
127        self
128    }
129
130    /// Check if this is a "None" soft mask (disables masking)
131    pub fn is_none(&self) -> bool {
132        self.group_ref.is_none()
133    }
134
135    /// Convert to PDF dictionary representation
136    pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
137        // If no group reference, return "None" soft mask
138        if self.group_ref.is_none() {
139            let mut dict = Dictionary::new();
140            dict.set("Type", Object::Name("Mask".to_string()));
141            dict.set("S", Object::Name("None".to_string()));
142            return Ok(dict);
143        }
144
145        let mut dict = Dictionary::new();
146        dict.set("Type", Object::Name("Mask".to_string()));
147        dict.set("S", Object::Name(self.mask_type.pdf_name().to_string()));
148
149        // Reference to transparency group (would be an indirect reference in real PDF)
150        if let Some(ref group_ref) = self.group_ref {
151            dict.set("G", Object::Name(group_ref.clone()));
152        }
153
154        // Background color array
155        if let Some(ref bc) = self.background_color {
156            let color_array: Vec<Object> = bc.iter().map(|&c| Object::Real(c)).collect();
157            dict.set("BC", Object::Array(color_array));
158        }
159
160        // Transfer function
161        if let Some(ref tr) = self.transfer_function {
162            dict.set("TR", tr.to_pdf_object());
163        }
164
165        // Bounding box
166        if let Some(bbox) = self.bbox {
167            let bbox_array = vec![
168                Object::Real(bbox[0]),
169                Object::Real(bbox[1]),
170                Object::Real(bbox[2]),
171                Object::Real(bbox[3]),
172            ];
173            dict.set("BBox", Object::Array(bbox_array));
174        }
175
176        Ok(dict)
177    }
178
179    /// Convert to PDF string representation for inline use
180    pub fn to_pdf_string(&self) -> String {
181        if self.is_none() {
182            "/None".to_string()
183        } else {
184            // In real implementation, this would reference an indirect object
185            format!("/SM{}", self.group_ref.as_ref().unwrap_or(&"1".to_string()))
186        }
187    }
188}
189
190impl fmt::Display for SoftMask {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        if self.is_none() {
193            write!(f, "SoftMask::None")
194        } else {
195            write!(f, "SoftMask::{:?}", self.mask_type)
196        }
197    }
198}
199
200/// Soft mask state for graphics context
201#[derive(Debug, Clone)]
202pub struct SoftMaskState {
203    /// Current soft mask
204    pub mask: SoftMask,
205
206    /// Stack of saved soft masks for nested operations
207    pub saved_masks: Vec<SoftMask>,
208}
209
210impl Default for SoftMaskState {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl SoftMaskState {
217    /// Create new soft mask state
218    pub fn new() -> Self {
219        Self {
220            mask: SoftMask::none(),
221            saved_masks: Vec::new(),
222        }
223    }
224
225    /// Set current soft mask
226    pub fn set_mask(&mut self, mask: SoftMask) {
227        self.mask = mask;
228    }
229
230    /// Push current mask to stack and set new mask
231    pub fn push_mask(&mut self, mask: SoftMask) {
232        self.saved_masks.push(self.mask.clone());
233        self.mask = mask;
234    }
235
236    /// Pop mask from stack and restore it
237    pub fn pop_mask(&mut self) -> Option<SoftMask> {
238        if let Some(mask) = self.saved_masks.pop() {
239            let current = self.mask.clone();
240            self.mask = mask;
241            Some(current)
242        } else {
243            None
244        }
245    }
246
247    /// Clear all masks (reset to None)
248    pub fn clear(&mut self) {
249        self.mask = SoftMask::none();
250        self.saved_masks.clear();
251    }
252
253    /// Check if any soft mask is active
254    pub fn is_active(&self) -> bool {
255        !self.mask.is_none()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_soft_mask_none() {
265        let mask = SoftMask::none();
266        assert!(mask.is_none());
267        assert_eq!(mask.to_pdf_string(), "/None");
268    }
269
270    #[test]
271    fn test_soft_mask_alpha() {
272        let mask = SoftMask::alpha("Group1".to_string());
273        assert!(!mask.is_none());
274        assert_eq!(mask.mask_type, SoftMaskType::Alpha);
275        assert_eq!(mask.group_ref, Some("Group1".to_string()));
276    }
277
278    #[test]
279    fn test_soft_mask_luminosity() {
280        let mask = SoftMask::luminosity("Group2".to_string());
281        assert!(!mask.is_none());
282        assert_eq!(mask.mask_type, SoftMaskType::Luminosity);
283        assert_eq!(mask.group_ref, Some("Group2".to_string()));
284    }
285
286    #[test]
287    fn test_soft_mask_with_background() {
288        let mask = SoftMask::alpha("Group1".to_string()).with_background_color(vec![1.0, 1.0, 1.0]);
289
290        assert_eq!(mask.background_color, Some(vec![1.0, 1.0, 1.0]));
291    }
292
293    #[test]
294    fn test_soft_mask_with_transfer_function() {
295        let mask = SoftMask::alpha("Group1".to_string())
296            .with_transfer_function(TransferFunction::Identity);
297
298        assert!(mask.transfer_function.is_some());
299    }
300
301    #[test]
302    fn test_soft_mask_with_bbox() {
303        let mask = SoftMask::alpha("Group1".to_string()).with_bbox([0.0, 0.0, 100.0, 100.0]);
304
305        assert_eq!(mask.bbox, Some([0.0, 0.0, 100.0, 100.0]));
306    }
307
308    #[test]
309    fn test_soft_mask_to_dictionary() {
310        let mask = SoftMask::luminosity("Group1".to_string())
311            .with_background_color(vec![0.5, 0.5, 0.5])
312            .with_transfer_function(TransferFunction::Identity)
313            .with_bbox([0.0, 0.0, 200.0, 200.0]);
314
315        let dict = mask.to_pdf_dictionary().unwrap();
316
317        assert_eq!(dict.get("Type"), Some(&Object::Name("Mask".to_string())));
318        assert_eq!(dict.get("S"), Some(&Object::Name("Luminosity".to_string())));
319        assert!(dict.contains_key("G"));
320        assert!(dict.contains_key("BC"));
321        assert!(dict.contains_key("TR"));
322        assert!(dict.contains_key("BBox"));
323    }
324
325    #[test]
326    fn test_soft_mask_none_dictionary() {
327        let mask = SoftMask::none();
328        let dict = mask.to_pdf_dictionary().unwrap();
329
330        assert_eq!(dict.get("Type"), Some(&Object::Name("Mask".to_string())));
331        assert_eq!(dict.get("S"), Some(&Object::Name("None".to_string())));
332    }
333
334    #[test]
335    fn test_soft_mask_state() {
336        let mut state = SoftMaskState::new();
337        assert!(state.mask.is_none());
338
339        let mask1 = SoftMask::alpha("Group1".to_string());
340        state.set_mask(mask1);
341        assert!(!state.mask.is_none());
342
343        let mask2 = SoftMask::luminosity("Group2".to_string());
344        state.push_mask(mask2);
345        assert_eq!(state.saved_masks.len(), 1);
346
347        let popped = state.pop_mask();
348        assert!(popped.is_some());
349        assert_eq!(state.mask.group_ref, Some("Group1".to_string()));
350    }
351
352    #[test]
353    fn test_transfer_function_to_pdf() {
354        let identity = TransferFunction::Identity;
355        assert_eq!(
356            identity.to_pdf_object(),
357            Object::Name("Identity".to_string())
358        );
359
360        let custom = TransferFunction::Custom("Custom1".to_string());
361        assert_eq!(custom.to_pdf_object(), Object::Name("Custom1".to_string()));
362
363        let array = TransferFunction::FunctionArray(vec![0.0, 0.5, 1.0]);
364        if let Object::Array(arr) = array.to_pdf_object() {
365            assert_eq!(arr.len(), 3);
366        } else {
367            panic!("Expected array");
368        }
369    }
370}