Skip to main content

pdfplumber_parse/
color_space.rs

1//! Color space resolution for advanced PDF color spaces.
2//!
3//! Resolves ICCBased, Indexed, Separation, and DeviceN color spaces
4//! from PDF resource dictionaries to concrete `Color` values.
5
6use pdfplumber_core::painting::Color;
7
8/// A resolved PDF color space with enough information to convert
9/// component values to a `Color`.
10#[derive(Debug, Clone)]
11pub enum ResolvedColorSpace {
12    /// DeviceGray (1 component).
13    DeviceGray,
14    /// DeviceRGB (3 components).
15    DeviceRGB,
16    /// DeviceCMYK (4 components).
17    DeviceCMYK,
18    /// ICCBased color space: stores the number of components and the
19    /// alternate color space to use for conversion.
20    ICCBased {
21        /// Number of color components (1, 3, or 4).
22        num_components: u32,
23        /// Alternate color space for fallback conversion.
24        alternate: Box<ResolvedColorSpace>,
25    },
26    /// Indexed color space: a palette-based color space.
27    Indexed {
28        /// Base color space that palette entries are specified in.
29        base: Box<ResolvedColorSpace>,
30        /// Maximum valid index value.
31        hival: u32,
32        /// Color lookup table: `(hival + 1) * num_base_components` bytes.
33        lookup_table: Vec<u8>,
34    },
35    /// Separation color space (single-component spot color).
36    /// Best-effort: uses the alternate color space.
37    Separation {
38        /// Alternate color space (fallback).
39        alternate: Box<ResolvedColorSpace>,
40    },
41    /// DeviceN color space (multi-component named colors).
42    /// Best-effort: uses the alternate color space.
43    DeviceN {
44        /// Number of color components.
45        num_components: u32,
46        /// Alternate color space (fallback).
47        alternate: Box<ResolvedColorSpace>,
48    },
49}
50
51impl ResolvedColorSpace {
52    /// Number of components expected for this color space.
53    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    /// Convert color components to a `Color` value using this color space.
66    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                // Use the alternate color space for interpretation
87                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                // Best-effort: pass the tint value through to the alternate space.
110                // Without evaluating the tint transform function, we use a simple
111                // approximation: treat the single tint component as if it's
112                // a grayscale value in the alternate space.
113                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                // Best-effort: pass components to alternate space
123                alternate.resolve_color(components)
124            }
125        }
126    }
127}
128
129/// Default color space inferred from component count (fallback behavior).
130pub 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, // fallback
136    }
137}
138
139/// Infer the alternate color space from the number of ICC profile components.
140fn 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, // default fallback
146    }
147}
148
149/// Resolve a color space name to a `ResolvedColorSpace`.
150///
151/// Handles both simple device color spaces (DeviceGray, DeviceRGB, DeviceCMYK)
152/// and named color spaces looked up in the page Resources.
153pub 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            // Look up in Resources /ColorSpace dictionary
164            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
174/// Resolve a color space from a lopdf Object (name or array).
175pub 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
201/// Resolve a color space array like [/ICCBased stream_ref] or [/Indexed base hival lookup].
202fn 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
227/// Resolve [/ICCBased stream_ref].
228fn resolve_icc_based(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
229    if arr.len() < 2 {
230        return None;
231    }
232
233    // Get the ICC profile stream
234    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    // Get /N (number of components)
245    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); // default to 3 (RGB)
254
255    // Try to get /Alternate color space
256    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
269/// Resolve [/Indexed base hival lookup].
270fn resolve_indexed(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
271    if arr.len() < 4 {
272        return None;
273    }
274
275    // Base color space
276    let base = resolve_color_space_object(&arr[1], doc)
277        .or_else(|| {
278            // Try resolving as reference
279            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    // hival (maximum valid index)
290    let hival = match &arr[2] {
291        lopdf::Object::Integer(n) => *n as u32,
292        _ => return None,
293    };
294
295    // Lookup table - can be a string or a stream
296    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
324/// Resolve [/Separation name alternateSpace tintTransform].
325fn resolve_separation(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
326    if arr.len() < 4 {
327        return None;
328    }
329
330    // alternateSpace is at index 2
331    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
339/// Resolve [/DeviceN names alternateSpace tintTransform].
340fn resolve_device_n(arr: &[lopdf::Object], doc: &lopdf::Document) -> Option<ResolvedColorSpace> {
341    if arr.len() < 4 {
342        return None;
343    }
344
345    // names is an array at index 1
346    let num_components = match &arr[1] {
347        lopdf::Object::Array(names) => names.len() as u32,
348        _ => return None,
349    };
350
351    // alternateSpace is at index 2
352    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    // --- ResolvedColorSpace::resolve_color tests ---
367
368    #[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        // Indexed with DeviceRGB base, 2 colors in palette
431        let cs = ResolvedColorSpace::Indexed {
432            base: Box::new(ResolvedColorSpace::DeviceRGB),
433            hival: 1,
434            lookup_table: vec![
435                255, 0, 0, // index 0: red
436                0, 255, 0, // index 1: green
437            ],
438        };
439        assert_eq!(cs.num_components(), 1);
440
441        // Look up index 0 → red
442        let color = cs.resolve_color(&[0.0]);
443        assert_eq!(color, Color::Rgb(1.0, 0.0, 0.0));
444
445        // Look up index 1 → green
446        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        // Index 5 should be clamped to hival=1
458        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        // Tint 1.0 → full color → K=0
469        let color = cs.resolve_color(&[1.0]);
470        assert_eq!(color, Color::Cmyk(0.0, 0.0, 0.0, 0.0));
471        // Tint 0.0 → no color → K=1
472        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        // Tint 0.5 → gray in RGB
482        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        // Components passed through to alternate
494        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    // --- Color space name resolution tests ---
506
507    #[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    // --- Color space object resolution tests ---
545
546    #[test]
547    fn resolve_icc_based_from_array() {
548        let mut doc = lopdf::Document::with_version("1.5");
549
550        // Create an ICC profile stream with /N=3
551        let icc_stream = Stream::new(
552            dictionary! {
553                "N" => Object::Integer(3),
554            },
555            vec![0u8; 10], // dummy ICC profile data
556        );
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        // Should resolve color as RGB via alternate
567        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        // [/Indexed /DeviceRGB 1 <FF0000 00FF00>]
604        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        // [/Separation /SpotColor /DeviceCMYK <tintTransform>]
621        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, // tint transform (ignored in best-effort)
626        ];
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        // [/DeviceN [/Cyan /Magenta] /DeviceCMYK <tintTransform>]
637        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, // tint transform
645        ];
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        // Create an ICC profile stream
656        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        // Resources with ColorSpace dictionary
665        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}