Skip to main content

oxidize_pdf/graphics/
separation_color.rs

1//! Separation color space for spot colors and custom inks
2//!
3//! Implements ISO 32000-1 Section 8.6.6.4 (Separation Color Spaces)
4//! Separation color spaces provide support for the use of additional colorants
5//! or for isolating the control of individual color components.
6
7use crate::graphics::Color;
8use crate::objects::{Dictionary, Object};
9
10/// Separation color space for spot colors
11#[derive(Debug, Clone)]
12pub struct SeparationColorSpace {
13    /// Name of the colorant (e.g., "PANTONE 185 C", "Gold", "Silver")
14    pub colorant_name: String,
15    /// Alternate color space (usually DeviceRGB or DeviceCMYK)
16    pub alternate_space: AlternateColorSpace,
17    /// Tint transformation function
18    pub tint_transform: TintTransform,
19}
20
21/// Alternate color space for separation
22#[derive(Debug, Clone)]
23pub enum AlternateColorSpace {
24    /// DeviceGray alternate
25    DeviceGray,
26    /// DeviceRGB alternate
27    DeviceRGB,
28    /// DeviceCMYK alternate
29    DeviceCMYK,
30    /// Lab alternate
31    Lab {
32        white_point: [f64; 3],
33        black_point: [f64; 3],
34        range: [f64; 4],
35    },
36}
37
38impl AlternateColorSpace {
39    /// Convert to PDF name or array
40    pub fn to_pdf_object(&self) -> Object {
41        match self {
42            AlternateColorSpace::DeviceGray => Object::Name("DeviceGray".to_string()),
43            AlternateColorSpace::DeviceRGB => Object::Name("DeviceRGB".to_string()),
44            AlternateColorSpace::DeviceCMYK => Object::Name("DeviceCMYK".to_string()),
45            AlternateColorSpace::Lab {
46                white_point,
47                black_point,
48                range,
49            } => {
50                let mut dict = Dictionary::new();
51                dict.set(
52                    "WhitePoint",
53                    Object::Array(white_point.iter().map(|&v| Object::Real(v)).collect()),
54                );
55                dict.set(
56                    "BlackPoint",
57                    Object::Array(black_point.iter().map(|&v| Object::Real(v)).collect()),
58                );
59                dict.set(
60                    "Range",
61                    Object::Array(range.iter().map(|&v| Object::Real(v)).collect()),
62                );
63
64                Object::Array(vec![
65                    Object::Name("Lab".to_string()),
66                    Object::Dictionary(dict),
67                ])
68            }
69        }
70    }
71
72    /// Get number of components in alternate space
73    pub fn num_components(&self) -> usize {
74        match self {
75            AlternateColorSpace::DeviceGray => 1,
76            AlternateColorSpace::DeviceRGB => 3,
77            AlternateColorSpace::DeviceCMYK => 4,
78            AlternateColorSpace::Lab { .. } => 3,
79        }
80    }
81}
82
83/// Tint transformation function
84#[derive(Debug, Clone)]
85pub enum TintTransform {
86    /// Linear interpolation between min and max values
87    Linear {
88        min_values: Vec<f64>,
89        max_values: Vec<f64>,
90    },
91    /// Exponential function with gamma
92    Exponential {
93        gamma: f64,
94        min_values: Vec<f64>,
95        max_values: Vec<f64>,
96    },
97    /// Custom function (PostScript Type 4 function)
98    Custom {
99        domain: [f64; 2],
100        range: Vec<f64>,
101        function_type: u8,
102        function_data: Vec<u8>,
103    },
104    /// Sampled function (lookup table)
105    Sampled {
106        samples: Vec<Vec<f64>>,
107        domain: [f64; 2],
108        range: Vec<f64>,
109    },
110}
111
112impl TintTransform {
113    /// Create a linear tint transform
114    pub fn linear(min_values: Vec<f64>, max_values: Vec<f64>) -> Self {
115        TintTransform::Linear {
116            min_values,
117            max_values,
118        }
119    }
120
121    /// Create an exponential tint transform
122    pub fn exponential(gamma: f64, min_values: Vec<f64>, max_values: Vec<f64>) -> Self {
123        TintTransform::Exponential {
124            gamma,
125            min_values,
126            max_values,
127        }
128    }
129
130    /// Apply tint transformation
131    pub fn apply(&self, tint: f64) -> Vec<f64> {
132        let tint = tint.clamp(0.0, 1.0);
133
134        match self {
135            TintTransform::Linear {
136                min_values,
137                max_values,
138            } => min_values
139                .iter()
140                .zip(max_values.iter())
141                .map(|(&min, &max)| min + tint * (max - min))
142                .collect(),
143            TintTransform::Exponential {
144                gamma,
145                min_values,
146                max_values,
147            } => {
148                let t = tint.powf(*gamma);
149                min_values
150                    .iter()
151                    .zip(max_values.iter())
152                    .map(|(&min, &max)| min + t * (max - min))
153                    .collect()
154            }
155            TintTransform::Sampled { samples, .. } => {
156                // Simple linear interpolation in lookup table
157                if samples.is_empty() {
158                    return vec![];
159                }
160
161                let index = (tint * (samples.len() - 1) as f64) as usize;
162                let index = index.min(samples.len() - 1);
163                samples[index].clone()
164            }
165            TintTransform::Custom { .. } => {
166                // For custom functions, return a default
167                vec![tint]
168            }
169        }
170    }
171
172    /// Convert to PDF function dictionary
173    pub fn to_pdf_dict(&self) -> Dictionary {
174        let mut dict = Dictionary::new();
175
176        match self {
177            TintTransform::Linear {
178                min_values,
179                max_values,
180            } => {
181                dict.set("FunctionType", Object::Integer(2));
182                dict.set(
183                    "Domain",
184                    Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
185                );
186                dict.set(
187                    "C0",
188                    Object::Array(min_values.iter().map(|&v| Object::Real(v)).collect()),
189                );
190                dict.set(
191                    "C1",
192                    Object::Array(max_values.iter().map(|&v| Object::Real(v)).collect()),
193                );
194                dict.set("N", Object::Real(1.0));
195            }
196            TintTransform::Exponential {
197                gamma,
198                min_values,
199                max_values,
200            } => {
201                dict.set("FunctionType", Object::Integer(2));
202                dict.set(
203                    "Domain",
204                    Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
205                );
206                dict.set(
207                    "C0",
208                    Object::Array(min_values.iter().map(|&v| Object::Real(v)).collect()),
209                );
210                dict.set(
211                    "C1",
212                    Object::Array(max_values.iter().map(|&v| Object::Real(v)).collect()),
213                );
214                dict.set("N", Object::Real(*gamma));
215            }
216            TintTransform::Sampled {
217                samples,
218                domain,
219                range,
220            } => {
221                dict.set("FunctionType", Object::Integer(0));
222                dict.set(
223                    "Domain",
224                    Object::Array(vec![Object::Real(domain[0]), Object::Real(domain[1])]),
225                );
226                dict.set(
227                    "Range",
228                    Object::Array(range.iter().map(|&v| Object::Real(v)).collect()),
229                );
230                dict.set(
231                    "Size",
232                    Object::Array(vec![Object::Integer(samples.len() as i64)]),
233                );
234                dict.set("BitsPerSample", Object::Integer(8));
235
236                // Flatten samples for stream data
237                let mut data = Vec::new();
238                for sample in samples {
239                    for &value in sample {
240                        data.push((value * 255.0) as u8);
241                    }
242                }
243                // Note: In real implementation, this would be a stream
244                dict.set("Length", Object::Integer(data.len() as i64));
245            }
246            TintTransform::Custom {
247                domain,
248                range,
249                function_type,
250                ..
251            } => {
252                dict.set("FunctionType", Object::Integer(*function_type as i64));
253                dict.set(
254                    "Domain",
255                    Object::Array(vec![Object::Real(domain[0]), Object::Real(domain[1])]),
256                );
257                dict.set(
258                    "Range",
259                    Object::Array(range.iter().map(|&v| Object::Real(v)).collect()),
260                );
261            }
262        }
263
264        dict
265    }
266}
267
268impl SeparationColorSpace {
269    /// Create a new separation color space
270    pub fn new(
271        colorant_name: impl Into<String>,
272        alternate_space: AlternateColorSpace,
273        tint_transform: TintTransform,
274    ) -> Self {
275        Self {
276            colorant_name: colorant_name.into(),
277            alternate_space,
278            tint_transform,
279        }
280    }
281
282    /// Create a simple RGB separation
283    pub fn rgb_separation(colorant_name: impl Into<String>, r: f64, g: f64, b: f64) -> Self {
284        Self::new(
285            colorant_name,
286            AlternateColorSpace::DeviceRGB,
287            TintTransform::linear(vec![1.0, 1.0, 1.0], vec![r, g, b]),
288        )
289    }
290
291    /// Create a simple CMYK separation
292    pub fn cmyk_separation(
293        colorant_name: impl Into<String>,
294        c: f64,
295        m: f64,
296        y: f64,
297        k: f64,
298    ) -> Self {
299        Self::new(
300            colorant_name,
301            AlternateColorSpace::DeviceCMYK,
302            TintTransform::linear(vec![0.0, 0.0, 0.0, 0.0], vec![c, m, y, k]),
303        )
304    }
305
306    /// Convert to PDF color space array
307    pub fn to_pdf_array(&self) -> Vec<Object> {
308        vec![
309            Object::Name("Separation".to_string()),
310            Object::Name(self.colorant_name.clone()),
311            self.alternate_space.to_pdf_object(),
312            Object::Dictionary(self.tint_transform.to_pdf_dict()),
313        ]
314    }
315
316    /// Apply tint value to get alternate color space values
317    pub fn apply_tint(&self, tint: f64) -> Vec<f64> {
318        self.tint_transform.apply(tint)
319    }
320
321    /// Convert tint to RGB approximation
322    pub fn tint_to_rgb(&self, tint: f64) -> Color {
323        let values = self.apply_tint(tint);
324
325        match &self.alternate_space {
326            AlternateColorSpace::DeviceGray => {
327                let gray = values.first().copied().unwrap_or(0.0);
328                Color::rgb(gray, gray, gray)
329            }
330            AlternateColorSpace::DeviceRGB => Color::rgb(
331                values.first().copied().unwrap_or(0.0),
332                values.get(1).copied().unwrap_or(0.0),
333                values.get(2).copied().unwrap_or(0.0),
334            ),
335            AlternateColorSpace::DeviceCMYK => {
336                // Simple CMYK to RGB conversion
337                let c = values.first().copied().unwrap_or(0.0);
338                let m = values.get(1).copied().unwrap_or(0.0);
339                let y = values.get(2).copied().unwrap_or(0.0);
340                let k = values.get(3).copied().unwrap_or(0.0);
341
342                Color::rgb(
343                    (1.0 - c) * (1.0 - k),
344                    (1.0 - m) * (1.0 - k),
345                    (1.0 - y) * (1.0 - k),
346                )
347            }
348            AlternateColorSpace::Lab { .. } => {
349                // Simplified Lab to RGB (would need proper conversion)
350                Color::rgb(
351                    values.first().copied().unwrap_or(0.0) / 100.0,
352                    (values.get(1).copied().unwrap_or(0.0) + 128.0) / 255.0,
353                    (values.get(2).copied().unwrap_or(0.0) + 128.0) / 255.0,
354                )
355            }
356        }
357    }
358}
359
360/// Common spot colors (Pantone approximations)
361pub struct SpotColors;
362
363impl SpotColors {
364    /// PANTONE 185 C (Red)
365    pub fn pantone_185c() -> SeparationColorSpace {
366        SeparationColorSpace::cmyk_separation("PANTONE 185 C", 0.0, 0.91, 0.76, 0.0)
367    }
368
369    /// PANTONE 286 C (Blue)
370    pub fn pantone_286c() -> SeparationColorSpace {
371        SeparationColorSpace::cmyk_separation("PANTONE 286 C", 1.0, 0.66, 0.0, 0.0)
372    }
373
374    /// PANTONE 376 C (Green)
375    pub fn pantone_376c() -> SeparationColorSpace {
376        SeparationColorSpace::cmyk_separation("PANTONE 376 C", 0.5, 0.0, 1.0, 0.0)
377    }
378
379    /// Metallic Gold
380    pub fn gold() -> SeparationColorSpace {
381        SeparationColorSpace::rgb_separation("Gold", 1.0, 0.843, 0.0)
382    }
383
384    /// Metallic Silver
385    pub fn silver() -> SeparationColorSpace {
386        SeparationColorSpace::rgb_separation("Silver", 0.753, 0.753, 0.753)
387    }
388
389    /// Custom varnish (transparent overlay)
390    pub fn varnish() -> SeparationColorSpace {
391        SeparationColorSpace::new(
392            "Varnish",
393            AlternateColorSpace::DeviceGray,
394            TintTransform::linear(vec![1.0], vec![0.9]),
395        )
396    }
397}
398
399/// Separation color value
400#[derive(Debug, Clone)]
401pub struct SeparationColor {
402    /// Associated color space
403    pub color_space: SeparationColorSpace,
404    /// Tint value (0.0 to 1.0)
405    pub tint: f64,
406}
407
408impl SeparationColor {
409    /// Create a new separation color
410    pub fn new(color_space: SeparationColorSpace, tint: f64) -> Self {
411        Self {
412            color_space,
413            tint: tint.clamp(0.0, 1.0),
414        }
415    }
416
417    /// Get alternate color space values
418    pub fn get_alternate_values(&self) -> Vec<f64> {
419        self.color_space.apply_tint(self.tint)
420    }
421
422    /// Convert to RGB approximation
423    pub fn to_rgb(&self) -> Color {
424        self.color_space.tint_to_rgb(self.tint)
425    }
426
427    /// Get the colorant name
428    pub fn colorant_name(&self) -> &str {
429        &self.color_space.colorant_name
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_separation_color_space_creation() {
439        let sep = SeparationColorSpace::new(
440            "MySpotColor",
441            AlternateColorSpace::DeviceRGB,
442            TintTransform::linear(vec![1.0, 1.0, 1.0], vec![1.0, 0.0, 0.0]),
443        );
444
445        assert_eq!(sep.colorant_name, "MySpotColor");
446        assert!(matches!(
447            sep.alternate_space,
448            AlternateColorSpace::DeviceRGB
449        ));
450    }
451
452    #[test]
453    fn test_rgb_separation() {
454        let sep = SeparationColorSpace::rgb_separation("Red", 1.0, 0.0, 0.0);
455
456        assert_eq!(sep.colorant_name, "Red");
457        let values = sep.apply_tint(1.0);
458        assert_eq!(values, vec![1.0, 0.0, 0.0]);
459
460        let values_half = sep.apply_tint(0.5);
461        assert_eq!(values_half, vec![1.0, 0.5, 0.5]);
462    }
463
464    #[test]
465    fn test_cmyk_separation() {
466        let sep = SeparationColorSpace::cmyk_separation("Cyan", 1.0, 0.0, 0.0, 0.0);
467
468        assert_eq!(sep.colorant_name, "Cyan");
469        let values = sep.apply_tint(1.0);
470        assert_eq!(values, vec![1.0, 0.0, 0.0, 0.0]);
471    }
472
473    #[test]
474    fn test_tint_transform_linear() {
475        let transform = TintTransform::linear(vec![0.0, 0.0, 0.0], vec![1.0, 0.5, 0.25]);
476
477        let values = transform.apply(0.0);
478        assert_eq!(values, vec![0.0, 0.0, 0.0]);
479
480        let values = transform.apply(1.0);
481        assert_eq!(values, vec![1.0, 0.5, 0.25]);
482
483        let values = transform.apply(0.5);
484        assert_eq!(values, vec![0.5, 0.25, 0.125]);
485    }
486
487    #[test]
488    fn test_tint_transform_exponential() {
489        let transform = TintTransform::exponential(2.0, vec![0.0], vec![1.0]);
490
491        let values = transform.apply(0.5);
492        assert_eq!(values[0], 0.25); // 0.5^2 = 0.25
493    }
494
495    #[test]
496    fn test_alternate_color_space_components() {
497        assert_eq!(AlternateColorSpace::DeviceGray.num_components(), 1);
498        assert_eq!(AlternateColorSpace::DeviceRGB.num_components(), 3);
499        assert_eq!(AlternateColorSpace::DeviceCMYK.num_components(), 4);
500
501        let lab = AlternateColorSpace::Lab {
502            white_point: [0.95, 1.0, 1.09],
503            black_point: [0.0, 0.0, 0.0],
504            range: [-100.0, 100.0, -100.0, 100.0],
505        };
506        assert_eq!(lab.num_components(), 3);
507    }
508
509    #[test]
510    fn test_separation_to_pdf_array() {
511        let sep = SeparationColorSpace::rgb_separation("TestColor", 0.5, 0.5, 1.0);
512        let pdf_array = sep.to_pdf_array();
513
514        assert_eq!(pdf_array.len(), 4);
515        assert_eq!(pdf_array[0], Object::Name("Separation".to_string()));
516        assert_eq!(pdf_array[1], Object::Name("TestColor".to_string()));
517    }
518
519    #[test]
520    fn test_tint_to_rgb() {
521        let sep = SeparationColorSpace::rgb_separation("Purple", 0.5, 0.0, 0.5);
522        let color = sep.tint_to_rgb(1.0);
523
524        assert_eq!(color.r(), 0.5);
525        assert_eq!(color.g(), 0.0);
526        assert_eq!(color.b(), 0.5);
527    }
528
529    #[test]
530    fn test_spot_colors() {
531        let pantone_red = SpotColors::pantone_185c();
532        assert_eq!(pantone_red.colorant_name, "PANTONE 185 C");
533
534        let gold = SpotColors::gold();
535        assert_eq!(gold.colorant_name, "Gold");
536
537        let varnish = SpotColors::varnish();
538        assert_eq!(varnish.colorant_name, "Varnish");
539    }
540
541    #[test]
542    fn test_separation_color() {
543        let color_space = SeparationColorSpace::rgb_separation("Blue", 0.0, 0.0, 1.0);
544        let color = SeparationColor::new(color_space, 0.75);
545
546        assert_eq!(color.tint, 0.75);
547        assert_eq!(color.colorant_name(), "Blue");
548
549        let alt_values = color.get_alternate_values();
550        assert_eq!(alt_values[0], 0.25); // Red component (white to blue transition)
551        assert_eq!(alt_values[1], 0.25); // Green component
552        assert_eq!(alt_values[2], 1.0); // Blue component stays at full
553    }
554
555    #[test]
556    fn test_tint_clamping() {
557        let color_space = SeparationColorSpace::rgb_separation("Test", 1.0, 0.0, 0.0);
558        let color = SeparationColor::new(color_space, 1.5); // Should be clamped to 1.0
559
560        assert_eq!(color.tint, 1.0);
561
562        let color2 = SeparationColor::new(
563            SeparationColorSpace::rgb_separation("Test2", 1.0, 0.0, 0.0),
564            -0.5, // Should be clamped to 0.0
565        );
566        assert_eq!(color2.tint, 0.0);
567    }
568}