hayro_interpret/
color.rs

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