Skip to main content

hayro_interpret/
color.rs

1//! PDF colors and color spaces.
2
3use crate::cache::{Cache, CacheKey};
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 moxcms::{
13    ColorProfile, DataColorSpace, Layout, Transform8BitExecutor, TransformF32Executor,
14    TransformOptions, Xyzd,
15};
16use smallvec::{SmallVec, ToSmallVec, smallvec};
17use std::borrow::Cow;
18use std::fmt::{Debug, Formatter};
19use std::ops::Deref;
20use std::sync::{Arc, LazyLock, OnceLock};
21
22/// A storage for the components of colors.
23pub type ColorComponents = SmallVec<[f32; 4]>;
24
25/// An RGB color with an alpha channel.
26#[derive(Debug, Copy, Clone)]
27pub struct AlphaColor {
28    components: [f32; 4],
29}
30
31impl AlphaColor {
32    /// A black color.
33    pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
34
35    /// A transparent color.
36    pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
37
38    /// A white color.
39    pub const WHITE: Self = Self::new([1., 1., 1., 1.]);
40
41    /// Create a new color from the given components.
42    pub const fn new(components: [f32; 4]) -> Self {
43        Self { components }
44    }
45
46    /// Create a new color from RGB8 values.
47    pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
48        let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.];
49        Self::new(components)
50    }
51
52    /// Return the color as premulitplied RGBF32.
53    pub fn premultiplied(&self) -> [f32; 4] {
54        [
55            self.components[0] * self.components[3],
56            self.components[1] * self.components[3],
57            self.components[2] * self.components[3],
58            self.components[3],
59        ]
60    }
61
62    /// Create a new color from RGBA8 values.
63    pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
64        let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)];
65        Self::new(components)
66    }
67
68    /// Return the color as RGBA8.
69    pub fn to_rgba8(&self) -> [u8; 4] {
70        [
71            (self.components[0] * 255.0 + 0.5) as u8,
72            (self.components[1] * 255.0 + 0.5) as u8,
73            (self.components[2] * 255.0 + 0.5) as u8,
74            (self.components[3] * 255.0 + 0.5) as u8,
75        ]
76    }
77
78    /// Return the components of the color as RGBF32.
79    pub fn components(&self) -> [f32; 4] {
80        self.components
81    }
82}
83
84const fn u8_to_f32(x: u8) -> f32 {
85    x as f32 * (1.0 / 255.0)
86}
87
88#[derive(Debug, Clone)]
89pub(crate) enum ColorSpaceType {
90    DeviceCmyk,
91    DeviceGray,
92    DeviceRgb,
93    Pattern(ColorSpace),
94    Indexed(Indexed),
95    ICCBased(ICCProfile),
96    CalGray(CalGray),
97    CalRgb(CalRgb),
98    Lab(Lab),
99    Separation(Separation),
100    DeviceN(DeviceN),
101}
102
103impl ColorSpaceType {
104    fn new(object: Object<'_>, cache: &Cache) -> Option<Self> {
105        Self::new_inner(object, cache)
106    }
107
108    fn new_inner(object: Object<'_>, cache: &Cache) -> Option<Self> {
109        if let Object::Name(name) = object {
110            return Self::new_from_name(&name);
111        } else if let Object::Array(color_array) = object {
112            let mut iter = color_array.flex_iter();
113            let name = iter.next::<Name<'_>>()?;
114
115            match name.deref() {
116                ICC_BASED => {
117                    let icc_stream = iter.next::<Stream<'_>>()?;
118                    let dict = icc_stream.dict();
119                    let num_components = dict.get::<usize>(N)?;
120
121                    return cache.get_or_insert_with(icc_stream.cache_key(), || {
122                        if let Some(decoded) = icc_stream.decoded().ok().as_ref() {
123                            ICCProfile::new(decoded, num_components)
124                                .map(|icc| {
125                                    // TODO: For SVG and PNG we can assume that the output color space is
126                                    // sRGB. If we ever implement PDF-to-PDF, we probably want to
127                                    // let the user pass the native color type and don't make this optimization
128                                    // if it's not sRGB.
129                                    if icc.is_srgb() {
130                                        Self::DeviceRgb
131                                    } else {
132                                        Self::ICCBased(icc)
133                                    }
134                                })
135                                .or_else(|| {
136                                    dict.get::<Object<'_>>(ALTERNATE)
137                                        .and_then(|o| Self::new(o, cache))
138                                })
139                                .or_else(|| match dict.get::<u8>(N) {
140                                    Some(1) => Some(Self::DeviceGray),
141                                    Some(3) => Some(Self::DeviceRgb),
142                                    Some(4) => Some(Self::DeviceCmyk),
143                                    _ => None,
144                                })
145                        } else {
146                            None
147                        }
148                    });
149                }
150                CALCMYK => return Some(Self::DeviceCmyk),
151                CALGRAY => {
152                    let cal_dict = iter.next::<Dict<'_>>()?;
153                    return Some(Self::CalGray(CalGray::new(&cal_dict)?));
154                }
155                CALRGB => {
156                    let cal_dict = iter.next::<Dict<'_>>()?;
157                    return Some(Self::CalRgb(CalRgb::new(&cal_dict)?));
158                }
159                DEVICE_RGB | RGB => return Some(Self::DeviceRgb),
160                DEVICE_GRAY | G => return Some(Self::DeviceGray),
161                DEVICE_CMYK | CMYK => return Some(Self::DeviceCmyk),
162                LAB => {
163                    let lab_dict = iter.next::<Dict<'_>>()?;
164                    return Some(Self::Lab(Lab::new(&lab_dict)?));
165                }
166                INDEXED | I => {
167                    return Some(Self::Indexed(Indexed::new(&color_array, cache)?));
168                }
169                SEPARATION => {
170                    return Some(Self::Separation(Separation::new(&color_array, cache)?));
171                }
172                DEVICE_N => {
173                    return Some(Self::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(Self::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(Self::DeviceRgb),
196            DEVICE_GRAY | G => Some(Self::DeviceGray),
197            DEVICE_CMYK | CMYK => Some(Self::DeviceCmyk),
198            CALCMYK => Some(Self::DeviceCmyk),
199            PATTERN => Some(Self::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<Self> {
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<Self> {
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() -> Self {
222        Self(Arc::new(ColorSpaceType::DeviceGray))
223    }
224
225    /// Return the device RGB color space.
226    pub(crate) fn device_rgb() -> Self {
227        Self(Arc::new(ColorSpaceType::DeviceRgb))
228    }
229
230    /// Return the device CMYK color space.
231    pub(crate) fn device_cmyk() -> Self {
232        Self(Arc::new(ColorSpaceType::DeviceCmyk))
233    }
234
235    /// Return the pattern color space.
236    pub(crate) fn pattern() -> Self {
237        Self(Arc::new(ColorSpaceType::Pattern(Self::device_gray())))
238    }
239
240    pub(crate) fn pattern_cs(&self) -> Option<Self> {
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    /// Get the default decode array for the color space.
258    pub(crate) fn default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
259        match self.0.as_ref() {
260            ColorSpaceType::DeviceCmyk => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
261            ColorSpaceType::DeviceGray => smallvec![(0.0, 1.0)],
262            ColorSpaceType::DeviceRgb => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
263            ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.0.number_components],
264            ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)],
265            ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
266            ColorSpaceType::Lab(l) => smallvec![
267                (0.0, 100.0),
268                (l.range[0], l.range[1]),
269                (l.range[2], l.range[3]),
270            ],
271            ColorSpaceType::Indexed(_) => smallvec![(0.0, 2.0_f32.powf(n) - 1.0)],
272            ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)],
273            ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components as usize],
274            // Not a valid image color space.
275            ColorSpaceType::Pattern(_) => smallvec![(0.0, 1.0)],
276        }
277    }
278
279    pub(crate) fn inverted_default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
280        self.default_decode_arr(n)
281            .iter()
282            .map(|(min, max)| (*max, *min))
283            .collect()
284    }
285
286    /// Get the initial color of the color space.
287    pub(crate) fn initial_color(&self) -> ColorComponents {
288        match self.0.as_ref() {
289            ColorSpaceType::DeviceCmyk => smallvec![0.0, 0.0, 0.0, 1.0],
290            ColorSpaceType::DeviceGray => smallvec![0.0],
291            ColorSpaceType::DeviceRgb => smallvec![0.0, 0.0, 0.0],
292            ColorSpaceType::ICCBased(icc) => match icc.0.number_components {
293                1 => smallvec![0.0],
294                3 => smallvec![0.0, 0.0, 0.0],
295                4 => smallvec![0.0, 0.0, 0.0, 1.0],
296                _ => unreachable!(),
297            },
298            ColorSpaceType::CalGray(_) => smallvec![0.0],
299            ColorSpaceType::CalRgb(_) => smallvec![0.0, 0.0, 0.0],
300            ColorSpaceType::Lab(_) => smallvec![0.0, 0.0, 0.0],
301            ColorSpaceType::Indexed(_) => smallvec![0.0],
302            ColorSpaceType::Separation(_) => smallvec![1.0],
303            ColorSpaceType::Pattern(c) => c.initial_color(),
304            ColorSpaceType::DeviceN(d) => smallvec![1.0; d.num_components as usize],
305        }
306    }
307
308    pub(crate) fn is_device_gray(&self) -> bool {
309        matches!(self.0.as_ref(), ColorSpaceType::DeviceGray)
310    }
311
312    /// Get the number of components of the color space.
313    pub(crate) fn num_components(&self) -> u8 {
314        match self.0.as_ref() {
315            ColorSpaceType::DeviceCmyk => 4,
316            ColorSpaceType::DeviceGray => 1,
317            ColorSpaceType::DeviceRgb => 3,
318            ColorSpaceType::ICCBased(icc) => icc.0.number_components as u8,
319            ColorSpaceType::CalGray(_) => 1,
320            ColorSpaceType::CalRgb(_) => 3,
321            ColorSpaceType::Lab(_) => 3,
322            ColorSpaceType::Indexed(_) => 1,
323            ColorSpaceType::Separation(_) => 1,
324            ColorSpaceType::Pattern(p) => p.num_components(),
325            ColorSpaceType::DeviceN(d) => d.num_components,
326        }
327    }
328
329    /// Turn the given component values and opacity into an RGBA color.
330    pub fn to_rgba(&self, c: &[f32], opacity: f32, manual_scale: bool) -> AlphaColor {
331        self.to_alpha_color(c, opacity, manual_scale)
332            .unwrap_or(AlphaColor::BLACK)
333    }
334}
335
336impl ToRgb for ColorSpace {
337    fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
338        match self.0.as_ref() {
339            ColorSpaceType::DeviceCmyk => {
340                if input.len() == 4 {
341                    let converted = [
342                        f32_to_u8(input[0]),
343                        f32_to_u8(input[1]),
344                        f32_to_u8(input[2]),
345                        f32_to_u8(input[3]),
346                    ];
347                    CMYK_TRANSFORM.convert_u8(&converted, output)
348                } else {
349                    let converted = input.iter().copied().map(f32_to_u8).collect::<Vec<_>>();
350                    CMYK_TRANSFORM.convert_u8(&converted, output)
351                }
352            }
353            ColorSpaceType::DeviceGray => {
354                for (gray, output) in input.iter().zip(output.chunks_exact_mut(3)) {
355                    let gray = f32_to_u8(*gray);
356                    output.copy_from_slice(&[gray, gray, gray]);
357                }
358
359                Some(())
360            }
361            ColorSpaceType::DeviceRgb => {
362                for (input, output) in input.iter().copied().zip(output) {
363                    *output = f32_to_u8(input);
364                }
365
366                Some(())
367            }
368            ColorSpaceType::Pattern(i) => i.convert_f32(input, output, manual_scale),
369            ColorSpaceType::Indexed(i) => i.convert_f32(input, output, manual_scale),
370            ColorSpaceType::ICCBased(i) => i.convert_f32(input, output, manual_scale),
371            ColorSpaceType::CalGray(i) => i.convert_f32(input, output, manual_scale),
372            ColorSpaceType::CalRgb(i) => i.convert_f32(input, output, manual_scale),
373            ColorSpaceType::Lab(i) => i.convert_f32(input, output, manual_scale),
374            ColorSpaceType::Separation(i) => i.convert_f32(input, output, manual_scale),
375            ColorSpaceType::DeviceN(i) => i.convert_f32(input, output, manual_scale),
376        }
377    }
378
379    fn supports_u8(&self) -> bool {
380        match self.0.as_ref() {
381            ColorSpaceType::DeviceCmyk => true,
382            ColorSpaceType::DeviceGray => true,
383            ColorSpaceType::DeviceRgb => true,
384            ColorSpaceType::Pattern(i) => i.supports_u8(),
385            ColorSpaceType::Indexed(i) => i.supports_u8(),
386            ColorSpaceType::ICCBased(i) => i.supports_u8(),
387            ColorSpaceType::CalGray(i) => i.supports_u8(),
388            ColorSpaceType::CalRgb(i) => i.supports_u8(),
389            ColorSpaceType::Lab(i) => i.supports_u8(),
390            ColorSpaceType::Separation(i) => i.supports_u8(),
391            ColorSpaceType::DeviceN(i) => i.supports_u8(),
392        }
393    }
394
395    fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
396        match self.0.as_ref() {
397            ColorSpaceType::DeviceCmyk => CMYK_TRANSFORM.convert_u8(input, output),
398            ColorSpaceType::DeviceGray => {
399                for (input, output) in input.iter().zip(output.chunks_exact_mut(3)) {
400                    output.copy_from_slice(&[*input, *input, *input]);
401                }
402
403                Some(())
404            }
405            ColorSpaceType::DeviceRgb => {
406                for (input, output) in input.iter().zip(output.iter_mut()) {
407                    *output = *input;
408                }
409
410                Some(())
411            }
412            ColorSpaceType::Pattern(i) => i.convert_u8(input, output),
413            ColorSpaceType::Indexed(i) => i.convert_u8(input, output),
414            ColorSpaceType::ICCBased(i) => i.convert_u8(input, output),
415            ColorSpaceType::CalGray(i) => i.convert_u8(input, output),
416            ColorSpaceType::CalRgb(i) => i.convert_u8(input, output),
417            ColorSpaceType::Lab(i) => i.convert_u8(input, output),
418            ColorSpaceType::Separation(i) => i.convert_u8(input, output),
419            ColorSpaceType::DeviceN(i) => i.convert_u8(input, output),
420        }
421    }
422
423    fn is_none(&self) -> bool {
424        match self.0.as_ref() {
425            ColorSpaceType::Separation(s) => s.is_none(),
426            ColorSpaceType::DeviceN(d) => d.is_none(),
427            _ => false,
428        }
429    }
430}
431
432#[derive(Debug, Clone)]
433pub(crate) struct CalGray {
434    white_point: [f32; 3],
435    black_point: [f32; 3],
436    gamma: f32,
437}
438
439// See <https://github.com/mozilla/pdf.js/blob/06f44916c8936b92f464d337fe3a0a6b2b78d5b4/src/core/colorspace.js#L752>
440impl CalGray {
441    fn new(dict: &Dict<'_>) -> Option<Self> {
442        let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
443        let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
444        let gamma = dict.get::<f32>(GAMMA).unwrap_or(1.0);
445
446        Some(Self {
447            white_point,
448            black_point,
449            gamma,
450        })
451    }
452}
453
454impl ToRgb for CalGray {
455    fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
456        for (input, output) in input.iter().copied().zip(output.chunks_exact_mut(3)) {
457            let g = self.gamma;
458            let (_xw, yw, _zw) = {
459                let wp = self.white_point;
460                (wp[0], wp[1], wp[2])
461            };
462            let (_xb, _yb, _zb) = {
463                let bp = self.black_point;
464                (bp[0], bp[1], bp[2])
465            };
466
467            let a = input;
468            let ag = a.powf(g);
469            let l = yw * ag;
470            let val = (0.0_f32.max(295.8 * l.powf(0.333_333_34) - 40.8) + 0.5) as u8;
471
472            output.copy_from_slice(&[val, val, val]);
473        }
474
475        Some(())
476    }
477}
478
479#[derive(Debug, Clone)]
480pub(crate) struct CalRgb {
481    white_point: [f32; 3],
482    black_point: [f32; 3],
483    matrix: [f32; 9],
484    gamma: [f32; 3],
485}
486
487// See <https://github.com/mozilla/pdf.js/blob/06f44916c8936b92f464d337fe3a0a6b2b78d5b4/src/core/colorspace.js#L846>
488// Completely copied from there without really understanding the logic, but we get the same results as Firefox
489// which should be good enough (and by viewing the `calrgb.pdf` test file in different viewers you will
490// see that in many cases each viewer does whatever it wants, even Acrobat), so this is good enough for us.
491impl CalRgb {
492    fn new(dict: &Dict<'_>) -> Option<Self> {
493        let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
494        let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
495        let matrix = dict
496            .get::<[f32; 9]>(MATRIX)
497            .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
498        let gamma = dict.get::<[f32; 3]>(GAMMA).unwrap_or([1.0, 1.0, 1.0]);
499
500        Some(Self {
501            white_point,
502            black_point,
503            matrix,
504            gamma,
505        })
506    }
507
508    const BRADFORD_SCALE_MATRIX: [f32; 9] = [
509        0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296,
510    ];
511
512    const BRADFORD_SCALE_INVERSE_MATRIX: [f32; 9] = [
513        0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428,
514        0.9684867,
515    ];
516
517    const SRGB_D65_XYZ_TO_RGB_MATRIX: [f32; 9] = [
518        3.2404542, -1.5371385, -0.4985314, -0.969_266, 1.8760108, 0.0415560, 0.0556434, -0.2040259,
519        1.0572252,
520    ];
521
522    const FLAT_WHITEPOINT: [f32; 3] = [1.0, 1.0, 1.0];
523    const D65_WHITEPOINT: [f32; 3] = [0.95047, 1.0, 1.08883];
524
525    fn decode_l_constant() -> f32 {
526        ((8.0_f32 + 16.0) / 116.0).powi(3) / 8.0
527    }
528
529    fn srgb_transfer_function(color: f32) -> f32 {
530        if color <= 0.0031308 {
531            (12.92 * color).clamp(0.0, 1.0)
532        } else if color >= 0.99554525 {
533            1.0
534        } else {
535            ((1.0 + 0.055) * color.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)
536        }
537    }
538
539    fn matrix_product(a: &[f32; 9], b: &[f32; 3]) -> [f32; 3] {
540        [
541            a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
542            a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
543            a[6] * b[0] + a[7] * b[1] + a[8] * b[2],
544        ]
545    }
546
547    fn to_flat(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
548        [
549            lms[0] / source_white_point[0],
550            lms[1] / source_white_point[1],
551            lms[2] / source_white_point[2],
552        ]
553    }
554
555    fn to_d65(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
556        [
557            lms[0] * Self::D65_WHITEPOINT[0] / source_white_point[0],
558            lms[1] * Self::D65_WHITEPOINT[1] / source_white_point[1],
559            lms[2] * Self::D65_WHITEPOINT[2] / source_white_point[2],
560        ]
561    }
562
563    fn decode_l(l: f32) -> f32 {
564        if l < 0.0 {
565            -Self::decode_l(-l)
566        } else if l > 8.0 {
567            ((l + 16.0) / 116.0).powi(3)
568        } else {
569            l * Self::decode_l_constant()
570        }
571    }
572
573    fn compensate_black_point(source_bp: &[f32; 3], xyz_flat: &[f32; 3]) -> [f32; 3] {
574        if source_bp == &[0.0, 0.0, 0.0] {
575            return *xyz_flat;
576        }
577
578        let zero_decode_l = Self::decode_l(0.0);
579
580        let mut out = [0.0; 3];
581        for i in 0..3 {
582            let src = Self::decode_l(source_bp[i]);
583            let scale = (1.0 - zero_decode_l) / (1.0 - src);
584            let offset = 1.0 - scale;
585            out[i] = xyz_flat[i] * scale + offset;
586        }
587
588        out
589    }
590
591    fn normalize_white_point_to_flat(
592        &self,
593        source_white_point: &[f32; 3],
594        xyz: &[f32; 3],
595    ) -> [f32; 3] {
596        if source_white_point[0] == 1.0 && source_white_point[2] == 1.0 {
597            return *xyz;
598        }
599        let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
600        let lms_flat = Self::to_flat(source_white_point, &lms);
601        Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat)
602    }
603
604    fn normalize_white_point_to_d65(
605        &self,
606        source_white_point: &[f32; 3],
607        xyz: &[f32; 3],
608    ) -> [f32; 3] {
609        let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
610        let lms_d65 = Self::to_d65(source_white_point, &lms);
611        Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65)
612    }
613}
614
615impl ToRgb for CalRgb {
616    fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
617        for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) {
618            let input = [
619                input[0].clamp(0.0, 1.0),
620                input[1].clamp(0.0, 1.0),
621                input[2].clamp(0.0, 1.0),
622            ];
623
624            let [r, g, b] = input;
625            let [gr, gg, gb] = self.gamma;
626            let [agr, bgg, cgb] = [
627                if r == 1.0 { 1.0 } else { r.powf(gr) },
628                if g == 1.0 { 1.0 } else { g.powf(gg) },
629                if b == 1.0 { 1.0 } else { b.powf(gb) },
630            ];
631
632            let m = &self.matrix;
633            let x = m[0] * agr + m[3] * bgg + m[6] * cgb;
634            let y = m[1] * agr + m[4] * bgg + m[7] * cgb;
635            let z = m[2] * agr + m[5] * bgg + m[8] * cgb;
636            let xyz = [x, y, z];
637
638            let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz);
639            let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat);
640            let xyz_d65 = self.normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
641            let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
642
643            output.copy_from_slice(&[
644                (Self::srgb_transfer_function(srgb_xyz[0]) * 255.0 + 0.5) as u8,
645                (Self::srgb_transfer_function(srgb_xyz[1]) * 255.0 + 0.5) as u8,
646                (Self::srgb_transfer_function(srgb_xyz[2]) * 255.0 + 0.5) as u8,
647            ]);
648        }
649
650        Some(())
651    }
652}
653
654#[derive(Debug, Clone)]
655pub(crate) struct Lab {
656    range: [f32; 4],
657    profile: ICCProfile,
658}
659
660impl Lab {
661    fn new(dict: &Dict<'_>) -> Option<Self> {
662        let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
663        // Not sure how this should be used.
664        let _black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
665        let range = dict
666            .get::<[f32; 4]>(RANGE)
667            .unwrap_or([-100.0, 100.0, -100.0, 100.0]);
668
669        let mut profile = ColorProfile::new_from_slice(include_bytes!("../assets/LAB.icc")).ok()?;
670        profile.white_point = Xyzd::new(
671            white_point[0] as f64,
672            white_point[1] as f64,
673            white_point[2] as f64,
674        );
675
676        let profile = ICCProfile::new_from_src_profile(
677            profile, false,
678            // This flag is only used to scale the values to [0.0, 1.0], but
679            // we already take care of this in the `convert_f32` method.
680            // Therefore, leave this as false, even though this is a LAB profile.
681            false, 3,
682        )?;
683
684        Some(Self { range, profile })
685    }
686}
687
688impl ToRgb for Lab {
689    fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
690        if !manual_scale {
691            // moxcms expects values between 0.0 and 1.0, so we need to undo
692            // the scaling.
693
694            let input = input
695                .chunks_exact(3)
696                .flat_map(|i| {
697                    let l = i[0] / 100.0;
698                    let a = (i[1] + 128.0) / 255.0;
699                    let b = (i[2] + 128.0) / 255.0;
700
701                    [l, a, b]
702                })
703                .collect::<Vec<_>>();
704
705            self.profile.convert_f32(&input, output, manual_scale)
706        } else {
707            self.profile.convert_f32(input, output, manual_scale)
708        }
709    }
710}
711
712#[derive(Debug, Clone)]
713pub(crate) struct Indexed {
714    values: Vec<Vec<f32>>,
715    hival: u8,
716    base: Box<ColorSpace>,
717}
718
719impl Indexed {
720    fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
721        let mut iter = array.flex_iter();
722        // Skip name
723        let _ = iter.next::<Name<'_>>()?;
724        let base_color_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
725        let hival = iter.next::<u32>()?.min(u8::MAX as u32) as u8;
726
727        let values = {
728            let data = iter
729                .next::<Stream<'_>>()
730                .and_then(|s| s.decoded().ok())
731                .or_else(|| {
732                    iter.next::<object::String<'_>>()
733                        .map(|s| Cow::Owned(s.to_vec()))
734                })?;
735
736            let num_components = base_color_space.num_components();
737
738            let mut byte_iter = data.iter().copied();
739
740            let mut vals = vec![];
741            for _ in 0..=hival {
742                let mut temp = vec![];
743
744                for _ in 0..num_components {
745                    temp.push(byte_iter.next()? as f32 / 255.0);
746                }
747
748                vals.push(temp);
749            }
750
751            vals
752        };
753
754        Some(Self {
755            values,
756            hival,
757            base: Box::new(base_color_space),
758        })
759    }
760}
761
762impl ToRgb for Indexed {
763    fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
764        let mut indexed = vec![0.0; input.len() * self.base.num_components() as usize];
765
766        for (input, output) in input
767            .iter()
768            .copied()
769            .zip(indexed.chunks_exact_mut(self.base.num_components() as usize))
770        {
771            let idx = (input.clamp(0.0, self.hival as f32) + 0.5) as usize;
772            output.copy_from_slice(&self.values[idx]);
773        }
774
775        self.base.convert_f32(&indexed, output, true)
776    }
777}
778
779#[derive(Debug, Clone)]
780pub(crate) struct Separation {
781    alternate_space: ColorSpace,
782    tint_transform: Function,
783    is_none_separation: bool,
784}
785
786impl Separation {
787    fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
788        let mut iter = array.flex_iter();
789        // Skip `/Separation`
790        let _ = iter.next::<Name<'_>>()?;
791        let name = iter.next::<Name<'_>>()?;
792        let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
793        let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
794        // Either I did something wrong, or no other viewers properly handles
795        // `All`, so let's just ignore it as well.
796        let is_none_separation = name.as_str() == "None";
797
798        Some(Self {
799            alternate_space,
800            tint_transform,
801            is_none_separation,
802        })
803    }
804}
805
806impl ToRgb for Separation {
807    fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
808        let evaluated = input
809            .iter()
810            .flat_map(|n| {
811                self.tint_transform
812                    .eval(smallvec![*n])
813                    .unwrap_or(self.alternate_space.initial_color())
814            })
815            .collect::<Vec<_>>();
816        self.alternate_space.convert_f32(&evaluated, output, false)
817    }
818
819    fn is_none(&self) -> bool {
820        self.is_none_separation
821    }
822}
823
824#[derive(Debug, Clone)]
825pub(crate) struct DeviceN {
826    alternate_space: ColorSpace,
827    num_components: u8,
828    tint_transform: Function,
829    is_none: bool,
830}
831
832impl DeviceN {
833    fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
834        let mut iter = array.flex_iter();
835        // Skip `/DeviceN`
836        let _ = iter.next::<Name<'_>>()?;
837        // Skip `Name`.
838        let names = iter
839            .next::<Array<'_>>()?
840            .iter::<Name<'_>>()
841            .collect::<Vec<_>>();
842        let num_components = u8::try_from(names.len()).ok()?;
843        let all_none = names.iter().all(|n| n.as_str() == "None");
844        let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
845        let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
846
847        if num_components == 0 {
848            return None;
849        }
850
851        Some(Self {
852            alternate_space,
853            num_components,
854            tint_transform,
855            is_none: all_none,
856        })
857    }
858}
859
860impl ToRgb for DeviceN {
861    fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
862        let evaluated = input
863            .chunks_exact(self.num_components as usize)
864            .flat_map(|n| {
865                self.tint_transform
866                    .eval(n.to_smallvec())
867                    .unwrap_or(self.alternate_space.initial_color())
868            })
869            .collect::<Vec<_>>();
870        self.alternate_space.convert_f32(&evaluated, output, false)
871    }
872
873    fn is_none(&self) -> bool {
874        self.is_none
875    }
876}
877
878struct ICCColorRepr {
879    src_profile: ColorProfile,
880    src_layout: Layout,
881    number_components: usize,
882    is_srgb: bool,
883    is_lab: bool,
884    transform_u8: Arc<Transform8BitExecutor>,
885    transform_f32: OnceLock<Arc<TransformF32Executor>>,
886}
887
888#[derive(Clone)]
889pub(crate) struct ICCProfile(Arc<ICCColorRepr>);
890
891impl Debug for ICCProfile {
892    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
893        write!(f, "ICCColor {{..}}")
894    }
895}
896
897impl ICCProfile {
898    fn new(profile: &[u8], number_components: usize) -> Option<Self> {
899        let src_profile = ColorProfile::new_from_slice(profile).ok()?;
900
901        const SRGB_MARKER: &[u8] = b"sRGB";
902
903        let is_srgb = profile
904            .get(52..56)
905            .map(|device_model| device_model == SRGB_MARKER)
906            .unwrap_or(false);
907        let is_lab = src_profile.color_space == DataColorSpace::Lab;
908
909        Self::new_from_src_profile(src_profile, is_srgb, is_lab, number_components)
910    }
911
912    fn new_from_src_profile(
913        src_profile: ColorProfile,
914        is_srgb: bool,
915        is_lab: bool,
916        number_components: usize,
917    ) -> Option<Self> {
918        let src_layout = match number_components {
919            1 => Layout::Gray,
920            3 => Layout::Rgb,
921            4 => Layout::Rgba,
922            _ => {
923                warn!("unsupported number of components {number_components} for ICC profile");
924
925                return None;
926            }
927        };
928
929        let dest_profile = ColorProfile::new_srgb();
930        let transform_u8 = src_profile
931            .clone()
932            .create_transform_8bit(
933                src_layout,
934                &dest_profile,
935                Layout::Rgb,
936                TransformOptions::default(),
937            )
938            .ok()?;
939
940        Some(Self(Arc::new(ICCColorRepr {
941            src_profile,
942            src_layout,
943            number_components,
944            is_srgb,
945            is_lab,
946            transform_u8,
947            transform_f32: OnceLock::new(),
948        })))
949    }
950
951    fn is_srgb(&self) -> bool {
952        self.0.is_srgb
953    }
954
955    fn is_lab(&self) -> bool {
956        self.0.is_lab
957    }
958
959    fn transform_u8(&self) -> &Arc<Transform8BitExecutor> {
960        &self.0.transform_u8
961    }
962
963    fn transform_f32(&self) -> &Arc<TransformF32Executor> {
964        // From my benchmarking, creating the f32 transforms is usually much
965        // more expensive than u8. Therefore, we only create it lazily when
966        // really needed.
967        self.0.transform_f32.get_or_init(|| {
968            let dest_profile = ColorProfile::new_srgb();
969            self.0
970                .src_profile
971                .clone()
972                .create_transform_f32(
973                    self.0.src_layout,
974                    &dest_profile,
975                    Layout::Rgb,
976                    TransformOptions::default(),
977                )
978                // Since the u8 version was valid, hopefully this should never panic?
979                .unwrap()
980        })
981    }
982}
983
984impl ToRgb for ICCProfile {
985    fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
986        let mut temp = vec![0.0_f32; output.len()];
987
988        if self.is_lab() {
989            // moxcms expects normalized values.
990            let scaled = input
991                .chunks_exact(3)
992                .flat_map(|i| {
993                    [
994                        i[0] * (1.0 / 100.0),
995                        (i[1] + 128.0) * (1.0 / 255.0),
996                        (i[2] + 128.0) * (1.0 / 255.0),
997                    ]
998                })
999                .collect::<Vec<_>>();
1000            self.transform_f32().transform(&scaled, &mut temp).ok()?;
1001        } else {
1002            self.transform_f32().transform(input, &mut temp).ok()?;
1003        };
1004
1005        for (input, output) in temp.iter().zip(output.iter_mut()) {
1006            *output = (input * 255.0 + 0.5) as u8;
1007        }
1008
1009        Some(())
1010    }
1011
1012    fn supports_u8(&self) -> bool {
1013        true
1014    }
1015
1016    fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
1017        if self.is_srgb() {
1018            output.copy_from_slice(input);
1019        } else {
1020            self.transform_u8().transform(input, output).ok()?;
1021        }
1022
1023        Some(())
1024    }
1025}
1026
1027#[inline(always)]
1028fn f32_to_u8(val: f32) -> u8 {
1029    (val * 255.0 + 0.5) as u8
1030}
1031
1032#[derive(Debug, Clone)]
1033/// A color.
1034pub struct Color {
1035    color_space: ColorSpace,
1036    components: ColorComponents,
1037    opacity: f32,
1038}
1039
1040impl Color {
1041    pub(crate) fn new(color_space: ColorSpace, components: ColorComponents, opacity: f32) -> Self {
1042        Self {
1043            color_space,
1044            components,
1045            opacity,
1046        }
1047    }
1048
1049    /// Return the color as an RGBA color.
1050    pub fn to_rgba(&self) -> AlphaColor {
1051        self.color_space
1052            .to_rgba(&self.components, self.opacity, false)
1053    }
1054
1055    /// Create a color from RGBA.
1056    pub fn from_rgba(rgba: AlphaColor) -> Self {
1057        let c = rgba.components();
1058        Self {
1059            color_space: ColorSpace::device_rgb(),
1060            components: smallvec![c[0], c[1], c[2]],
1061            opacity: c[3],
1062        }
1063    }
1064}
1065
1066static CMYK_TRANSFORM: LazyLock<ICCProfile> = LazyLock::new(|| {
1067    ICCProfile::new(include_bytes!("../assets/CGATS001Compat-v2-micro.icc"), 4).unwrap()
1068});
1069
1070pub(crate) trait ToRgb {
1071    fn convert_sample(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
1072        // We prefer using the u8 variant for single samples, which is especially
1073        // important for ICC profiles to avoid constructing the (more expensive)
1074        // f32 variant.
1075        if self.supports_u8() {
1076            let converted = input
1077                .iter()
1078                .copied()
1079                .map(f32_to_u8)
1080                .collect::<SmallVec<[u8; 4]>>();
1081
1082            if self.convert_u8(&converted, output).is_some() {
1083                return Some(());
1084            }
1085        }
1086
1087        self.convert_f32(input, output, manual_scale)
1088    }
1089
1090    fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()>;
1091    fn supports_u8(&self) -> bool {
1092        false
1093    }
1094    fn convert_u8(&self, _: &[u8], _: &mut [u8]) -> Option<()> {
1095        unimplemented!();
1096    }
1097    fn is_none(&self) -> bool {
1098        false
1099    }
1100    fn to_alpha_color(
1101        &self,
1102        input: &[f32],
1103        mut opacity: f32,
1104        manual_scale: bool,
1105    ) -> Option<AlphaColor> {
1106        let mut output = [0; 3];
1107        self.convert_sample(input, &mut output, manual_scale)?;
1108
1109        // For separation color spaces:
1110        // "The special colourant name None shall not produce any visible output.
1111        // Painting operations in a Separation space with this colourant name
1112        // shall have no effect on the current page."
1113        if self.is_none() {
1114            opacity = 0.0;
1115        }
1116
1117        Some(AlphaColor::from_rgba8(
1118            output[0],
1119            output[1],
1120            output[2],
1121            (opacity * 255.0 + 0.5) as u8,
1122        ))
1123    }
1124}