1use crate::error::{PdfError, Result};
8use crate::objects::{Dictionary, Object};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq)]
20pub struct DeviceNColorSpace {
21 pub colorant_names: Vec<String>,
23 pub alternate_space: AlternateColorSpace,
25 pub tint_transform: TintTransformFunction,
27 pub attributes: Option<DeviceNAttributes>,
29}
30
31#[derive(Debug, Clone, PartialEq)]
33pub enum AlternateColorSpace {
34 DeviceRGB,
36 DeviceCMYK,
38 DeviceGray,
40 CIEBased(String),
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum TintTransformFunction {
47 Linear(LinearTransform),
49 Function(Vec<u8>),
51 Sampled(SampledFunction),
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct LinearTransform {
58 pub matrix: Vec<Vec<f64>>,
60 pub black_generation: Option<Vec<f64>>,
62 pub undercolor_removal: Option<Vec<f64>>,
64}
65
66#[derive(Debug, Clone, PartialEq)]
68pub struct SampledFunction {
69 pub domain: Vec<(f64, f64)>,
71 pub range: Vec<(f64, f64)>,
73 pub size: Vec<usize>,
75 pub samples: Vec<u8>,
77 pub bits_per_sample: u8,
79 pub order: u8,
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub struct DeviceNAttributes {
86 pub colorants: HashMap<String, ColorantDefinition>,
88 pub process: Option<String>,
90 pub mix: Option<String>,
92 pub dot_gain: HashMap<String, Vec<f64>>,
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub struct ColorantDefinition {
99 pub colorant_type: ColorantType,
101 pub cmyk_equivalent: Option<[f64; 4]>,
103 pub rgb_approximation: Option<[f64; 3]>,
105 pub lab_color: Option<[f64; 3]>,
107 pub density: Option<f64>,
109}
110
111#[derive(Debug, Clone, PartialEq)]
113pub enum ColorantType {
114 Process,
116 Spot,
118 Special,
120}
121
122impl DeviceNColorSpace {
123 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 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 let n_colorants = colorants.len();
149 let mut matrix = vec![vec![0.0; 4]; n_colorants]; for (i, row) in matrix.iter_mut().enumerate().take(4) {
153 row[i] = 1.0;
154 }
155
156 for row in matrix.iter_mut().skip(4).take(n_colorants - 4) {
158 row[3] = 1.0; }
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 pub fn with_attributes(mut self, attributes: DeviceNAttributes) -> Self {
174 self.attributes = Some(attributes);
175 self
176 }
177
178 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 self.linear_approximation(devicen_values)
194 }
195 TintTransformFunction::Sampled(sampled) => {
196 self.apply_sampled_function(devicen_values, sampled)
197 }
198 }
199 }
200
201 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, };
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 for val in &mut output {
235 *val = val.clamp(0.0, 1.0);
236 }
237
238 Ok(output)
239 }
240
241 fn linear_approximation(&self, input: &[f64]) -> Result<Vec<f64>> {
243 match self.alternate_space {
244 AlternateColorSpace::DeviceRGB => {
245 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 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 Ok(vec![50.0, 0.0, 0.0])
264 }
265 }
266 }
267
268 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 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 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 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 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, }
332 }
333
334 pub fn colorant_count(&self) -> usize {
336 self.colorant_names.len()
337 }
338
339 pub fn colorant_name(&self, index: usize) -> Option<&str> {
341 self.colorant_names.get(index).map(|s| s.as_str())
342 }
343
344 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 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 pub fn to_pdf_object(&self) -> Object {
370 let mut array = Vec::new();
371
372 array.push(Object::Name("DeviceN".to_string()));
374
375 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 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 match &self.tint_transform {
393 TintTransformFunction::Function(data) => {
394 let mut func_dict = Dictionary::new();
395 func_dict.set("FunctionType", Object::Integer(4)); 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 let mut func_dict = Dictionary::new();
404 func_dict.set("FunctionType", Object::Integer(2)); 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)); array.push(Object::Dictionary(func_dict));
410 }
411 }
412
413 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 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 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 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 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 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 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), }
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], vec![0.0, 1.0, 0.0], ],
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); }
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]; 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); assert!((rgb[1] - 1.0).abs() < 0.001); assert!((rgb[2] - 1.0).abs() < 0.001); }
740
741 #[test]
742 fn test_colorant_definition_spot() {
743 let cmyk = [0.0, 1.0, 1.0, 0.0]; 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]; 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]; 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); 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![]), );
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 assert_eq!(result[0], 50.0);
904 assert_eq!(result[1], 0.0);
905 assert_eq!(result[2], 0.0);
906 }
907}