hayro_interpret/
color.rs

1//! PDF colors and color spaces.
2
3use crate::cache::Cache;
4use crate::function::Function;
5use hayro_syntax::object;
6use hayro_syntax::object::Array;
7use hayro_syntax::object::Dict;
8use hayro_syntax::object::Name;
9use hayro_syntax::object::Object;
10use hayro_syntax::object::Stream;
11use hayro_syntax::object::dict::keys::*;
12use log::warn;
13use moxcms::{ColorProfile, DataColorSpace, Layout, Transform8BitExecutor, TransformOptions};
14use smallvec::{SmallVec, ToSmallVec, smallvec};
15use std::fmt::{Debug, Formatter};
16use std::ops::Deref;
17use std::sync::{Arc, LazyLock};
18
19/// A storage for the components of colors.
20pub type ColorComponents = SmallVec<[f32; 4]>;
21
22/// An RGB color with an alpha channel.
23#[derive(Debug, Copy, Clone)]
24pub struct AlphaColor {
25    components: [f32; 4],
26}
27
28impl AlphaColor {
29    /// A black color.
30    pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
31
32    /// A transparent color.
33    pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
34
35    /// A white color.
36    pub const WHITE: Self = Self::new([1., 1., 1., 1.]);
37
38    /// Create a new color from the given components.
39    pub const fn new(components: [f32; 4]) -> Self {
40        Self { components }
41    }
42
43    /// Create a new color from RGB8 values.
44    pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
45        let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.];
46        Self::new(components)
47    }
48
49    /// Return the color as premulitplied RGBF32.
50    pub fn premultiplied(&self) -> [f32; 4] {
51        [
52            self.components[0] * self.components[3],
53            self.components[1] * self.components[3],
54            self.components[2] * self.components[3],
55            self.components[3],
56        ]
57    }
58
59    /// Create a new color from RGBA8 values.
60    pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
61        let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)];
62        Self::new(components)
63    }
64
65    /// Return the color as RGBA8.
66    pub fn to_rgba8(&self) -> [u8; 4] {
67        [
68            (self.components[0] * 255.0 + 0.5) as u8,
69            (self.components[1] * 255.0 + 0.5) as u8,
70            (self.components[2] * 255.0 + 0.5) as u8,
71            (self.components[3] * 255.0 + 0.5) as u8,
72        ]
73    }
74
75    /// Return the components of the color as RGBF32.
76    pub fn components(&self) -> [f32; 4] {
77        self.components
78    }
79}
80
81const fn u8_to_f32(x: u8) -> f32 {
82    x as f32 * (1.0 / 255.0)
83}
84
85#[derive(Debug, Clone)]
86enum ColorSpaceType {
87    DeviceCmyk,
88    DeviceGray,
89    DeviceRgb,
90    Pattern(ColorSpace),
91    Indexed(Indexed),
92    ICCBased(ICCProfile),
93    CalGray(CalGray),
94    CalRgb(CalRgb),
95    Lab(Lab),
96    Separation(Separation),
97    DeviceN(DeviceN),
98}
99
100impl ColorSpaceType {
101    fn new(object: Object, cache: &Cache) -> Option<Self> {
102        Self::new_inner(object, cache)
103    }
104
105    fn new_inner(object: Object, cache: &Cache) -> Option<ColorSpaceType> {
106        if let Some(name) = object.clone().into_name() {
107            return Self::new_from_name(name.clone());
108        } else if let Some(color_array) = object.clone().into_array() {
109            let mut iter = color_array.clone().flex_iter();
110            let name = iter.next::<Name>()?;
111
112            match name.deref() {
113                ICC_BASED => {
114                    let icc_stream = iter.next::<Stream>()?;
115                    let dict = icc_stream.dict();
116                    let num_components = dict.get::<usize>(N)?;
117
118                    return cache.get_or_insert_with(icc_stream.obj_id(), || {
119                        if let Some(decoded) = icc_stream.decoded().ok().as_ref() {
120                            ICCProfile::new(decoded, num_components)
121                                .map(|icc| {
122                                    // TODO: For SVG and PNG we can assume that the output color space is
123                                    // sRGB. If we ever implement PDF-to-PDF, we probably want to
124                                    // let the user pass the native color type and don't make this optimization
125                                    // if it's not sRGB.
126                                    if icc.is_srgb() {
127                                        ColorSpaceType::DeviceRgb
128                                    } else {
129                                        ColorSpaceType::ICCBased(icc)
130                                    }
131                                })
132                                .or_else(|| {
133                                    dict.get::<Object>(ALTERNATE)
134                                        .and_then(|o| ColorSpaceType::new(o, cache))
135                                })
136                                .or_else(|| match dict.get::<u8>(N) {
137                                    Some(1) => Some(ColorSpaceType::DeviceGray),
138                                    Some(3) => Some(ColorSpaceType::DeviceRgb),
139                                    Some(4) => Some(ColorSpaceType::DeviceCmyk),
140                                    _ => None,
141                                })
142                        } else {
143                            None
144                        }
145                    });
146                }
147                CALCMYK => return Some(ColorSpaceType::DeviceCmyk),
148                CALGRAY => {
149                    let cal_dict = iter.next::<Dict>()?;
150                    return Some(ColorSpaceType::CalGray(CalGray::new(&cal_dict)?));
151                }
152                CALRGB => {
153                    let cal_dict = iter.next::<Dict>()?;
154                    return Some(ColorSpaceType::CalRgb(CalRgb::new(&cal_dict)?));
155                }
156                DEVICE_RGB | RGB => return Some(ColorSpaceType::DeviceRgb),
157                DEVICE_GRAY | G => return Some(ColorSpaceType::DeviceGray),
158                DEVICE_CMYK | CMYK => return Some(ColorSpaceType::DeviceCmyk),
159                LAB => {
160                    let lab_dict = iter.next::<Dict>()?;
161                    return Some(ColorSpaceType::Lab(Lab::new(&lab_dict)?));
162                }
163                INDEXED | I => {
164                    return Some(ColorSpaceType::Indexed(Indexed::new(&color_array, cache)?));
165                }
166                SEPARATION => {
167                    return Some(ColorSpaceType::Separation(Separation::new(
168                        &color_array,
169                        cache,
170                    )?));
171                }
172                DEVICE_N => {
173                    return Some(ColorSpaceType::DeviceN(DeviceN::new(&color_array, cache)?));
174                }
175                PATTERN => {
176                    let _ = iter.next::<Name>();
177                    let cs = iter
178                        .next::<Object>()
179                        .and_then(|o| ColorSpace::new(o, cache))
180                        .unwrap_or(ColorSpace::device_rgb());
181                    return Some(ColorSpaceType::Pattern(cs));
182                }
183                _ => {
184                    warn!("unsupported color space: {}", name.as_str());
185                    return None;
186                }
187            }
188        }
189
190        None
191    }
192
193    fn new_from_name(name: Name) -> Option<Self> {
194        match name.deref() {
195            DEVICE_RGB | RGB => Some(ColorSpaceType::DeviceRgb),
196            DEVICE_GRAY | G => Some(ColorSpaceType::DeviceGray),
197            DEVICE_CMYK | CMYK => Some(ColorSpaceType::DeviceCmyk),
198            CALCMYK => Some(ColorSpaceType::DeviceCmyk),
199            PATTERN => Some(ColorSpaceType::Pattern(ColorSpace::device_rgb())),
200            _ => None,
201        }
202    }
203}
204
205/// A PDF color space.
206#[derive(Debug, Clone)]
207pub struct ColorSpace(Arc<ColorSpaceType>);
208
209impl ColorSpace {
210    /// Create a new color space from the given object.
211    pub(crate) fn new(object: Object, cache: &Cache) -> Option<ColorSpace> {
212        Some(Self(Arc::new(ColorSpaceType::new(object, cache)?)))
213    }
214
215    /// Create a new color space from the name.
216    pub(crate) fn new_from_name(name: Name) -> Option<ColorSpace> {
217        ColorSpaceType::new_from_name(name).map(|c| Self(Arc::new(c)))
218    }
219
220    /// Return the device gray color space.
221    pub(crate) fn device_gray() -> ColorSpace {
222        Self(Arc::new(ColorSpaceType::DeviceGray))
223    }
224
225    /// Return the device RGB color space.
226    pub(crate) fn device_rgb() -> ColorSpace {
227        Self(Arc::new(ColorSpaceType::DeviceRgb))
228    }
229
230    /// Return the device CMYK color space.
231    pub(crate) fn device_cmyk() -> ColorSpace {
232        Self(Arc::new(ColorSpaceType::DeviceCmyk))
233    }
234
235    /// Return the pattern color space.
236    pub(crate) fn pattern() -> ColorSpace {
237        Self(Arc::new(ColorSpaceType::Pattern(ColorSpace::device_gray())))
238    }
239
240    pub(crate) fn pattern_cs(&self) -> Option<ColorSpace> {
241        match self.0.as_ref() {
242            ColorSpaceType::Pattern(cs) => Some(cs.clone()),
243            _ => None,
244        }
245    }
246
247    /// Return `true` if the current color space is the pattern color space.
248    pub(crate) fn is_pattern(&self) -> bool {
249        matches!(self.0.as_ref(), ColorSpaceType::Pattern(_))
250    }
251
252    /// Return `true` if the current color space is an indexed color space.
253    pub(crate) fn is_indexed(&self) -> bool {
254        matches!(self.0.as_ref(), ColorSpaceType::Indexed(_))
255    }
256
257    /// Return `true` if the current color space is the RGB color space.
258    pub(crate) fn is_rgb(&self) -> bool {
259        matches!(self.0.as_ref(), ColorSpaceType::DeviceRgb)
260    }
261
262    /// Get the default decode array for the color space.
263    pub(crate) fn default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
264        match self.0.as_ref() {
265            ColorSpaceType::DeviceCmyk => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
266            ColorSpaceType::DeviceGray => smallvec![(0.0, 1.0)],
267            ColorSpaceType::DeviceRgb => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
268            ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.0.number_components],
269            ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)],
270            ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
271            ColorSpaceType::Lab(l) => smallvec![
272                (0.0, 100.0),
273                (l.range[0], l.range[1]),
274                (l.range[2], l.range[3]),
275            ],
276            ColorSpaceType::Indexed(_) => smallvec![(0.0, 2.0f32.powf(n) - 1.0)],
277            ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)],
278            ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components],
279            // Not a valid image color space.
280            ColorSpaceType::Pattern(_) => smallvec![(0.0, 1.0)],
281        }
282    }
283
284    /// Get the initial color of the color space.
285    pub(crate) fn initial_color(&self) -> ColorComponents {
286        match self.0.as_ref() {
287            ColorSpaceType::DeviceCmyk => smallvec![0.0, 0.0, 0.0, 1.0],
288            ColorSpaceType::DeviceGray => smallvec![0.0],
289            ColorSpaceType::DeviceRgb => smallvec![0.0, 0.0, 0.0],
290            ColorSpaceType::ICCBased(icc) => match icc.0.number_components {
291                1 => smallvec![0.0],
292                3 => smallvec![0.0, 0.0, 0.0],
293                4 => smallvec![0.0, 0.0, 0.0, 1.0],
294                _ => unreachable!(),
295            },
296            ColorSpaceType::CalGray(_) => smallvec![0.0],
297            ColorSpaceType::CalRgb(_) => smallvec![0.0, 0.0, 0.0],
298            ColorSpaceType::Lab(_) => smallvec![0.0, 0.0, 0.0],
299            ColorSpaceType::Indexed(_) => smallvec![0.0],
300            ColorSpaceType::Separation(_) => smallvec![1.0],
301            ColorSpaceType::Pattern(c) => c.initial_color(),
302            ColorSpaceType::DeviceN(d) => smallvec![1.0; d.num_components],
303        }
304    }
305
306    /// Get the number of components of the color space.
307    pub(crate) fn num_components(&self) -> u8 {
308        match self.0.as_ref() {
309            ColorSpaceType::DeviceCmyk => 4,
310            ColorSpaceType::DeviceGray => 1,
311            ColorSpaceType::DeviceRgb => 3,
312            ColorSpaceType::ICCBased(icc) => icc.0.number_components as u8,
313            ColorSpaceType::CalGray(_) => 1,
314            ColorSpaceType::CalRgb(_) => 3,
315            ColorSpaceType::Lab(_) => 3,
316            ColorSpaceType::Indexed(_) => 1,
317            ColorSpaceType::Separation(_) => 1,
318            ColorSpaceType::Pattern(p) => p.num_components(),
319            ColorSpaceType::DeviceN(d) => d.num_components as u8,
320        }
321    }
322
323    /// Turn the given component values and opacity into an RGBA color.
324    pub fn to_rgba(&self, c: &[f32], opacity: f32, manual_scale: bool) -> AlphaColor {
325        self.to_rgba_inner(c, opacity, manual_scale)
326            .unwrap_or(AlphaColor::BLACK)
327    }
328
329    fn to_rgba_inner(&self, c: &[f32], opacity: f32, manual_scale: bool) -> Option<AlphaColor> {
330        let color = match self.0.as_ref() {
331            ColorSpaceType::DeviceRgb => {
332                AlphaColor::new([*c.first()?, *c.get(1)?, *c.get(2)?, opacity])
333            }
334            ColorSpaceType::DeviceGray => {
335                AlphaColor::new([*c.first()?, *c.first()?, *c.first()?, opacity])
336            }
337            ColorSpaceType::DeviceCmyk => {
338                let opacity = f32_to_u8(opacity);
339                let srgb = CMYK_TRANSFORM.to_rgb(c)?;
340
341                AlphaColor::from_rgba8(srgb[0], srgb[1], srgb[2], opacity)
342            }
343            ColorSpaceType::ICCBased(icc) => {
344                let opacity = f32_to_u8(opacity);
345                let srgb = icc.to_rgb(c)?;
346
347                AlphaColor::from_rgba8(srgb[0], srgb[1], srgb[2], opacity)
348            }
349            ColorSpaceType::CalGray(cal) => {
350                let opacity = f32_to_u8(opacity);
351                let srgb = cal.to_rgb(*c.first()?);
352
353                AlphaColor::from_rgba8(srgb[0], srgb[1], srgb[2], opacity)
354            }
355            ColorSpaceType::CalRgb(cal) => {
356                let opacity = f32_to_u8(opacity);
357                let srgb = cal.to_rgb([*c.first()?, *c.get(1)?, *c.get(2)?]);
358
359                AlphaColor::from_rgba8(srgb[0], srgb[1], srgb[2], opacity)
360            }
361            ColorSpaceType::Lab(lab) => {
362                let opacity = f32_to_u8(opacity);
363                let srgb = lab.to_rgb([*c.first()?, *c.get(1)?, *c.get(2)?], manual_scale);
364
365                AlphaColor::from_rgba8(srgb[0], srgb[1], srgb[2], opacity)
366            }
367            ColorSpaceType::Indexed(i) => i.to_rgb(*c.first()?, opacity),
368            ColorSpaceType::Separation(s) => s.to_rgba(*c.first()?, opacity),
369            ColorSpaceType::Pattern(_) => AlphaColor::BLACK,
370            ColorSpaceType::DeviceN(d) => d.to_rgba(c, opacity),
371        };
372
373        Some(color)
374    }
375}
376
377#[derive(Debug, Clone)]
378struct CalGray {
379    white_point: [f32; 3],
380    black_point: [f32; 3],
381    gamma: f32,
382}
383
384// See <https://github.com/mozilla/pdf.js/blob/06f44916c8936b92f464d337fe3a0a6b2b78d5b4/src/core/colorspace.js#L752>
385impl CalGray {
386    fn new(dict: &Dict) -> Option<Self> {
387        let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
388        let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
389        let gamma = dict.get::<f32>(GAMMA).unwrap_or(1.0);
390
391        Some(Self {
392            white_point,
393            black_point,
394            gamma,
395        })
396    }
397
398    fn to_rgb(&self, c: f32) -> [u8; 3] {
399        let g = self.gamma;
400        let (_xw, yw, _zw) = {
401            let wp = self.white_point;
402            (wp[0], wp[1], wp[2])
403        };
404        let (_xb, _yb, _zb) = {
405            let bp = self.black_point;
406            (bp[0], bp[1], bp[2])
407        };
408
409        let a = c;
410        let ag = a.powf(g);
411        let l = yw * ag;
412        let val = (0.0f32.max(295.8 * l.powf(0.333_333_34) - 40.8) + 0.5) as u8;
413
414        [val, val, val]
415    }
416}
417
418#[derive(Debug, Clone)]
419struct CalRgb {
420    white_point: [f32; 3],
421    black_point: [f32; 3],
422    matrix: [f32; 9],
423    gamma: [f32; 3],
424}
425
426// See <https://github.com/mozilla/pdf.js/blob/06f44916c8936b92f464d337fe3a0a6b2b78d5b4/src/core/colorspace.js#L846>
427// Completely copied from there without really understanding the logic, but we get the same results as Firefox
428// which should be good enough (and by viewing the `calrgb.pdf` test file in different viewers you will
429// see that in many cases each viewer does whatever it wants, even Acrobat), so this is good enough for us.
430impl CalRgb {
431    fn new(dict: &Dict) -> Option<Self> {
432        let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
433        let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
434        let matrix = dict
435            .get::<[f32; 9]>(MATRIX)
436            .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
437        let gamma = dict.get::<[f32; 3]>(GAMMA).unwrap_or([1.0, 1.0, 1.0]);
438
439        Some(Self {
440            white_point,
441            black_point,
442            matrix,
443            gamma,
444        })
445    }
446
447    const BRADFORD_SCALE_MATRIX: [f32; 9] = [
448        0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296,
449    ];
450
451    const BRADFORD_SCALE_INVERSE_MATRIX: [f32; 9] = [
452        0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428,
453        0.9684867,
454    ];
455
456    const SRGB_D65_XYZ_TO_RGB_MATRIX: [f32; 9] = [
457        3.2404542, -1.5371385, -0.4985314, -0.969_266, 1.8760108, 0.0415560, 0.0556434, -0.2040259,
458        1.0572252,
459    ];
460
461    const FLAT_WHITEPOINT: [f32; 3] = [1.0, 1.0, 1.0];
462    const D65_WHITEPOINT: [f32; 3] = [0.95047, 1.0, 1.08883];
463
464    fn decode_l_constant() -> f32 {
465        ((8.0f32 + 16.0) / 116.0).powi(3) / 8.0
466    }
467
468    fn srgb_transfer_function(color: f32) -> f32 {
469        if color <= 0.0031308 {
470            (12.92 * color).clamp(0.0, 1.0)
471        } else if color >= 0.99554525 {
472            1.0
473        } else {
474            ((1.0 + 0.055) * color.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)
475        }
476    }
477
478    fn matrix_product(a: &[f32; 9], b: &[f32; 3]) -> [f32; 3] {
479        [
480            a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
481            a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
482            a[6] * b[0] + a[7] * b[1] + a[8] * b[2],
483        ]
484    }
485
486    fn to_flat(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
487        [
488            lms[0] / source_white_point[0],
489            lms[1] / source_white_point[1],
490            lms[2] / source_white_point[2],
491        ]
492    }
493
494    fn to_d65(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
495        [
496            lms[0] * Self::D65_WHITEPOINT[0] / source_white_point[0],
497            lms[1] * Self::D65_WHITEPOINT[1] / source_white_point[1],
498            lms[2] * Self::D65_WHITEPOINT[2] / source_white_point[2],
499        ]
500    }
501
502    fn decode_l(l: f32) -> f32 {
503        if l < 0.0 {
504            -Self::decode_l(-l)
505        } else if l > 8.0 {
506            ((l + 16.0) / 116.0).powi(3)
507        } else {
508            l * Self::decode_l_constant()
509        }
510    }
511
512    fn compensate_black_point(source_bp: &[f32; 3], xyz_flat: &[f32; 3]) -> [f32; 3] {
513        if source_bp == &[0.0, 0.0, 0.0] {
514            return *xyz_flat;
515        }
516
517        let zero_decode_l = Self::decode_l(0.0);
518
519        let mut out = [0.0; 3];
520        for i in 0..3 {
521            let src = Self::decode_l(source_bp[i]);
522            let scale = (1.0 - zero_decode_l) / (1.0 - src);
523            let offset = 1.0 - scale;
524            out[i] = xyz_flat[i] * scale + offset;
525        }
526
527        out
528    }
529
530    fn normalize_white_point_to_flat(
531        &self,
532        source_white_point: &[f32; 3],
533        xyz: &[f32; 3],
534    ) -> [f32; 3] {
535        if source_white_point[0] == 1.0 && source_white_point[2] == 1.0 {
536            return *xyz;
537        }
538        let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
539        let lms_flat = Self::to_flat(source_white_point, &lms);
540        Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat)
541    }
542
543    fn normalize_white_point_to_d65(
544        &self,
545        source_white_point: &[f32; 3],
546        xyz: &[f32; 3],
547    ) -> [f32; 3] {
548        let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
549        let lms_d65 = Self::to_d65(source_white_point, &lms);
550        Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65)
551    }
552
553    fn to_rgb(&self, mut c: [f32; 3]) -> [u8; 3] {
554        for i in &mut c {
555            *i = i.clamp(0.0, 1.0);
556        }
557
558        let [r, g, b] = c;
559        let [gr, gg, gb] = self.gamma;
560        let [agr, bgg, cgb] = [
561            if r == 1.0 { 1.0 } else { r.powf(gr) },
562            if g == 1.0 { 1.0 } else { g.powf(gg) },
563            if b == 1.0 { 1.0 } else { b.powf(gb) },
564        ];
565
566        let m = &self.matrix;
567        let x = m[0] * agr + m[3] * bgg + m[6] * cgb;
568        let y = m[1] * agr + m[4] * bgg + m[7] * cgb;
569        let z = m[2] * agr + m[5] * bgg + m[8] * cgb;
570        let xyz = [x, y, z];
571
572        let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz);
573        let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat);
574        let xyz_d65 = self.normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
575        let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
576
577        [
578            (Self::srgb_transfer_function(srgb_xyz[0]) * 255.0 + 0.5) as u8,
579            (Self::srgb_transfer_function(srgb_xyz[1]) * 255.0 + 0.5) as u8,
580            (Self::srgb_transfer_function(srgb_xyz[2]) * 255.0 + 0.5) as u8,
581        ]
582    }
583}
584
585#[derive(Debug, Clone)]
586struct Lab {
587    white_point: [f32; 3],
588    _black_point: [f32; 3],
589    range: [f32; 4],
590}
591
592impl Lab {
593    fn new(dict: &Dict) -> Option<Self> {
594        let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
595        let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
596        let range = dict
597            .get::<[f32; 4]>(RANGE)
598            .unwrap_or([-100.0, 100.0, -100.0, 100.0]);
599
600        Some(Self {
601            white_point,
602            _black_point: black_point,
603            range,
604        })
605    }
606
607    fn fn_g(x: f32) -> f32 {
608        if x >= 6.0 / 29.0 {
609            x.powi(3)
610        } else {
611            (108.0 / 841.0) * (x - 4.0 / 29.0)
612        }
613    }
614
615    fn to_rgb(&self, c: [f32; 3], manual_scale: bool) -> [u8; 3] {
616        let (mut l, mut a, mut b) = (c[0], c[1], c[2]);
617
618        // If we used an indexed color space, the values will be between 0.0 and 1.0,
619        // so we need to manually scale them.
620        if manual_scale {
621            l *= 100.0;
622            a = self.range[0] + a * (self.range[1] - self.range[0]);
623            b = self.range[2] + b * (self.range[3] - self.range[2]);
624        }
625
626        let m = (l + 16.0) / 116.0;
627        let l = m + a / 500.0;
628        let n = m - b / 200.0;
629
630        let x = self.white_point[0] * Self::fn_g(l);
631        let y = self.white_point[1] * Self::fn_g(m);
632        let z = self.white_point[2] * Self::fn_g(n);
633
634        let (r, g, b) = if self.white_point[2] < 1.0 {
635            (
636                x * 3.1339 + y * -1.617 + z * -0.4906,
637                x * -0.9785 + y * 1.916 + z * 0.0333,
638                x * 0.072 + y * -0.229 + z * 1.4057,
639            )
640        } else {
641            (
642                x * 3.2406 + y * -1.5372 + z * -0.4986,
643                x * -0.9689 + y * 1.8758 + z * 0.0415,
644                x * 0.0557 + y * -0.204 + z * 1.057,
645            )
646        };
647
648        let conv = |v: f32| (v.max(0.0).sqrt() * 255.0).clamp(0.0, 255.0) as u8;
649
650        [conv(r), conv(g), conv(b)]
651    }
652}
653
654#[derive(Debug, Clone)]
655struct Indexed {
656    values: Vec<Vec<f32>>,
657    hival: u8,
658    base: Box<ColorSpace>,
659}
660
661impl Indexed {
662    fn new(array: &Array, cache: &Cache) -> Option<Self> {
663        let mut iter = array.flex_iter();
664        // Skip name
665        let _ = iter.next::<Name>()?;
666        let base_color_space = ColorSpace::new(iter.next::<Object>()?, cache)?;
667        let hival = iter.next::<u8>()?;
668
669        let values = {
670            let data = iter
671                .next::<Stream>()
672                .and_then(|s| s.decoded().ok())
673                .or_else(|| iter.next::<object::String>().map(|s| s.get().to_vec()))?;
674
675            let num_components = base_color_space.num_components();
676
677            let mut byte_iter = data.iter().copied();
678
679            let mut vals = vec![];
680            for _ in 0..=hival {
681                let mut temp = vec![];
682
683                for _ in 0..num_components {
684                    temp.push(byte_iter.next()? as f32 / 255.0)
685                }
686
687                vals.push(temp);
688            }
689
690            vals
691        };
692
693        Some(Self {
694            values,
695            hival,
696            base: Box::new(base_color_space),
697        })
698    }
699
700    pub fn to_rgb(&self, val: f32, opacity: f32) -> AlphaColor {
701        let idx = (val.clamp(0.0, self.hival as f32) + 0.5) as usize;
702        self.base
703            .to_rgba(self.values[idx].as_slice(), opacity, true)
704    }
705}
706
707#[derive(Debug, Clone)]
708struct Separation {
709    alternate_space: ColorSpace,
710    tint_transform: Function,
711}
712
713impl Separation {
714    fn new(array: &Array, cache: &Cache) -> Option<Self> {
715        let mut iter = array.flex_iter();
716        // Skip `/Separation`
717        let _ = iter.next::<Name>()?;
718        let name = iter.next::<Name>()?;
719        let alternate_space = ColorSpace::new(iter.next::<Object>()?, cache)?;
720        let tint_transform = Function::new(&iter.next::<Object>()?)?;
721
722        if matches!(name.as_str(), "All" | "None") {
723            warn!("Separation color spaces with `All` or `None` as name are not supported yet");
724        }
725
726        Some(Self {
727            alternate_space,
728            tint_transform,
729        })
730    }
731
732    fn to_rgba(&self, c: f32, opacity: f32) -> AlphaColor {
733        let res = self
734            .tint_transform
735            .eval(smallvec![c])
736            .unwrap_or(self.alternate_space.initial_color());
737
738        self.alternate_space.to_rgba(&res, opacity, false)
739    }
740}
741
742#[derive(Debug, Clone)]
743struct DeviceN {
744    alternate_space: ColorSpace,
745    num_components: usize,
746    tint_transform: Function,
747}
748
749impl DeviceN {
750    fn new(array: &Array, cache: &Cache) -> Option<Self> {
751        let mut iter = array.flex_iter();
752        // Skip `/DeviceN`
753        let _ = iter.next::<Name>()?;
754        // Skip `Name`.
755        let num_components = iter.next::<Array>()?.iter::<Name>().count();
756        let alternate_space = ColorSpace::new(iter.next::<Object>()?, cache)?;
757        let tint_transform = Function::new(&iter.next::<Object>()?)?;
758
759        Some(Self {
760            alternate_space,
761            num_components,
762            tint_transform,
763        })
764    }
765
766    fn to_rgba(&self, c: &[f32], opacity: f32) -> AlphaColor {
767        let res = self
768            .tint_transform
769            .eval(c.to_smallvec())
770            .unwrap_or(self.alternate_space.initial_color());
771        self.alternate_space.to_rgba(&res, opacity, false)
772    }
773}
774
775struct ICCColorRepr {
776    transform: Box<Transform8BitExecutor>,
777    number_components: usize,
778    is_srgb: bool,
779}
780
781#[derive(Clone)]
782struct ICCProfile(Arc<ICCColorRepr>);
783
784impl Debug for ICCProfile {
785    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
786        write!(f, "ICCColor {{..}}")
787    }
788}
789
790impl ICCProfile {
791    fn new(profile: &[u8], number_components: usize) -> Option<Self> {
792        let src_profile = ColorProfile::new_from_slice(profile).ok()?;
793
794        // Temporary workaround as 3 PDFs don't render correctly without this.
795        if src_profile.color_space == DataColorSpace::Lab {
796            return None;
797        }
798
799        let dest_profile = ColorProfile::new_srgb();
800
801        let src_layout = match number_components {
802            1 => Layout::Gray,
803            3 => Layout::Rgb,
804            4 => Layout::Rgba,
805            _ => {
806                warn!("unsupported number of components {number_components} for ICC profile");
807
808                return None;
809            }
810        };
811
812        let transform = src_profile
813            .create_transform_8bit(
814                src_layout,
815                &dest_profile,
816                Layout::Rgb,
817                TransformOptions::default(),
818            )
819            .ok()?;
820
821        const SRGB_MARKER: &[u8] = b"sRGB";
822        let is_srgb = profile
823            .get(52..56)
824            .map(|device_model| device_model == SRGB_MARKER)
825            .unwrap_or(false);
826
827        Some(Self(Arc::new(ICCColorRepr {
828            transform,
829            number_components,
830            is_srgb,
831        })))
832    }
833
834    fn is_srgb(&self) -> bool {
835        self.0.is_srgb
836    }
837
838    fn to_rgb(&self, c: &[f32]) -> Option<[u8; 3]> {
839        let mut srgb = [0, 0, 0];
840
841        match self.0.number_components {
842            1 => self
843                .0
844                .transform
845                .transform(&[f32_to_u8(*c.first()?)], &mut srgb),
846            3 => self.0.transform.transform(
847                &[
848                    f32_to_u8(*c.first()?),
849                    f32_to_u8(*c.get(1)?),
850                    f32_to_u8(*c.get(2)?),
851                ],
852                &mut srgb,
853            ),
854            4 => self.0.transform.transform(
855                &[
856                    f32_to_u8(*c.first()?),
857                    f32_to_u8(*c.get(1)?),
858                    f32_to_u8(*c.get(2)?),
859                    f32_to_u8(*c.get(3)?),
860                ],
861                &mut srgb,
862            ),
863            _ => return None,
864        }
865        .ok()?;
866
867        Some(srgb)
868    }
869}
870
871fn f32_to_u8(val: f32) -> u8 {
872    (val * 255.0 + 0.5) as u8
873}
874
875#[derive(Debug, Clone)]
876/// A color.
877pub struct Color {
878    color_space: ColorSpace,
879    components: ColorComponents,
880    opacity: f32,
881}
882
883impl Color {
884    pub(crate) fn new(color_space: ColorSpace, components: ColorComponents, opacity: f32) -> Self {
885        Self {
886            color_space,
887            components,
888            opacity,
889        }
890    }
891
892    /// Return the color as an RGBA color.
893    pub fn to_rgba(&self) -> AlphaColor {
894        self.color_space
895            .to_rgba(&self.components, self.opacity, false)
896    }
897}
898
899static CMYK_TRANSFORM: LazyLock<ICCProfile> = LazyLock::new(|| {
900    ICCProfile::new(include_bytes!("../assets/CGATS001Compat-v2-micro.icc"), 4).unwrap()
901});