moxcms/
profile.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved.
3 * //
4 * // Redistribution and use in source and binary forms, with or without modification,
5 * // are permitted provided that the following conditions are met:
6 * //
7 * // 1.  Redistributions of source code must retain the above copyright notice, this
8 * // list of conditions and the following disclaimer.
9 * //
10 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * // this list of conditions and the following disclaimer in the documentation
12 * // and/or other materials provided with the distribution.
13 * //
14 * // 3.  Neither the name of the copyright holder nor the names of its
15 * // contributors may be used to endorse or promote products derived from
16 * // this software without specific prior written permission.
17 * //
18 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29use crate::chad::BRADFORD_D;
30use crate::cicp::{
31    CicpColorPrimaries, ColorPrimaries, MatrixCoefficients, TransferCharacteristics,
32};
33use crate::dat::ColorDateTime;
34use crate::err::CmsError;
35use crate::matrix::{Matrix3f, Xyz};
36use crate::reader::s15_fixed16_number_to_float;
37use crate::safe_math::{SafeAdd, SafeMul};
38use crate::tag::{TAG_SIZE, Tag};
39use crate::trc::ToneReprCurve;
40use crate::{Chromaticity, Layout, Matrix3d, Vector3d, XyY, Xyzd, adapt_to_d50_d};
41use std::io::Read;
42
43const MAX_PROFILE_SIZE: usize = 1024 * 1024 * 10; // 10 MB max, for Fogra39 etc
44
45#[repr(u32)]
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ProfileSignature {
48    Acsp,
49}
50
51impl TryFrom<u32> for ProfileSignature {
52    type Error = CmsError;
53    #[inline]
54    fn try_from(value: u32) -> Result<Self, Self::Error> {
55        if value == u32::from_ne_bytes(*b"acsp").to_be() {
56            return Ok(ProfileSignature::Acsp);
57        }
58        Err(CmsError::InvalidProfile)
59    }
60}
61
62impl From<ProfileSignature> for u32 {
63    #[inline]
64    fn from(value: ProfileSignature) -> Self {
65        match value {
66            ProfileSignature::Acsp => u32::from_ne_bytes(*b"acsp").to_be(),
67        }
68    }
69}
70
71#[repr(u32)]
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)]
73pub enum ProfileVersion {
74    V2_0 = 0x02000000,
75    V2_1 = 0x02100000,
76    V2_2 = 0x02200000,
77    V2_3 = 0x02300000,
78    V2_4 = 0x02400000,
79    V4_0 = 0x04000000,
80    V4_1 = 0x04100000,
81    V4_2 = 0x04200000,
82    V4_3 = 0x04300000,
83    #[default]
84    V4_4 = 0x04400000,
85    Unknown,
86}
87
88impl TryFrom<u32> for ProfileVersion {
89    type Error = CmsError;
90    fn try_from(value: u32) -> Result<Self, Self::Error> {
91        match value {
92            0x02000000 => Ok(ProfileVersion::V2_0),
93            0x02100000 => Ok(ProfileVersion::V2_1),
94            0x02200000 => Ok(ProfileVersion::V2_2),
95            0x02300000 => Ok(ProfileVersion::V2_3),
96            0x02400000 => Ok(ProfileVersion::V2_4),
97            0x04000000 => Ok(ProfileVersion::V4_0),
98            0x04100000 => Ok(ProfileVersion::V4_1),
99            0x04200000 => Ok(ProfileVersion::V4_2),
100            0x04300000 => Ok(ProfileVersion::V4_3),
101            0x04400000 => Ok(ProfileVersion::V4_3),
102            _ => Err(CmsError::InvalidProfile),
103        }
104    }
105}
106
107impl From<ProfileVersion> for u32 {
108    fn from(value: ProfileVersion) -> Self {
109        match value {
110            ProfileVersion::V2_0 => 0x02000000,
111            ProfileVersion::V2_1 => 0x02100000,
112            ProfileVersion::V2_2 => 0x02200000,
113            ProfileVersion::V2_3 => 0x02300000,
114            ProfileVersion::V2_4 => 0x02400000,
115            ProfileVersion::V4_0 => 0x04000000,
116            ProfileVersion::V4_1 => 0x04100000,
117            ProfileVersion::V4_2 => 0x04200000,
118            ProfileVersion::V4_3 => 0x04300000,
119            ProfileVersion::V4_4 => 0x04400000,
120            ProfileVersion::Unknown => 0x02000000,
121        }
122    }
123}
124
125#[repr(u32)]
126#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Hash)]
127pub enum DataColorSpace {
128    #[default]
129    Xyz,
130    Lab,
131    Luv,
132    YCbr,
133    Yxy,
134    Rgb,
135    Gray,
136    Hsv,
137    Hls,
138    Cmyk,
139    Cmy,
140    Color2,
141    Color3,
142    Color4,
143    Color5,
144    Color6,
145    Color7,
146    Color8,
147    Color9,
148    Color10,
149    Color11,
150    Color12,
151    Color13,
152    Color14,
153    Color15,
154}
155
156impl DataColorSpace {
157    #[inline]
158    pub fn check_layout(self, layout: Layout) -> Result<(), CmsError> {
159        let unsupported: bool = match self {
160            DataColorSpace::Xyz => layout != Layout::Rgb,
161            DataColorSpace::Lab => layout != Layout::Rgb,
162            DataColorSpace::Luv => layout != Layout::Rgb,
163            DataColorSpace::YCbr => layout != Layout::Rgb,
164            DataColorSpace::Yxy => layout != Layout::Rgb,
165            DataColorSpace::Rgb => layout != Layout::Rgb && layout != Layout::Rgba,
166            DataColorSpace::Gray => layout != Layout::Gray && layout != Layout::GrayAlpha,
167            DataColorSpace::Hsv => layout != Layout::Rgb,
168            DataColorSpace::Hls => layout != Layout::Rgb,
169            DataColorSpace::Cmyk => layout != Layout::Rgba,
170            DataColorSpace::Cmy => layout != Layout::Rgb,
171            DataColorSpace::Color2 => layout != Layout::GrayAlpha,
172            DataColorSpace::Color3 => layout != Layout::Rgb,
173            DataColorSpace::Color4 => layout != Layout::Rgba,
174            DataColorSpace::Color5 => layout != Layout::Inks5,
175            DataColorSpace::Color6 => layout != Layout::Inks6,
176            DataColorSpace::Color7 => layout != Layout::Inks7,
177            DataColorSpace::Color8 => layout != Layout::Inks8,
178            DataColorSpace::Color9 => layout != Layout::Inks9,
179            DataColorSpace::Color10 => layout != Layout::Inks10,
180            DataColorSpace::Color11 => layout != Layout::Inks11,
181            DataColorSpace::Color12 => layout != Layout::Inks12,
182            DataColorSpace::Color13 => layout != Layout::Inks13,
183            DataColorSpace::Color14 => layout != Layout::Inks14,
184            DataColorSpace::Color15 => layout != Layout::Inks15,
185        };
186        if unsupported {
187            Err(CmsError::InvalidLayout)
188        } else {
189            Ok(())
190        }
191    }
192
193    pub(crate) fn is_three_channels(self) -> bool {
194        matches!(
195            self,
196            DataColorSpace::Xyz
197                | DataColorSpace::Lab
198                | DataColorSpace::Luv
199                | DataColorSpace::YCbr
200                | DataColorSpace::Yxy
201                | DataColorSpace::Rgb
202                | DataColorSpace::Hsv
203                | DataColorSpace::Hls
204                | DataColorSpace::Cmy
205                | DataColorSpace::Color3
206        )
207    }
208}
209
210#[repr(u32)]
211#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)]
212pub enum ProfileClass {
213    InputDevice,
214    #[default]
215    DisplayDevice,
216    OutputDevice,
217    DeviceLink,
218    ColorSpace,
219    Abstract,
220    Named,
221}
222
223impl TryFrom<u32> for ProfileClass {
224    type Error = CmsError;
225    fn try_from(value: u32) -> Result<Self, Self::Error> {
226        if value == u32::from_ne_bytes(*b"scnr").to_be() {
227            return Ok(ProfileClass::InputDevice);
228        } else if value == u32::from_ne_bytes(*b"mntr").to_be() {
229            return Ok(ProfileClass::DisplayDevice);
230        } else if value == u32::from_ne_bytes(*b"prtr").to_be() {
231            return Ok(ProfileClass::OutputDevice);
232        } else if value == u32::from_ne_bytes(*b"link").to_be() {
233            return Ok(ProfileClass::DeviceLink);
234        } else if value == u32::from_ne_bytes(*b"spac").to_be() {
235            return Ok(ProfileClass::ColorSpace);
236        } else if value == u32::from_ne_bytes(*b"abst").to_be() {
237            return Ok(ProfileClass::Abstract);
238        } else if value == u32::from_ne_bytes(*b"nmcl").to_be() {
239            return Ok(ProfileClass::Named);
240        }
241        Err(CmsError::InvalidProfile)
242    }
243}
244
245impl From<ProfileClass> for u32 {
246    fn from(val: ProfileClass) -> Self {
247        match val {
248            ProfileClass::InputDevice => u32::from_ne_bytes(*b"scnr").to_be(),
249            ProfileClass::DisplayDevice => u32::from_ne_bytes(*b"mntr").to_be(),
250            ProfileClass::OutputDevice => u32::from_ne_bytes(*b"prtr").to_be(),
251            ProfileClass::DeviceLink => u32::from_ne_bytes(*b"link").to_be(),
252            ProfileClass::ColorSpace => u32::from_ne_bytes(*b"spac").to_be(),
253            ProfileClass::Abstract => u32::from_ne_bytes(*b"abst").to_be(),
254            ProfileClass::Named => u32::from_ne_bytes(*b"nmcl").to_be(),
255        }
256    }
257}
258
259#[derive(Debug, Clone)]
260pub enum LutStore {
261    Store8(Vec<u8>),
262    Store16(Vec<u16>),
263}
264
265#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
266pub enum LutType {
267    Lut8,
268    Lut16,
269    LutMab,
270    LutMba,
271}
272
273impl TryFrom<u32> for LutType {
274    type Error = CmsError;
275    fn try_from(value: u32) -> Result<Self, Self::Error> {
276        if value == u32::from_ne_bytes(*b"mft1").to_be() {
277            return Ok(LutType::Lut8);
278        } else if value == u32::from_ne_bytes(*b"mft2").to_be() {
279            return Ok(LutType::Lut16);
280        } else if value == u32::from_ne_bytes(*b"mAB ").to_be() {
281            return Ok(LutType::LutMab);
282        } else if value == u32::from_ne_bytes(*b"mBA ").to_be() {
283            return Ok(LutType::LutMba);
284        }
285        Err(CmsError::InvalidProfile)
286    }
287}
288
289impl From<LutType> for u32 {
290    fn from(val: LutType) -> Self {
291        match val {
292            LutType::Lut8 => u32::from_ne_bytes(*b"mft1").to_be(),
293            LutType::Lut16 => u32::from_ne_bytes(*b"mft2").to_be(),
294            LutType::LutMab => u32::from_ne_bytes(*b"mAB ").to_be(),
295            LutType::LutMba => u32::from_ne_bytes(*b"mBA ").to_be(),
296        }
297    }
298}
299
300impl TryFrom<u32> for DataColorSpace {
301    type Error = CmsError;
302    fn try_from(value: u32) -> Result<Self, Self::Error> {
303        if value == u32::from_ne_bytes(*b"XYZ ").to_be() {
304            return Ok(DataColorSpace::Xyz);
305        } else if value == u32::from_ne_bytes(*b"Lab ").to_be() {
306            return Ok(DataColorSpace::Lab);
307        } else if value == u32::from_ne_bytes(*b"Luv ").to_be() {
308            return Ok(DataColorSpace::Luv);
309        } else if value == u32::from_ne_bytes(*b"YCbr").to_be() {
310            return Ok(DataColorSpace::YCbr);
311        } else if value == u32::from_ne_bytes(*b"Yxy ").to_be() {
312            return Ok(DataColorSpace::Yxy);
313        } else if value == u32::from_ne_bytes(*b"RGB ").to_be() {
314            return Ok(DataColorSpace::Rgb);
315        } else if value == u32::from_ne_bytes(*b"GRAY").to_be() {
316            return Ok(DataColorSpace::Gray);
317        } else if value == u32::from_ne_bytes(*b"HSV ").to_be() {
318            return Ok(DataColorSpace::Hsv);
319        } else if value == u32::from_ne_bytes(*b"HLS ").to_be() {
320            return Ok(DataColorSpace::Hls);
321        } else if value == u32::from_ne_bytes(*b"CMYK").to_be() {
322            return Ok(DataColorSpace::Cmyk);
323        } else if value == u32::from_ne_bytes(*b"CMY ").to_be() {
324            return Ok(DataColorSpace::Cmy);
325        } else if value == u32::from_ne_bytes(*b"2CLR").to_be() {
326            return Ok(DataColorSpace::Color2);
327        } else if value == u32::from_ne_bytes(*b"3CLR").to_be() {
328            return Ok(DataColorSpace::Color3);
329        } else if value == u32::from_ne_bytes(*b"4CLR").to_be() {
330            return Ok(DataColorSpace::Color4);
331        } else if value == u32::from_ne_bytes(*b"5CLR").to_be() {
332            return Ok(DataColorSpace::Color5);
333        } else if value == u32::from_ne_bytes(*b"6CLR").to_be() {
334            return Ok(DataColorSpace::Color6);
335        } else if value == u32::from_ne_bytes(*b"7CLR").to_be() {
336            return Ok(DataColorSpace::Color7);
337        } else if value == u32::from_ne_bytes(*b"8CLR").to_be() {
338            return Ok(DataColorSpace::Color8);
339        } else if value == u32::from_ne_bytes(*b"9CLR").to_be() {
340            return Ok(DataColorSpace::Color9);
341        } else if value == u32::from_ne_bytes(*b"ACLR").to_be() {
342            return Ok(DataColorSpace::Color10);
343        } else if value == u32::from_ne_bytes(*b"BCLR").to_be() {
344            return Ok(DataColorSpace::Color11);
345        } else if value == u32::from_ne_bytes(*b"CCLR").to_be() {
346            return Ok(DataColorSpace::Color12);
347        } else if value == u32::from_ne_bytes(*b"DCLR").to_be() {
348            return Ok(DataColorSpace::Color13);
349        } else if value == u32::from_ne_bytes(*b"ECLR").to_be() {
350            return Ok(DataColorSpace::Color14);
351        } else if value == u32::from_ne_bytes(*b"FCLR").to_be() {
352            return Ok(DataColorSpace::Color15);
353        }
354        Err(CmsError::InvalidProfile)
355    }
356}
357
358impl From<DataColorSpace> for u32 {
359    fn from(val: DataColorSpace) -> Self {
360        match val {
361            DataColorSpace::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(),
362            DataColorSpace::Lab => u32::from_ne_bytes(*b"Lab ").to_be(),
363            DataColorSpace::Luv => u32::from_ne_bytes(*b"Luv ").to_be(),
364            DataColorSpace::YCbr => u32::from_ne_bytes(*b"YCbr").to_be(),
365            DataColorSpace::Yxy => u32::from_ne_bytes(*b"Yxy ").to_be(),
366            DataColorSpace::Rgb => u32::from_ne_bytes(*b"RGB ").to_be(),
367            DataColorSpace::Gray => u32::from_ne_bytes(*b"GRAY").to_be(),
368            DataColorSpace::Hsv => u32::from_ne_bytes(*b"HSV ").to_be(),
369            DataColorSpace::Hls => u32::from_ne_bytes(*b"HLS ").to_be(),
370            DataColorSpace::Cmyk => u32::from_ne_bytes(*b"CMYK").to_be(),
371            DataColorSpace::Cmy => u32::from_ne_bytes(*b"CMY ").to_be(),
372            DataColorSpace::Color2 => u32::from_ne_bytes(*b"2CLR").to_be(),
373            DataColorSpace::Color3 => u32::from_ne_bytes(*b"3CLR").to_be(),
374            DataColorSpace::Color4 => u32::from_ne_bytes(*b"4CLR").to_be(),
375            DataColorSpace::Color5 => u32::from_ne_bytes(*b"5CLR").to_be(),
376            DataColorSpace::Color6 => u32::from_ne_bytes(*b"6CLR").to_be(),
377            DataColorSpace::Color7 => u32::from_ne_bytes(*b"7CLR").to_be(),
378            DataColorSpace::Color8 => u32::from_ne_bytes(*b"8CLR").to_be(),
379            DataColorSpace::Color9 => u32::from_ne_bytes(*b"9CLR").to_be(),
380            DataColorSpace::Color10 => u32::from_ne_bytes(*b"ACLR").to_be(),
381            DataColorSpace::Color11 => u32::from_ne_bytes(*b"BCLR").to_be(),
382            DataColorSpace::Color12 => u32::from_ne_bytes(*b"CCLR").to_be(),
383            DataColorSpace::Color13 => u32::from_ne_bytes(*b"DCLR").to_be(),
384            DataColorSpace::Color14 => u32::from_ne_bytes(*b"ECLR").to_be(),
385            DataColorSpace::Color15 => u32::from_ne_bytes(*b"FCLR").to_be(),
386        }
387    }
388}
389
390#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
391pub enum TechnologySignatures {
392    FilmScanner,
393    DigitalCamera,
394    ReflectiveScanner,
395    InkJetPrinter,
396    ThermalWaxPrinter,
397    ElectrophotographicPrinter,
398    ElectrostaticPrinter,
399    DyeSublimationPrinter,
400    PhotographicPaperPrinter,
401    FilmWriter,
402    VideoMonitor,
403    VideoCamera,
404    ProjectionTelevision,
405    CathodeRayTubeDisplay,
406    PassiveMatrixDisplay,
407    ActiveMatrixDisplay,
408    LiquidCrystalDisplay,
409    OrganicLedDisplay,
410    PhotoCd,
411    PhotographicImageSetter,
412    Gravure,
413    OffsetLithography,
414    Silkscreen,
415    Flexography,
416    MotionPictureFilmScanner,
417    MotionPictureFilmRecorder,
418    DigitalMotionPictureCamera,
419    DigitalCinemaProjector,
420    Unknown(u32),
421}
422
423impl From<u32> for TechnologySignatures {
424    fn from(value: u32) -> Self {
425        if value == u32::from_ne_bytes(*b"fscn").to_be() {
426            return TechnologySignatures::FilmScanner;
427        } else if value == u32::from_ne_bytes(*b"dcam").to_be() {
428            return TechnologySignatures::DigitalCamera;
429        } else if value == u32::from_ne_bytes(*b"rscn").to_be() {
430            return TechnologySignatures::ReflectiveScanner;
431        } else if value == u32::from_ne_bytes(*b"ijet").to_be() {
432            return TechnologySignatures::InkJetPrinter;
433        } else if value == u32::from_ne_bytes(*b"twax").to_be() {
434            return TechnologySignatures::ThermalWaxPrinter;
435        } else if value == u32::from_ne_bytes(*b"epho").to_be() {
436            return TechnologySignatures::ElectrophotographicPrinter;
437        } else if value == u32::from_ne_bytes(*b"esta").to_be() {
438            return TechnologySignatures::ElectrostaticPrinter;
439        } else if value == u32::from_ne_bytes(*b"dsub").to_be() {
440            return TechnologySignatures::DyeSublimationPrinter;
441        } else if value == u32::from_ne_bytes(*b"rpho").to_be() {
442            return TechnologySignatures::PhotographicPaperPrinter;
443        } else if value == u32::from_ne_bytes(*b"fprn").to_be() {
444            return TechnologySignatures::FilmWriter;
445        } else if value == u32::from_ne_bytes(*b"vidm").to_be() {
446            return TechnologySignatures::VideoMonitor;
447        } else if value == u32::from_ne_bytes(*b"vidc").to_be() {
448            return TechnologySignatures::VideoCamera;
449        } else if value == u32::from_ne_bytes(*b"pjtv").to_be() {
450            return TechnologySignatures::ProjectionTelevision;
451        } else if value == u32::from_ne_bytes(*b"CRT ").to_be() {
452            return TechnologySignatures::CathodeRayTubeDisplay;
453        } else if value == u32::from_ne_bytes(*b"PMD ").to_be() {
454            return TechnologySignatures::PassiveMatrixDisplay;
455        } else if value == u32::from_ne_bytes(*b"AMD ").to_be() {
456            return TechnologySignatures::ActiveMatrixDisplay;
457        } else if value == u32::from_ne_bytes(*b"LCD ").to_be() {
458            return TechnologySignatures::LiquidCrystalDisplay;
459        } else if value == u32::from_ne_bytes(*b"OLED").to_be() {
460            return TechnologySignatures::OrganicLedDisplay;
461        } else if value == u32::from_ne_bytes(*b"KPCD").to_be() {
462            return TechnologySignatures::PhotoCd;
463        } else if value == u32::from_ne_bytes(*b"imgs").to_be() {
464            return TechnologySignatures::PhotographicImageSetter;
465        } else if value == u32::from_ne_bytes(*b"grav").to_be() {
466            return TechnologySignatures::Gravure;
467        } else if value == u32::from_ne_bytes(*b"offs").to_be() {
468            return TechnologySignatures::OffsetLithography;
469        } else if value == u32::from_ne_bytes(*b"silk").to_be() {
470            return TechnologySignatures::Silkscreen;
471        } else if value == u32::from_ne_bytes(*b"flex").to_be() {
472            return TechnologySignatures::Flexography;
473        } else if value == u32::from_ne_bytes(*b"mpfs").to_be() {
474            return TechnologySignatures::MotionPictureFilmScanner;
475        } else if value == u32::from_ne_bytes(*b"mpfr").to_be() {
476            return TechnologySignatures::MotionPictureFilmRecorder;
477        } else if value == u32::from_ne_bytes(*b"dmpc").to_be() {
478            return TechnologySignatures::DigitalMotionPictureCamera;
479        } else if value == u32::from_ne_bytes(*b"dcpj").to_be() {
480            return TechnologySignatures::DigitalCinemaProjector;
481        }
482        TechnologySignatures::Unknown(value)
483    }
484}
485
486#[derive(Debug, Clone)]
487pub enum LutWarehouse {
488    Lut(LutDataType),
489    Multidimensional(LutMultidimensionalType),
490}
491
492#[derive(Debug, Clone)]
493pub struct LutDataType {
494    // used by lut8Type/lut16Type (mft2) only
495    pub num_input_channels: u8,
496    pub num_output_channels: u8,
497    pub num_clut_grid_points: u8,
498    pub matrix: Matrix3d,
499    pub num_input_table_entries: u16,
500    pub num_output_table_entries: u16,
501    pub input_table: LutStore,
502    pub clut_table: LutStore,
503    pub output_table: LutStore,
504    pub lut_type: LutType,
505}
506
507impl LutDataType {
508    pub(crate) fn has_same_kind(&self) -> bool {
509        matches!(
510            (&self.input_table, &self.clut_table, &self.output_table),
511            (
512                LutStore::Store8(_),
513                LutStore::Store8(_),
514                LutStore::Store8(_)
515            ) | (
516                LutStore::Store16(_),
517                LutStore::Store16(_),
518                LutStore::Store16(_)
519            )
520        )
521    }
522}
523
524#[derive(Debug, Clone)]
525pub struct LutMultidimensionalType {
526    pub num_input_channels: u8,
527    pub num_output_channels: u8,
528    pub grid_points: [u8; 16],
529    pub clut: Option<LutStore>,
530    pub a_curves: Vec<ToneReprCurve>,
531    pub b_curves: Vec<ToneReprCurve>,
532    pub m_curves: Vec<ToneReprCurve>,
533    pub matrix: Matrix3d,
534    pub bias: Vector3d,
535}
536
537#[repr(u32)]
538#[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)]
539pub enum RenderingIntent {
540    AbsoluteColorimetric = 3,
541    Saturation = 2,
542    RelativeColorimetric = 1,
543    #[default]
544    Perceptual = 0,
545}
546
547impl TryFrom<u32> for RenderingIntent {
548    type Error = CmsError;
549
550    #[inline]
551    fn try_from(value: u32) -> Result<Self, Self::Error> {
552        match value {
553            0 => Ok(RenderingIntent::Perceptual),
554            1 => Ok(RenderingIntent::RelativeColorimetric),
555            2 => Ok(RenderingIntent::Saturation),
556            3 => Ok(RenderingIntent::AbsoluteColorimetric),
557            _ => Err(CmsError::InvalidRenderingIntent),
558        }
559    }
560}
561
562impl From<RenderingIntent> for u32 {
563    #[inline]
564    fn from(value: RenderingIntent) -> Self {
565        match value {
566            RenderingIntent::AbsoluteColorimetric => 3,
567            RenderingIntent::Saturation => 2,
568            RenderingIntent::RelativeColorimetric => 1,
569            RenderingIntent::Perceptual => 0,
570        }
571    }
572}
573
574/// ICC Header
575#[repr(C)]
576#[derive(Debug, Clone, Copy)]
577pub(crate) struct ProfileHeader {
578    pub size: u32,                         // Size of the profile (computed)
579    pub cmm_type: u32,                     // Preferred CMM type (ignored)
580    pub version: ProfileVersion,           // Version (4.3 or 4.4 if CICP is included)
581    pub profile_class: ProfileClass,       // Display device profile
582    pub data_color_space: DataColorSpace,  // RGB input color space
583    pub pcs: DataColorSpace,               // Profile connection space
584    pub creation_date_time: ColorDateTime, // Date and time
585    pub signature: ProfileSignature,       // Profile signature
586    pub platform: u32,                     // Platform target (ignored)
587    pub flags: u32,                        // Flags (not embedded, can be used independently)
588    pub device_manufacturer: u32,          // Device manufacturer (ignored)
589    pub device_model: u32,                 // Device model (ignored)
590    pub device_attributes: [u8; 8],        // Device attributes (ignored)
591    pub rendering_intent: RenderingIntent, // Relative colorimetric rendering intent
592    pub illuminant: Xyz,                   // D50 standard illuminant X
593    pub creator: u32,                      // Profile creator (ignored)
594    pub profile_id: [u8; 16],              // Profile id checksum (ignored)
595    pub reserved: [u8; 28],                // Reserved (ignored)
596    pub tag_count: u32,                    // Technically not part of header, but required
597}
598
599impl ProfileHeader {
600    #[allow(dead_code)]
601    pub(crate) fn new(size: u32) -> Self {
602        Self {
603            size,
604            cmm_type: 0,
605            version: ProfileVersion::V4_3,
606            profile_class: ProfileClass::DisplayDevice,
607            data_color_space: DataColorSpace::Rgb,
608            pcs: DataColorSpace::Xyz,
609            creation_date_time: ColorDateTime::default(),
610            signature: ProfileSignature::Acsp,
611            platform: 0,
612            flags: 0x00000000,
613            device_manufacturer: 0,
614            device_model: 0,
615            device_attributes: [0; 8],
616            rendering_intent: RenderingIntent::Perceptual,
617            illuminant: Chromaticity::D50.to_xyz(),
618            creator: 0,
619            profile_id: [0; 16],
620            reserved: [0; 28],
621            tag_count: 0,
622        }
623    }
624
625    /// Creates profile from the buffer
626    pub(crate) fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> {
627        if slice.len() < size_of::<ProfileHeader>() {
628            return Err(CmsError::InvalidProfile);
629        }
630        let mut cursor = std::io::Cursor::new(slice);
631        let mut buffer = [0u8; size_of::<ProfileHeader>()];
632        cursor
633            .read_exact(&mut buffer)
634            .map_err(|_| CmsError::InvalidProfile)?;
635
636        let header = Self {
637            size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()),
638            cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()),
639            version: ProfileVersion::try_from(u32::from_be_bytes(
640                buffer[8..12].try_into().unwrap(),
641            ))?,
642            profile_class: ProfileClass::try_from(u32::from_be_bytes(
643                buffer[12..16].try_into().unwrap(),
644            ))?,
645            data_color_space: DataColorSpace::try_from(u32::from_be_bytes(
646                buffer[16..20].try_into().unwrap(),
647            ))?,
648            pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?,
649            creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?,
650            signature: ProfileSignature::try_from(u32::from_be_bytes(
651                buffer[36..40].try_into().unwrap(),
652            ))?,
653            platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()),
654            flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()),
655            device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()),
656            device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()),
657            device_attributes: buffer[56..64].try_into().unwrap(),
658            rendering_intent: RenderingIntent::try_from(u32::from_be_bytes(
659                buffer[64..68].try_into().unwrap(),
660            ))?,
661            illuminant: Xyz::new(
662                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())),
663                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())),
664                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())),
665            ),
666            creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()),
667            profile_id: buffer[84..100].try_into().unwrap(),
668            reserved: buffer[100..128].try_into().unwrap(),
669            tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()),
670        };
671        Ok(header)
672    }
673}
674
675/// A [Coding Independent Code Point](https://en.wikipedia.org/wiki/Coding-independent_code_points).
676#[repr(C)]
677#[derive(Debug, Clone, Copy)]
678pub struct CicpProfile {
679    pub color_primaries: CicpColorPrimaries,
680    pub transfer_characteristics: TransferCharacteristics,
681    pub matrix_coefficients: MatrixCoefficients,
682    pub full_range: bool,
683}
684
685#[derive(Debug, Clone)]
686pub struct LocalizableString {
687    /// An ISO 639-1 value is expected; any text w. more than two symbols will be truncated
688    pub language: String,
689    /// An ISO 3166-1 value is expected; any text w. more than two symbols will be truncated
690    pub country: String,
691    pub value: String,
692}
693
694impl LocalizableString {
695    /// Creates new localizable string
696    ///
697    /// # Arguments
698    ///
699    /// * `language`: an ISO 639-1 value is expected, any text more than 2 symbols will be truncated
700    /// * `country`: an ISO 3166-1 value is expected, any text more than 2 symbols will be truncated
701    /// * `value`: String value
702    ///
703    pub fn new(language: String, country: String, value: String) -> Self {
704        Self {
705            language,
706            country,
707            value,
708        }
709    }
710}
711
712#[derive(Debug, Clone)]
713pub struct DescriptionString {
714    pub ascii_string: String,
715    pub unicode_language_code: u32,
716    pub unicode_string: String,
717    pub script_code_code: i8,
718    pub mac_string: String,
719}
720
721#[derive(Debug, Clone)]
722pub enum ProfileText {
723    PlainString(String),
724    Localizable(Vec<LocalizableString>),
725    Description(DescriptionString),
726}
727
728impl ProfileText {
729    pub(crate) fn has_values(&self) -> bool {
730        match self {
731            ProfileText::PlainString(_) => true,
732            ProfileText::Localizable(lc) => !lc.is_empty(),
733            ProfileText::Description(_) => true,
734        }
735    }
736}
737
738#[derive(Debug, Clone, Copy)]
739pub enum StandardObserver {
740    D50,
741    D65,
742    Unknown,
743}
744
745impl From<u32> for StandardObserver {
746    fn from(value: u32) -> Self {
747        if value == 1 {
748            return StandardObserver::D50;
749        } else if value == 2 {
750            return StandardObserver::D65;
751        }
752        StandardObserver::Unknown
753    }
754}
755
756#[derive(Debug, Clone, Copy)]
757pub struct ViewingConditions {
758    pub illuminant: Xyz,
759    pub surround: Xyz,
760    pub observer: StandardObserver,
761}
762
763#[derive(Debug, Clone, Copy)]
764pub enum MeasurementGeometry {
765    Unknown,
766    /// 0°:45° or 45°:0°
767    D45to45,
768    /// 0°:d or d:0°
769    D0to0,
770}
771
772impl From<u32> for MeasurementGeometry {
773    fn from(value: u32) -> Self {
774        if value == 1 {
775            Self::D45to45
776        } else if value == 2 {
777            Self::D0to0
778        } else {
779            Self::Unknown
780        }
781    }
782}
783
784#[derive(Debug, Clone, Copy)]
785pub enum StandardIlluminant {
786    Unknown,
787    D50,
788    D65,
789    D93,
790    F2,
791    D55,
792    A,
793    EquiPower,
794    F8,
795}
796
797impl From<u32> for StandardIlluminant {
798    fn from(value: u32) -> Self {
799        match value {
800            1 => StandardIlluminant::D50,
801            2 => StandardIlluminant::D65,
802            3 => StandardIlluminant::D93,
803            4 => StandardIlluminant::F2,
804            5 => StandardIlluminant::D55,
805            6 => StandardIlluminant::A,
806            7 => StandardIlluminant::EquiPower,
807            8 => StandardIlluminant::F8,
808            _ => Self::Unknown,
809        }
810    }
811}
812
813impl From<StandardIlluminant> for u32 {
814    fn from(value: StandardIlluminant) -> Self {
815        match value {
816            StandardIlluminant::Unknown => 0u32,
817            StandardIlluminant::D50 => 1u32,
818            StandardIlluminant::D65 => 2u32,
819            StandardIlluminant::D93 => 3,
820            StandardIlluminant::F2 => 4,
821            StandardIlluminant::D55 => 5,
822            StandardIlluminant::A => 6,
823            StandardIlluminant::EquiPower => 7,
824            StandardIlluminant::F8 => 8,
825        }
826    }
827}
828
829#[derive(Debug, Clone, Copy)]
830pub struct Measurement {
831    pub observer: StandardObserver,
832    pub backing: Xyz,
833    pub geometry: MeasurementGeometry,
834    pub flare: f32,
835    pub illuminant: StandardIlluminant,
836}
837
838/// ICC Profile representation
839#[repr(C)]
840#[derive(Debug, Clone, Default)]
841pub struct ColorProfile {
842    pub pcs: DataColorSpace,
843    pub color_space: DataColorSpace,
844    pub profile_class: ProfileClass,
845    pub rendering_intent: RenderingIntent,
846    pub red_colorant: Xyzd,
847    pub green_colorant: Xyzd,
848    pub blue_colorant: Xyzd,
849    pub white_point: Xyzd,
850    pub black_point: Option<Xyzd>,
851    pub media_white_point: Option<Xyzd>,
852    pub luminance: Option<Xyzd>,
853    pub measurement: Option<Measurement>,
854    pub red_trc: Option<ToneReprCurve>,
855    pub green_trc: Option<ToneReprCurve>,
856    pub blue_trc: Option<ToneReprCurve>,
857    pub gray_trc: Option<ToneReprCurve>,
858    pub cicp: Option<CicpProfile>,
859    pub chromatic_adaptation: Option<Matrix3d>,
860    pub lut_a_to_b_perceptual: Option<LutWarehouse>,
861    pub lut_a_to_b_colorimetric: Option<LutWarehouse>,
862    pub lut_a_to_b_saturation: Option<LutWarehouse>,
863    pub lut_b_to_a_perceptual: Option<LutWarehouse>,
864    pub lut_b_to_a_colorimetric: Option<LutWarehouse>,
865    pub lut_b_to_a_saturation: Option<LutWarehouse>,
866    pub gamut: Option<LutWarehouse>,
867    pub copyright: Option<ProfileText>,
868    pub description: Option<ProfileText>,
869    pub device_manufacturer: Option<ProfileText>,
870    pub device_model: Option<ProfileText>,
871    pub char_target: Option<ProfileText>,
872    pub viewing_conditions: Option<ViewingConditions>,
873    pub viewing_conditions_description: Option<ProfileText>,
874    pub technology: Option<TechnologySignatures>,
875    pub calibration_date: Option<ColorDateTime>,
876    /// Version for internal and viewing purposes only.
877    /// On encoding added value to profile will always be V4.
878    pub(crate) version_internal: ProfileVersion,
879}
880
881#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)]
882pub struct ParsingOptions {
883    // Maximum allowed profile size in bytes
884    pub max_profile_size: usize,
885    // Maximum allowed CLUT size in bytes
886    pub max_allowed_clut_size: usize,
887    // Maximum allowed TRC size in elements count
888    pub max_allowed_trc_size: usize,
889}
890
891impl Default for ParsingOptions {
892    fn default() -> Self {
893        Self {
894            max_profile_size: MAX_PROFILE_SIZE,
895            max_allowed_clut_size: 10_000_000,
896            max_allowed_trc_size: 40_000,
897        }
898    }
899}
900
901impl ColorProfile {
902    /// Returns profile version
903    pub fn version(&self) -> ProfileVersion {
904        self.version_internal
905    }
906
907    pub fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> {
908        Self::new_from_slice_with_options(slice, Default::default())
909    }
910
911    pub fn new_from_slice_with_options(
912        slice: &[u8],
913        options: ParsingOptions,
914    ) -> Result<Self, CmsError> {
915        let header = ProfileHeader::new_from_slice(slice)?;
916        let tags_count = header.tag_count as usize;
917        if slice.len() >= options.max_profile_size {
918            return Err(CmsError::InvalidProfile);
919        }
920        let tags_end = tags_count
921            .safe_mul(TAG_SIZE)?
922            .safe_add(size_of::<ProfileHeader>())?;
923        if slice.len() < tags_end {
924            return Err(CmsError::InvalidProfile);
925        }
926        let tags_slice = &slice[size_of::<ProfileHeader>()..tags_end];
927        let mut profile = ColorProfile {
928            rendering_intent: header.rendering_intent,
929            pcs: header.pcs,
930            profile_class: header.profile_class,
931            color_space: header.data_color_space,
932            white_point: header.illuminant.to_xyzd(),
933            version_internal: header.version,
934            ..Default::default()
935        };
936        let color_space = profile.color_space;
937        for tag in tags_slice.chunks_exact(TAG_SIZE) {
938            let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
939            let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]);
940            let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize;
941            // Just ignore unknown tags
942            if let Ok(tag) = Tag::try_from(tag_value) {
943                match tag {
944                    Tag::RedXyz => {
945                        if color_space == DataColorSpace::Rgb {
946                            profile.red_colorant =
947                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
948                        }
949                    }
950                    Tag::GreenXyz => {
951                        if color_space == DataColorSpace::Rgb {
952                            profile.green_colorant =
953                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
954                        }
955                    }
956                    Tag::BlueXyz => {
957                        if color_space == DataColorSpace::Rgb {
958                            profile.blue_colorant =
959                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
960                        }
961                    }
962                    Tag::RedToneReproduction => {
963                        if color_space == DataColorSpace::Rgb {
964                            profile.red_trc = Self::read_trc_tag_s(
965                                slice,
966                                tag_entry as usize,
967                                tag_size,
968                                &options,
969                            )?;
970                        }
971                    }
972                    Tag::GreenToneReproduction => {
973                        if color_space == DataColorSpace::Rgb {
974                            profile.green_trc = Self::read_trc_tag_s(
975                                slice,
976                                tag_entry as usize,
977                                tag_size,
978                                &options,
979                            )?;
980                        }
981                    }
982                    Tag::BlueToneReproduction => {
983                        if color_space == DataColorSpace::Rgb {
984                            profile.blue_trc = Self::read_trc_tag_s(
985                                slice,
986                                tag_entry as usize,
987                                tag_size,
988                                &options,
989                            )?;
990                        }
991                    }
992                    Tag::GreyToneReproduction => {
993                        if color_space == DataColorSpace::Gray {
994                            profile.gray_trc = Self::read_trc_tag_s(
995                                slice,
996                                tag_entry as usize,
997                                tag_size,
998                                &options,
999                            )?;
1000                        }
1001                    }
1002                    Tag::MediaWhitePoint => {
1003                        profile.media_white_point =
1004                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?;
1005                    }
1006                    Tag::Luminance => {
1007                        profile.luminance =
1008                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?;
1009                    }
1010                    Tag::Measurement => {
1011                        profile.measurement =
1012                            Self::read_meas_tag(slice, tag_entry as usize, tag_size)?;
1013                    }
1014                    Tag::CodeIndependentPoints => {
1015                        // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the
1016                        // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces
1017                        // or profile classes indicated in the profile header.
1018                        if (profile.profile_class == ProfileClass::InputDevice
1019                            || profile.profile_class == ProfileClass::DisplayDevice)
1020                            && (profile.color_space == DataColorSpace::Rgb
1021                                || profile.color_space == DataColorSpace::YCbr
1022                                || profile.color_space == DataColorSpace::Xyz)
1023                        {
1024                            profile.cicp =
1025                                Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?;
1026                        }
1027                    }
1028                    Tag::ChromaticAdaptation => {
1029                        profile.chromatic_adaptation =
1030                            Self::read_chad_tag(slice, tag_entry as usize, tag_size)?;
1031                    }
1032                    Tag::BlackPoint => {
1033                        profile.black_point =
1034                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?
1035                    }
1036                    Tag::DeviceToPcsLutPerceptual => {
1037                        profile.lut_a_to_b_perceptual =
1038                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1039                    }
1040                    Tag::DeviceToPcsLutColorimetric => {
1041                        profile.lut_a_to_b_colorimetric =
1042                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1043                    }
1044                    Tag::DeviceToPcsLutSaturation => {
1045                        profile.lut_a_to_b_saturation =
1046                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1047                    }
1048                    Tag::PcsToDeviceLutPerceptual => {
1049                        profile.lut_b_to_a_perceptual =
1050                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1051                    }
1052                    Tag::PcsToDeviceLutColorimetric => {
1053                        profile.lut_b_to_a_colorimetric =
1054                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1055                    }
1056                    Tag::PcsToDeviceLutSaturation => {
1057                        profile.lut_b_to_a_saturation =
1058                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1059                    }
1060                    Tag::Gamut => {
1061                        profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1062                    }
1063                    Tag::Copyright => {
1064                        profile.copyright =
1065                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1066                    }
1067                    Tag::ProfileDescription => {
1068                        profile.description =
1069                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1070                    }
1071                    Tag::ViewingConditionsDescription => {
1072                        profile.viewing_conditions_description =
1073                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1074                    }
1075                    Tag::DeviceModel => {
1076                        profile.device_model =
1077                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1078                    }
1079                    Tag::DeviceManufacturer => {
1080                        profile.device_manufacturer =
1081                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1082                    }
1083                    Tag::CharTarget => {
1084                        profile.char_target =
1085                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1086                    }
1087                    Tag::Chromaticity => {}
1088                    Tag::ObserverConditions => {
1089                        profile.viewing_conditions =
1090                            Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?;
1091                    }
1092                    Tag::Technology => {
1093                        profile.technology =
1094                            Self::read_tech_tag(slice, tag_entry as usize, tag_size)?;
1095                    }
1096                    Tag::CalibrationDateTime => {
1097                        profile.calibration_date =
1098                            Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?;
1099                    }
1100                }
1101            }
1102        }
1103
1104        Ok(profile)
1105    }
1106}
1107
1108impl ColorProfile {
1109    #[inline]
1110    pub fn colorant_matrix(&self) -> Matrix3d {
1111        Matrix3d {
1112            v: [
1113                [
1114                    self.red_colorant.x,
1115                    self.green_colorant.x,
1116                    self.blue_colorant.x,
1117                ],
1118                [
1119                    self.red_colorant.y,
1120                    self.green_colorant.y,
1121                    self.blue_colorant.y,
1122                ],
1123                [
1124                    self.red_colorant.z,
1125                    self.green_colorant.z,
1126                    self.blue_colorant.z,
1127                ],
1128            ],
1129        }
1130    }
1131
1132    /// Computes colorants matrix. Returns not transposed matrix.
1133    ///
1134    /// To work on `const` context this method does have restrictions.
1135    /// If invalid values were provided it may return invalid matrix or NaNs.
1136    pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d {
1137        let red_xyz = primaries.red.to_xyzd();
1138        let green_xyz = primaries.green.to_xyzd();
1139        let blue_xyz = primaries.blue.to_xyzd();
1140
1141        let xyz_matrix = Matrix3d {
1142            v: [
1143                [red_xyz.x, green_xyz.x, blue_xyz.x],
1144                [red_xyz.y, green_xyz.y, blue_xyz.y],
1145                [red_xyz.z, green_xyz.z, blue_xyz.z],
1146            ],
1147        };
1148        let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd());
1149        adapt_to_d50_d(colorants, white_point)
1150    }
1151
1152    /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point
1153    pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) {
1154        let red_xyz = primaries.red.to_xyzd();
1155        let green_xyz = primaries.green.to_xyzd();
1156        let blue_xyz = primaries.blue.to_xyzd();
1157
1158        self.chromatic_adaptation = Some(BRADFORD_D);
1159        self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz)
1160    }
1161
1162    /// Updates RGB triple colorimetry from 3 [Xyzd] and white point
1163    ///
1164    /// To work on `const` context this method does have restrictions.
1165    /// If invalid values were provided it may return invalid matrix or NaNs.
1166    pub const fn update_rgb_colorimetry_triplet(
1167        &mut self,
1168        white_point: XyY,
1169        red_xyz: Xyzd,
1170        green_xyz: Xyzd,
1171        blue_xyz: Xyzd,
1172    ) {
1173        let xyz_matrix = Matrix3d {
1174            v: [
1175                [red_xyz.x, green_xyz.x, blue_xyz.x],
1176                [red_xyz.y, green_xyz.y, blue_xyz.y],
1177                [red_xyz.z, green_xyz.z, blue_xyz.z],
1178            ],
1179        };
1180        let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd());
1181        let colorants = adapt_to_d50_d(colorants, white_point);
1182
1183        self.update_colorants(colorants);
1184    }
1185
1186    pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) {
1187        // note: there's a transpose type of operation going on here
1188        self.red_colorant.x = colorants.v[0][0];
1189        self.red_colorant.y = colorants.v[1][0];
1190        self.red_colorant.z = colorants.v[2][0];
1191        self.green_colorant.x = colorants.v[0][1];
1192        self.green_colorant.y = colorants.v[1][1];
1193        self.green_colorant.z = colorants.v[2][1];
1194        self.blue_colorant.x = colorants.v[0][2];
1195        self.blue_colorant.y = colorants.v[1][2];
1196        self.blue_colorant.z = colorants.v[2][2];
1197    }
1198
1199    /// Updates RGB triple colorimetry from CICP
1200    pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool {
1201        self.cicp = Some(cicp);
1202        if !cicp.color_primaries.has_chromaticity()
1203            || !cicp.transfer_characteristics.has_transfer_curve()
1204        {
1205            return false;
1206        }
1207        let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() {
1208            Ok(primaries) => primaries,
1209            Err(_) => return false,
1210        };
1211        let white_point: Chromaticity = match cicp.color_primaries.white_point() {
1212            Ok(v) => v,
1213            Err(_) => return false,
1214        };
1215        self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy);
1216
1217        let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() {
1218            Ok(trc) => trc,
1219            Err(_) => return false,
1220        };
1221        self.green_trc = Some(red_trc.clone());
1222        self.blue_trc = Some(red_trc.clone());
1223        self.red_trc = Some(red_trc);
1224        false
1225    }
1226
1227    pub const fn rgb_to_xyz(&self, xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f {
1228        let xyz_inverse = xyz_matrix.inverse();
1229        let s = xyz_inverse.mul_vector(wp.to_vector());
1230        let mut v = xyz_matrix.mul_row_vector::<0>(s);
1231        v = v.mul_row_vector::<1>(s);
1232        v.mul_row_vector::<2>(s)
1233    }
1234
1235    ///TODO: make primary instead of [rgb_to_xyz] in the next major version
1236    pub(crate) const fn rgb_to_xyz_static(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f {
1237        let xyz_inverse = xyz_matrix.inverse();
1238        let s = xyz_inverse.mul_vector(wp.to_vector());
1239        let mut v = xyz_matrix.mul_row_vector::<0>(s);
1240        v = v.mul_row_vector::<1>(s);
1241        v.mul_row_vector::<2>(s)
1242    }
1243
1244    /// If Primaries is invalid will return invalid matrix on const context.
1245    /// This assumes not transposed matrix and returns not transposed matrix.
1246    pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d {
1247        let xyz_inverse = xyz_matrix.inverse();
1248        let s = xyz_inverse.mul_vector(wp.to_vector_d());
1249        let mut v = xyz_matrix.mul_row_vector::<0>(s);
1250        v = v.mul_row_vector::<1>(s);
1251        v = v.mul_row_vector::<2>(s);
1252        v
1253    }
1254
1255    pub fn rgb_to_xyz_matrix(&self) -> Matrix3d {
1256        let xyz_matrix = self.colorant_matrix();
1257        let white_point = Chromaticity::D50.to_xyzd();
1258        ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point)
1259    }
1260
1261    /// Computes transform matrix RGB -> XYZ -> RGB
1262    /// Current profile is used as source, other as destination
1263    pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d {
1264        let source = self.rgb_to_xyz_matrix();
1265        let dst = dest.rgb_to_xyz_matrix();
1266        let dest_inverse = dst.inverse();
1267        dest_inverse.mat_mul(source)
1268    }
1269
1270    /// Returns volume of colors stored in profile
1271    pub fn profile_volume(&self) -> Option<f32> {
1272        let red_prim = self.red_colorant;
1273        let green_prim = self.green_colorant;
1274        let blue_prim = self.blue_colorant;
1275        let tetrahedral_vertices = Matrix3d {
1276            v: [
1277                [red_prim.x, red_prim.y, red_prim.z],
1278                [green_prim.x, green_prim.y, green_prim.z],
1279                [blue_prim.x, blue_prim.y, blue_prim.z],
1280            ],
1281        };
1282        let det = tetrahedral_vertices.determinant()?;
1283        Some((det / 6.0f64) as f32)
1284    }
1285
1286    pub(crate) fn has_device_to_pcs_lut(&self) -> bool {
1287        self.lut_a_to_b_perceptual.is_some()
1288            || self.lut_a_to_b_saturation.is_some()
1289            || self.lut_a_to_b_colorimetric.is_some()
1290    }
1291
1292    pub(crate) fn has_pcs_to_device_lut(&self) -> bool {
1293        self.lut_b_to_a_perceptual.is_some()
1294            || self.lut_b_to_a_saturation.is_some()
1295            || self.lut_b_to_a_colorimetric.is_some()
1296    }
1297}
1298
1299#[cfg(test)]
1300mod tests {
1301    use super::*;
1302    use std::fs;
1303
1304    #[test]
1305    fn test_gray() {
1306        if let Ok(gray_icc) = fs::read("./assets/Generic Gray Gamma 2.2 Profile.icc") {
1307            let f_p = ColorProfile::new_from_slice(&gray_icc).unwrap();
1308            assert!(f_p.gray_trc.is_some());
1309        }
1310    }
1311
1312    #[test]
1313    fn test_perceptual() {
1314        if let Ok(srgb_perceptual_icc) = fs::read("./assets/srgb_perceptual.icc") {
1315            let f_p = ColorProfile::new_from_slice(&srgb_perceptual_icc).unwrap();
1316            assert_eq!(f_p.pcs, DataColorSpace::Lab);
1317            assert_eq!(f_p.color_space, DataColorSpace::Rgb);
1318            assert_eq!(f_p.version(), ProfileVersion::V4_2);
1319            assert!(f_p.lut_a_to_b_perceptual.is_some());
1320            assert!(f_p.lut_b_to_a_perceptual.is_some());
1321        }
1322    }
1323
1324    #[test]
1325    fn test_us_swop_coated() {
1326        if let Ok(us_swop_coated) = fs::read("./assets/us_swop_coated.icc") {
1327            let f_p = ColorProfile::new_from_slice(&us_swop_coated).unwrap();
1328            assert_eq!(f_p.pcs, DataColorSpace::Lab);
1329            assert_eq!(f_p.color_space, DataColorSpace::Cmyk);
1330            assert_eq!(f_p.version(), ProfileVersion::V2_0);
1331
1332            assert!(f_p.lut_a_to_b_perceptual.is_some());
1333            assert!(f_p.lut_b_to_a_perceptual.is_some());
1334
1335            assert!(f_p.lut_a_to_b_colorimetric.is_some());
1336            assert!(f_p.lut_b_to_a_colorimetric.is_some());
1337
1338            assert!(f_p.gamut.is_some());
1339
1340            assert!(f_p.copyright.is_some());
1341            assert!(f_p.description.is_some());
1342        }
1343    }
1344
1345    #[test]
1346    fn test_matrix_shaper() {
1347        if let Ok(matrix_shaper) = fs::read("./assets/Display P3.icc") {
1348            let f_p = ColorProfile::new_from_slice(&matrix_shaper).unwrap();
1349            assert_eq!(f_p.pcs, DataColorSpace::Xyz);
1350            assert_eq!(f_p.color_space, DataColorSpace::Rgb);
1351            assert_eq!(f_p.version(), ProfileVersion::V4_0);
1352
1353            assert!(f_p.red_trc.is_some());
1354            assert!(f_p.blue_trc.is_some());
1355            assert!(f_p.green_trc.is_some());
1356
1357            assert_ne!(f_p.red_colorant, Xyzd::default());
1358            assert_ne!(f_p.blue_colorant, Xyzd::default());
1359            assert_ne!(f_p.green_colorant, Xyzd::default());
1360
1361            assert!(f_p.copyright.is_some());
1362            assert!(f_p.description.is_some());
1363        }
1364    }
1365}