1use crate::error::{PdfError, Result};
10use crate::objects::{Dictionary, Object};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
15pub struct IccProfile {
16 pub name: String,
18 pub data: Vec<u8>,
20 pub components: u8,
22 pub color_space: IccColorSpace,
24 pub range: Option<Vec<f64>>,
26 pub metadata: HashMap<String, String>,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32pub enum IccColorSpace {
33 Rgb,
35 Cmyk,
37 Lab,
39 Gray,
41 Generic(u8),
43}
44
45impl IccColorSpace {
46 pub fn component_count(&self) -> u8 {
48 match self {
49 IccColorSpace::Gray => 1,
50 IccColorSpace::Rgb | IccColorSpace::Lab => 3,
51 IccColorSpace::Cmyk => 4,
52 IccColorSpace::Generic(n) => *n,
53 }
54 }
55
56 pub fn pdf_name(&self) -> &'static str {
58 match self {
59 IccColorSpace::Gray => "DeviceGray",
60 IccColorSpace::Rgb => "DeviceRGB",
61 IccColorSpace::Cmyk => "DeviceCMYK",
62 IccColorSpace::Lab => "Lab",
63 IccColorSpace::Generic(_) => "ICCBased",
64 }
65 }
66
67 pub fn default_range(&self) -> Vec<f64> {
69 match self {
70 IccColorSpace::Gray => vec![0.0, 1.0],
71 IccColorSpace::Rgb => vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
72 IccColorSpace::Cmyk => vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
73 IccColorSpace::Lab => vec![0.0, 100.0, -128.0, 127.0, -128.0, 127.0],
74 IccColorSpace::Generic(n) => {
75 let mut range = Vec::new();
76 for _ in 0..*n {
77 range.extend_from_slice(&[0.0, 1.0]);
78 }
79 range
80 }
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq)]
87pub enum StandardIccProfile {
88 SRgb,
90 AdobeRgb,
92 ProPhotoRgb,
94 UswcSwopV2,
96 CoatedFogra39,
98 UncoatedFogra29,
100 GrayGamma22,
102}
103
104impl StandardIccProfile {
105 pub fn color_space(&self) -> IccColorSpace {
107 match self {
108 StandardIccProfile::SRgb
109 | StandardIccProfile::AdobeRgb
110 | StandardIccProfile::ProPhotoRgb => IccColorSpace::Rgb,
111 StandardIccProfile::UswcSwopV2
112 | StandardIccProfile::CoatedFogra39
113 | StandardIccProfile::UncoatedFogra29 => IccColorSpace::Cmyk,
114 StandardIccProfile::GrayGamma22 => IccColorSpace::Gray,
115 }
116 }
117
118 pub fn profile_name(&self) -> &'static str {
120 match self {
121 StandardIccProfile::SRgb => "sRGB IEC61966-2.1",
122 StandardIccProfile::AdobeRgb => "Adobe RGB (1998)",
123 StandardIccProfile::ProPhotoRgb => "ProPhoto RGB",
124 StandardIccProfile::UswcSwopV2 => "U.S. Web Coated (SWOP) v2",
125 StandardIccProfile::CoatedFogra39 => "Coated FOGRA39 (ISO 12647-2:2004)",
126 StandardIccProfile::UncoatedFogra29 => "Uncoated FOGRA29 (ISO 12647-2:2004)",
127 StandardIccProfile::GrayGamma22 => "Generic Gray Gamma 2.2",
128 }
129 }
130
131 pub fn minimal_profile_data(&self) -> Vec<u8> {
134 let profile_name = self.profile_name();
137 let mut data = Vec::new();
138
139 data.extend_from_slice(b"ADSP"); data.extend_from_slice(&[0; 124]); data.extend_from_slice(profile_name.as_bytes());
145
146 while data.len() < 128 {
148 data.push(0);
149 }
150
151 data
152 }
153}
154
155impl IccProfile {
156 pub fn new(name: String, data: Vec<u8>, color_space: IccColorSpace) -> Self {
158 let components = color_space.component_count();
159
160 Self {
161 name,
162 data,
163 components,
164 color_space,
165 range: Some(color_space.default_range()),
166 metadata: HashMap::new(),
167 }
168 }
169
170 pub fn from_standard(profile: StandardIccProfile) -> Self {
172 let color_space = profile.color_space();
173 let data = profile.minimal_profile_data();
174
175 Self::new(profile.profile_name().to_string(), data, color_space)
176 }
177
178 pub fn with_range(mut self, range: Vec<f64>) -> Self {
180 let expected_len = (self.components as usize) * 2;
182 if range.len() == expected_len {
183 self.range = Some(range);
184 }
185 self
186 }
187
188 pub fn with_metadata(mut self, key: String, value: String) -> Self {
190 self.metadata.insert(key, value);
191 self
192 }
193
194 pub fn to_pdf_color_space_array(&self) -> Result<Vec<Object>> {
196 let mut array = Vec::new();
197
198 array.push(Object::Name("ICCBased".to_string()));
200
201 let mut icc_dict = Dictionary::new();
203 icc_dict.set("N", Object::Integer(self.components as i64));
204
205 icc_dict.set(
207 "Alternate",
208 Object::Name(self.color_space.pdf_name().to_string()),
209 );
210
211 if let Some(ref range) = self.range {
213 let range_objects: Vec<Object> = range.iter().map(|&x| Object::Real(x)).collect();
214 icc_dict.set("Range", Object::Array(range_objects));
215 }
216
217 if !self.metadata.is_empty() {
219 if let Some(desc) = self.metadata.get("Description") {
222 icc_dict.set("Description", Object::String(desc.clone()));
223 }
224 }
225
226 array.push(Object::Dictionary(icc_dict));
227
228 Ok(array)
229 }
230
231 pub fn validate(&self) -> Result<()> {
233 if self.data.is_empty() {
235 return Err(PdfError::InvalidStructure(
236 "ICC profile data cannot be empty".to_string(),
237 ));
238 }
239
240 if self.data.len() < 128 {
241 return Err(PdfError::InvalidStructure(
242 "ICC profile data too small (minimum 128 bytes)".to_string(),
243 ));
244 }
245
246 if self.components == 0 || self.components > 15 {
247 return Err(PdfError::InvalidStructure(
248 "Invalid number of color components".to_string(),
249 ));
250 }
251
252 if let Some(ref range) = self.range {
254 let expected_len = (self.components as usize) * 2;
255 if range.len() != expected_len {
256 return Err(PdfError::InvalidStructure(format!(
257 "Range array length {range_len} does not match expected {expected_len} for {components} components",
258 range_len = range.len(),
259 components = self.components
260 )));
261 }
262
263 for i in 0..self.components as usize {
265 let min = range[i * 2];
266 let max = range[i * 2 + 1];
267 if min > max {
268 return Err(PdfError::InvalidStructure(format!(
269 "Invalid range for component {i}: min {min} > max {max}"
270 )));
271 }
272 }
273 }
274
275 Ok(())
276 }
277
278 pub fn size(&self) -> usize {
280 self.data.len()
281 }
282
283 pub fn is_rgb(&self) -> bool {
285 matches!(self.color_space, IccColorSpace::Rgb)
286 }
287
288 pub fn is_cmyk(&self) -> bool {
290 matches!(self.color_space, IccColorSpace::Cmyk)
291 }
292
293 pub fn is_gray(&self) -> bool {
295 matches!(self.color_space, IccColorSpace::Gray)
296 }
297}
298
299#[derive(Debug, Clone)]
301pub struct IccProfileManager {
302 profiles: HashMap<String, IccProfile>,
304 next_id: usize,
306}
307
308impl Default for IccProfileManager {
309 fn default() -> Self {
310 Self::new()
311 }
312}
313
314impl IccProfileManager {
315 pub fn new() -> Self {
317 Self {
318 profiles: HashMap::new(),
319 next_id: 1,
320 }
321 }
322
323 pub fn add_profile(&mut self, mut profile: IccProfile) -> Result<String> {
325 profile.validate()?;
327
328 if profile.name.is_empty() || self.profiles.contains_key(&profile.name) {
330 profile.name = format!("ICC{}", self.next_id);
331 self.next_id += 1;
332 }
333
334 let name = profile.name.clone();
335 self.profiles.insert(name.clone(), profile);
336 Ok(name)
337 }
338
339 pub fn add_standard_profile(&mut self, standard_profile: StandardIccProfile) -> Result<String> {
341 let profile = IccProfile::from_standard(standard_profile);
342 self.add_profile(profile)
343 }
344
345 pub fn get_profile(&self, name: &str) -> Option<&IccProfile> {
347 self.profiles.get(name)
348 }
349
350 pub fn profiles(&self) -> &HashMap<String, IccProfile> {
352 &self.profiles
353 }
354
355 pub fn remove_profile(&mut self, name: &str) -> Option<IccProfile> {
357 self.profiles.remove(name)
358 }
359
360 pub fn clear(&mut self) {
362 self.profiles.clear();
363 self.next_id = 1;
364 }
365
366 pub fn count(&self) -> usize {
368 self.profiles.len()
369 }
370
371 pub fn to_resource_dictionary(&self) -> Result<String> {
373 if self.profiles.is_empty() {
374 return Ok(String::new());
375 }
376
377 let mut dict = String::from("/ColorSpace <<");
378
379 for name in self.profiles.keys() {
380 dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
382 }
383
384 dict.push_str(" >>");
385 Ok(dict)
386 }
387
388 pub fn get_profiles_by_type(&self, color_space: IccColorSpace) -> Vec<&IccProfile> {
390 self.profiles
391 .values()
392 .filter(|profile| profile.color_space == color_space)
393 .collect()
394 }
395
396 pub fn get_rgb_profiles(&self) -> Vec<&IccProfile> {
398 self.get_profiles_by_type(IccColorSpace::Rgb)
399 }
400
401 pub fn get_cmyk_profiles(&self) -> Vec<&IccProfile> {
403 self.get_profiles_by_type(IccColorSpace::Cmyk)
404 }
405
406 pub fn get_gray_profiles(&self) -> Vec<&IccProfile> {
408 self.get_profiles_by_type(IccColorSpace::Gray)
409 }
410
411 pub fn create_default_srgb(&mut self) -> Result<String> {
413 self.add_standard_profile(StandardIccProfile::SRgb)
414 }
415
416 pub fn create_default_cmyk(&mut self) -> Result<String> {
418 self.add_standard_profile(StandardIccProfile::CoatedFogra39)
419 }
420
421 pub fn create_default_gray(&mut self) -> Result<String> {
423 self.add_standard_profile(StandardIccProfile::GrayGamma22)
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_icc_color_space_component_count() {
433 assert_eq!(IccColorSpace::Gray.component_count(), 1);
434 assert_eq!(IccColorSpace::Rgb.component_count(), 3);
435 assert_eq!(IccColorSpace::Cmyk.component_count(), 4);
436 assert_eq!(IccColorSpace::Lab.component_count(), 3);
437 assert_eq!(IccColorSpace::Generic(5).component_count(), 5);
438 }
439
440 #[test]
441 fn test_icc_color_space_pdf_names() {
442 assert_eq!(IccColorSpace::Gray.pdf_name(), "DeviceGray");
443 assert_eq!(IccColorSpace::Rgb.pdf_name(), "DeviceRGB");
444 assert_eq!(IccColorSpace::Cmyk.pdf_name(), "DeviceCMYK");
445 assert_eq!(IccColorSpace::Lab.pdf_name(), "Lab");
446 assert_eq!(IccColorSpace::Generic(3).pdf_name(), "ICCBased");
447 }
448
449 #[test]
450 fn test_icc_color_space_default_range() {
451 let gray_range = IccColorSpace::Gray.default_range();
452 assert_eq!(gray_range, vec![0.0, 1.0]);
453
454 let rgb_range = IccColorSpace::Rgb.default_range();
455 assert_eq!(rgb_range, vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
456
457 let cmyk_range = IccColorSpace::Cmyk.default_range();
458 assert_eq!(cmyk_range, vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
459 }
460
461 #[test]
462 fn test_standard_icc_profile_properties() {
463 assert_eq!(StandardIccProfile::SRgb.color_space(), IccColorSpace::Rgb);
464 assert_eq!(
465 StandardIccProfile::UswcSwopV2.color_space(),
466 IccColorSpace::Cmyk
467 );
468 assert_eq!(
469 StandardIccProfile::GrayGamma22.color_space(),
470 IccColorSpace::Gray
471 );
472
473 assert_eq!(StandardIccProfile::SRgb.profile_name(), "sRGB IEC61966-2.1");
474 assert_eq!(
475 StandardIccProfile::AdobeRgb.profile_name(),
476 "Adobe RGB (1998)"
477 );
478 }
479
480 #[test]
481 fn test_standard_icc_profile_minimal_data() {
482 let profile_data = StandardIccProfile::SRgb.minimal_profile_data();
483 assert!(!profile_data.is_empty());
484 assert!(profile_data.len() >= 128);
485 }
486
487 #[test]
488 fn test_icc_profile_creation() {
489 let data = vec![0u8; 200];
490 let profile = IccProfile::new("TestProfile".to_string(), data.clone(), IccColorSpace::Rgb);
491
492 assert_eq!(profile.name, "TestProfile");
493 assert_eq!(profile.data, data);
494 assert_eq!(profile.components, 3);
495 assert_eq!(profile.color_space, IccColorSpace::Rgb);
496 assert!(profile.range.is_some());
497 }
498
499 #[test]
500 fn test_icc_profile_from_standard() {
501 let profile = IccProfile::from_standard(StandardIccProfile::SRgb);
502 assert_eq!(profile.name, "sRGB IEC61966-2.1");
503 assert_eq!(profile.color_space, IccColorSpace::Rgb);
504 assert_eq!(profile.components, 3);
505 assert!(!profile.data.is_empty());
506 }
507
508 #[test]
509 fn test_icc_profile_with_range() {
510 let data = vec![0u8; 200];
511 let custom_range = vec![0.0, 255.0, 0.0, 255.0, 0.0, 255.0];
512 let profile = IccProfile::new("TestProfile".to_string(), data, IccColorSpace::Rgb)
513 .with_range(custom_range.clone());
514
515 assert_eq!(profile.range, Some(custom_range));
516 }
517
518 #[test]
519 fn test_icc_profile_with_metadata() {
520 let data = vec![0u8; 200];
521 let profile = IccProfile::new("TestProfile".to_string(), data, IccColorSpace::Rgb)
522 .with_metadata("Description".to_string(), "Test RGB Profile".to_string());
523
524 assert_eq!(
525 profile.metadata.get("Description"),
526 Some(&"Test RGB Profile".to_string())
527 );
528 }
529
530 #[test]
531 fn test_icc_profile_validation_valid() {
532 let data = vec![0u8; 200];
533 let profile = IccProfile::new("TestProfile".to_string(), data, IccColorSpace::Rgb);
534
535 assert!(profile.validate().is_ok());
536 }
537
538 #[test]
539 fn test_icc_profile_validation_empty_data() {
540 let profile = IccProfile::new("TestProfile".to_string(), Vec::new(), IccColorSpace::Rgb);
541
542 assert!(profile.validate().is_err());
543 }
544
545 #[test]
546 fn test_icc_profile_validation_too_small() {
547 let data = vec![0u8; 50]; let profile = IccProfile::new("TestProfile".to_string(), data, IccColorSpace::Rgb);
549
550 assert!(profile.validate().is_err());
551 }
552
553 #[test]
554 fn test_icc_profile_validation_invalid_range() {
555 let data = vec![0u8; 200];
556 let invalid_range = vec![1.0, 0.0]; let profile = IccProfile::new("TestProfile".to_string(), data, IccColorSpace::Gray)
558 .with_range(invalid_range);
559
560 assert!(profile.validate().is_err());
561 }
562
563 #[test]
564 fn test_icc_profile_color_space_checks() {
565 let rgb_profile = IccProfile::from_standard(StandardIccProfile::SRgb);
566 assert!(rgb_profile.is_rgb());
567 assert!(!rgb_profile.is_cmyk());
568 assert!(!rgb_profile.is_gray());
569
570 let cmyk_profile = IccProfile::from_standard(StandardIccProfile::CoatedFogra39);
571 assert!(!cmyk_profile.is_rgb());
572 assert!(cmyk_profile.is_cmyk());
573 assert!(!cmyk_profile.is_gray());
574
575 let gray_profile = IccProfile::from_standard(StandardIccProfile::GrayGamma22);
576 assert!(!gray_profile.is_rgb());
577 assert!(!gray_profile.is_cmyk());
578 assert!(gray_profile.is_gray());
579 }
580
581 #[test]
582 fn test_icc_profile_to_pdf_color_space_array() {
583 let profile = IccProfile::from_standard(StandardIccProfile::SRgb);
584 let array = profile.to_pdf_color_space_array().unwrap();
585
586 assert_eq!(array.len(), 2);
587
588 if let Object::Name(name) = &array[0] {
589 assert_eq!(name, "ICCBased");
590 } else {
591 panic!("First element should be ICCBased name");
592 }
593
594 if let Object::Dictionary(dict) = &array[1] {
595 assert!(dict.contains_key("N"));
596 assert!(dict.contains_key("Alternate"));
597 } else {
598 panic!("Second element should be dictionary");
599 }
600 }
601
602 #[test]
603 fn test_icc_profile_manager_creation() {
604 let manager = IccProfileManager::new();
605 assert_eq!(manager.count(), 0);
606 assert!(manager.profiles().is_empty());
607 }
608
609 #[test]
610 fn test_icc_profile_manager_add_profile() {
611 let mut manager = IccProfileManager::new();
612 let profile = IccProfile::from_standard(StandardIccProfile::SRgb);
613
614 let name = manager.add_profile(profile).unwrap();
615 assert_eq!(name, "sRGB IEC61966-2.1");
616 assert_eq!(manager.count(), 1);
617
618 let retrieved = manager.get_profile(&name).unwrap();
619 assert_eq!(retrieved.name, "sRGB IEC61966-2.1");
620 }
621
622 #[test]
623 fn test_icc_profile_manager_add_standard() {
624 let mut manager = IccProfileManager::new();
625
626 let name = manager
627 .add_standard_profile(StandardIccProfile::SRgb)
628 .unwrap();
629 assert_eq!(name, "sRGB IEC61966-2.1");
630 assert_eq!(manager.count(), 1);
631 }
632
633 #[test]
634 fn test_icc_profile_manager_auto_naming() {
635 let mut manager = IccProfileManager::new();
636
637 let data = vec![0u8; 200];
638 let profile = IccProfile::new(
639 String::new(), data,
641 IccColorSpace::Rgb,
642 );
643
644 let name = manager.add_profile(profile).unwrap();
645 assert_eq!(name, "ICC1");
646
647 let data2 = vec![0u8; 200];
648 let profile2 = IccProfile::new(String::new(), data2, IccColorSpace::Cmyk);
649
650 let name2 = manager.add_profile(profile2).unwrap();
651 assert_eq!(name2, "ICC2");
652 }
653
654 #[test]
655 fn test_icc_profile_manager_get_by_type() {
656 let mut manager = IccProfileManager::new();
657
658 manager
659 .add_standard_profile(StandardIccProfile::SRgb)
660 .unwrap();
661 manager
662 .add_standard_profile(StandardIccProfile::AdobeRgb)
663 .unwrap();
664 manager
665 .add_standard_profile(StandardIccProfile::CoatedFogra39)
666 .unwrap();
667 manager
668 .add_standard_profile(StandardIccProfile::GrayGamma22)
669 .unwrap();
670
671 let rgb_profiles = manager.get_rgb_profiles();
672 assert_eq!(rgb_profiles.len(), 2);
673
674 let cmyk_profiles = manager.get_cmyk_profiles();
675 assert_eq!(cmyk_profiles.len(), 1);
676
677 let gray_profiles = manager.get_gray_profiles();
678 assert_eq!(gray_profiles.len(), 1);
679 }
680
681 #[test]
682 fn test_icc_profile_manager_defaults() {
683 let mut manager = IccProfileManager::new();
684
685 let srgb_name = manager.create_default_srgb().unwrap();
686 let cmyk_name = manager.create_default_cmyk().unwrap();
687 let gray_name = manager.create_default_gray().unwrap();
688
689 assert_eq!(manager.count(), 3);
690 assert!(manager.get_profile(&srgb_name).unwrap().is_rgb());
691 assert!(manager.get_profile(&cmyk_name).unwrap().is_cmyk());
692 assert!(manager.get_profile(&gray_name).unwrap().is_gray());
693 }
694
695 #[test]
696 fn test_icc_profile_manager_clear() {
697 let mut manager = IccProfileManager::new();
698
699 manager
700 .add_standard_profile(StandardIccProfile::SRgb)
701 .unwrap();
702 manager
703 .add_standard_profile(StandardIccProfile::CoatedFogra39)
704 .unwrap();
705 assert_eq!(manager.count(), 2);
706
707 manager.clear();
708 assert_eq!(manager.count(), 0);
709 assert!(manager.profiles().is_empty());
710 }
711
712 #[test]
713 fn test_icc_profile_manager_remove() {
714 let mut manager = IccProfileManager::new();
715
716 let name = manager
717 .add_standard_profile(StandardIccProfile::SRgb)
718 .unwrap();
719 assert_eq!(manager.count(), 1);
720
721 let removed = manager.remove_profile(&name);
722 assert!(removed.is_some());
723 assert_eq!(manager.count(), 0);
724
725 let not_found = manager.remove_profile(&name);
727 assert!(not_found.is_none());
728 }
729}