1use 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#[derive(Debug, Clone)]
16pub struct IndexedColorSpace {
17 pub base_space: BaseColorSpace,
19 pub hival: u8,
21 pub lookup_table: ColorLookupTable,
23 pub name: Option<String>,
25}
26
27#[derive(Debug, Clone, PartialEq)]
29pub enum BaseColorSpace {
30 DeviceRGB,
32 DeviceCMYK,
34 DeviceGray,
36 ICCBased(IccColorSpace),
38 Separation(String),
40 Lab,
42}
43
44impl BaseColorSpace {
45 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 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 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 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#[derive(Debug, Clone)]
92pub struct ColorLookupTable {
93 data: Vec<u8>,
95 components_per_color: usize,
97 color_count: usize,
99}
100
101impl ColorLookupTable {
102 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 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 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 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 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 pub fn color_count(&self) -> usize {
235 self.color_count
236 }
237
238 pub fn components_per_color(&self) -> usize {
240 self.components_per_color
241 }
242
243 pub fn raw_data(&self) -> &[u8] {
245 &self.data
246 }
247}
248
249impl IndexedColorSpace {
250 pub fn new(base_space: BaseColorSpace, lookup_table: ColorLookupTable) -> Result<Self> {
252 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 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 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 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 pub fn with_name(mut self, name: String) -> Self {
320 self.name = Some(name);
321 self
322 }
323
324 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 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 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 pub fn to_pdf_array(&self) -> Result<Vec<Object>> {
384 let array = vec![
385 Object::Name("Indexed".to_string()),
387 self.base_space.to_pdf_object(),
389 Object::Integer(self.hival as i64),
391 Object::String(String::from_utf8_lossy(self.lookup_table.raw_data()).to_string()),
393 ];
394
395 Ok(array)
396 }
397
398 pub fn max_index(&self) -> u8 {
400 self.hival
401 }
402
403 pub fn color_count(&self) -> usize {
405 (self.hival as usize) + 1
406 }
407
408 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#[derive(Debug, Clone, Default)]
424pub struct IndexedColorManager {
425 spaces: HashMap<String, IndexedColorSpace>,
427 cache: HashMap<String, HashMap<String, u8>>,
429}
430
431impl IndexedColorManager {
432 pub fn new() -> Self {
434 Self::default()
435 }
436
437 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 pub fn get_space(&self, name: &str) -> Option<&IndexedColorSpace> {
447 self.spaces.get(name)
448 }
449
450 pub fn get_color_index(&mut self, space_name: &str, color: &Color) -> Option<u8> {
452 let space = self.spaces.get(space_name)?;
453
454 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 let index = space.find_closest_index(color);
464
465 if let Some(cache) = self.cache.get_mut(space_name) {
467 cache.insert(color_key, index);
468 }
469
470 Some(index)
471 }
472
473 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 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 pub fn space_names(&self) -> Vec<String> {
491 self.spaces.keys().cloned().collect()
492 }
493
494 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]; 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 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 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), Color::rgb(0.0, 1.0, 0.0), Color::rgb(0.0, 0.0, 1.0), ];
608
609 let space = IndexedColorSpace::from_palette(&colors).unwrap();
610
611 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 assert_eq!(space.find_closest_index(&Color::rgb(0.8, 0.2, 0.1)), 0);
618
619 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 let result = ColorLookupTable::new(vec![255, 0], 3);
659 assert!(result.is_err());
660
661 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), ];
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), Color::cmyk(0.0, 1.0, 0.0, 0.0), Color::cmyk(0.0, 0.0, 1.0, 0.0), Color::cmyk(0.0, 0.0, 0.0, 1.0), ];
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}