Skip to main content

oxidize_pdf/graphics/
devicen_color.rs

1//! DeviceN Color Space Implementation (ISO 32000-1 ยง8.6.6.5)
2//!
3//! DeviceN color spaces allow the specification of color values for multiple colorants,
4//! including process colorants (CMYK) and spot colorants. This is essential for professional
5//! printing applications where special inks, varnishes, or metallic colors are required.
6
7use crate::error::{PdfError, Result};
8use crate::objects::{Dictionary, Object};
9use std::collections::HashMap;
10
11/// DeviceN color space for multi-colorant printing
12///
13/// DeviceN is a generalization of Separation color space that supports multiple colorants.
14/// It's commonly used in professional printing for:
15/// - Spot colors combined with process colors
16/// - Special inks (metallic, fluorescent)
17/// - Varnishes and coatings
18/// - Multi-ink printing systems
19#[derive(Debug, Clone, PartialEq)]
20pub struct DeviceNColorSpace {
21    /// Names of the individual colorants (e.g., ["Cyan", "Magenta", "Yellow", "Black", "PANTONE 185 C"])
22    pub colorant_names: Vec<String>,
23    /// Alternative color space for fallback rendering
24    pub alternate_space: AlternateColorSpace,
25    /// Tint transform function that maps DeviceN values to alternate space
26    pub tint_transform: TintTransformFunction,
27    /// Optional attributes dictionary for additional properties
28    pub attributes: Option<DeviceNAttributes>,
29}
30
31/// Alternative color space for DeviceN fallback
32#[derive(Debug, Clone, PartialEq)]
33pub enum AlternateColorSpace {
34    /// DeviceRGB for RGB output
35    DeviceRGB,
36    /// DeviceCMYK for CMYK output  
37    DeviceCMYK,
38    /// DeviceGray for grayscale output
39    DeviceGray,
40    /// CIE-based color space
41    CIEBased(String),
42}
43
44/// Tint transform function for DeviceN color conversion
45#[derive(Debug, Clone, PartialEq)]
46pub enum TintTransformFunction {
47    /// Simple linear combination (most common)
48    Linear(LinearTransform),
49    /// PostScript function for complex transforms
50    Function(Vec<u8>),
51    /// Sampled function with lookup table
52    Sampled(SampledFunction),
53}
54
55/// Linear transform for simple DeviceN conversions
56#[derive(Debug, Clone, PartialEq)]
57pub struct LinearTransform {
58    /// Transformation matrix [n_colorants x n_alternate_components]
59    pub matrix: Vec<Vec<f64>>,
60    /// Optional black generation function
61    pub black_generation: Option<Vec<f64>>,
62    /// Optional undercolor removal function  
63    pub undercolor_removal: Option<Vec<f64>>,
64}
65
66/// Sampled function with interpolation
67#[derive(Debug, Clone, PartialEq)]
68pub struct SampledFunction {
69    /// Domain ranges for input values
70    pub domain: Vec<(f64, f64)>,
71    /// Range values for output
72    pub range: Vec<(f64, f64)>,
73    /// Size of sample table in each dimension
74    pub size: Vec<usize>,
75    /// Sample data as bytes
76    pub samples: Vec<u8>,
77    /// Bits per sample (1, 2, 4, 8, 12, 16, 24, 32)
78    pub bits_per_sample: u8,
79    /// Interpolation order (1 = linear, 3 = cubic)
80    pub order: u8,
81}
82
83/// DeviceN attributes for enhanced control
84#[derive(Debug, Clone, PartialEq)]
85pub struct DeviceNAttributes {
86    /// Colorant definitions for spot colors
87    pub colorants: HashMap<String, ColorantDefinition>,
88    /// Process color space (usually CMYK)
89    pub process: Option<String>,
90    /// Mix color space for mixing process and spot colors
91    pub mix: Option<String>,
92    /// Optional dot gain functions per colorant
93    pub dot_gain: HashMap<String, Vec<f64>>,
94}
95
96/// Definition of individual colorants
97#[derive(Debug, Clone, PartialEq)]
98pub struct ColorantDefinition {
99    /// Colorant type (Process, Spot, etc.)
100    pub colorant_type: ColorantType,
101    /// Alternate representation in CMYK
102    pub cmyk_equivalent: Option<[f64; 4]>,
103    /// RGB approximation for screen display
104    pub rgb_approximation: Option<[f64; 3]>,
105    /// Lab color specification
106    pub lab_color: Option<[f64; 3]>,
107    /// Density or opacity value
108    pub density: Option<f64>,
109}
110
111/// Type of colorant in DeviceN space
112#[derive(Debug, Clone, PartialEq)]
113pub enum ColorantType {
114    /// Process color (CMYK)
115    Process,
116    /// Spot color (named ink)
117    Spot,
118    /// Special effect (varnish, metallic)
119    Special,
120}
121
122impl DeviceNColorSpace {
123    /// Create a new DeviceN color space
124    pub fn new(
125        colorant_names: Vec<String>,
126        alternate_space: AlternateColorSpace,
127        tint_transform: TintTransformFunction,
128    ) -> Self {
129        Self {
130            colorant_names,
131            alternate_space,
132            tint_transform,
133            attributes: None,
134        }
135    }
136
137    /// Create DeviceN for CMYK + spot colors (common case)
138    pub fn cmyk_plus_spots(spot_names: Vec<String>) -> Self {
139        let mut colorants = vec![
140            "Cyan".to_string(),
141            "Magenta".to_string(),
142            "Yellow".to_string(),
143            "Black".to_string(),
144        ];
145        colorants.extend(spot_names);
146
147        // Create linear transform matrix (CMYK pass-through + spot handling)
148        let n_colorants = colorants.len();
149        let mut matrix = vec![vec![0.0; 4]; n_colorants]; // 4 = CMYK components
150
151        // CMYK pass-through
152        for (i, row) in matrix.iter_mut().enumerate().take(4) {
153            row[i] = 1.0;
154        }
155
156        // Spot colors convert to approximate CMYK (can be customized)
157        for row in matrix.iter_mut().skip(4).take(n_colorants - 4) {
158            row[3] = 1.0; // Default: spot colors contribute to black
159        }
160
161        Self::new(
162            colorants,
163            AlternateColorSpace::DeviceCMYK,
164            TintTransformFunction::Linear(LinearTransform {
165                matrix,
166                black_generation: None,
167                undercolor_removal: None,
168            }),
169        )
170    }
171
172    /// Add colorant attributes for better color management
173    pub fn with_attributes(mut self, attributes: DeviceNAttributes) -> Self {
174        self.attributes = Some(attributes);
175        self
176    }
177
178    /// Convert DeviceN color values to alternate color space
179    pub fn convert_to_alternate(&self, devicen_values: &[f64]) -> Result<Vec<f64>> {
180        if devicen_values.len() != self.colorant_names.len() {
181            return Err(PdfError::InvalidStructure(
182                "DeviceN values count must match colorant names count".to_string(),
183            ));
184        }
185
186        match &self.tint_transform {
187            TintTransformFunction::Linear(transform) => {
188                self.apply_linear_transform(devicen_values, transform)
189            }
190            TintTransformFunction::Function(_) => {
191                // For PostScript functions, we'd need a PostScript interpreter
192                // For now, fall back to linear approximation
193                self.linear_approximation(devicen_values)
194            }
195            TintTransformFunction::Sampled(sampled) => {
196                self.apply_sampled_function(devicen_values, sampled)
197            }
198        }
199    }
200
201    /// Apply linear transformation matrix
202    fn apply_linear_transform(
203        &self,
204        input: &[f64],
205        transform: &LinearTransform,
206    ) -> Result<Vec<f64>> {
207        let n_output = match self.alternate_space {
208            AlternateColorSpace::DeviceRGB => 3,
209            AlternateColorSpace::DeviceCMYK => 4,
210            AlternateColorSpace::DeviceGray => 1,
211            AlternateColorSpace::CIEBased(_) => 3, // Assume Lab/XYZ
212        };
213
214        if transform.matrix.len() != input.len() {
215            return Err(PdfError::InvalidStructure(
216                "Transform matrix size mismatch".to_string(),
217            ));
218        }
219
220        let mut output = vec![0.0; n_output];
221        for (i, input_val) in input.iter().enumerate() {
222            if transform.matrix[i].len() != n_output {
223                return Err(PdfError::InvalidStructure(
224                    "Transform matrix column size mismatch".to_string(),
225                ));
226            }
227
228            for (j, transform_val) in transform.matrix[i].iter().enumerate() {
229                output[j] += input_val * transform_val;
230            }
231        }
232
233        // Clamp values to valid range [0.0, 1.0]
234        for val in &mut output {
235            *val = val.clamp(0.0, 1.0);
236        }
237
238        Ok(output)
239    }
240
241    /// Simple linear approximation fallback
242    fn linear_approximation(&self, input: &[f64]) -> Result<Vec<f64>> {
243        match self.alternate_space {
244            AlternateColorSpace::DeviceRGB => {
245                // Simple grayscale to RGB
246                let gray = input.iter().sum::<f64>() / input.len() as f64;
247                Ok(vec![1.0 - gray, 1.0 - gray, 1.0 - gray])
248            }
249            AlternateColorSpace::DeviceCMYK => {
250                // Distribute colorants across CMYK
251                let mut cmyk = vec![0.0; 4];
252                for (i, val) in input.iter().enumerate() {
253                    cmyk[i % 4] += val / (input.len() / 4 + 1) as f64;
254                }
255                Ok(cmyk)
256            }
257            AlternateColorSpace::DeviceGray => {
258                let gray = input.iter().sum::<f64>() / input.len() as f64;
259                Ok(vec![gray])
260            }
261            AlternateColorSpace::CIEBased(_) => {
262                // Default to neutral gray in Lab
263                Ok(vec![50.0, 0.0, 0.0])
264            }
265        }
266    }
267
268    /// Apply sampled function with interpolation
269    fn apply_sampled_function(&self, input: &[f64], sampled: &SampledFunction) -> Result<Vec<f64>> {
270        if input.len() != sampled.domain.len() {
271            return Err(PdfError::InvalidStructure(
272                "Input dimension mismatch for sampled function".to_string(),
273            ));
274        }
275
276        // Normalize input to sample table coordinates
277        let mut coords = Vec::new();
278        for (i, &val) in input.iter().enumerate() {
279            let (min, max) = sampled.domain[i];
280            let normalized = (val - min) / (max - min);
281            let coord = normalized * (sampled.size[i] - 1) as f64;
282            coords.push(coord.max(0.0).min((sampled.size[i] - 1) as f64));
283        }
284
285        // For simplicity, use nearest neighbor interpolation
286        // Production code would implement proper multilinear interpolation
287        let mut sample_index = 0;
288        let mut stride = 1;
289
290        for i in (0..coords.len()).rev() {
291            sample_index += (coords[i] as usize) * stride;
292            stride *= sampled.size[i];
293        }
294
295        let output_components = sampled.range.len();
296        let bytes_per_sample = (sampled.bits_per_sample as f64 / 8.0).ceil() as usize;
297        let start_byte = sample_index * output_components * bytes_per_sample;
298
299        let mut output = Vec::new();
300        for i in 0..output_components {
301            let byte_offset = start_byte + i * bytes_per_sample;
302            if byte_offset + bytes_per_sample <= sampled.samples.len() {
303                let sample_value = self.extract_sample_value(
304                    &sampled.samples[byte_offset..byte_offset + bytes_per_sample],
305                    sampled.bits_per_sample,
306                );
307
308                // Map to output range
309                let (min, max) = sampled.range[i];
310                let normalized = sample_value / ((1 << sampled.bits_per_sample) - 1) as f64;
311                output.push(min + normalized * (max - min));
312            }
313        }
314
315        Ok(output)
316    }
317
318    /// Extract numeric value from sample bytes
319    fn extract_sample_value(&self, bytes: &[u8], bits_per_sample: u8) -> f64 {
320        match bits_per_sample {
321            8 => bytes[0] as f64,
322            16 => ((bytes[0] as u16) << 8 | bytes[1] as u16) as f64,
323            32 => {
324                let value = ((bytes[0] as u32) << 24)
325                    | ((bytes[1] as u32) << 16)
326                    | ((bytes[2] as u32) << 8)
327                    | bytes[3] as u32;
328                value as f64
329            }
330            _ => bytes[0] as f64, // Fallback
331        }
332    }
333
334    /// Get number of colorants
335    pub fn colorant_count(&self) -> usize {
336        self.colorant_names.len()
337    }
338
339    /// Get colorant name by index
340    pub fn colorant_name(&self, index: usize) -> Option<&str> {
341        self.colorant_names.get(index).map(|s| s.as_str())
342    }
343
344    /// Check if this DeviceN includes process colors (CMYK)
345    pub fn has_process_colors(&self) -> bool {
346        self.colorant_names.iter().any(|name| {
347            matches!(
348                name.as_str(),
349                "Cyan" | "Magenta" | "Yellow" | "Black" | "C" | "M" | "Y" | "K"
350            )
351        })
352    }
353
354    /// Get spot color names (non-process colors)
355    pub fn spot_color_names(&self) -> Vec<&str> {
356        self.colorant_names
357            .iter()
358            .filter(|name| {
359                !matches!(
360                    name.as_str(),
361                    "Cyan" | "Magenta" | "Yellow" | "Black" | "C" | "M" | "Y" | "K"
362                )
363            })
364            .map(|s| s.as_str())
365            .collect()
366    }
367
368    /// Create PDF object representation
369    pub fn to_pdf_object(&self) -> Object {
370        let mut array = Vec::new();
371
372        // DeviceN color space array: [/DeviceN names alternate tint_transform]
373        array.push(Object::Name("DeviceN".to_string()));
374
375        // Colorant names array
376        let mut names_array = Vec::new();
377        for name in &self.colorant_names {
378            names_array.push(Object::Name(name.clone()));
379        }
380        array.push(Object::Array(names_array));
381
382        // Alternate color space
383        let alternate_obj = match &self.alternate_space {
384            AlternateColorSpace::DeviceRGB => Object::Name("DeviceRGB".to_string()),
385            AlternateColorSpace::DeviceCMYK => Object::Name("DeviceCMYK".to_string()),
386            AlternateColorSpace::DeviceGray => Object::Name("DeviceGray".to_string()),
387            AlternateColorSpace::CIEBased(name) => Object::Name(name.clone()),
388        };
389        array.push(alternate_obj);
390
391        // Tint transform (simplified for now)
392        match &self.tint_transform {
393            TintTransformFunction::Function(data) => {
394                let mut func_dict = Dictionary::new();
395                func_dict.set("FunctionType", Object::Integer(4)); // PostScript function
396                func_dict.set("Domain", self.create_domain_array());
397                func_dict.set("Range", self.create_range_array());
398
399                array.push(Object::Stream(func_dict, data.clone()));
400            }
401            _ => {
402                // For linear/sampled, create identity function for now
403                let mut func_dict = Dictionary::new();
404                func_dict.set("FunctionType", Object::Integer(2)); // Exponential function
405                func_dict.set("Domain", self.create_domain_array());
406                func_dict.set("Range", self.create_range_array());
407                func_dict.set("N", Object::Real(1.0)); // Linear
408
409                array.push(Object::Dictionary(func_dict));
410            }
411        }
412
413        // Optional attributes dictionary
414        if let Some(attributes) = &self.attributes {
415            let mut attr_dict = Dictionary::new();
416
417            if let Some(process) = &attributes.process {
418                attr_dict.set("Process", Object::Name(process.clone()));
419            }
420
421            // Add colorant definitions
422            if !attributes.colorants.is_empty() {
423                let mut colorants_dict = Dictionary::new();
424                for (name, def) in &attributes.colorants {
425                    let mut colorant_dict = Dictionary::new();
426
427                    match def.colorant_type {
428                        ColorantType::Process => {
429                            colorant_dict.set("Type", Object::Name("Process".to_string()))
430                        }
431                        ColorantType::Spot => {
432                            colorant_dict.set("Type", Object::Name("Spot".to_string()))
433                        }
434                        ColorantType::Special => {
435                            colorant_dict.set("Type", Object::Name("Special".to_string()))
436                        }
437                    }
438
439                    if let Some(cmyk) = def.cmyk_equivalent {
440                        let cmyk_array: Vec<Object> =
441                            cmyk.iter().map(|&v| Object::Real(v)).collect();
442                        colorant_dict.set("CMYK", Object::Array(cmyk_array));
443                    }
444
445                    colorants_dict.set(name, Object::Dictionary(colorant_dict));
446                }
447                attr_dict.set("Colorants", Object::Dictionary(colorants_dict));
448            }
449
450            array.push(Object::Dictionary(attr_dict));
451        }
452
453        Object::Array(array)
454    }
455
456    /// Create domain array for function
457    fn create_domain_array(&self) -> Object {
458        let mut domain = Vec::new();
459        for _ in 0..self.colorant_names.len() {
460            domain.push(Object::Real(0.0));
461            domain.push(Object::Real(1.0));
462        }
463        Object::Array(domain)
464    }
465
466    /// Create range array for function based on alternate space
467    fn create_range_array(&self) -> Object {
468        let mut range = Vec::new();
469        let components = match self.alternate_space {
470            AlternateColorSpace::DeviceRGB => 3,
471            AlternateColorSpace::DeviceCMYK => 4,
472            AlternateColorSpace::DeviceGray => 1,
473            AlternateColorSpace::CIEBased(_) => 3,
474        };
475
476        for _ in 0..components {
477            range.push(Object::Real(0.0));
478            range.push(Object::Real(1.0));
479        }
480        Object::Array(range)
481    }
482}
483
484impl ColorantDefinition {
485    /// Create a process colorant (CMYK)
486    pub fn process(cmyk_equivalent: [f64; 4]) -> Self {
487        Self {
488            colorant_type: ColorantType::Process,
489            cmyk_equivalent: Some(cmyk_equivalent),
490            rgb_approximation: Some([
491                1.0 - cmyk_equivalent[0].min(1.0),
492                1.0 - cmyk_equivalent[1].min(1.0),
493                1.0 - cmyk_equivalent[2].min(1.0),
494            ]),
495            lab_color: None,
496            density: None,
497        }
498    }
499
500    /// Create a spot colorant with CMYK approximation
501    pub fn spot(_name: &str, cmyk_equivalent: [f64; 4]) -> Self {
502        Self {
503            colorant_type: ColorantType::Spot,
504            cmyk_equivalent: Some(cmyk_equivalent),
505            rgb_approximation: Some([
506                1.0 - cmyk_equivalent[0].min(1.0),
507                1.0 - cmyk_equivalent[1].min(1.0),
508                1.0 - cmyk_equivalent[2].min(1.0),
509            ]),
510            lab_color: None,
511            density: None,
512        }
513    }
514
515    /// Create a special effect colorant (varnish, metallic)
516    pub fn special_effect(rgb_approximation: [f64; 3]) -> Self {
517        Self {
518            colorant_type: ColorantType::Special,
519            cmyk_equivalent: None,
520            rgb_approximation: Some(rgb_approximation),
521            lab_color: None,
522            density: Some(0.5), // Default opacity
523        }
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_devicen_new() {
533        let colorants = vec!["Cyan".to_string(), "Magenta".to_string()];
534        let transform = TintTransformFunction::Linear(LinearTransform {
535            matrix: vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]],
536            black_generation: None,
537            undercolor_removal: None,
538        });
539        let space =
540            DeviceNColorSpace::new(colorants.clone(), AlternateColorSpace::DeviceRGB, transform);
541
542        assert_eq!(space.colorant_names, colorants);
543        assert_eq!(space.alternate_space, AlternateColorSpace::DeviceRGB);
544        assert!(space.attributes.is_none());
545    }
546
547    #[test]
548    fn test_cmyk_plus_spots() {
549        let spot_names = vec!["PANTONE 185 C".to_string(), "Gold".to_string()];
550        let space = DeviceNColorSpace::cmyk_plus_spots(spot_names);
551
552        assert_eq!(space.colorant_count(), 6);
553        assert_eq!(space.colorant_name(0), Some("Cyan"));
554        assert_eq!(space.colorant_name(1), Some("Magenta"));
555        assert_eq!(space.colorant_name(2), Some("Yellow"));
556        assert_eq!(space.colorant_name(3), Some("Black"));
557        assert_eq!(space.colorant_name(4), Some("PANTONE 185 C"));
558        assert_eq!(space.colorant_name(5), Some("Gold"));
559        assert_eq!(space.colorant_name(6), None);
560    }
561
562    #[test]
563    fn test_has_process_colors() {
564        let with_cmyk = DeviceNColorSpace::cmyk_plus_spots(vec![]);
565        assert!(with_cmyk.has_process_colors());
566
567        let spot_only = DeviceNColorSpace::new(
568            vec!["PANTONE Red".to_string()],
569            AlternateColorSpace::DeviceCMYK,
570            TintTransformFunction::Linear(LinearTransform {
571                matrix: vec![vec![0.0, 1.0, 0.0, 0.0]],
572                black_generation: None,
573                undercolor_removal: None,
574            }),
575        );
576        assert!(!spot_only.has_process_colors());
577    }
578
579    #[test]
580    fn test_spot_color_names() {
581        let space = DeviceNColorSpace::cmyk_plus_spots(vec![
582            "PANTONE 185 C".to_string(),
583            "Gold".to_string(),
584        ]);
585
586        let spots = space.spot_color_names();
587        assert_eq!(spots.len(), 2);
588        assert!(spots.contains(&"PANTONE 185 C"));
589        assert!(spots.contains(&"Gold"));
590    }
591
592    #[test]
593    fn test_colorant_count() {
594        let space = DeviceNColorSpace::new(
595            vec!["A".to_string(), "B".to_string(), "C".to_string()],
596            AlternateColorSpace::DeviceGray,
597            TintTransformFunction::Linear(LinearTransform {
598                matrix: vec![vec![1.0], vec![1.0], vec![1.0]],
599                black_generation: None,
600                undercolor_removal: None,
601            }),
602        );
603        assert_eq!(space.colorant_count(), 3);
604    }
605
606    #[test]
607    fn test_with_attributes() {
608        let mut colorants = HashMap::new();
609        colorants.insert(
610            "Spot1".to_string(),
611            ColorantDefinition::spot("Spot1", [0.0, 1.0, 0.0, 0.0]),
612        );
613
614        let attributes = DeviceNAttributes {
615            colorants,
616            process: Some("CMYK".to_string()),
617            mix: None,
618            dot_gain: HashMap::new(),
619        };
620
621        let space = DeviceNColorSpace::new(
622            vec!["Cyan".to_string()],
623            AlternateColorSpace::DeviceCMYK,
624            TintTransformFunction::Linear(LinearTransform {
625                matrix: vec![vec![1.0, 0.0, 0.0, 0.0]],
626                black_generation: None,
627                undercolor_removal: None,
628            }),
629        )
630        .with_attributes(attributes);
631
632        assert!(space.attributes.is_some());
633        let attrs = space.attributes.unwrap();
634        assert_eq!(attrs.process, Some("CMYK".to_string()));
635        assert!(attrs.colorants.contains_key("Spot1"));
636    }
637
638    #[test]
639    fn test_convert_to_alternate_rgb() {
640        let transform = TintTransformFunction::Linear(LinearTransform {
641            matrix: vec![
642                vec![1.0, 0.0, 0.0], // Cyan -> Red
643                vec![0.0, 1.0, 0.0], // Magenta -> Green
644            ],
645            black_generation: None,
646            undercolor_removal: None,
647        });
648
649        let space = DeviceNColorSpace::new(
650            vec!["Cyan".to_string(), "Magenta".to_string()],
651            AlternateColorSpace::DeviceRGB,
652            transform,
653        );
654
655        let result = space.convert_to_alternate(&[0.5, 0.3]).unwrap();
656        assert_eq!(result.len(), 3);
657        assert!((result[0] - 0.5).abs() < 0.001);
658        assert!((result[1] - 0.3).abs() < 0.001);
659        assert!((result[2] - 0.0).abs() < 0.001);
660    }
661
662    #[test]
663    fn test_convert_to_alternate_cmyk() {
664        let space = DeviceNColorSpace::cmyk_plus_spots(vec![]);
665        let result = space.convert_to_alternate(&[0.5, 0.3, 0.2, 0.1]).unwrap();
666
667        assert_eq!(result.len(), 4);
668        assert!((result[0] - 0.5).abs() < 0.001);
669        assert!((result[1] - 0.3).abs() < 0.001);
670        assert!((result[2] - 0.2).abs() < 0.001);
671        assert!((result[3] - 0.1).abs() < 0.001);
672    }
673
674    #[test]
675    fn test_convert_to_alternate_wrong_count() {
676        let space = DeviceNColorSpace::cmyk_plus_spots(vec![]);
677        let result = space.convert_to_alternate(&[0.5, 0.3]);
678
679        assert!(result.is_err());
680    }
681
682    #[test]
683    fn test_convert_clamping() {
684        let transform = TintTransformFunction::Linear(LinearTransform {
685            matrix: vec![vec![2.0, 0.0, 0.0]],
686            black_generation: None,
687            undercolor_removal: None,
688        });
689
690        let space = DeviceNColorSpace::new(
691            vec!["Intense".to_string()],
692            AlternateColorSpace::DeviceRGB,
693            transform,
694        );
695
696        let result = space.convert_to_alternate(&[0.8]).unwrap();
697        assert_eq!(result[0], 1.0); // Should be clamped to 1.0
698    }
699
700    #[test]
701    fn test_alternate_color_space_variants() {
702        assert_eq!(
703            AlternateColorSpace::DeviceRGB,
704            AlternateColorSpace::DeviceRGB
705        );
706        assert_eq!(
707            AlternateColorSpace::DeviceCMYK,
708            AlternateColorSpace::DeviceCMYK
709        );
710        assert_eq!(
711            AlternateColorSpace::DeviceGray,
712            AlternateColorSpace::DeviceGray
713        );
714
715        let cie = AlternateColorSpace::CIEBased("sRGB".to_string());
716        assert_eq!(cie, AlternateColorSpace::CIEBased("sRGB".to_string()));
717    }
718
719    #[test]
720    fn test_colorant_type_variants() {
721        assert_eq!(ColorantType::Process, ColorantType::Process);
722        assert_eq!(ColorantType::Spot, ColorantType::Spot);
723        assert_eq!(ColorantType::Special, ColorantType::Special);
724    }
725
726    #[test]
727    fn test_colorant_definition_process() {
728        let cmyk = [1.0, 0.0, 0.0, 0.0]; // Pure Cyan
729        let def = ColorantDefinition::process(cmyk);
730
731        assert_eq!(def.colorant_type, ColorantType::Process);
732        assert_eq!(def.cmyk_equivalent, Some(cmyk));
733        assert!(def.rgb_approximation.is_some());
734
735        let rgb = def.rgb_approximation.unwrap();
736        assert!((rgb[0] - 0.0).abs() < 0.001); // 1 - 1.0 = 0
737        assert!((rgb[1] - 1.0).abs() < 0.001); // 1 - 0.0 = 1
738        assert!((rgb[2] - 1.0).abs() < 0.001); // 1 - 0.0 = 1
739    }
740
741    #[test]
742    fn test_colorant_definition_spot() {
743        let cmyk = [0.0, 1.0, 1.0, 0.0]; // Red-ish
744        let def = ColorantDefinition::spot("PANTONE Red", cmyk);
745
746        assert_eq!(def.colorant_type, ColorantType::Spot);
747        assert_eq!(def.cmyk_equivalent, Some(cmyk));
748        assert!(def.rgb_approximation.is_some());
749    }
750
751    #[test]
752    fn test_colorant_definition_special_effect() {
753        let rgb = [0.8, 0.8, 0.4]; // Gold-ish
754        let def = ColorantDefinition::special_effect(rgb);
755
756        assert_eq!(def.colorant_type, ColorantType::Special);
757        assert_eq!(def.cmyk_equivalent, None);
758        assert_eq!(def.rgb_approximation, Some(rgb));
759        assert_eq!(def.density, Some(0.5));
760    }
761
762    #[test]
763    fn test_linear_transform_struct() {
764        let transform = LinearTransform {
765            matrix: vec![vec![1.0, 0.0], vec![0.0, 1.0]],
766            black_generation: Some(vec![0.1, 0.2]),
767            undercolor_removal: Some(vec![0.05]),
768        };
769
770        assert_eq!(transform.matrix.len(), 2);
771        assert_eq!(transform.black_generation, Some(vec![0.1, 0.2]));
772        assert_eq!(transform.undercolor_removal, Some(vec![0.05]));
773    }
774
775    #[test]
776    fn test_sampled_function_struct() {
777        let sampled = SampledFunction {
778            domain: vec![(0.0, 1.0), (0.0, 1.0)],
779            range: vec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
780            size: vec![4, 4],
781            samples: vec![0; 48],
782            bits_per_sample: 8,
783            order: 1,
784        };
785
786        assert_eq!(sampled.domain.len(), 2);
787        assert_eq!(sampled.range.len(), 3);
788        assert_eq!(sampled.bits_per_sample, 8);
789        assert_eq!(sampled.order, 1);
790    }
791
792    #[test]
793    fn test_extract_sample_value_8bit() {
794        let space = DeviceNColorSpace::new(
795            vec!["Test".to_string()],
796            AlternateColorSpace::DeviceGray,
797            TintTransformFunction::Linear(LinearTransform {
798                matrix: vec![vec![1.0]],
799                black_generation: None,
800                undercolor_removal: None,
801            }),
802        );
803
804        let bytes = [128u8];
805        let value = space.extract_sample_value(&bytes, 8);
806        assert_eq!(value, 128.0);
807    }
808
809    #[test]
810    fn test_extract_sample_value_16bit() {
811        let space = DeviceNColorSpace::new(
812            vec!["Test".to_string()],
813            AlternateColorSpace::DeviceGray,
814            TintTransformFunction::Linear(LinearTransform {
815                matrix: vec![vec![1.0]],
816                black_generation: None,
817                undercolor_removal: None,
818            }),
819        );
820
821        let bytes = [0x01, 0x00]; // 256 in big-endian
822        let value = space.extract_sample_value(&bytes, 16);
823        assert_eq!(value, 256.0);
824    }
825
826    #[test]
827    fn test_to_pdf_object() {
828        let space = DeviceNColorSpace::cmyk_plus_spots(vec!["Gold".to_string()]);
829        let obj = space.to_pdf_object();
830
831        if let Object::Array(arr) = obj {
832            assert!(arr.len() >= 4); // At least DeviceN, names, alternate, function
833            if let Object::Name(name) = &arr[0] {
834                assert_eq!(name, "DeviceN");
835            } else {
836                panic!("First element should be Name");
837            }
838        } else {
839            panic!("Should return Array object");
840        }
841    }
842
843    #[test]
844    fn test_devicen_attributes() {
845        let mut colorants = HashMap::new();
846        colorants.insert(
847            "Cyan".to_string(),
848            ColorantDefinition::process([1.0, 0.0, 0.0, 0.0]),
849        );
850
851        let mut dot_gain = HashMap::new();
852        dot_gain.insert("Cyan".to_string(), vec![0.0, 0.1, 0.2]);
853
854        let attrs = DeviceNAttributes {
855            colorants,
856            process: Some("DeviceCMYK".to_string()),
857            mix: Some("DeviceRGB".to_string()),
858            dot_gain,
859        };
860
861        assert!(attrs.colorants.contains_key("Cyan"));
862        assert_eq!(attrs.process, Some("DeviceCMYK".to_string()));
863        assert_eq!(attrs.mix, Some("DeviceRGB".to_string()));
864        assert!(attrs.dot_gain.contains_key("Cyan"));
865    }
866
867    #[test]
868    fn test_linear_approximation_rgb() {
869        let space = DeviceNColorSpace::new(
870            vec!["Test".to_string()],
871            AlternateColorSpace::DeviceRGB,
872            TintTransformFunction::Function(vec![]), // Triggers linear_approximation
873        );
874
875        let result = space.convert_to_alternate(&[0.5]).unwrap();
876        assert_eq!(result.len(), 3);
877    }
878
879    #[test]
880    fn test_linear_approximation_gray() {
881        let space = DeviceNColorSpace::new(
882            vec!["Test".to_string()],
883            AlternateColorSpace::DeviceGray,
884            TintTransformFunction::Function(vec![]),
885        );
886
887        let result = space.convert_to_alternate(&[0.5]).unwrap();
888        assert_eq!(result.len(), 1);
889        assert!((result[0] - 0.5).abs() < 0.001);
890    }
891
892    #[test]
893    fn test_linear_approximation_cie() {
894        let space = DeviceNColorSpace::new(
895            vec!["Test".to_string()],
896            AlternateColorSpace::CIEBased("Lab".to_string()),
897            TintTransformFunction::Function(vec![]),
898        );
899
900        let result = space.convert_to_alternate(&[0.5]).unwrap();
901        assert_eq!(result.len(), 3);
902        // Default Lab neutral gray
903        assert_eq!(result[0], 50.0);
904        assert_eq!(result[1], 0.0);
905        assert_eq!(result[2], 0.0);
906    }
907}