Skip to main content

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        // First try exact match for known versions
92        match value {
93            0x02000000 => return Ok(ProfileVersion::V2_0),
94            0x02100000 => return Ok(ProfileVersion::V2_1),
95            0x02200000 => return Ok(ProfileVersion::V2_2),
96            0x02300000 => return Ok(ProfileVersion::V2_3),
97            0x02400000 => return Ok(ProfileVersion::V2_4),
98            0x04000000 => return Ok(ProfileVersion::V4_0),
99            0x04100000 => return Ok(ProfileVersion::V4_1),
100            0x04200000 => return Ok(ProfileVersion::V4_2),
101            0x04300000 => return Ok(ProfileVersion::V4_3),
102            0x04400000 => return Ok(ProfileVersion::V4_4),
103            _ => {}
104        }
105
106        // Extract major version (first byte) for range matching
107        // ICC version format: major.minor.bugfix.zero in bytes [0][1][2][3]
108        let major = (value >> 24) & 0xFF;
109        let minor = (value >> 20) & 0x0F;
110
111        // Accept profiles with patch versions (e.g., v2.0.2, v3.4, v4.2.9)
112        // but reject invalid versions (v0.x) and unsupported versions (v5.x+ / ICC MAX)
113        match major {
114            0 => {
115                // Version 0.x is invalid - reject
116                Err(CmsError::InvalidProfile)
117            }
118            2 => {
119                // v2.x - map to the appropriate v2 minor version or highest known
120                match minor {
121                    0 => Ok(ProfileVersion::V2_0),
122                    1 => Ok(ProfileVersion::V2_1),
123                    2 => Ok(ProfileVersion::V2_2),
124                    3 => Ok(ProfileVersion::V2_3),
125                    _ => Ok(ProfileVersion::V2_4), // Higher minor versions -> v2.4
126                }
127            }
128            3 => {
129                // v3.x (rare but exists) - treat as v2.4 (functionally similar)
130                Ok(ProfileVersion::V2_4)
131            }
132            4 => {
133                // v4.x - map to the appropriate v4 minor version or highest known
134                match minor {
135                    0 => Ok(ProfileVersion::V4_0),
136                    1 => Ok(ProfileVersion::V4_1),
137                    2 => Ok(ProfileVersion::V4_2),
138                    3 => Ok(ProfileVersion::V4_3),
139                    _ => Ok(ProfileVersion::V4_4), // Higher minor versions -> v4.4
140                }
141            }
142            _ => {
143                // v5.x+ (ICC MAX) and other unknown versions - reject
144                // ICC MAX has different white point requirements and would produce wrong colors
145                Err(CmsError::InvalidProfile)
146            }
147        }
148    }
149}
150
151impl From<ProfileVersion> for u32 {
152    fn from(value: ProfileVersion) -> Self {
153        match value {
154            ProfileVersion::V2_0 => 0x02000000,
155            ProfileVersion::V2_1 => 0x02100000,
156            ProfileVersion::V2_2 => 0x02200000,
157            ProfileVersion::V2_3 => 0x02300000,
158            ProfileVersion::V2_4 => 0x02400000,
159            ProfileVersion::V4_0 => 0x04000000,
160            ProfileVersion::V4_1 => 0x04100000,
161            ProfileVersion::V4_2 => 0x04200000,
162            ProfileVersion::V4_3 => 0x04300000,
163            ProfileVersion::V4_4 => 0x04400000,
164            ProfileVersion::Unknown => 0x02000000,
165        }
166    }
167}
168
169#[repr(u32)]
170#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Hash)]
171pub enum DataColorSpace {
172    #[default]
173    Xyz,
174    Lab,
175    Luv,
176    YCbr,
177    Yxy,
178    Rgb,
179    Gray,
180    Hsv,
181    Hls,
182    Cmyk,
183    Cmy,
184    Color2,
185    Color3,
186    Color4,
187    Color5,
188    Color6,
189    Color7,
190    Color8,
191    Color9,
192    Color10,
193    Color11,
194    Color12,
195    Color13,
196    Color14,
197    Color15,
198}
199
200impl DataColorSpace {
201    #[inline]
202    pub fn check_layout(self, layout: Layout) -> Result<(), CmsError> {
203        let unsupported: bool = match self {
204            DataColorSpace::Xyz => layout != Layout::Rgb,
205            DataColorSpace::Lab => layout != Layout::Rgb && layout != Layout::Rgba,
206            DataColorSpace::Luv => layout != Layout::Rgb,
207            DataColorSpace::YCbr => layout != Layout::Rgb,
208            DataColorSpace::Yxy => layout != Layout::Rgb,
209            DataColorSpace::Rgb => layout != Layout::Rgb && layout != Layout::Rgba,
210            DataColorSpace::Gray => layout != Layout::Gray && layout != Layout::GrayAlpha,
211            DataColorSpace::Hsv => layout != Layout::Rgb,
212            DataColorSpace::Hls => layout != Layout::Rgb,
213            DataColorSpace::Cmyk => layout != Layout::Rgba,
214            DataColorSpace::Cmy => layout != Layout::Rgb,
215            DataColorSpace::Color2 => layout != Layout::GrayAlpha,
216            DataColorSpace::Color3 => layout != Layout::Rgb,
217            DataColorSpace::Color4 => layout != Layout::Rgba,
218            DataColorSpace::Color5 => layout != Layout::Inks5,
219            DataColorSpace::Color6 => layout != Layout::Inks6,
220            DataColorSpace::Color7 => layout != Layout::Inks7,
221            DataColorSpace::Color8 => layout != Layout::Inks8,
222            DataColorSpace::Color9 => layout != Layout::Inks9,
223            DataColorSpace::Color10 => layout != Layout::Inks10,
224            DataColorSpace::Color11 => layout != Layout::Inks11,
225            DataColorSpace::Color12 => layout != Layout::Inks12,
226            DataColorSpace::Color13 => layout != Layout::Inks13,
227            DataColorSpace::Color14 => layout != Layout::Inks14,
228            DataColorSpace::Color15 => layout != Layout::Inks15,
229        };
230        if unsupported {
231            Err(CmsError::InvalidLayout)
232        } else {
233            Ok(())
234        }
235    }
236
237    pub(crate) fn is_three_channels(self) -> bool {
238        matches!(
239            self,
240            DataColorSpace::Xyz
241                | DataColorSpace::Lab
242                | DataColorSpace::Luv
243                | DataColorSpace::YCbr
244                | DataColorSpace::Yxy
245                | DataColorSpace::Rgb
246                | DataColorSpace::Hsv
247                | DataColorSpace::Hls
248                | DataColorSpace::Cmy
249                | DataColorSpace::Color3
250        )
251    }
252}
253
254#[repr(u32)]
255#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)]
256pub enum ProfileClass {
257    InputDevice,
258    #[default]
259    DisplayDevice,
260    OutputDevice,
261    DeviceLink,
262    ColorSpace,
263    Abstract,
264    Named,
265}
266
267impl TryFrom<u32> for ProfileClass {
268    type Error = CmsError;
269    fn try_from(value: u32) -> Result<Self, Self::Error> {
270        if value == u32::from_ne_bytes(*b"scnr").to_be() {
271            return Ok(ProfileClass::InputDevice);
272        } else if value == u32::from_ne_bytes(*b"mntr").to_be() {
273            return Ok(ProfileClass::DisplayDevice);
274        } else if value == u32::from_ne_bytes(*b"prtr").to_be() {
275            return Ok(ProfileClass::OutputDevice);
276        } else if value == u32::from_ne_bytes(*b"link").to_be() {
277            return Ok(ProfileClass::DeviceLink);
278        } else if value == u32::from_ne_bytes(*b"spac").to_be() {
279            return Ok(ProfileClass::ColorSpace);
280        } else if value == u32::from_ne_bytes(*b"abst").to_be() {
281            return Ok(ProfileClass::Abstract);
282        } else if value == u32::from_ne_bytes(*b"nmcl").to_be() {
283            return Ok(ProfileClass::Named);
284        }
285        Err(CmsError::InvalidProfile)
286    }
287}
288
289impl From<ProfileClass> for u32 {
290    fn from(val: ProfileClass) -> Self {
291        match val {
292            ProfileClass::InputDevice => u32::from_ne_bytes(*b"scnr").to_be(),
293            ProfileClass::DisplayDevice => u32::from_ne_bytes(*b"mntr").to_be(),
294            ProfileClass::OutputDevice => u32::from_ne_bytes(*b"prtr").to_be(),
295            ProfileClass::DeviceLink => u32::from_ne_bytes(*b"link").to_be(),
296            ProfileClass::ColorSpace => u32::from_ne_bytes(*b"spac").to_be(),
297            ProfileClass::Abstract => u32::from_ne_bytes(*b"abst").to_be(),
298            ProfileClass::Named => u32::from_ne_bytes(*b"nmcl").to_be(),
299        }
300    }
301}
302
303#[derive(Debug, Clone, PartialEq)]
304pub enum LutStore {
305    Store8(Vec<u8>),
306    Store16(Vec<u16>),
307}
308
309#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
310pub enum LutType {
311    Lut8,
312    Lut16,
313    LutMab,
314    LutMba,
315}
316
317impl TryFrom<u32> for LutType {
318    type Error = CmsError;
319    fn try_from(value: u32) -> Result<Self, Self::Error> {
320        if value == u32::from_ne_bytes(*b"mft1").to_be() {
321            return Ok(LutType::Lut8);
322        } else if value == u32::from_ne_bytes(*b"mft2").to_be() {
323            return Ok(LutType::Lut16);
324        } else if value == u32::from_ne_bytes(*b"mAB ").to_be() {
325            return Ok(LutType::LutMab);
326        } else if value == u32::from_ne_bytes(*b"mBA ").to_be() {
327            return Ok(LutType::LutMba);
328        }
329        Err(CmsError::InvalidProfile)
330    }
331}
332
333impl From<LutType> for u32 {
334    fn from(val: LutType) -> Self {
335        match val {
336            LutType::Lut8 => u32::from_ne_bytes(*b"mft1").to_be(),
337            LutType::Lut16 => u32::from_ne_bytes(*b"mft2").to_be(),
338            LutType::LutMab => u32::from_ne_bytes(*b"mAB ").to_be(),
339            LutType::LutMba => u32::from_ne_bytes(*b"mBA ").to_be(),
340        }
341    }
342}
343
344impl TryFrom<u32> for DataColorSpace {
345    type Error = CmsError;
346    fn try_from(value: u32) -> Result<Self, Self::Error> {
347        if value == u32::from_ne_bytes(*b"XYZ ").to_be() {
348            return Ok(DataColorSpace::Xyz);
349        } else if value == u32::from_ne_bytes(*b"Lab ").to_be() {
350            return Ok(DataColorSpace::Lab);
351        } else if value == u32::from_ne_bytes(*b"Luv ").to_be() {
352            return Ok(DataColorSpace::Luv);
353        } else if value == u32::from_ne_bytes(*b"YCbr").to_be() {
354            return Ok(DataColorSpace::YCbr);
355        } else if value == u32::from_ne_bytes(*b"Yxy ").to_be() {
356            return Ok(DataColorSpace::Yxy);
357        } else if value == u32::from_ne_bytes(*b"RGB ").to_be() {
358            return Ok(DataColorSpace::Rgb);
359        } else if value == u32::from_ne_bytes(*b"GRAY").to_be() {
360            return Ok(DataColorSpace::Gray);
361        } else if value == u32::from_ne_bytes(*b"HSV ").to_be() {
362            return Ok(DataColorSpace::Hsv);
363        } else if value == u32::from_ne_bytes(*b"HLS ").to_be() {
364            return Ok(DataColorSpace::Hls);
365        } else if value == u32::from_ne_bytes(*b"CMYK").to_be() {
366            return Ok(DataColorSpace::Cmyk);
367        } else if value == u32::from_ne_bytes(*b"CMY ").to_be() {
368            return Ok(DataColorSpace::Cmy);
369        } else if value == u32::from_ne_bytes(*b"2CLR").to_be() {
370            return Ok(DataColorSpace::Color2);
371        } else if value == u32::from_ne_bytes(*b"3CLR").to_be() {
372            return Ok(DataColorSpace::Color3);
373        } else if value == u32::from_ne_bytes(*b"4CLR").to_be() {
374            return Ok(DataColorSpace::Color4);
375        } else if value == u32::from_ne_bytes(*b"5CLR").to_be() {
376            return Ok(DataColorSpace::Color5);
377        } else if value == u32::from_ne_bytes(*b"6CLR").to_be() {
378            return Ok(DataColorSpace::Color6);
379        } else if value == u32::from_ne_bytes(*b"7CLR").to_be() {
380            return Ok(DataColorSpace::Color7);
381        } else if value == u32::from_ne_bytes(*b"8CLR").to_be() {
382            return Ok(DataColorSpace::Color8);
383        } else if value == u32::from_ne_bytes(*b"9CLR").to_be() {
384            return Ok(DataColorSpace::Color9);
385        } else if value == u32::from_ne_bytes(*b"ACLR").to_be() {
386            return Ok(DataColorSpace::Color10);
387        } else if value == u32::from_ne_bytes(*b"BCLR").to_be() {
388            return Ok(DataColorSpace::Color11);
389        } else if value == u32::from_ne_bytes(*b"CCLR").to_be() {
390            return Ok(DataColorSpace::Color12);
391        } else if value == u32::from_ne_bytes(*b"DCLR").to_be() {
392            return Ok(DataColorSpace::Color13);
393        } else if value == u32::from_ne_bytes(*b"ECLR").to_be() {
394            return Ok(DataColorSpace::Color14);
395        } else if value == u32::from_ne_bytes(*b"FCLR").to_be() {
396            return Ok(DataColorSpace::Color15);
397        }
398        Err(CmsError::InvalidProfile)
399    }
400}
401
402impl From<DataColorSpace> for u32 {
403    fn from(val: DataColorSpace) -> Self {
404        match val {
405            DataColorSpace::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(),
406            DataColorSpace::Lab => u32::from_ne_bytes(*b"Lab ").to_be(),
407            DataColorSpace::Luv => u32::from_ne_bytes(*b"Luv ").to_be(),
408            DataColorSpace::YCbr => u32::from_ne_bytes(*b"YCbr").to_be(),
409            DataColorSpace::Yxy => u32::from_ne_bytes(*b"Yxy ").to_be(),
410            DataColorSpace::Rgb => u32::from_ne_bytes(*b"RGB ").to_be(),
411            DataColorSpace::Gray => u32::from_ne_bytes(*b"GRAY").to_be(),
412            DataColorSpace::Hsv => u32::from_ne_bytes(*b"HSV ").to_be(),
413            DataColorSpace::Hls => u32::from_ne_bytes(*b"HLS ").to_be(),
414            DataColorSpace::Cmyk => u32::from_ne_bytes(*b"CMYK").to_be(),
415            DataColorSpace::Cmy => u32::from_ne_bytes(*b"CMY ").to_be(),
416            DataColorSpace::Color2 => u32::from_ne_bytes(*b"2CLR").to_be(),
417            DataColorSpace::Color3 => u32::from_ne_bytes(*b"3CLR").to_be(),
418            DataColorSpace::Color4 => u32::from_ne_bytes(*b"4CLR").to_be(),
419            DataColorSpace::Color5 => u32::from_ne_bytes(*b"5CLR").to_be(),
420            DataColorSpace::Color6 => u32::from_ne_bytes(*b"6CLR").to_be(),
421            DataColorSpace::Color7 => u32::from_ne_bytes(*b"7CLR").to_be(),
422            DataColorSpace::Color8 => u32::from_ne_bytes(*b"8CLR").to_be(),
423            DataColorSpace::Color9 => u32::from_ne_bytes(*b"9CLR").to_be(),
424            DataColorSpace::Color10 => u32::from_ne_bytes(*b"ACLR").to_be(),
425            DataColorSpace::Color11 => u32::from_ne_bytes(*b"BCLR").to_be(),
426            DataColorSpace::Color12 => u32::from_ne_bytes(*b"CCLR").to_be(),
427            DataColorSpace::Color13 => u32::from_ne_bytes(*b"DCLR").to_be(),
428            DataColorSpace::Color14 => u32::from_ne_bytes(*b"ECLR").to_be(),
429            DataColorSpace::Color15 => u32::from_ne_bytes(*b"FCLR").to_be(),
430        }
431    }
432}
433
434#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
435pub enum TechnologySignatures {
436    FilmScanner,
437    DigitalCamera,
438    ReflectiveScanner,
439    InkJetPrinter,
440    ThermalWaxPrinter,
441    ElectrophotographicPrinter,
442    ElectrostaticPrinter,
443    DyeSublimationPrinter,
444    PhotographicPaperPrinter,
445    FilmWriter,
446    VideoMonitor,
447    VideoCamera,
448    ProjectionTelevision,
449    CathodeRayTubeDisplay,
450    PassiveMatrixDisplay,
451    ActiveMatrixDisplay,
452    LiquidCrystalDisplay,
453    OrganicLedDisplay,
454    PhotoCd,
455    PhotographicImageSetter,
456    Gravure,
457    OffsetLithography,
458    Silkscreen,
459    Flexography,
460    MotionPictureFilmScanner,
461    MotionPictureFilmRecorder,
462    DigitalMotionPictureCamera,
463    DigitalCinemaProjector,
464    Unknown(u32),
465}
466
467impl From<u32> for TechnologySignatures {
468    fn from(value: u32) -> Self {
469        if value == u32::from_ne_bytes(*b"fscn").to_be() {
470            return TechnologySignatures::FilmScanner;
471        } else if value == u32::from_ne_bytes(*b"dcam").to_be() {
472            return TechnologySignatures::DigitalCamera;
473        } else if value == u32::from_ne_bytes(*b"rscn").to_be() {
474            return TechnologySignatures::ReflectiveScanner;
475        } else if value == u32::from_ne_bytes(*b"ijet").to_be() {
476            return TechnologySignatures::InkJetPrinter;
477        } else if value == u32::from_ne_bytes(*b"twax").to_be() {
478            return TechnologySignatures::ThermalWaxPrinter;
479        } else if value == u32::from_ne_bytes(*b"epho").to_be() {
480            return TechnologySignatures::ElectrophotographicPrinter;
481        } else if value == u32::from_ne_bytes(*b"esta").to_be() {
482            return TechnologySignatures::ElectrostaticPrinter;
483        } else if value == u32::from_ne_bytes(*b"dsub").to_be() {
484            return TechnologySignatures::DyeSublimationPrinter;
485        } else if value == u32::from_ne_bytes(*b"rpho").to_be() {
486            return TechnologySignatures::PhotographicPaperPrinter;
487        } else if value == u32::from_ne_bytes(*b"fprn").to_be() {
488            return TechnologySignatures::FilmWriter;
489        } else if value == u32::from_ne_bytes(*b"vidm").to_be() {
490            return TechnologySignatures::VideoMonitor;
491        } else if value == u32::from_ne_bytes(*b"vidc").to_be() {
492            return TechnologySignatures::VideoCamera;
493        } else if value == u32::from_ne_bytes(*b"pjtv").to_be() {
494            return TechnologySignatures::ProjectionTelevision;
495        } else if value == u32::from_ne_bytes(*b"CRT ").to_be() {
496            return TechnologySignatures::CathodeRayTubeDisplay;
497        } else if value == u32::from_ne_bytes(*b"PMD ").to_be() {
498            return TechnologySignatures::PassiveMatrixDisplay;
499        } else if value == u32::from_ne_bytes(*b"AMD ").to_be() {
500            return TechnologySignatures::ActiveMatrixDisplay;
501        } else if value == u32::from_ne_bytes(*b"LCD ").to_be() {
502            return TechnologySignatures::LiquidCrystalDisplay;
503        } else if value == u32::from_ne_bytes(*b"OLED").to_be() {
504            return TechnologySignatures::OrganicLedDisplay;
505        } else if value == u32::from_ne_bytes(*b"KPCD").to_be() {
506            return TechnologySignatures::PhotoCd;
507        } else if value == u32::from_ne_bytes(*b"imgs").to_be() {
508            return TechnologySignatures::PhotographicImageSetter;
509        } else if value == u32::from_ne_bytes(*b"grav").to_be() {
510            return TechnologySignatures::Gravure;
511        } else if value == u32::from_ne_bytes(*b"offs").to_be() {
512            return TechnologySignatures::OffsetLithography;
513        } else if value == u32::from_ne_bytes(*b"silk").to_be() {
514            return TechnologySignatures::Silkscreen;
515        } else if value == u32::from_ne_bytes(*b"flex").to_be() {
516            return TechnologySignatures::Flexography;
517        } else if value == u32::from_ne_bytes(*b"mpfs").to_be() {
518            return TechnologySignatures::MotionPictureFilmScanner;
519        } else if value == u32::from_ne_bytes(*b"mpfr").to_be() {
520            return TechnologySignatures::MotionPictureFilmRecorder;
521        } else if value == u32::from_ne_bytes(*b"dmpc").to_be() {
522            return TechnologySignatures::DigitalMotionPictureCamera;
523        } else if value == u32::from_ne_bytes(*b"dcpj").to_be() {
524            return TechnologySignatures::DigitalCinemaProjector;
525        }
526        TechnologySignatures::Unknown(value)
527    }
528}
529
530#[derive(Debug, Clone)]
531pub enum LutWarehouse {
532    Lut(LutDataType),
533    Multidimensional(LutMultidimensionalType),
534}
535
536impl PartialEq for LutWarehouse {
537    fn eq(&self, other: &Self) -> bool {
538        match (self, other) {
539            (LutWarehouse::Lut(a), LutWarehouse::Lut(b)) => a == b,
540            (LutWarehouse::Multidimensional(a), LutWarehouse::Multidimensional(b)) => a == b,
541            _ => false, // Different variants are not equal
542        }
543    }
544}
545
546#[derive(Debug, Clone, PartialEq)]
547pub struct LutDataType {
548    // used by lut8Type/lut16Type (mft2) only
549    pub num_input_channels: u8,
550    pub num_output_channels: u8,
551    pub num_clut_grid_points: u8,
552    pub matrix: Matrix3d,
553    pub num_input_table_entries: u16,
554    pub num_output_table_entries: u16,
555    pub input_table: LutStore,
556    pub clut_table: LutStore,
557    pub output_table: LutStore,
558    pub lut_type: LutType,
559}
560
561impl LutDataType {
562    pub(crate) fn has_same_kind(&self) -> bool {
563        matches!(
564            (&self.input_table, &self.clut_table, &self.output_table),
565            (
566                LutStore::Store8(_),
567                LutStore::Store8(_),
568                LutStore::Store8(_)
569            ) | (
570                LutStore::Store16(_),
571                LutStore::Store16(_),
572                LutStore::Store16(_)
573            )
574        )
575    }
576}
577
578#[derive(Debug, Clone, PartialEq)]
579pub struct LutMultidimensionalType {
580    pub num_input_channels: u8,
581    pub num_output_channels: u8,
582    pub grid_points: [u8; 16],
583    pub clut: Option<LutStore>,
584    pub a_curves: Vec<ToneReprCurve>,
585    pub b_curves: Vec<ToneReprCurve>,
586    pub m_curves: Vec<ToneReprCurve>,
587    pub matrix: Matrix3d,
588    pub bias: Vector3d,
589}
590
591#[repr(u32)]
592#[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)]
593pub enum RenderingIntent {
594    AbsoluteColorimetric = 3,
595    Saturation = 2,
596    RelativeColorimetric = 1,
597    #[default]
598    Perceptual = 0,
599}
600
601impl TryFrom<u32> for RenderingIntent {
602    type Error = CmsError;
603
604    #[inline]
605    fn try_from(value: u32) -> Result<Self, Self::Error> {
606        // Rendering intent is a big-endian u32 at bytes 64-67 with valid
607        // values 0-3. Non-conforming profiles (e.g. old Linotype "Lino"
608        // v2.1 profiles with byte-swapped values) may have invalid values.
609        // Default to Perceptual rather than rejecting the entire profile,
610        // since this field is advisory — moxcms uses TransformOptions for
611        // actual LUT selection.
612        match value {
613            0 => Ok(RenderingIntent::Perceptual),
614            1 => Ok(RenderingIntent::RelativeColorimetric),
615            2 => Ok(RenderingIntent::Saturation),
616            3 => Ok(RenderingIntent::AbsoluteColorimetric),
617            _ => Ok(RenderingIntent::Perceptual),
618        }
619    }
620}
621
622impl From<RenderingIntent> for u32 {
623    #[inline]
624    fn from(value: RenderingIntent) -> Self {
625        match value {
626            RenderingIntent::AbsoluteColorimetric => 3,
627            RenderingIntent::Saturation => 2,
628            RenderingIntent::RelativeColorimetric => 1,
629            RenderingIntent::Perceptual => 0,
630        }
631    }
632}
633
634/// ICC Header
635#[repr(C)]
636#[derive(Debug, Clone, Copy)]
637pub(crate) struct ProfileHeader {
638    pub size: u32,                         // Size of the profile (computed)
639    pub cmm_type: u32,                     // Preferred CMM type (ignored)
640    pub version: ProfileVersion,           // Version (4.3 or 4.4 if CICP is included)
641    pub profile_class: ProfileClass,       // Display device profile
642    pub data_color_space: DataColorSpace,  // RGB input color space
643    pub pcs: DataColorSpace,               // Profile connection space
644    pub creation_date_time: ColorDateTime, // Date and time
645    pub signature: ProfileSignature,       // Profile signature
646    pub platform: u32,                     // Platform target (ignored)
647    pub flags: u32,                        // Flags (not embedded, can be used independently)
648    pub device_manufacturer: u32,          // Device manufacturer (ignored)
649    pub device_model: u32,                 // Device model (ignored)
650    pub device_attributes: [u8; 8],        // Device attributes (ignored)
651    pub rendering_intent: RenderingIntent, // Relative colorimetric rendering intent
652    pub illuminant: Xyz,                   // D50 standard illuminant X
653    pub creator: u32,                      // Profile creator (ignored)
654    pub profile_id: [u8; 16],              // Profile id checksum (ignored)
655    pub reserved: [u8; 28],                // Reserved (ignored)
656    pub tag_count: u32,                    // Technically not part of header, but required
657}
658
659impl ProfileHeader {
660    #[allow(dead_code)]
661    pub(crate) fn new(size: u32) -> Self {
662        Self {
663            size,
664            cmm_type: 0,
665            version: ProfileVersion::V4_3,
666            profile_class: ProfileClass::DisplayDevice,
667            data_color_space: DataColorSpace::Rgb,
668            pcs: DataColorSpace::Xyz,
669            creation_date_time: ColorDateTime::default(),
670            signature: ProfileSignature::Acsp,
671            platform: 0,
672            flags: 0x00000000,
673            device_manufacturer: 0,
674            device_model: 0,
675            device_attributes: [0; 8],
676            rendering_intent: RenderingIntent::Perceptual,
677            illuminant: Chromaticity::D50.to_xyz(),
678            creator: 0,
679            profile_id: [0; 16],
680            reserved: [0; 28],
681            tag_count: 0,
682        }
683    }
684
685    /// Creates profile from the buffer
686    pub(crate) fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> {
687        if slice.len() < size_of::<ProfileHeader>() {
688            return Err(CmsError::InvalidProfile);
689        }
690        let mut cursor = std::io::Cursor::new(slice);
691        let mut buffer = [0u8; size_of::<ProfileHeader>()];
692        cursor
693            .read_exact(&mut buffer)
694            .map_err(|_| CmsError::InvalidProfile)?;
695
696        let header = Self {
697            size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()),
698            cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()),
699            version: ProfileVersion::try_from(u32::from_be_bytes(
700                buffer[8..12].try_into().unwrap(),
701            ))?,
702            profile_class: ProfileClass::try_from(u32::from_be_bytes(
703                buffer[12..16].try_into().unwrap(),
704            ))?,
705            data_color_space: DataColorSpace::try_from(u32::from_be_bytes(
706                buffer[16..20].try_into().unwrap(),
707            ))?,
708            pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?,
709            creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?,
710            signature: ProfileSignature::try_from(u32::from_be_bytes(
711                buffer[36..40].try_into().unwrap(),
712            ))?,
713            platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()),
714            flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()),
715            device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()),
716            device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()),
717            device_attributes: buffer[56..64].try_into().unwrap(),
718            rendering_intent: RenderingIntent::try_from(u32::from_be_bytes(
719                buffer[64..68].try_into().unwrap(),
720            ))?,
721            illuminant: Xyz::new(
722                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())),
723                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())),
724                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())),
725            ),
726            creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()),
727            profile_id: buffer[84..100].try_into().unwrap(),
728            reserved: buffer[100..128].try_into().unwrap(),
729            tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()),
730        };
731        Ok(header)
732    }
733}
734
735/// A [Coding Independent Code Point](https://en.wikipedia.org/wiki/Coding-independent_code_points).
736#[repr(C)]
737#[derive(Debug, Clone, Copy)]
738pub struct CicpProfile {
739    pub color_primaries: CicpColorPrimaries,
740    pub transfer_characteristics: TransferCharacteristics,
741    pub matrix_coefficients: MatrixCoefficients,
742    pub full_range: bool,
743}
744
745#[derive(Debug, Clone)]
746pub struct LocalizableString {
747    /// An ISO 639-1 value is expected; any text w. more than two symbols will be truncated
748    pub language: String,
749    /// An ISO 3166-1 value is expected; any text w. more than two symbols will be truncated
750    pub country: String,
751    pub value: String,
752}
753
754impl LocalizableString {
755    /// Creates new localizable string
756    ///
757    /// # Arguments
758    ///
759    /// * `language`: an ISO 639-1 value is expected, any text more than 2 symbols will be truncated
760    /// * `country`: an ISO 3166-1 value is expected, any text more than 2 symbols will be truncated
761    /// * `value`: String value
762    ///
763    pub fn new(language: String, country: String, value: String) -> Self {
764        Self {
765            language,
766            country,
767            value,
768        }
769    }
770}
771
772#[derive(Debug, Clone)]
773pub struct DescriptionString {
774    pub ascii_string: String,
775    pub unicode_language_code: u32,
776    pub unicode_string: String,
777    pub script_code_code: i8,
778    pub mac_string: String,
779}
780
781#[derive(Debug, Clone)]
782pub enum ProfileText {
783    PlainString(String),
784    Localizable(Vec<LocalizableString>),
785    Description(DescriptionString),
786}
787
788impl ProfileText {
789    pub(crate) fn has_values(&self) -> bool {
790        match self {
791            ProfileText::PlainString(_) => true,
792            ProfileText::Localizable(lc) => !lc.is_empty(),
793            ProfileText::Description(_) => true,
794        }
795    }
796}
797
798#[derive(Debug, Clone, Copy)]
799pub enum StandardObserver {
800    D50,
801    D65,
802    Unknown,
803}
804
805impl From<u32> for StandardObserver {
806    fn from(value: u32) -> Self {
807        if value == 1 {
808            return StandardObserver::D50;
809        } else if value == 2 {
810            return StandardObserver::D65;
811        }
812        StandardObserver::Unknown
813    }
814}
815
816impl From<StandardObserver> for u32 {
817    fn from(value: StandardObserver) -> Self {
818        match value {
819            StandardObserver::D50 => 1,
820            StandardObserver::D65 => 2,
821            StandardObserver::Unknown => 0,
822        }
823    }
824}
825
826#[derive(Debug, Clone, Copy)]
827pub struct ViewingConditions {
828    pub illuminant: Xyz,
829    pub surround: Xyz,
830    pub observer: StandardObserver,
831}
832
833#[derive(Debug, Clone, Copy)]
834pub enum MeasurementGeometry {
835    Unknown,
836    /// 0°:45° or 45°:0°
837    D45to45,
838    /// 0°:d or d:0°
839    D0to0,
840}
841
842impl From<u32> for MeasurementGeometry {
843    fn from(value: u32) -> Self {
844        if value == 1 {
845            Self::D45to45
846        } else if value == 2 {
847            Self::D0to0
848        } else {
849            Self::Unknown
850        }
851    }
852}
853
854#[derive(Debug, Clone, Copy)]
855pub enum StandardIlluminant {
856    Unknown,
857    D50,
858    D65,
859    D93,
860    F2,
861    D55,
862    A,
863    EquiPower,
864    F8,
865}
866
867impl From<u32> for StandardIlluminant {
868    fn from(value: u32) -> Self {
869        match value {
870            1 => StandardIlluminant::D50,
871            2 => StandardIlluminant::D65,
872            3 => StandardIlluminant::D93,
873            4 => StandardIlluminant::F2,
874            5 => StandardIlluminant::D55,
875            6 => StandardIlluminant::A,
876            7 => StandardIlluminant::EquiPower,
877            8 => StandardIlluminant::F8,
878            _ => Self::Unknown,
879        }
880    }
881}
882
883impl From<StandardIlluminant> for u32 {
884    fn from(value: StandardIlluminant) -> Self {
885        match value {
886            StandardIlluminant::Unknown => 0u32,
887            StandardIlluminant::D50 => 1u32,
888            StandardIlluminant::D65 => 2u32,
889            StandardIlluminant::D93 => 3,
890            StandardIlluminant::F2 => 4,
891            StandardIlluminant::D55 => 5,
892            StandardIlluminant::A => 6,
893            StandardIlluminant::EquiPower => 7,
894            StandardIlluminant::F8 => 8,
895        }
896    }
897}
898
899#[derive(Debug, Clone, Copy)]
900pub struct Measurement {
901    pub observer: StandardObserver,
902    pub backing: Xyz,
903    pub geometry: MeasurementGeometry,
904    pub flare: f32,
905    pub illuminant: StandardIlluminant,
906}
907
908/// ICC Profile representation
909#[repr(C)]
910#[derive(Debug, Clone, Default)]
911pub struct ColorProfile {
912    pub pcs: DataColorSpace,
913    pub color_space: DataColorSpace,
914    pub profile_class: ProfileClass,
915    pub rendering_intent: RenderingIntent,
916    pub red_colorant: Xyzd,
917    pub green_colorant: Xyzd,
918    pub blue_colorant: Xyzd,
919    pub white_point: Xyzd,
920    pub black_point: Option<Xyzd>,
921    pub media_white_point: Option<Xyzd>,
922    pub luminance: Option<Xyzd>,
923    pub measurement: Option<Measurement>,
924    pub red_trc: Option<ToneReprCurve>,
925    pub green_trc: Option<ToneReprCurve>,
926    pub blue_trc: Option<ToneReprCurve>,
927    pub gray_trc: Option<ToneReprCurve>,
928    pub cicp: Option<CicpProfile>,
929    pub chromatic_adaptation: Option<Matrix3d>,
930    pub lut_a_to_b_perceptual: Option<LutWarehouse>,
931    pub lut_a_to_b_colorimetric: Option<LutWarehouse>,
932    pub lut_a_to_b_saturation: Option<LutWarehouse>,
933    pub lut_b_to_a_perceptual: Option<LutWarehouse>,
934    pub lut_b_to_a_colorimetric: Option<LutWarehouse>,
935    pub lut_b_to_a_saturation: Option<LutWarehouse>,
936    pub gamut: Option<LutWarehouse>,
937    pub copyright: Option<ProfileText>,
938    pub description: Option<ProfileText>,
939    pub device_manufacturer: Option<ProfileText>,
940    pub device_model: Option<ProfileText>,
941    pub char_target: Option<ProfileText>,
942    pub viewing_conditions: Option<ViewingConditions>,
943    pub viewing_conditions_description: Option<ProfileText>,
944    pub technology: Option<TechnologySignatures>,
945    pub calibration_date: Option<ColorDateTime>,
946    pub creation_date_time: ColorDateTime,
947    /// Version for internal and viewing purposes only.
948    /// On encoding this is computable property which will set at least V4.
949    pub(crate) version_internal: ProfileVersion,
950}
951
952#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)]
953pub struct ParsingOptions {
954    // Maximum allowed profile size in bytes
955    pub max_profile_size: usize,
956    // Maximum allowed CLUT size in bytes
957    pub max_allowed_clut_size: usize,
958    // Maximum allowed TRC size in elements count
959    pub max_allowed_trc_size: usize,
960}
961
962impl Default for ParsingOptions {
963    fn default() -> Self {
964        Self {
965            max_profile_size: MAX_PROFILE_SIZE,
966            max_allowed_clut_size: 10_000_000,
967            max_allowed_trc_size: 40_000,
968        }
969    }
970}
971
972impl ColorProfile {
973    /// Returns profile version
974    pub fn version(&self) -> ProfileVersion {
975        self.version_internal
976    }
977
978    pub fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> {
979        Self::new_from_slice_with_options(slice, Default::default())
980    }
981
982    pub fn new_from_slice_with_options(
983        slice: &[u8],
984        options: ParsingOptions,
985    ) -> Result<Self, CmsError> {
986        let header = ProfileHeader::new_from_slice(slice)?;
987        let tags_count = header.tag_count as usize;
988        if slice.len() >= options.max_profile_size {
989            return Err(CmsError::InvalidProfile);
990        }
991        let tags_end = tags_count
992            .safe_mul(TAG_SIZE)?
993            .safe_add(size_of::<ProfileHeader>())?;
994        if slice.len() < tags_end {
995            return Err(CmsError::InvalidProfile);
996        }
997        let tags_slice = &slice[size_of::<ProfileHeader>()..tags_end];
998        let mut profile = ColorProfile {
999            rendering_intent: header.rendering_intent,
1000            pcs: header.pcs,
1001            profile_class: header.profile_class,
1002            color_space: header.data_color_space,
1003            white_point: header.illuminant.to_xyzd(),
1004            version_internal: header.version,
1005            creation_date_time: header.creation_date_time,
1006            ..Default::default()
1007        };
1008        let color_space = profile.color_space;
1009        for tag in tags_slice.chunks_exact(TAG_SIZE) {
1010            let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
1011            let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]);
1012            let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize;
1013            // Just ignore unknown tags
1014            if let Ok(tag) = Tag::try_from(tag_value) {
1015                match tag {
1016                    Tag::RedXyz => {
1017                        if color_space == DataColorSpace::Rgb {
1018                            profile.red_colorant =
1019                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
1020                        }
1021                    }
1022                    Tag::GreenXyz => {
1023                        if color_space == DataColorSpace::Rgb {
1024                            profile.green_colorant =
1025                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
1026                        }
1027                    }
1028                    Tag::BlueXyz => {
1029                        if color_space == DataColorSpace::Rgb {
1030                            profile.blue_colorant =
1031                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
1032                        }
1033                    }
1034                    Tag::RedToneReproduction => {
1035                        if color_space == DataColorSpace::Rgb {
1036                            profile.red_trc = Self::read_trc_tag_s(
1037                                slice,
1038                                tag_entry as usize,
1039                                tag_size,
1040                                &options,
1041                            )?;
1042                        }
1043                    }
1044                    Tag::GreenToneReproduction => {
1045                        if color_space == DataColorSpace::Rgb {
1046                            profile.green_trc = Self::read_trc_tag_s(
1047                                slice,
1048                                tag_entry as usize,
1049                                tag_size,
1050                                &options,
1051                            )?;
1052                        }
1053                    }
1054                    Tag::BlueToneReproduction => {
1055                        if color_space == DataColorSpace::Rgb {
1056                            profile.blue_trc = Self::read_trc_tag_s(
1057                                slice,
1058                                tag_entry as usize,
1059                                tag_size,
1060                                &options,
1061                            )?;
1062                        }
1063                    }
1064                    Tag::GreyToneReproduction => {
1065                        if color_space == DataColorSpace::Gray {
1066                            profile.gray_trc = Self::read_trc_tag_s(
1067                                slice,
1068                                tag_entry as usize,
1069                                tag_size,
1070                                &options,
1071                            )?;
1072                        }
1073                    }
1074                    Tag::MediaWhitePoint => {
1075                        profile.media_white_point =
1076                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?;
1077                    }
1078                    Tag::Luminance => {
1079                        profile.luminance =
1080                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?;
1081                    }
1082                    Tag::Measurement => {
1083                        profile.measurement =
1084                            Self::read_meas_tag(slice, tag_entry as usize, tag_size)?;
1085                    }
1086                    Tag::CodeIndependentPoints => {
1087                        // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the
1088                        // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces
1089                        // or profile classes indicated in the profile header.
1090                        if (profile.profile_class == ProfileClass::InputDevice
1091                            || profile.profile_class == ProfileClass::DisplayDevice)
1092                            && (profile.color_space == DataColorSpace::Rgb
1093                                || profile.color_space == DataColorSpace::YCbr
1094                                || profile.color_space == DataColorSpace::Xyz)
1095                        {
1096                            profile.cicp =
1097                                Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?;
1098                        }
1099                    }
1100                    Tag::ChromaticAdaptation => {
1101                        profile.chromatic_adaptation =
1102                            Self::read_chad_tag(slice, tag_entry as usize, tag_size)?;
1103                    }
1104                    Tag::BlackPoint => {
1105                        profile.black_point =
1106                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?
1107                    }
1108                    Tag::DeviceToPcsLutPerceptual => {
1109                        profile.lut_a_to_b_perceptual =
1110                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1111                    }
1112                    Tag::DeviceToPcsLutColorimetric => {
1113                        profile.lut_a_to_b_colorimetric =
1114                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1115                    }
1116                    Tag::DeviceToPcsLutSaturation => {
1117                        profile.lut_a_to_b_saturation =
1118                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1119                    }
1120                    Tag::PcsToDeviceLutPerceptual => {
1121                        profile.lut_b_to_a_perceptual =
1122                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1123                    }
1124                    Tag::PcsToDeviceLutColorimetric => {
1125                        profile.lut_b_to_a_colorimetric =
1126                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1127                    }
1128                    Tag::PcsToDeviceLutSaturation => {
1129                        profile.lut_b_to_a_saturation =
1130                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1131                    }
1132                    Tag::Gamut => {
1133                        profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1134                    }
1135                    Tag::Copyright => {
1136                        profile.copyright =
1137                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1138                    }
1139                    Tag::ProfileDescription => {
1140                        profile.description =
1141                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1142                    }
1143                    Tag::ViewingConditionsDescription => {
1144                        profile.viewing_conditions_description =
1145                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1146                    }
1147                    Tag::DeviceModel => {
1148                        profile.device_model =
1149                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1150                    }
1151                    Tag::DeviceManufacturer => {
1152                        profile.device_manufacturer =
1153                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1154                    }
1155                    Tag::CharTarget => {
1156                        profile.char_target =
1157                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1158                    }
1159                    Tag::Chromaticity => {}
1160                    Tag::ObserverConditions => {
1161                        profile.viewing_conditions =
1162                            Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?;
1163                    }
1164                    Tag::Technology => {
1165                        profile.technology =
1166                            Self::read_tech_tag(slice, tag_entry as usize, tag_size)?;
1167                    }
1168                    Tag::CalibrationDateTime => {
1169                        profile.calibration_date =
1170                            Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?;
1171                    }
1172                }
1173            }
1174        }
1175
1176        Ok(profile)
1177    }
1178}
1179
1180impl ColorProfile {
1181    #[inline]
1182    pub fn colorant_matrix(&self) -> Matrix3d {
1183        Matrix3d {
1184            v: [
1185                [
1186                    self.red_colorant.x,
1187                    self.green_colorant.x,
1188                    self.blue_colorant.x,
1189                ],
1190                [
1191                    self.red_colorant.y,
1192                    self.green_colorant.y,
1193                    self.blue_colorant.y,
1194                ],
1195                [
1196                    self.red_colorant.z,
1197                    self.green_colorant.z,
1198                    self.blue_colorant.z,
1199                ],
1200            ],
1201        }
1202    }
1203
1204    /// Computes colorants matrix. Returns not transposed matrix.
1205    ///
1206    /// To work on `const` context this method does have restrictions.
1207    /// If invalid values were provided it may return invalid matrix or NaNs.
1208    pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d {
1209        let red_xyz = primaries.red.to_xyzd();
1210        let green_xyz = primaries.green.to_xyzd();
1211        let blue_xyz = primaries.blue.to_xyzd();
1212
1213        let xyz_matrix = Matrix3d {
1214            v: [
1215                [red_xyz.x, green_xyz.x, blue_xyz.x],
1216                [red_xyz.y, green_xyz.y, blue_xyz.y],
1217                [red_xyz.z, green_xyz.z, blue_xyz.z],
1218            ],
1219        };
1220        let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd());
1221        adapt_to_d50_d(colorants, white_point)
1222    }
1223
1224    /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point
1225    /// This will nullify CICP.
1226    pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) {
1227        self.cicp = None;
1228        let red_xyz = primaries.red.to_xyzd();
1229        let green_xyz = primaries.green.to_xyzd();
1230        let blue_xyz = primaries.blue.to_xyzd();
1231
1232        self.chromatic_adaptation = Some(BRADFORD_D);
1233        self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz)
1234    }
1235
1236    /// Updates RGB triple colorimetry from 3 [Xyzd] and white point
1237    ///
1238    /// To work on `const` context this method does have restrictions.
1239    /// If invalid values were provided it may return invalid matrix or NaNs.
1240    ///
1241    /// This will void CICP tag.
1242    pub const fn update_rgb_colorimetry_triplet(
1243        &mut self,
1244        white_point: XyY,
1245        red_xyz: Xyzd,
1246        green_xyz: Xyzd,
1247        blue_xyz: Xyzd,
1248    ) {
1249        self.cicp = None;
1250        let xyz_matrix = Matrix3d {
1251            v: [
1252                [red_xyz.x, green_xyz.x, blue_xyz.x],
1253                [red_xyz.y, green_xyz.y, blue_xyz.y],
1254                [red_xyz.z, green_xyz.z, blue_xyz.z],
1255            ],
1256        };
1257        let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd());
1258        let colorants = adapt_to_d50_d(colorants, white_point);
1259
1260        self.update_colorants(colorants);
1261    }
1262
1263    pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) {
1264        // note: there's a transpose type of operation going on here
1265        self.red_colorant.x = colorants.v[0][0];
1266        self.red_colorant.y = colorants.v[1][0];
1267        self.red_colorant.z = colorants.v[2][0];
1268        self.green_colorant.x = colorants.v[0][1];
1269        self.green_colorant.y = colorants.v[1][1];
1270        self.green_colorant.z = colorants.v[2][1];
1271        self.blue_colorant.x = colorants.v[0][2];
1272        self.blue_colorant.y = colorants.v[1][2];
1273        self.blue_colorant.z = colorants.v[2][2];
1274    }
1275
1276    /// Updates RGB triple colorimetry from CICP
1277    pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool {
1278        if !cicp.color_primaries.has_chromaticity()
1279            || !cicp.transfer_characteristics.has_transfer_curve()
1280        {
1281            return false;
1282        }
1283        let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() {
1284            Ok(primaries) => primaries,
1285            Err(_) => return false,
1286        };
1287        let white_point: Chromaticity = match cicp.color_primaries.white_point() {
1288            Ok(v) => v,
1289            Err(_) => return false,
1290        };
1291        self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy);
1292        self.cicp = Some(cicp);
1293
1294        let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() {
1295            Ok(trc) => trc,
1296            Err(_) => return false,
1297        };
1298        self.green_trc = Some(red_trc.clone());
1299        self.blue_trc = Some(red_trc.clone());
1300        self.red_trc = Some(red_trc);
1301        false
1302    }
1303
1304    pub const fn rgb_to_xyz(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f {
1305        let xyz_inverse = xyz_matrix.inverse();
1306        let s = xyz_inverse.mul_vector(wp.to_vector());
1307        let mut v = xyz_matrix.mul_row_vector::<0>(s);
1308        v = v.mul_row_vector::<1>(s);
1309        v.mul_row_vector::<2>(s)
1310    }
1311
1312    /// If Primaries is invalid will return invalid matrix on const context.
1313    /// This assumes not transposed matrix and returns not transposed matrix.
1314    pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d {
1315        let xyz_inverse = xyz_matrix.inverse();
1316        let s = xyz_inverse.mul_vector(wp.to_vector_d());
1317        let mut v = xyz_matrix.mul_row_vector::<0>(s);
1318        v = v.mul_row_vector::<1>(s);
1319        v = v.mul_row_vector::<2>(s);
1320        v
1321    }
1322
1323    /// Returns the RGB to XYZ transformation matrix.
1324    ///
1325    /// Per ICC.1:2022-05 Section F.3, the computational model is:
1326    ///   connection = colorantMatrix × linear_rgb
1327    ///
1328    /// The colorant tags (rXYZ, gXYZ, bXYZ) are used directly as matrix columns.
1329    /// This matches skcms and lcms2 behavior.
1330    pub fn rgb_to_xyz_matrix(&self) -> Matrix3d {
1331        self.colorant_matrix()
1332    }
1333
1334    /// Computes transform matrix RGB -> XYZ -> RGB
1335    /// Current profile is used as source, other as destination
1336    pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d {
1337        let source = self.rgb_to_xyz_matrix();
1338        let dst = dest.rgb_to_xyz_matrix();
1339        let dest_inverse = dst.inverse();
1340        dest_inverse.mat_mul(source)
1341    }
1342
1343    /// Returns volume of colors stored in profile
1344    pub fn profile_volume(&self) -> Option<f32> {
1345        let red_prim = self.red_colorant;
1346        let green_prim = self.green_colorant;
1347        let blue_prim = self.blue_colorant;
1348        let tetrahedral_vertices = Matrix3d {
1349            v: [
1350                [red_prim.x, red_prim.y, red_prim.z],
1351                [green_prim.x, green_prim.y, green_prim.z],
1352                [blue_prim.x, blue_prim.y, blue_prim.z],
1353            ],
1354        };
1355        let det = tetrahedral_vertices.determinant()?;
1356        Some((det / 6.0f64) as f32)
1357    }
1358
1359    #[allow(unused)]
1360    pub(crate) fn has_device_to_pcs_lut(&self) -> bool {
1361        self.lut_a_to_b_perceptual.is_some()
1362            || self.lut_a_to_b_saturation.is_some()
1363            || self.lut_a_to_b_colorimetric.is_some()
1364    }
1365
1366    #[allow(unused)]
1367    pub(crate) fn has_pcs_to_device_lut(&self) -> bool {
1368        self.lut_b_to_a_perceptual.is_some()
1369            || self.lut_b_to_a_saturation.is_some()
1370            || self.lut_b_to_a_colorimetric.is_some()
1371    }
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376    use super::*;
1377    use std::fs;
1378
1379    #[test]
1380    fn test_gray() {
1381        if let Ok(gray_icc) = fs::read("./assets/Generic Gray Gamma 2.2 Profile.icc") {
1382            let f_p = ColorProfile::new_from_slice(&gray_icc).unwrap();
1383            assert!(f_p.gray_trc.is_some());
1384        }
1385    }
1386
1387    #[test]
1388    fn test_perceptual() {
1389        if let Ok(srgb_perceptual_icc) = fs::read("./assets/srgb_perceptual.icc") {
1390            let f_p = ColorProfile::new_from_slice(&srgb_perceptual_icc).unwrap();
1391            assert_eq!(f_p.pcs, DataColorSpace::Lab);
1392            assert_eq!(f_p.color_space, DataColorSpace::Rgb);
1393            assert_eq!(f_p.version(), ProfileVersion::V4_2);
1394            assert!(f_p.lut_a_to_b_perceptual.is_some());
1395            assert!(f_p.lut_b_to_a_perceptual.is_some());
1396        }
1397    }
1398
1399    #[test]
1400    fn test_us_swop_coated() {
1401        if let Ok(us_swop_coated) = fs::read("./assets/us_swop_coated.icc") {
1402            let f_p = ColorProfile::new_from_slice(&us_swop_coated).unwrap();
1403            assert_eq!(f_p.pcs, DataColorSpace::Lab);
1404            assert_eq!(f_p.color_space, DataColorSpace::Cmyk);
1405            assert_eq!(f_p.version(), ProfileVersion::V2_0);
1406
1407            assert!(f_p.lut_a_to_b_perceptual.is_some());
1408            assert!(f_p.lut_b_to_a_perceptual.is_some());
1409
1410            assert!(f_p.lut_a_to_b_colorimetric.is_some());
1411            assert!(f_p.lut_b_to_a_colorimetric.is_some());
1412
1413            assert!(f_p.gamut.is_some());
1414
1415            assert!(f_p.copyright.is_some());
1416            assert!(f_p.description.is_some());
1417        }
1418    }
1419
1420    #[test]
1421    fn test_matrix_shaper() {
1422        if let Ok(matrix_shaper) = fs::read("./assets/Display P3.icc") {
1423            let f_p = ColorProfile::new_from_slice(&matrix_shaper).unwrap();
1424            assert_eq!(f_p.pcs, DataColorSpace::Xyz);
1425            assert_eq!(f_p.color_space, DataColorSpace::Rgb);
1426            assert_eq!(f_p.version(), ProfileVersion::V4_0);
1427
1428            assert!(f_p.red_trc.is_some());
1429            assert!(f_p.blue_trc.is_some());
1430            assert!(f_p.green_trc.is_some());
1431
1432            assert_ne!(f_p.red_colorant, Xyzd::default());
1433            assert_ne!(f_p.blue_colorant, Xyzd::default());
1434            assert_ne!(f_p.green_colorant, Xyzd::default());
1435
1436            assert!(f_p.copyright.is_some());
1437            assert!(f_p.description.is_some());
1438        }
1439    }
1440
1441    /// Verify rgb_to_xyz_matrix returns colorant_matrix directly per ICC.1:2022-05 F.3.
1442    ///
1443    /// SM245B.icc is a V2 Samsung monitor profile with D65 colorants and no CHAD tag.
1444    /// Source: https://skia.googlesource.com/skcms/+/refs/heads/main/profiles/misc/SM245B.icc
1445    #[test]
1446    fn test_rgb_to_xyz_matrix_equals_colorant_matrix() {
1447        // Test with SM245B.icc (D65 colorants, no CHAD tag)
1448        if let Ok(icc_data) = fs::read("./assets/SM245B.icc") {
1449            if let Ok(profile) = ColorProfile::new_from_slice(&icc_data) {
1450                let rgb_to_xyz = profile.rgb_to_xyz_matrix();
1451                let colorants = profile.colorant_matrix();
1452
1453                for i in 0..3 {
1454                    for j in 0..3 {
1455                        assert!(
1456                            (rgb_to_xyz.v[i][j] - colorants.v[i][j]).abs() < 1e-10,
1457                            "rgb_to_xyz_matrix should equal colorant_matrix at [{i}][{j}]"
1458                        );
1459                    }
1460                }
1461            }
1462        }
1463
1464        // Also verify with sRGB
1465        let srgb = ColorProfile::new_srgb();
1466        let rgb_to_xyz = srgb.rgb_to_xyz_matrix();
1467        let colorants = srgb.colorant_matrix();
1468
1469        for i in 0..3 {
1470            for j in 0..3 {
1471                assert!(
1472                    (rgb_to_xyz.v[i][j] - colorants.v[i][j]).abs() < 1e-10,
1473                    "sRGB: rgb_to_xyz_matrix should equal colorant_matrix at [{i}][{j}]"
1474                );
1475            }
1476        }
1477    }
1478
1479    #[test]
1480    fn test_profile_version_parsing_standard() {
1481        // Standard versions should work
1482        assert_eq!(
1483            ProfileVersion::try_from(0x02000000).unwrap(),
1484            ProfileVersion::V2_0
1485        );
1486        assert_eq!(
1487            ProfileVersion::try_from(0x02400000).unwrap(),
1488            ProfileVersion::V2_4
1489        );
1490        assert_eq!(
1491            ProfileVersion::try_from(0x04000000).unwrap(),
1492            ProfileVersion::V4_0
1493        );
1494        assert_eq!(
1495            ProfileVersion::try_from(0x04400000).unwrap(),
1496            ProfileVersion::V4_4
1497        );
1498    }
1499
1500    #[test]
1501    fn test_profile_version_parsing_patch_versions() {
1502        // Patch versions found in real ICC profiles should be accepted
1503
1504        // v2.0.2 (SM245B.icc) - minor bugfix version
1505        assert!(
1506            ProfileVersion::try_from(0x02020000).is_ok(),
1507            "v2.0.2 should be accepted"
1508        );
1509
1510        // v3.4 (ibm-t61.icc, new.icc) - intermediate version
1511        assert!(
1512            ProfileVersion::try_from(0x03400000).is_ok(),
1513            "v3.4 should be accepted"
1514        );
1515
1516        // v4.2.9 (lcms_samsung_syncmaster.icc) - patch version
1517        assert!(
1518            ProfileVersion::try_from(0x04290000).is_ok(),
1519            "v4.2.9 should be accepted"
1520        );
1521    }
1522
1523    #[test]
1524    fn test_profile_version_parsing_rejected() {
1525        // Invalid and unsupported versions should be rejected
1526
1527        // v0.0 - invalid version (no such ICC spec exists)
1528        assert!(
1529            ProfileVersion::try_from(0x00000000).is_err(),
1530            "v0.0 should be rejected"
1531        );
1532
1533        // v5.0 (iccMAX) - reject because it has different white point requirements
1534        assert!(
1535            ProfileVersion::try_from(0x05000000).is_err(),
1536            "v5.0 should be rejected"
1537        );
1538
1539        // v6.0 - future/unknown version
1540        assert!(
1541            ProfileVersion::try_from(0x06000000).is_err(),
1542            "v6.0 should be rejected"
1543        );
1544    }
1545
1546    #[test]
1547    fn test_profile_version_v4_4_mapping() {
1548        // V4.4 should map to V4_4, not V4_3 (regression test for typo)
1549        assert_eq!(
1550            ProfileVersion::try_from(0x04400000).unwrap(),
1551            ProfileVersion::V4_4
1552        );
1553    }
1554
1555    #[test]
1556    fn test_rendering_intent_invalid_defaults_to_perceptual() {
1557        // Valid values are 0-3. Invalid values default to Perceptual
1558        // rather than rejecting the profile.
1559        assert_eq!(
1560            RenderingIntent::try_from(0x01000000).unwrap(),
1561            RenderingIntent::Perceptual
1562        );
1563        assert_eq!(
1564            RenderingIntent::try_from(0x04000000).unwrap(),
1565            RenderingIntent::Perceptual
1566        );
1567        assert_eq!(
1568            RenderingIntent::try_from(0xFFFFFFFF).unwrap(),
1569            RenderingIntent::Perceptual
1570        );
1571    }
1572
1573    /// Parse a profile with a non-conforming rendering intent value.
1574    /// Synthesized from SM245B.icc with bytes 64-67 set to 0x01000000
1575    /// (byte-swapped, as found in old Linotype "Lino" profiles).
1576    /// The invalid value should default to Perceptual per our policy.
1577    #[test]
1578    fn test_invalid_rendering_intent_defaults_to_perceptual() {
1579        let icc_data =
1580            fs::read("./assets/swapped_intent.icc").expect("swapped_intent.icc test asset");
1581        let profile = ColorProfile::new_from_slice(&icc_data)
1582            .expect("Profile with invalid rendering intent should parse");
1583        assert_eq!(profile.rendering_intent, RenderingIntent::Perceptual);
1584        // Verify the rest of the profile parsed correctly
1585        assert_eq!(profile.color_space, DataColorSpace::Rgb);
1586        assert!(profile.red_trc.is_some());
1587        assert!(profile.green_trc.is_some());
1588        assert!(profile.blue_trc.is_some());
1589        let dst = ColorProfile::new_srgb();
1590        let transform = profile
1591            .create_transform_8bit(Layout::Rgba, &dst, Layout::Rgba, Default::default())
1592            .expect("Should create transform from profile with defaulted intent");
1593        let src = [128u8, 128, 128, 255];
1594        let mut out = [0u8; 4];
1595        transform.transform(&src, &mut out).unwrap();
1596        assert_eq!(out[3], 255, "Alpha should be preserved");
1597    }
1598
1599    /// v4 profile with correct mluc description tag should parse as
1600    /// Localizable (regression: ensure desc-tolerance doesn't break mluc).
1601    #[test]
1602    fn test_v4_mluc_description_parses_as_localizable() {
1603        let icc_data = fs::read("./assets/Display P3.icc").expect("Display P3.icc test asset");
1604        let profile =
1605            ColorProfile::new_from_slice(&icc_data).expect("Display P3 profile should parse");
1606        assert_eq!(profile.version(), ProfileVersion::V4_0);
1607        let desc = profile
1608            .description
1609            .clone()
1610            .expect("description should be present");
1611        match desc {
1612            super::ProfileText::Localizable(records) => {
1613                assert!(!records.is_empty(), "mluc should have at least one record");
1614                assert!(
1615                    records[0].value.contains("Display P3"),
1616                    "mluc should contain 'Display P3', got: {}",
1617                    records[0].value
1618                );
1619            }
1620            other => panic!("v4 mluc should parse as Localizable, got {:?}", other),
1621        }
1622    }
1623
1624    /// v4 profile with non-conforming truncated desc tag should parse.
1625    /// Synthesized from Display P3.icc with the mluc tag replaced by a
1626    /// minimal desc tag (ASCII only, no Unicode/ScriptCode sections).
1627    #[test]
1628    fn test_v4_truncated_desc_tag() {
1629        let icc_data =
1630            fs::read("./assets/truncated_desc_v4.icc").expect("truncated_desc_v4.icc test asset");
1631        let profile = ColorProfile::new_from_slice(&icc_data)
1632            .expect("v4 profile with truncated desc should parse");
1633        assert_eq!(profile.version(), ProfileVersion::V4_0);
1634        assert_eq!(profile.color_space, DataColorSpace::Rgb);
1635        let desc = profile
1636            .description
1637            .clone()
1638            .expect("description should be present");
1639        match desc {
1640            ProfileText::Description(d) => {
1641                assert!(
1642                    d.ascii_string.contains("Display P3"),
1643                    "desc should contain 'Display P3', got: {}",
1644                    d.ascii_string
1645                );
1646            }
1647            other => panic!(
1648                "v4 truncated desc should parse as Description, got {:?}",
1649                other
1650            ),
1651        }
1652        let dst = ColorProfile::new_srgb();
1653        let transform = profile
1654            .create_transform_8bit(Layout::Rgba, &dst, Layout::Rgba, Default::default())
1655            .expect("Should create transform from v4 profile with truncated desc");
1656        let src = [128u8, 128, 128, 255];
1657        let mut out = [0u8; 4];
1658        transform.transform(&src, &mut out).unwrap();
1659        assert_eq!(out[3], 255, "Alpha should be preserved");
1660    }
1661}