1use pdfplumber_core::painting::Color;
7
8#[derive(Debug, Clone)]
11pub enum ResolvedColorSpace {
12 DeviceGray,
14 DeviceRGB,
16 DeviceCMYK,
18 ICCBased {
21 num_components: u32,
23 alternate: Box<ResolvedColorSpace>,
25 },
26 Indexed {
28 base: Box<ResolvedColorSpace>,
30 hival: u32,
32 lookup_table: Vec<u8>,
34 },
35 Separation {
38 alternate: Box<ResolvedColorSpace>,
40 },
41 DeviceN {
44 num_components: u32,
46 alternate: Box<ResolvedColorSpace>,
48 },
49}
50
51impl ResolvedColorSpace {
52 pub fn num_components(&self) -> u32 {
54 match self {
55 ResolvedColorSpace::DeviceGray => 1,
56 ResolvedColorSpace::DeviceRGB => 3,
57 ResolvedColorSpace::DeviceCMYK => 4,
58 ResolvedColorSpace::ICCBased { num_components, .. } => *num_components,
59 ResolvedColorSpace::Indexed { .. } => 1,
60 ResolvedColorSpace::Separation { .. } => 1,
61 ResolvedColorSpace::DeviceN { num_components, .. } => *num_components,
62 }
63 }
64
65 pub fn resolve_color(&self, components: &[f32]) -> Color {
67 match self {
68 ResolvedColorSpace::DeviceGray => {
69 let g = components.first().copied().unwrap_or(0.0);
70 Color::Gray(g)
71 }
72 ResolvedColorSpace::DeviceRGB => {
73 let r = components.first().copied().unwrap_or(0.0);
74 let g = components.get(1).copied().unwrap_or(0.0);
75 let b = components.get(2).copied().unwrap_or(0.0);
76 Color::Rgb(r, g, b)
77 }
78 ResolvedColorSpace::DeviceCMYK => {
79 let c = components.first().copied().unwrap_or(0.0);
80 let m = components.get(1).copied().unwrap_or(0.0);
81 let y = components.get(2).copied().unwrap_or(0.0);
82 let k = components.get(3).copied().unwrap_or(0.0);
83 Color::Cmyk(c, m, y, k)
84 }
85 ResolvedColorSpace::ICCBased { alternate, .. } => {
86 alternate.resolve_color(components)
88 }
89 ResolvedColorSpace::Indexed {
90 base,
91 hival,
92 lookup_table,
93 } => {
94 let index = components.first().copied().unwrap_or(0.0) as u32;
95 let index = index.min(*hival);
96 let base_n = base.num_components() as usize;
97 let offset = index as usize * base_n;
98 if offset + base_n <= lookup_table.len() {
99 let base_components: Vec<f32> = lookup_table[offset..offset + base_n]
100 .iter()
101 .map(|&b| b as f32 / 255.0)
102 .collect();
103 base.resolve_color(&base_components)
104 } else {
105 Color::Other(components.to_vec())
106 }
107 }
108 ResolvedColorSpace::Separation { alternate } => {
109 let tint = components.first().copied().unwrap_or(0.0);
114 match alternate.as_ref() {
115 ResolvedColorSpace::DeviceGray => Color::Gray(tint),
116 ResolvedColorSpace::DeviceRGB => Color::Rgb(tint, tint, tint),
117 ResolvedColorSpace::DeviceCMYK => Color::Cmyk(0.0, 0.0, 0.0, 1.0 - tint),
118 _ => Color::Other(components.to_vec()),
119 }
120 }
121 ResolvedColorSpace::DeviceN { alternate, .. } => {
122 alternate.resolve_color(components)
124 }
125 }
126 }
127}
128
129pub fn default_color_space_from_components(n: usize) -> ResolvedColorSpace {
131 match n {
132 1 => ResolvedColorSpace::DeviceGray,
133 3 => ResolvedColorSpace::DeviceRGB,
134 4 => ResolvedColorSpace::DeviceCMYK,
135 _ => ResolvedColorSpace::DeviceGray, }
137}
138
139fn alternate_from_num_components(n: u32) -> ResolvedColorSpace {
141 match n {
142 1 => ResolvedColorSpace::DeviceGray,
143 3 => ResolvedColorSpace::DeviceRGB,
144 4 => ResolvedColorSpace::DeviceCMYK,
145 _ => ResolvedColorSpace::DeviceRGB, }
147}
148
149pub fn resolve_color_space_name(
154 name: &str,
155 doc: &lopdf::Document,
156 resources: &lopdf::Dictionary,
157) -> Option<ResolvedColorSpace> {
158 match name {
159 "DeviceGray" | "G" => Some(ResolvedColorSpace::DeviceGray),
160 "DeviceRGB" | "RGB" => Some(ResolvedColorSpace::DeviceRGB),
161 "DeviceCMYK" | "CMYK" => Some(ResolvedColorSpace::DeviceCMYK),
162 _ => {
163 if let Ok(cs_dict) = resources.get(b"ColorSpace").and_then(|o| o.as_dict()) {
165 if let Ok(cs_obj) = cs_dict.get(name.as_bytes()) {
166 return resolve_color_space_object(cs_obj, doc);
167 }
168 }
169 None
170 }
171 }
172}
173
174pub fn resolve_color_space_object(
176 obj: &lopdf::Object,
177 doc: &lopdf::Document,
178) -> Option<ResolvedColorSpace> {
179 match obj {
180 lopdf::Object::Name(name) => {
181 let name_str = String::from_utf8_lossy(name);
182 match name_str.as_ref() {
183 "DeviceGray" | "G" => Some(ResolvedColorSpace::DeviceGray),
184 "DeviceRGB" | "RGB" => Some(ResolvedColorSpace::DeviceRGB),
185 "DeviceCMYK" | "CMYK" => Some(ResolvedColorSpace::DeviceCMYK),
186 _ => None,
187 }
188 }
189 lopdf::Object::Array(arr) => resolve_color_space_array(arr, doc),
190 lopdf::Object::Reference(id) => {
191 if let Ok(resolved) = doc.get_object(*id) {
192 resolve_color_space_object(resolved, doc)
193 } else {
194 None
195 }
196 }
197 _ => None,
198 }
199}
200
201fn resolve_color_space_array(
203 arr: &[lopdf::Object],
204 doc: &lopdf::Document,
205) -> Option<ResolvedColorSpace> {
206 if arr.is_empty() {
207 return None;
208 }
209
210 let cs_type = match &arr[0] {
211 lopdf::Object::Name(n) => String::from_utf8_lossy(n).to_string(),
212 _ => return None,
213 };
214
215 match cs_type.as_str() {
216 "ICCBased" => resolve_icc_based(arr, doc),
217 "Indexed" | "I" => resolve_indexed(arr, doc),
218 "Separation" => resolve_separation(arr, doc),
219 "DeviceN" => resolve_device_n(arr, doc),
220 "DeviceGray" | "G" => Some(ResolvedColorSpace::DeviceGray),
221 "DeviceRGB" | "RGB" => Some(ResolvedColorSpace::DeviceRGB),
222 "DeviceCMYK" | "CMYK" => Some(ResolvedColorSpace::DeviceCMYK),
223 _ => None,
224 }
225}
226
227fn resolve_icc_based(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
229 if arr.len() < 2 {
230 return None;
231 }
232
233 let stream_obj = match &arr[1] {
235 lopdf::Object::Reference(id) => doc.get_object(*id).ok()?,
236 other => other,
237 };
238
239 let stream = match stream_obj {
240 lopdf::Object::Stream(s) => s,
241 _ => return None,
242 };
243
244 let num_components = stream
246 .dict
247 .get(b"N")
248 .ok()
249 .and_then(|o| match o {
250 lopdf::Object::Integer(n) => Some(*n as u32),
251 _ => None,
252 })
253 .unwrap_or(3); let alternate = stream
257 .dict
258 .get(b"Alternate")
259 .ok()
260 .and_then(|o| resolve_color_space_object(o, doc))
261 .unwrap_or_else(|| alternate_from_num_components(num_components));
262
263 Some(ResolvedColorSpace::ICCBased {
264 num_components,
265 alternate: Box::new(alternate),
266 })
267}
268
269fn resolve_indexed(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
271 if arr.len() < 4 {
272 return None;
273 }
274
275 let base = resolve_color_space_object(&arr[1], doc)
277 .or_else(|| {
278 if let lopdf::Object::Reference(id) = &arr[1] {
280 doc.get_object(*id)
281 .ok()
282 .and_then(|o| resolve_color_space_object(o, doc))
283 } else {
284 None
285 }
286 })
287 .unwrap_or(ResolvedColorSpace::DeviceRGB);
288
289 let hival = match &arr[2] {
291 lopdf::Object::Integer(n) => *n as u32,
292 _ => return None,
293 };
294
295 let lookup_table = match &arr[3] {
297 lopdf::Object::String(bytes, _) => bytes.clone(),
298 lopdf::Object::Reference(id) => {
299 if let Ok(obj) = doc.get_object(*id) {
300 match obj {
301 lopdf::Object::Stream(s) => s
302 .decompressed_content()
303 .unwrap_or_else(|_| s.content.clone()),
304 lopdf::Object::String(bytes, _) => bytes.clone(),
305 _ => return None,
306 }
307 } else {
308 return None;
309 }
310 }
311 lopdf::Object::Stream(s) => s
312 .decompressed_content()
313 .unwrap_or_else(|_| s.content.clone()),
314 _ => return None,
315 };
316
317 Some(ResolvedColorSpace::Indexed {
318 base: Box::new(base),
319 hival,
320 lookup_table,
321 })
322}
323
324fn resolve_separation(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
326 if arr.len() < 4 {
327 return None;
328 }
329
330 let alternate =
332 resolve_color_space_object(&arr[2], doc).unwrap_or(ResolvedColorSpace::DeviceCMYK);
333
334 Some(ResolvedColorSpace::Separation {
335 alternate: Box::new(alternate),
336 })
337}
338
339fn resolve_device_n(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
341 if arr.len() < 4 {
342 return None;
343 }
344
345 let num_components = match &arr[1] {
347 lopdf::Object::Array(names) => names.len() as u32,
348 _ => return None,
349 };
350
351 let alternate =
353 resolve_color_space_object(&arr[2], doc).unwrap_or(ResolvedColorSpace::DeviceCMYK);
354
355 Some(ResolvedColorSpace::DeviceN {
356 num_components,
357 alternate: Box::new(alternate),
358 })
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use lopdf::{Object, Stream, dictionary};
365
366 #[test]
369 fn resolve_device_gray() {
370 let cs = ResolvedColorSpace::DeviceGray;
371 assert_eq!(cs.resolve_color(&[0.5]), Color::Gray(0.5));
372 }
373
374 #[test]
375 fn resolve_device_rgb() {
376 let cs = ResolvedColorSpace::DeviceRGB;
377 assert_eq!(
378 cs.resolve_color(&[0.1, 0.2, 0.3]),
379 Color::Rgb(0.1, 0.2, 0.3)
380 );
381 }
382
383 #[test]
384 fn resolve_device_cmyk() {
385 let cs = ResolvedColorSpace::DeviceCMYK;
386 assert_eq!(
387 cs.resolve_color(&[0.1, 0.2, 0.3, 0.4]),
388 Color::Cmyk(0.1, 0.2, 0.3, 0.4)
389 );
390 }
391
392 #[test]
393 fn resolve_icc_based_3_components_as_rgb() {
394 let cs = ResolvedColorSpace::ICCBased {
395 num_components: 3,
396 alternate: Box::new(ResolvedColorSpace::DeviceRGB),
397 };
398 assert_eq!(cs.num_components(), 3);
399 assert_eq!(
400 cs.resolve_color(&[0.5, 0.6, 0.7]),
401 Color::Rgb(0.5, 0.6, 0.7)
402 );
403 }
404
405 #[test]
406 fn resolve_icc_based_1_component_as_gray() {
407 let cs = ResolvedColorSpace::ICCBased {
408 num_components: 1,
409 alternate: Box::new(ResolvedColorSpace::DeviceGray),
410 };
411 assert_eq!(cs.num_components(), 1);
412 assert_eq!(cs.resolve_color(&[0.3]), Color::Gray(0.3));
413 }
414
415 #[test]
416 fn resolve_icc_based_4_components_as_cmyk() {
417 let cs = ResolvedColorSpace::ICCBased {
418 num_components: 4,
419 alternate: Box::new(ResolvedColorSpace::DeviceCMYK),
420 };
421 assert_eq!(cs.num_components(), 4);
422 assert_eq!(
423 cs.resolve_color(&[0.1, 0.2, 0.3, 0.4]),
424 Color::Cmyk(0.1, 0.2, 0.3, 0.4)
425 );
426 }
427
428 #[test]
429 fn resolve_indexed_lookup() {
430 let cs = ResolvedColorSpace::Indexed {
432 base: Box::new(ResolvedColorSpace::DeviceRGB),
433 hival: 1,
434 lookup_table: vec![
435 255, 0, 0, 0, 255, 0, ],
438 };
439 assert_eq!(cs.num_components(), 1);
440
441 let color = cs.resolve_color(&[0.0]);
443 assert_eq!(color, Color::Rgb(1.0, 0.0, 0.0));
444
445 let color = cs.resolve_color(&[1.0]);
447 assert_eq!(color, Color::Rgb(0.0, 1.0, 0.0));
448 }
449
450 #[test]
451 fn resolve_indexed_clamps_to_hival() {
452 let cs = ResolvedColorSpace::Indexed {
453 base: Box::new(ResolvedColorSpace::DeviceRGB),
454 hival: 1,
455 lookup_table: vec![255, 0, 0, 0, 0, 255],
456 };
457 let color = cs.resolve_color(&[5.0]);
459 assert_eq!(color, Color::Rgb(0.0, 0.0, 1.0));
460 }
461
462 #[test]
463 fn resolve_separation_with_cmyk_alternate() {
464 let cs = ResolvedColorSpace::Separation {
465 alternate: Box::new(ResolvedColorSpace::DeviceCMYK),
466 };
467 assert_eq!(cs.num_components(), 1);
468 let color = cs.resolve_color(&[1.0]);
470 assert_eq!(color, Color::Cmyk(0.0, 0.0, 0.0, 0.0));
471 let color = cs.resolve_color(&[0.0]);
473 assert_eq!(color, Color::Cmyk(0.0, 0.0, 0.0, 1.0));
474 }
475
476 #[test]
477 fn resolve_separation_with_rgb_alternate() {
478 let cs = ResolvedColorSpace::Separation {
479 alternate: Box::new(ResolvedColorSpace::DeviceRGB),
480 };
481 let color = cs.resolve_color(&[0.5]);
483 assert_eq!(color, Color::Rgb(0.5, 0.5, 0.5));
484 }
485
486 #[test]
487 fn resolve_device_n_with_alternate() {
488 let cs = ResolvedColorSpace::DeviceN {
489 num_components: 2,
490 alternate: Box::new(ResolvedColorSpace::DeviceRGB),
491 };
492 assert_eq!(cs.num_components(), 2);
493 let color = cs.resolve_color(&[0.3, 0.7, 0.5]);
495 assert_eq!(color, Color::Rgb(0.3, 0.7, 0.5));
496 }
497
498 #[test]
499 fn num_components_correct() {
500 assert_eq!(ResolvedColorSpace::DeviceGray.num_components(), 1);
501 assert_eq!(ResolvedColorSpace::DeviceRGB.num_components(), 3);
502 assert_eq!(ResolvedColorSpace::DeviceCMYK.num_components(), 4);
503 }
504
505 #[test]
508 fn resolve_name_device_gray() {
509 let doc = lopdf::Document::with_version("1.5");
510 let resources = dictionary! {};
511 assert!(matches!(
512 resolve_color_space_name("DeviceGray", &doc, &resources),
513 Some(ResolvedColorSpace::DeviceGray)
514 ));
515 }
516
517 #[test]
518 fn resolve_name_device_rgb() {
519 let doc = lopdf::Document::with_version("1.5");
520 let resources = dictionary! {};
521 assert!(matches!(
522 resolve_color_space_name("DeviceRGB", &doc, &resources),
523 Some(ResolvedColorSpace::DeviceRGB)
524 ));
525 }
526
527 #[test]
528 fn resolve_name_device_cmyk() {
529 let doc = lopdf::Document::with_version("1.5");
530 let resources = dictionary! {};
531 assert!(matches!(
532 resolve_color_space_name("DeviceCMYK", &doc, &resources),
533 Some(ResolvedColorSpace::DeviceCMYK)
534 ));
535 }
536
537 #[test]
538 fn resolve_name_unknown_returns_none() {
539 let doc = lopdf::Document::with_version("1.5");
540 let resources = dictionary! {};
541 assert!(resolve_color_space_name("UnknownCS", &doc, &resources).is_none());
542 }
543
544 #[test]
547 fn resolve_icc_based_from_array() {
548 let mut doc = lopdf::Document::with_version("1.5");
549
550 let icc_stream = Stream::new(
552 dictionary! {
553 "N" => Object::Integer(3),
554 },
555 vec![0u8; 10], );
557 let icc_id = doc.add_object(icc_stream);
558
559 let arr = vec![
560 Object::Name(b"ICCBased".to_vec()),
561 Object::Reference(icc_id),
562 ];
563
564 let cs = resolve_color_space_array(&arr, &doc).unwrap();
565 assert_eq!(cs.num_components(), 3);
566 assert_eq!(
568 cs.resolve_color(&[0.5, 0.6, 0.7]),
569 Color::Rgb(0.5, 0.6, 0.7)
570 );
571 }
572
573 #[test]
574 fn resolve_icc_based_with_alternate() {
575 let mut doc = lopdf::Document::with_version("1.5");
576
577 let icc_stream = Stream::new(
578 dictionary! {
579 "N" => Object::Integer(4),
580 "Alternate" => Object::Name(b"DeviceCMYK".to_vec()),
581 },
582 vec![0u8; 10],
583 );
584 let icc_id = doc.add_object(icc_stream);
585
586 let arr = vec![
587 Object::Name(b"ICCBased".to_vec()),
588 Object::Reference(icc_id),
589 ];
590
591 let cs = resolve_color_space_array(&arr, &doc).unwrap();
592 assert_eq!(cs.num_components(), 4);
593 assert_eq!(
594 cs.resolve_color(&[0.1, 0.2, 0.3, 0.4]),
595 Color::Cmyk(0.1, 0.2, 0.3, 0.4)
596 );
597 }
598
599 #[test]
600 fn resolve_indexed_from_array() {
601 let doc = lopdf::Document::with_version("1.5");
602
603 let arr = vec![
605 Object::Name(b"Indexed".to_vec()),
606 Object::Name(b"DeviceRGB".to_vec()),
607 Object::Integer(1),
608 Object::String(vec![255, 0, 0, 0, 255, 0], lopdf::StringFormat::Hexadecimal),
609 ];
610
611 let cs = resolve_color_space_array(&arr, &doc).unwrap();
612 assert_eq!(cs.resolve_color(&[0.0]), Color::Rgb(1.0, 0.0, 0.0));
613 assert_eq!(cs.resolve_color(&[1.0]), Color::Rgb(0.0, 1.0, 0.0));
614 }
615
616 #[test]
617 fn resolve_separation_from_array() {
618 let doc = lopdf::Document::with_version("1.5");
619
620 let arr = vec![
622 Object::Name(b"Separation".to_vec()),
623 Object::Name(b"SpotColor".to_vec()),
624 Object::Name(b"DeviceCMYK".to_vec()),
625 Object::Null, ];
627
628 let cs = resolve_color_space_array(&arr, &doc).unwrap();
629 assert_eq!(cs.num_components(), 1);
630 }
631
632 #[test]
633 fn resolve_device_n_from_array() {
634 let doc = lopdf::Document::with_version("1.5");
635
636 let arr = vec![
638 Object::Name(b"DeviceN".to_vec()),
639 Object::Array(vec![
640 Object::Name(b"Cyan".to_vec()),
641 Object::Name(b"Magenta".to_vec()),
642 ]),
643 Object::Name(b"DeviceCMYK".to_vec()),
644 Object::Null, ];
646
647 let cs = resolve_color_space_array(&arr, &doc).unwrap();
648 assert_eq!(cs.num_components(), 2);
649 }
650
651 #[test]
652 fn resolve_named_color_space_from_resources() {
653 let mut doc = lopdf::Document::with_version("1.5");
654
655 let icc_stream = Stream::new(
657 dictionary! {
658 "N" => Object::Integer(3),
659 },
660 vec![0u8; 10],
661 );
662 let icc_id = doc.add_object(icc_stream);
663
664 let resources = dictionary! {
666 "ColorSpace" => dictionary! {
667 "CS1" => Object::Array(vec![
668 Object::Name(b"ICCBased".to_vec()),
669 Object::Reference(icc_id),
670 ]),
671 },
672 };
673
674 let cs = resolve_color_space_name("CS1", &doc, &resources).unwrap();
675 assert_eq!(cs.num_components(), 3);
676 }
677}