hayro_interpret/
color.rs

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