Skip to main content

oxidize_pdf/graphics/
indexed_color.rs

1//! Indexed color space support according to ISO 32000-1 Section 8.6.6.3
2//!
3//! This module provides comprehensive support for indexed color spaces which allow
4//! efficient encoding of images with limited color palettes. Indexed color spaces
5//! map index values to colors in a base color space, reducing file size for images
6//! with few colors.
7
8use crate::error::{PdfError, Result};
9use crate::graphics::color::Color;
10use crate::graphics::color_profiles::IccColorSpace;
11use crate::objects::{Dictionary, Object};
12use std::collections::HashMap;
13
14/// Indexed color space representation
15#[derive(Debug, Clone)]
16pub struct IndexedColorSpace {
17    /// Base color space (DeviceRGB, DeviceCMYK, DeviceGray, or ICCBased)
18    pub base_space: BaseColorSpace,
19    /// Maximum valid index value (0 to hival, max 255)
20    pub hival: u8,
21    /// Color lookup table
22    pub lookup_table: ColorLookupTable,
23    /// Optional name for the indexed color space
24    pub name: Option<String>,
25}
26
27/// Base color space for indexed colors
28#[derive(Debug, Clone, PartialEq)]
29pub enum BaseColorSpace {
30    /// Device RGB
31    DeviceRGB,
32    /// Device CMYK
33    DeviceCMYK,
34    /// Device Gray
35    DeviceGray,
36    /// ICC-based color space
37    ICCBased(IccColorSpace),
38    /// Separation color space
39    Separation(String),
40    /// Lab color space
41    Lab,
42}
43
44impl BaseColorSpace {
45    /// Get the number of components for this color space
46    pub fn component_count(&self) -> usize {
47        match self {
48            BaseColorSpace::DeviceGray => 1,
49            BaseColorSpace::DeviceRGB | BaseColorSpace::Lab => 3,
50            BaseColorSpace::DeviceCMYK => 4,
51            BaseColorSpace::ICCBased(icc) => icc.component_count() as usize,
52            BaseColorSpace::Separation(_) => 1,
53        }
54    }
55
56    /// Get the PDF name for this color space
57    pub fn pdf_name(&self) -> String {
58        match self {
59            BaseColorSpace::DeviceGray => "DeviceGray".to_string(),
60            BaseColorSpace::DeviceRGB => "DeviceRGB".to_string(),
61            BaseColorSpace::DeviceCMYK => "DeviceCMYK".to_string(),
62            BaseColorSpace::ICCBased(_) => "ICCBased".to_string(),
63            BaseColorSpace::Separation(name) => format!("Separation({})", name),
64            BaseColorSpace::Lab => "Lab".to_string(),
65        }
66    }
67
68    /// Convert to PDF object representation
69    pub fn to_pdf_object(&self) -> Object {
70        match self {
71            BaseColorSpace::DeviceGray => Object::Name("DeviceGray".to_string()),
72            BaseColorSpace::DeviceRGB => Object::Name("DeviceRGB".to_string()),
73            BaseColorSpace::DeviceCMYK => Object::Name("DeviceCMYK".to_string()),
74            BaseColorSpace::Lab => Object::Name("Lab".to_string()),
75            BaseColorSpace::ICCBased(_) => {
76                // In real implementation, this would reference the ICC profile
77                Object::Array(vec![
78                    Object::Name("ICCBased".to_string()),
79                    Object::Dictionary(Dictionary::new()),
80                ])
81            }
82            BaseColorSpace::Separation(name) => Object::Array(vec![
83                Object::Name("Separation".to_string()),
84                Object::Name(name.clone()),
85            ]),
86        }
87    }
88}
89
90/// Color lookup table for indexed color space
91#[derive(Debug, Clone)]
92pub struct ColorLookupTable {
93    /// Raw color data (packed according to base color space)
94    data: Vec<u8>,
95    /// Number of components per color
96    components_per_color: usize,
97    /// Number of colors in the table
98    color_count: usize,
99}
100
101impl ColorLookupTable {
102    /// Create a new color lookup table
103    pub fn new(data: Vec<u8>, components_per_color: usize) -> Result<Self> {
104        if components_per_color == 0 {
105            return Err(PdfError::InvalidStructure(
106                "Components per color must be greater than 0".to_string(),
107            ));
108        }
109
110        if data.len() % components_per_color != 0 {
111            return Err(PdfError::InvalidStructure(format!(
112                "Color data length {} is not a multiple of components per color {}",
113                data.len(),
114                components_per_color
115            )));
116        }
117
118        let color_count = data.len() / components_per_color;
119        if color_count > 256 {
120            return Err(PdfError::InvalidStructure(format!(
121                "Color count {} exceeds maximum of 256",
122                color_count
123            )));
124        }
125
126        Ok(Self {
127            data,
128            components_per_color,
129            color_count,
130        })
131    }
132
133    /// Create from a list of colors
134    pub fn from_colors(colors: &[Color]) -> Result<Self> {
135        if colors.is_empty() {
136            return Err(PdfError::InvalidStructure(
137                "Color list cannot be empty".to_string(),
138            ));
139        }
140
141        if colors.len() > 256 {
142            return Err(PdfError::InvalidStructure(format!(
143                "Color count {} exceeds maximum of 256",
144                colors.len()
145            )));
146        }
147
148        // Determine base color space from first color
149        let (components_per_color, data) = match &colors[0] {
150            Color::Gray(_) => {
151                let mut data = Vec::with_capacity(colors.len());
152                for color in colors {
153                    if let Color::Gray(g) = color {
154                        data.push((g * 255.0) as u8);
155                    } else {
156                        return Err(PdfError::InvalidStructure(
157                            "All colors must be of the same type".to_string(),
158                        ));
159                    }
160                }
161                (1, data)
162            }
163            Color::Rgb(_, _, _) => {
164                let mut data = Vec::with_capacity(colors.len() * 3);
165                for color in colors {
166                    if let Color::Rgb(r, g, b) = color {
167                        data.push((r * 255.0) as u8);
168                        data.push((g * 255.0) as u8);
169                        data.push((b * 255.0) as u8);
170                    } else {
171                        return Err(PdfError::InvalidStructure(
172                            "All colors must be of the same type".to_string(),
173                        ));
174                    }
175                }
176                (3, data)
177            }
178            Color::Cmyk(_, _, _, _) => {
179                let mut data = Vec::with_capacity(colors.len() * 4);
180                for color in colors {
181                    if let Color::Cmyk(c, m, y, k) = color {
182                        data.push((c * 255.0) as u8);
183                        data.push((m * 255.0) as u8);
184                        data.push((y * 255.0) as u8);
185                        data.push((k * 255.0) as u8);
186                    } else {
187                        return Err(PdfError::InvalidStructure(
188                            "All colors must be of the same type".to_string(),
189                        ));
190                    }
191                }
192                (4, data)
193            }
194        };
195
196        Ok(Self {
197            data,
198            components_per_color,
199            color_count: colors.len(),
200        })
201    }
202
203    /// Get color at index
204    pub fn get_color(&self, index: u8) -> Option<Vec<f64>> {
205        let idx = index as usize;
206        if idx >= self.color_count {
207            return None;
208        }
209
210        let start = idx * self.components_per_color;
211        let end = start + self.components_per_color;
212
213        let components: Vec<f64> = self.data[start..end]
214            .iter()
215            .map(|&b| b as f64 / 255.0)
216            .collect();
217
218        Some(components)
219    }
220
221    /// Get raw color data at index (as bytes)
222    pub fn get_raw_color(&self, index: u8) -> Option<&[u8]> {
223        let idx = index as usize;
224        if idx >= self.color_count {
225            return None;
226        }
227
228        let start = idx * self.components_per_color;
229        let end = start + self.components_per_color;
230        Some(&self.data[start..end])
231    }
232
233    /// Get the number of colors in the table
234    pub fn color_count(&self) -> usize {
235        self.color_count
236    }
237
238    /// Get components per color
239    pub fn components_per_color(&self) -> usize {
240        self.components_per_color
241    }
242
243    /// Get raw data
244    pub fn raw_data(&self) -> &[u8] {
245        &self.data
246    }
247}
248
249impl IndexedColorSpace {
250    /// Create a new indexed color space
251    pub fn new(base_space: BaseColorSpace, lookup_table: ColorLookupTable) -> Result<Self> {
252        // Validate that lookup table matches base space
253        let expected_components = base_space.component_count();
254        if lookup_table.components_per_color != expected_components {
255            return Err(PdfError::InvalidStructure(format!(
256                "Lookup table has {} components per color but base space {} requires {}",
257                lookup_table.components_per_color,
258                base_space.pdf_name(),
259                expected_components
260            )));
261        }
262
263        let hival = (lookup_table.color_count() - 1) as u8;
264
265        Ok(Self {
266            base_space,
267            hival,
268            lookup_table,
269            name: None,
270        })
271    }
272
273    /// Create an indexed color space from a palette
274    pub fn from_palette(colors: &[Color]) -> Result<Self> {
275        let lookup_table = ColorLookupTable::from_colors(colors)?;
276
277        let base_space = match &colors[0] {
278            Color::Gray(_) => BaseColorSpace::DeviceGray,
279            Color::Rgb(_, _, _) => BaseColorSpace::DeviceRGB,
280            Color::Cmyk(_, _, _, _) => BaseColorSpace::DeviceCMYK,
281        };
282
283        Self::new(base_space, lookup_table)
284    }
285
286    /// Create a web-safe color palette (216 colors)
287    pub fn web_safe_palette() -> Result<Self> {
288        let mut colors = Vec::with_capacity(216);
289
290        for r in 0..6 {
291            for g in 0..6 {
292                for b in 0..6 {
293                    colors.push(Color::rgb(r as f64 * 0.2, g as f64 * 0.2, b as f64 * 0.2));
294                }
295            }
296        }
297
298        Self::from_palette(&colors)
299    }
300
301    /// Create a grayscale palette
302    pub fn grayscale_palette(levels: u8) -> Result<Self> {
303        if levels == 0 {
304            return Err(PdfError::InvalidStructure(
305                "Grayscale levels must be between 1 and 255".to_string(),
306            ));
307        }
308
309        let mut colors = Vec::with_capacity(levels as usize);
310        for i in 0..levels {
311            let gray = i as f64 / (levels - 1) as f64;
312            colors.push(Color::gray(gray));
313        }
314
315        Self::from_palette(&colors)
316    }
317
318    /// Set the name for this indexed color space
319    pub fn with_name(mut self, name: String) -> Self {
320        self.name = Some(name);
321        self
322    }
323
324    /// Get color at index
325    pub fn get_color(&self, index: u8) -> Option<Color> {
326        let components = self.lookup_table.get_color(index)?;
327
328        match self.base_space {
329            BaseColorSpace::DeviceGray => Some(Color::gray(components[0])),
330            BaseColorSpace::DeviceRGB | BaseColorSpace::Lab => {
331                Some(Color::rgb(components[0], components[1], components[2]))
332            }
333            BaseColorSpace::DeviceCMYK => Some(Color::cmyk(
334                components[0],
335                components[1],
336                components[2],
337                components[3],
338            )),
339            _ => None,
340        }
341    }
342
343    /// Find closest color index for a given color
344    pub fn find_closest_index(&self, target: &Color) -> u8 {
345        let mut best_index = 0;
346        let mut best_distance = f64::MAX;
347
348        for i in 0..=self.hival {
349            if let Some(color) = self.get_color(i) {
350                let distance = self.color_distance(target, &color);
351                if distance < best_distance {
352                    best_distance = distance;
353                    best_index = i;
354                }
355            }
356        }
357
358        best_index
359    }
360
361    /// Calculate color distance (Euclidean)
362    fn color_distance(&self, c1: &Color, c2: &Color) -> f64 {
363        match (c1, c2) {
364            (Color::Gray(g1), Color::Gray(g2)) => (g1 - g2).abs(),
365            (Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
366                let dr = r1 - r2;
367                let dg = g1 - g2;
368                let db = b1 - b2;
369                (dr * dr + dg * dg + db * db).sqrt()
370            }
371            (Color::Cmyk(c1, m1, y1, k1), Color::Cmyk(c2, m2, y2, k2)) => {
372                let dc = c1 - c2;
373                let dm = m1 - m2;
374                let dy = y1 - y2;
375                let dk = k1 - k2;
376                (dc * dc + dm * dm + dy * dy + dk * dk).sqrt()
377            }
378            _ => f64::MAX,
379        }
380    }
381
382    /// Convert to PDF color space array
383    pub fn to_pdf_array(&self) -> Result<Vec<Object>> {
384        let array = vec![
385            // Color space name
386            Object::Name("Indexed".to_string()),
387            // Base color space
388            self.base_space.to_pdf_object(),
389            // Maximum index value
390            Object::Integer(self.hival as i64),
391            // Lookup table as string (raw bytes)
392            Object::String(String::from_utf8_lossy(self.lookup_table.raw_data()).to_string()),
393        ];
394
395        Ok(array)
396    }
397
398    /// Get the maximum valid index
399    pub fn max_index(&self) -> u8 {
400        self.hival
401    }
402
403    /// Get the number of colors
404    pub fn color_count(&self) -> usize {
405        (self.hival as usize) + 1
406    }
407
408    /// Validate the indexed color space
409    pub fn validate(&self) -> Result<()> {
410        if self.hival as usize >= self.lookup_table.color_count() {
411            return Err(PdfError::InvalidStructure(format!(
412                "hival {} exceeds lookup table size {}",
413                self.hival,
414                self.lookup_table.color_count()
415            )));
416        }
417
418        Ok(())
419    }
420}
421
422/// Indexed color space manager
423#[derive(Debug, Clone, Default)]
424pub struct IndexedColorManager {
425    /// Registered indexed color spaces
426    spaces: HashMap<String, IndexedColorSpace>,
427    /// Color to index cache for performance
428    cache: HashMap<String, HashMap<String, u8>>,
429}
430
431impl IndexedColorManager {
432    /// Create a new indexed color manager
433    pub fn new() -> Self {
434        Self::default()
435    }
436
437    /// Add an indexed color space
438    pub fn add_space(&mut self, name: String, space: IndexedColorSpace) -> Result<()> {
439        space.validate()?;
440        self.spaces.insert(name.clone(), space);
441        self.cache.insert(name, HashMap::new());
442        Ok(())
443    }
444
445    /// Get an indexed color space
446    pub fn get_space(&self, name: &str) -> Option<&IndexedColorSpace> {
447        self.spaces.get(name)
448    }
449
450    /// Get or create index for a color in a space
451    pub fn get_color_index(&mut self, space_name: &str, color: &Color) -> Option<u8> {
452        let space = self.spaces.get(space_name)?;
453
454        // Check cache first
455        let color_key = format!("{:?}", color);
456        if let Some(cache) = self.cache.get(space_name) {
457            if let Some(&index) = cache.get(&color_key) {
458                return Some(index);
459            }
460        }
461
462        // Find closest color
463        let index = space.find_closest_index(color);
464
465        // Update cache
466        if let Some(cache) = self.cache.get_mut(space_name) {
467            cache.insert(color_key, index);
468        }
469
470        Some(index)
471    }
472
473    /// Create standard palettes
474    pub fn create_web_safe(&mut self) -> Result<String> {
475        let name = "WebSafe".to_string();
476        let space = IndexedColorSpace::web_safe_palette()?;
477        self.add_space(name.clone(), space)?;
478        Ok(name)
479    }
480
481    /// Create grayscale palette
482    pub fn create_grayscale(&mut self, levels: u8) -> Result<String> {
483        let name = format!("Gray{}", levels);
484        let space = IndexedColorSpace::grayscale_palette(levels)?;
485        self.add_space(name.clone(), space)?;
486        Ok(name)
487    }
488
489    /// Get all space names
490    pub fn space_names(&self) -> Vec<String> {
491        self.spaces.keys().cloned().collect()
492    }
493
494    /// Clear all spaces
495    pub fn clear(&mut self) {
496        self.spaces.clear();
497        self.cache.clear();
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn test_base_color_space_components() {
507        assert_eq!(BaseColorSpace::DeviceGray.component_count(), 1);
508        assert_eq!(BaseColorSpace::DeviceRGB.component_count(), 3);
509        assert_eq!(BaseColorSpace::DeviceCMYK.component_count(), 4);
510        assert_eq!(BaseColorSpace::Lab.component_count(), 3);
511        assert_eq!(
512            BaseColorSpace::Separation("Spot".to_string()).component_count(),
513            1
514        );
515    }
516
517    #[test]
518    fn test_color_lookup_table_creation() {
519        let data = vec![255, 0, 0, 0, 255, 0, 0, 0, 255]; // RGB: red, green, blue
520        let table = ColorLookupTable::new(data, 3).unwrap();
521
522        assert_eq!(table.color_count(), 3);
523        assert_eq!(table.components_per_color(), 3);
524    }
525
526    #[test]
527    fn test_color_lookup_table_from_colors() {
528        let colors = vec![
529            Color::rgb(1.0, 0.0, 0.0),
530            Color::rgb(0.0, 1.0, 0.0),
531            Color::rgb(0.0, 0.0, 1.0),
532        ];
533
534        let table = ColorLookupTable::from_colors(&colors).unwrap();
535        assert_eq!(table.color_count(), 3);
536        assert_eq!(table.components_per_color(), 3);
537
538        // Check first color (red)
539        let red = table.get_color(0).unwrap();
540        assert!((red[0] - 1.0).abs() < 0.01);
541        assert!((red[1] - 0.0).abs() < 0.01);
542        assert!((red[2] - 0.0).abs() < 0.01);
543    }
544
545    #[test]
546    fn test_indexed_color_space_creation() {
547        let colors = vec![
548            Color::rgb(1.0, 0.0, 0.0),
549            Color::rgb(0.0, 1.0, 0.0),
550            Color::rgb(0.0, 0.0, 1.0),
551        ];
552
553        let space = IndexedColorSpace::from_palette(&colors).unwrap();
554        assert_eq!(space.hival, 2);
555        assert_eq!(space.color_count(), 3);
556    }
557
558    #[test]
559    fn test_indexed_color_space_get_color() {
560        let colors = vec![
561            Color::rgb(1.0, 0.0, 0.0),
562            Color::rgb(0.0, 1.0, 0.0),
563            Color::rgb(0.0, 0.0, 1.0),
564        ];
565
566        let space = IndexedColorSpace::from_palette(&colors).unwrap();
567
568        let red = space.get_color(0).unwrap();
569        assert_eq!(red, Color::rgb(1.0, 0.0, 0.0));
570
571        let green = space.get_color(1).unwrap();
572        assert_eq!(green, Color::rgb(0.0, 1.0, 0.0));
573
574        let blue = space.get_color(2).unwrap();
575        assert_eq!(blue, Color::rgb(0.0, 0.0, 1.0));
576
577        assert!(space.get_color(3).is_none());
578    }
579
580    #[test]
581    fn test_web_safe_palette() {
582        let space = IndexedColorSpace::web_safe_palette().unwrap();
583        assert_eq!(space.color_count(), 216);
584        assert_eq!(space.hival, 215);
585    }
586
587    #[test]
588    fn test_grayscale_palette() {
589        let space = IndexedColorSpace::grayscale_palette(16).unwrap();
590        assert_eq!(space.color_count(), 16);
591        assert_eq!(space.hival, 15);
592
593        // Check first and last colors
594        let black = space.get_color(0).unwrap();
595        assert_eq!(black, Color::gray(0.0));
596
597        let white = space.get_color(15).unwrap();
598        assert!(matches!(white, Color::Gray(g) if (g - 1.0).abs() < 0.01));
599    }
600
601    #[test]
602    fn test_find_closest_index() {
603        let colors = vec![
604            Color::rgb(1.0, 0.0, 0.0), // Red
605            Color::rgb(0.0, 1.0, 0.0), // Green
606            Color::rgb(0.0, 0.0, 1.0), // Blue
607        ];
608
609        let space = IndexedColorSpace::from_palette(&colors).unwrap();
610
611        // Exact matches
612        assert_eq!(space.find_closest_index(&Color::rgb(1.0, 0.0, 0.0)), 0);
613        assert_eq!(space.find_closest_index(&Color::rgb(0.0, 1.0, 0.0)), 1);
614        assert_eq!(space.find_closest_index(&Color::rgb(0.0, 0.0, 1.0)), 2);
615
616        // Close to red
617        assert_eq!(space.find_closest_index(&Color::rgb(0.8, 0.2, 0.1)), 0);
618
619        // Close to green
620        assert_eq!(space.find_closest_index(&Color::rgb(0.1, 0.8, 0.2)), 1);
621    }
622
623    #[test]
624    fn test_indexed_color_manager() {
625        let mut manager = IndexedColorManager::new();
626
627        let colors = vec![
628            Color::rgb(1.0, 0.0, 0.0),
629            Color::rgb(0.0, 1.0, 0.0),
630            Color::rgb(0.0, 0.0, 1.0),
631        ];
632
633        let space = IndexedColorSpace::from_palette(&colors).unwrap();
634        manager.add_space("TestPalette".to_string(), space).unwrap();
635
636        assert!(manager.get_space("TestPalette").is_some());
637
638        let index = manager.get_color_index("TestPalette", &Color::rgb(1.0, 0.0, 0.0));
639        assert_eq!(index, Some(0));
640    }
641
642    #[test]
643    fn test_manager_standard_palettes() {
644        let mut manager = IndexedColorManager::new();
645
646        let web_name = manager.create_web_safe().unwrap();
647        assert_eq!(web_name, "WebSafe");
648        assert!(manager.get_space(&web_name).is_some());
649
650        let gray_name = manager.create_grayscale(255).unwrap();
651        assert_eq!(gray_name, "Gray255");
652        assert!(manager.get_space(&gray_name).is_some());
653    }
654
655    #[test]
656    fn test_invalid_lookup_table() {
657        // Data length not multiple of components
658        let result = ColorLookupTable::new(vec![255, 0], 3);
659        assert!(result.is_err());
660
661        // Zero components
662        let result = ColorLookupTable::new(vec![255, 0, 0], 0);
663        assert!(result.is_err());
664    }
665
666    #[test]
667    fn test_mismatched_color_types() {
668        let colors = vec![
669            Color::rgb(1.0, 0.0, 0.0),
670            Color::gray(0.5), // Different type
671        ];
672
673        let result = ColorLookupTable::from_colors(&colors);
674        assert!(result.is_err());
675    }
676
677    #[test]
678    fn test_too_many_colors() {
679        let mut colors = Vec::new();
680        for i in 0..257 {
681            colors.push(Color::gray(i as f64 / 256.0));
682        }
683
684        let result = ColorLookupTable::from_colors(&colors);
685        assert!(result.is_err());
686    }
687
688    #[test]
689    fn test_cmyk_indexed_space() {
690        let colors = vec![
691            Color::cmyk(1.0, 0.0, 0.0, 0.0), // Cyan
692            Color::cmyk(0.0, 1.0, 0.0, 0.0), // Magenta
693            Color::cmyk(0.0, 0.0, 1.0, 0.0), // Yellow
694            Color::cmyk(0.0, 0.0, 0.0, 1.0), // Black
695        ];
696
697        let space = IndexedColorSpace::from_palette(&colors).unwrap();
698        assert_eq!(space.base_space, BaseColorSpace::DeviceCMYK);
699        assert_eq!(space.color_count(), 4);
700
701        let cyan = space.get_color(0).unwrap();
702        assert_eq!(cyan, Color::cmyk(1.0, 0.0, 0.0, 0.0));
703    }
704}