Coverage Report

Created: 2026-03-10 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/profile.rs
Line
Count
Source
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
 */
29
use crate::chad::BRADFORD_D;
30
use crate::cicp::{
31
    CicpColorPrimaries, ColorPrimaries, MatrixCoefficients, TransferCharacteristics,
32
};
33
use crate::dat::ColorDateTime;
34
use crate::err::CmsError;
35
use crate::matrix::{Matrix3f, Xyz};
36
use crate::reader::s15_fixed16_number_to_float;
37
use crate::safe_math::{SafeAdd, SafeMul};
38
use crate::tag::{TAG_SIZE, Tag};
39
use crate::trc::ToneReprCurve;
40
use crate::{Chromaticity, Layout, Matrix3d, Vector3d, XyY, Xyzd, adapt_to_d50_d};
41
use std::io::Read;
42
43
const MAX_PROFILE_SIZE: usize = 1024 * 1024 * 10; // 10 MB max, for Fogra39 etc
44
45
#[repr(u32)]
46
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47
pub enum ProfileSignature {
48
    Acsp,
49
}
50
51
impl TryFrom<u32> for ProfileSignature {
52
    type Error = CmsError;
53
    #[inline]
54
0
    fn try_from(value: u32) -> Result<Self, Self::Error> {
55
0
        if value == u32::from_ne_bytes(*b"acsp").to_be() {
56
0
            return Ok(ProfileSignature::Acsp);
57
0
        }
58
0
        Err(CmsError::InvalidProfile)
59
0
    }
60
}
61
62
impl From<ProfileSignature> for u32 {
63
    #[inline]
64
0
    fn from(value: ProfileSignature) -> Self {
65
0
        match value {
66
0
            ProfileSignature::Acsp => u32::from_ne_bytes(*b"acsp").to_be(),
67
        }
68
0
    }
69
}
70
71
#[repr(u32)]
72
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)]
73
pub 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
88
impl TryFrom<u32> for ProfileVersion {
89
    type Error = CmsError;
90
0
    fn try_from(value: u32) -> Result<Self, Self::Error> {
91
        // First try exact match for known versions
92
0
        match value {
93
0
            0x02000000 => return Ok(ProfileVersion::V2_0),
94
0
            0x02100000 => return Ok(ProfileVersion::V2_1),
95
0
            0x02200000 => return Ok(ProfileVersion::V2_2),
96
0
            0x02300000 => return Ok(ProfileVersion::V2_3),
97
0
            0x02400000 => return Ok(ProfileVersion::V2_4),
98
0
            0x04000000 => return Ok(ProfileVersion::V4_0),
99
0
            0x04100000 => return Ok(ProfileVersion::V4_1),
100
0
            0x04200000 => return Ok(ProfileVersion::V4_2),
101
0
            0x04300000 => return Ok(ProfileVersion::V4_3),
102
0
            0x04400000 => return Ok(ProfileVersion::V4_4),
103
0
            _ => {}
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
0
        let major = (value >> 24) & 0xFF;
109
0
        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
0
        match major {
114
            0 => {
115
                // Version 0.x is invalid - reject
116
0
                Err(CmsError::InvalidProfile)
117
            }
118
            2 => {
119
                // v2.x - map to the appropriate v2 minor version or highest known
120
0
                match minor {
121
0
                    0 => Ok(ProfileVersion::V2_0),
122
0
                    1 => Ok(ProfileVersion::V2_1),
123
0
                    2 => Ok(ProfileVersion::V2_2),
124
0
                    3 => Ok(ProfileVersion::V2_3),
125
0
                    _ => 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
0
                Ok(ProfileVersion::V2_4)
131
            }
132
            4 => {
133
                // v4.x - map to the appropriate v4 minor version or highest known
134
0
                match minor {
135
0
                    0 => Ok(ProfileVersion::V4_0),
136
0
                    1 => Ok(ProfileVersion::V4_1),
137
0
                    2 => Ok(ProfileVersion::V4_2),
138
0
                    3 => Ok(ProfileVersion::V4_3),
139
0
                    _ => 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
0
                Err(CmsError::InvalidProfile)
146
            }
147
        }
148
0
    }
149
}
150
151
impl From<ProfileVersion> for u32 {
152
0
    fn from(value: ProfileVersion) -> Self {
153
0
        match value {
154
0
            ProfileVersion::V2_0 => 0x02000000,
155
0
            ProfileVersion::V2_1 => 0x02100000,
156
0
            ProfileVersion::V2_2 => 0x02200000,
157
0
            ProfileVersion::V2_3 => 0x02300000,
158
0
            ProfileVersion::V2_4 => 0x02400000,
159
0
            ProfileVersion::V4_0 => 0x04000000,
160
0
            ProfileVersion::V4_1 => 0x04100000,
161
0
            ProfileVersion::V4_2 => 0x04200000,
162
0
            ProfileVersion::V4_3 => 0x04300000,
163
0
            ProfileVersion::V4_4 => 0x04400000,
164
0
            ProfileVersion::Unknown => 0x02000000,
165
        }
166
0
    }
167
}
168
169
#[repr(u32)]
170
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Hash)]
171
pub 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
200
impl DataColorSpace {
201
    #[inline]
202
0
    pub fn check_layout(self, layout: Layout) -> Result<(), CmsError> {
203
0
        let unsupported: bool = match self {
204
0
            DataColorSpace::Xyz => layout != Layout::Rgb,
205
0
            DataColorSpace::Lab => layout != Layout::Rgb && layout != Layout::Rgba,
206
0
            DataColorSpace::Luv => layout != Layout::Rgb,
207
0
            DataColorSpace::YCbr => layout != Layout::Rgb,
208
0
            DataColorSpace::Yxy => layout != Layout::Rgb,
209
0
            DataColorSpace::Rgb => layout != Layout::Rgb && layout != Layout::Rgba,
210
0
            DataColorSpace::Gray => layout != Layout::Gray && layout != Layout::GrayAlpha,
211
0
            DataColorSpace::Hsv => layout != Layout::Rgb,
212
0
            DataColorSpace::Hls => layout != Layout::Rgb,
213
0
            DataColorSpace::Cmyk => layout != Layout::Rgba,
214
0
            DataColorSpace::Cmy => layout != Layout::Rgb,
215
0
            DataColorSpace::Color2 => layout != Layout::GrayAlpha,
216
0
            DataColorSpace::Color3 => layout != Layout::Rgb,
217
0
            DataColorSpace::Color4 => layout != Layout::Rgba,
218
0
            DataColorSpace::Color5 => layout != Layout::Inks5,
219
0
            DataColorSpace::Color6 => layout != Layout::Inks6,
220
0
            DataColorSpace::Color7 => layout != Layout::Inks7,
221
0
            DataColorSpace::Color8 => layout != Layout::Inks8,
222
0
            DataColorSpace::Color9 => layout != Layout::Inks9,
223
0
            DataColorSpace::Color10 => layout != Layout::Inks10,
224
0
            DataColorSpace::Color11 => layout != Layout::Inks11,
225
0
            DataColorSpace::Color12 => layout != Layout::Inks12,
226
0
            DataColorSpace::Color13 => layout != Layout::Inks13,
227
0
            DataColorSpace::Color14 => layout != Layout::Inks14,
228
0
            DataColorSpace::Color15 => layout != Layout::Inks15,
229
        };
230
0
        if unsupported {
231
0
            Err(CmsError::InvalidLayout)
232
        } else {
233
0
            Ok(())
234
        }
235
0
    }
236
237
0
    pub(crate) fn is_three_channels(self) -> bool {
238
0
        matches!(
239
0
            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
0
    }
252
}
253
254
#[repr(u32)]
255
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)]
256
pub enum ProfileClass {
257
    InputDevice,
258
    #[default]
259
    DisplayDevice,
260
    OutputDevice,
261
    DeviceLink,
262
    ColorSpace,
263
    Abstract,
264
    Named,
265
}
266
267
impl TryFrom<u32> for ProfileClass {
268
    type Error = CmsError;
269
0
    fn try_from(value: u32) -> Result<Self, Self::Error> {
270
0
        if value == u32::from_ne_bytes(*b"scnr").to_be() {
271
0
            return Ok(ProfileClass::InputDevice);
272
0
        } else if value == u32::from_ne_bytes(*b"mntr").to_be() {
273
0
            return Ok(ProfileClass::DisplayDevice);
274
0
        } else if value == u32::from_ne_bytes(*b"prtr").to_be() {
275
0
            return Ok(ProfileClass::OutputDevice);
276
0
        } else if value == u32::from_ne_bytes(*b"link").to_be() {
277
0
            return Ok(ProfileClass::DeviceLink);
278
0
        } else if value == u32::from_ne_bytes(*b"spac").to_be() {
279
0
            return Ok(ProfileClass::ColorSpace);
280
0
        } else if value == u32::from_ne_bytes(*b"abst").to_be() {
281
0
            return Ok(ProfileClass::Abstract);
282
0
        } else if value == u32::from_ne_bytes(*b"nmcl").to_be() {
283
0
            return Ok(ProfileClass::Named);
284
0
        }
285
0
        Err(CmsError::InvalidProfile)
286
0
    }
287
}
288
289
impl From<ProfileClass> for u32 {
290
0
    fn from(val: ProfileClass) -> Self {
291
0
        match val {
292
0
            ProfileClass::InputDevice => u32::from_ne_bytes(*b"scnr").to_be(),
293
0
            ProfileClass::DisplayDevice => u32::from_ne_bytes(*b"mntr").to_be(),
294
0
            ProfileClass::OutputDevice => u32::from_ne_bytes(*b"prtr").to_be(),
295
0
            ProfileClass::DeviceLink => u32::from_ne_bytes(*b"link").to_be(),
296
0
            ProfileClass::ColorSpace => u32::from_ne_bytes(*b"spac").to_be(),
297
0
            ProfileClass::Abstract => u32::from_ne_bytes(*b"abst").to_be(),
298
0
            ProfileClass::Named => u32::from_ne_bytes(*b"nmcl").to_be(),
299
        }
300
0
    }
301
}
302
303
#[derive(Debug, Clone, PartialEq)]
304
pub enum LutStore {
305
    Store8(Vec<u8>),
306
    Store16(Vec<u16>),
307
}
308
309
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
310
pub enum LutType {
311
    Lut8,
312
    Lut16,
313
    LutMab,
314
    LutMba,
315
}
316
317
impl TryFrom<u32> for LutType {
318
    type Error = CmsError;
319
0
    fn try_from(value: u32) -> Result<Self, Self::Error> {
320
0
        if value == u32::from_ne_bytes(*b"mft1").to_be() {
321
0
            return Ok(LutType::Lut8);
322
0
        } else if value == u32::from_ne_bytes(*b"mft2").to_be() {
323
0
            return Ok(LutType::Lut16);
324
0
        } else if value == u32::from_ne_bytes(*b"mAB ").to_be() {
325
0
            return Ok(LutType::LutMab);
326
0
        } else if value == u32::from_ne_bytes(*b"mBA ").to_be() {
327
0
            return Ok(LutType::LutMba);
328
0
        }
329
0
        Err(CmsError::InvalidProfile)
330
0
    }
331
}
332
333
impl From<LutType> for u32 {
334
0
    fn from(val: LutType) -> Self {
335
0
        match val {
336
0
            LutType::Lut8 => u32::from_ne_bytes(*b"mft1").to_be(),
337
0
            LutType::Lut16 => u32::from_ne_bytes(*b"mft2").to_be(),
338
0
            LutType::LutMab => u32::from_ne_bytes(*b"mAB ").to_be(),
339
0
            LutType::LutMba => u32::from_ne_bytes(*b"mBA ").to_be(),
340
        }
341
0
    }
342
}
343
344
impl TryFrom<u32> for DataColorSpace {
345
    type Error = CmsError;
346
0
    fn try_from(value: u32) -> Result<Self, Self::Error> {
347
0
        if value == u32::from_ne_bytes(*b"XYZ ").to_be() {
348
0
            return Ok(DataColorSpace::Xyz);
349
0
        } else if value == u32::from_ne_bytes(*b"Lab ").to_be() {
350
0
            return Ok(DataColorSpace::Lab);
351
0
        } else if value == u32::from_ne_bytes(*b"Luv ").to_be() {
352
0
            return Ok(DataColorSpace::Luv);
353
0
        } else if value == u32::from_ne_bytes(*b"YCbr").to_be() {
354
0
            return Ok(DataColorSpace::YCbr);
355
0
        } else if value == u32::from_ne_bytes(*b"Yxy ").to_be() {
356
0
            return Ok(DataColorSpace::Yxy);
357
0
        } else if value == u32::from_ne_bytes(*b"RGB ").to_be() {
358
0
            return Ok(DataColorSpace::Rgb);
359
0
        } else if value == u32::from_ne_bytes(*b"GRAY").to_be() {
360
0
            return Ok(DataColorSpace::Gray);
361
0
        } else if value == u32::from_ne_bytes(*b"HSV ").to_be() {
362
0
            return Ok(DataColorSpace::Hsv);
363
0
        } else if value == u32::from_ne_bytes(*b"HLS ").to_be() {
364
0
            return Ok(DataColorSpace::Hls);
365
0
        } else if value == u32::from_ne_bytes(*b"CMYK").to_be() {
366
0
            return Ok(DataColorSpace::Cmyk);
367
0
        } else if value == u32::from_ne_bytes(*b"CMY ").to_be() {
368
0
            return Ok(DataColorSpace::Cmy);
369
0
        } else if value == u32::from_ne_bytes(*b"2CLR").to_be() {
370
0
            return Ok(DataColorSpace::Color2);
371
0
        } else if value == u32::from_ne_bytes(*b"3CLR").to_be() {
372
0
            return Ok(DataColorSpace::Color3);
373
0
        } else if value == u32::from_ne_bytes(*b"4CLR").to_be() {
374
0
            return Ok(DataColorSpace::Color4);
375
0
        } else if value == u32::from_ne_bytes(*b"5CLR").to_be() {
376
0
            return Ok(DataColorSpace::Color5);
377
0
        } else if value == u32::from_ne_bytes(*b"6CLR").to_be() {
378
0
            return Ok(DataColorSpace::Color6);
379
0
        } else if value == u32::from_ne_bytes(*b"7CLR").to_be() {
380
0
            return Ok(DataColorSpace::Color7);
381
0
        } else if value == u32::from_ne_bytes(*b"8CLR").to_be() {
382
0
            return Ok(DataColorSpace::Color8);
383
0
        } else if value == u32::from_ne_bytes(*b"9CLR").to_be() {
384
0
            return Ok(DataColorSpace::Color9);
385
0
        } else if value == u32::from_ne_bytes(*b"ACLR").to_be() {
386
0
            return Ok(DataColorSpace::Color10);
387
0
        } else if value == u32::from_ne_bytes(*b"BCLR").to_be() {
388
0
            return Ok(DataColorSpace::Color11);
389
0
        } else if value == u32::from_ne_bytes(*b"CCLR").to_be() {
390
0
            return Ok(DataColorSpace::Color12);
391
0
        } else if value == u32::from_ne_bytes(*b"DCLR").to_be() {
392
0
            return Ok(DataColorSpace::Color13);
393
0
        } else if value == u32::from_ne_bytes(*b"ECLR").to_be() {
394
0
            return Ok(DataColorSpace::Color14);
395
0
        } else if value == u32::from_ne_bytes(*b"FCLR").to_be() {
396
0
            return Ok(DataColorSpace::Color15);
397
0
        }
398
0
        Err(CmsError::InvalidProfile)
399
0
    }
400
}
401
402
impl From<DataColorSpace> for u32 {
403
0
    fn from(val: DataColorSpace) -> Self {
404
0
        match val {
405
0
            DataColorSpace::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(),
406
0
            DataColorSpace::Lab => u32::from_ne_bytes(*b"Lab ").to_be(),
407
0
            DataColorSpace::Luv => u32::from_ne_bytes(*b"Luv ").to_be(),
408
0
            DataColorSpace::YCbr => u32::from_ne_bytes(*b"YCbr").to_be(),
409
0
            DataColorSpace::Yxy => u32::from_ne_bytes(*b"Yxy ").to_be(),
410
0
            DataColorSpace::Rgb => u32::from_ne_bytes(*b"RGB ").to_be(),
411
0
            DataColorSpace::Gray => u32::from_ne_bytes(*b"GRAY").to_be(),
412
0
            DataColorSpace::Hsv => u32::from_ne_bytes(*b"HSV ").to_be(),
413
0
            DataColorSpace::Hls => u32::from_ne_bytes(*b"HLS ").to_be(),
414
0
            DataColorSpace::Cmyk => u32::from_ne_bytes(*b"CMYK").to_be(),
415
0
            DataColorSpace::Cmy => u32::from_ne_bytes(*b"CMY ").to_be(),
416
0
            DataColorSpace::Color2 => u32::from_ne_bytes(*b"2CLR").to_be(),
417
0
            DataColorSpace::Color3 => u32::from_ne_bytes(*b"3CLR").to_be(),
418
0
            DataColorSpace::Color4 => u32::from_ne_bytes(*b"4CLR").to_be(),
419
0
            DataColorSpace::Color5 => u32::from_ne_bytes(*b"5CLR").to_be(),
420
0
            DataColorSpace::Color6 => u32::from_ne_bytes(*b"6CLR").to_be(),
421
0
            DataColorSpace::Color7 => u32::from_ne_bytes(*b"7CLR").to_be(),
422
0
            DataColorSpace::Color8 => u32::from_ne_bytes(*b"8CLR").to_be(),
423
0
            DataColorSpace::Color9 => u32::from_ne_bytes(*b"9CLR").to_be(),
424
0
            DataColorSpace::Color10 => u32::from_ne_bytes(*b"ACLR").to_be(),
425
0
            DataColorSpace::Color11 => u32::from_ne_bytes(*b"BCLR").to_be(),
426
0
            DataColorSpace::Color12 => u32::from_ne_bytes(*b"CCLR").to_be(),
427
0
            DataColorSpace::Color13 => u32::from_ne_bytes(*b"DCLR").to_be(),
428
0
            DataColorSpace::Color14 => u32::from_ne_bytes(*b"ECLR").to_be(),
429
0
            DataColorSpace::Color15 => u32::from_ne_bytes(*b"FCLR").to_be(),
430
        }
431
0
    }
432
}
433
434
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
435
pub 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
467
impl From<u32> for TechnologySignatures {
468
0
    fn from(value: u32) -> Self {
469
0
        if value == u32::from_ne_bytes(*b"fscn").to_be() {
470
0
            return TechnologySignatures::FilmScanner;
471
0
        } else if value == u32::from_ne_bytes(*b"dcam").to_be() {
472
0
            return TechnologySignatures::DigitalCamera;
473
0
        } else if value == u32::from_ne_bytes(*b"rscn").to_be() {
474
0
            return TechnologySignatures::ReflectiveScanner;
475
0
        } else if value == u32::from_ne_bytes(*b"ijet").to_be() {
476
0
            return TechnologySignatures::InkJetPrinter;
477
0
        } else if value == u32::from_ne_bytes(*b"twax").to_be() {
478
0
            return TechnologySignatures::ThermalWaxPrinter;
479
0
        } else if value == u32::from_ne_bytes(*b"epho").to_be() {
480
0
            return TechnologySignatures::ElectrophotographicPrinter;
481
0
        } else if value == u32::from_ne_bytes(*b"esta").to_be() {
482
0
            return TechnologySignatures::ElectrostaticPrinter;
483
0
        } else if value == u32::from_ne_bytes(*b"dsub").to_be() {
484
0
            return TechnologySignatures::DyeSublimationPrinter;
485
0
        } else if value == u32::from_ne_bytes(*b"rpho").to_be() {
486
0
            return TechnologySignatures::PhotographicPaperPrinter;
487
0
        } else if value == u32::from_ne_bytes(*b"fprn").to_be() {
488
0
            return TechnologySignatures::FilmWriter;
489
0
        } else if value == u32::from_ne_bytes(*b"vidm").to_be() {
490
0
            return TechnologySignatures::VideoMonitor;
491
0
        } else if value == u32::from_ne_bytes(*b"vidc").to_be() {
492
0
            return TechnologySignatures::VideoCamera;
493
0
        } else if value == u32::from_ne_bytes(*b"pjtv").to_be() {
494
0
            return TechnologySignatures::ProjectionTelevision;
495
0
        } else if value == u32::from_ne_bytes(*b"CRT ").to_be() {
496
0
            return TechnologySignatures::CathodeRayTubeDisplay;
497
0
        } else if value == u32::from_ne_bytes(*b"PMD ").to_be() {
498
0
            return TechnologySignatures::PassiveMatrixDisplay;
499
0
        } else if value == u32::from_ne_bytes(*b"AMD ").to_be() {
500
0
            return TechnologySignatures::ActiveMatrixDisplay;
501
0
        } else if value == u32::from_ne_bytes(*b"LCD ").to_be() {
502
0
            return TechnologySignatures::LiquidCrystalDisplay;
503
0
        } else if value == u32::from_ne_bytes(*b"OLED").to_be() {
504
0
            return TechnologySignatures::OrganicLedDisplay;
505
0
        } else if value == u32::from_ne_bytes(*b"KPCD").to_be() {
506
0
            return TechnologySignatures::PhotoCd;
507
0
        } else if value == u32::from_ne_bytes(*b"imgs").to_be() {
508
0
            return TechnologySignatures::PhotographicImageSetter;
509
0
        } else if value == u32::from_ne_bytes(*b"grav").to_be() {
510
0
            return TechnologySignatures::Gravure;
511
0
        } else if value == u32::from_ne_bytes(*b"offs").to_be() {
512
0
            return TechnologySignatures::OffsetLithography;
513
0
        } else if value == u32::from_ne_bytes(*b"silk").to_be() {
514
0
            return TechnologySignatures::Silkscreen;
515
0
        } else if value == u32::from_ne_bytes(*b"flex").to_be() {
516
0
            return TechnologySignatures::Flexography;
517
0
        } else if value == u32::from_ne_bytes(*b"mpfs").to_be() {
518
0
            return TechnologySignatures::MotionPictureFilmScanner;
519
0
        } else if value == u32::from_ne_bytes(*b"mpfr").to_be() {
520
0
            return TechnologySignatures::MotionPictureFilmRecorder;
521
0
        } else if value == u32::from_ne_bytes(*b"dmpc").to_be() {
522
0
            return TechnologySignatures::DigitalMotionPictureCamera;
523
0
        } else if value == u32::from_ne_bytes(*b"dcpj").to_be() {
524
0
            return TechnologySignatures::DigitalCinemaProjector;
525
0
        }
526
0
        TechnologySignatures::Unknown(value)
527
0
    }
528
}
529
530
#[derive(Debug, Clone)]
531
pub enum LutWarehouse {
532
    Lut(LutDataType),
533
    Multidimensional(LutMultidimensionalType),
534
}
535
536
impl PartialEq for LutWarehouse {
537
0
    fn eq(&self, other: &Self) -> bool {
538
0
        match (self, other) {
539
0
            (LutWarehouse::Lut(a), LutWarehouse::Lut(b)) => a == b,
540
0
            (LutWarehouse::Multidimensional(a), LutWarehouse::Multidimensional(b)) => a == b,
541
0
            _ => false, // Different variants are not equal
542
        }
543
0
    }
544
}
545
546
#[derive(Debug, Clone, PartialEq)]
547
pub 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
561
impl LutDataType {
562
0
    pub(crate) fn has_same_kind(&self) -> bool {
563
0
        matches!(
564
0
            (&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
0
    }
576
}
577
578
#[derive(Debug, Clone, PartialEq)]
579
pub 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)]
593
pub enum RenderingIntent {
594
    AbsoluteColorimetric = 3,
595
    Saturation = 2,
596
    RelativeColorimetric = 1,
597
    #[default]
598
    Perceptual = 0,
599
}
600
601
impl TryFrom<u32> for RenderingIntent {
602
    type Error = CmsError;
603
604
    #[inline]
605
0
    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
0
        match value {
613
0
            0 => Ok(RenderingIntent::Perceptual),
614
0
            1 => Ok(RenderingIntent::RelativeColorimetric),
615
0
            2 => Ok(RenderingIntent::Saturation),
616
0
            3 => Ok(RenderingIntent::AbsoluteColorimetric),
617
0
            _ => Ok(RenderingIntent::Perceptual),
618
        }
619
0
    }
620
}
621
622
impl From<RenderingIntent> for u32 {
623
    #[inline]
624
0
    fn from(value: RenderingIntent) -> Self {
625
0
        match value {
626
0
            RenderingIntent::AbsoluteColorimetric => 3,
627
0
            RenderingIntent::Saturation => 2,
628
0
            RenderingIntent::RelativeColorimetric => 1,
629
0
            RenderingIntent::Perceptual => 0,
630
        }
631
0
    }
632
}
633
634
/// ICC Header
635
#[repr(C)]
636
#[derive(Debug, Clone, Copy)]
637
pub(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
659
impl ProfileHeader {
660
    #[allow(dead_code)]
661
0
    pub(crate) fn new(size: u32) -> Self {
662
0
        Self {
663
0
            size,
664
0
            cmm_type: 0,
665
0
            version: ProfileVersion::V4_3,
666
0
            profile_class: ProfileClass::DisplayDevice,
667
0
            data_color_space: DataColorSpace::Rgb,
668
0
            pcs: DataColorSpace::Xyz,
669
0
            creation_date_time: ColorDateTime::default(),
670
0
            signature: ProfileSignature::Acsp,
671
0
            platform: 0,
672
0
            flags: 0x00000000,
673
0
            device_manufacturer: 0,
674
0
            device_model: 0,
675
0
            device_attributes: [0; 8],
676
0
            rendering_intent: RenderingIntent::Perceptual,
677
0
            illuminant: Chromaticity::D50.to_xyz(),
678
0
            creator: 0,
679
0
            profile_id: [0; 16],
680
0
            reserved: [0; 28],
681
0
            tag_count: 0,
682
0
        }
683
0
    }
684
685
    /// Creates profile from the buffer
686
0
    pub(crate) fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> {
687
0
        if slice.len() < size_of::<ProfileHeader>() {
688
0
            return Err(CmsError::InvalidProfile);
689
0
        }
690
0
        let mut cursor = std::io::Cursor::new(slice);
691
0
        let mut buffer = [0u8; size_of::<ProfileHeader>()];
692
0
        cursor
693
0
            .read_exact(&mut buffer)
694
0
            .map_err(|_| CmsError::InvalidProfile)?;
695
696
0
        let header = Self {
697
0
            size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()),
698
0
            cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()),
699
0
            version: ProfileVersion::try_from(u32::from_be_bytes(
700
0
                buffer[8..12].try_into().unwrap(),
701
0
            ))?,
702
0
            profile_class: ProfileClass::try_from(u32::from_be_bytes(
703
0
                buffer[12..16].try_into().unwrap(),
704
0
            ))?,
705
0
            data_color_space: DataColorSpace::try_from(u32::from_be_bytes(
706
0
                buffer[16..20].try_into().unwrap(),
707
0
            ))?,
708
0
            pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?,
709
0
            creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?,
710
0
            signature: ProfileSignature::try_from(u32::from_be_bytes(
711
0
                buffer[36..40].try_into().unwrap(),
712
0
            ))?,
713
0
            platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()),
714
0
            flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()),
715
0
            device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()),
716
0
            device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()),
717
0
            device_attributes: buffer[56..64].try_into().unwrap(),
718
0
            rendering_intent: RenderingIntent::try_from(u32::from_be_bytes(
719
0
                buffer[64..68].try_into().unwrap(),
720
0
            ))?,
721
0
            illuminant: Xyz::new(
722
0
                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())),
723
0
                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())),
724
0
                s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())),
725
            ),
726
0
            creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()),
727
0
            profile_id: buffer[84..100].try_into().unwrap(),
728
0
            reserved: buffer[100..128].try_into().unwrap(),
729
0
            tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()),
730
        };
731
0
        Ok(header)
732
0
    }
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)]
738
pub 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)]
746
pub 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
754
impl 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
0
    pub fn new(language: String, country: String, value: String) -> Self {
764
0
        Self {
765
0
            language,
766
0
            country,
767
0
            value,
768
0
        }
769
0
    }
770
}
771
772
#[derive(Debug, Clone)]
773
pub 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)]
782
pub enum ProfileText {
783
    PlainString(String),
784
    Localizable(Vec<LocalizableString>),
785
    Description(DescriptionString),
786
}
787
788
impl ProfileText {
789
0
    pub(crate) fn has_values(&self) -> bool {
790
0
        match self {
791
0
            ProfileText::PlainString(_) => true,
792
0
            ProfileText::Localizable(lc) => !lc.is_empty(),
793
0
            ProfileText::Description(_) => true,
794
        }
795
0
    }
796
}
797
798
#[derive(Debug, Clone, Copy)]
799
pub enum StandardObserver {
800
    D50,
801
    D65,
802
    Unknown,
803
}
804
805
impl From<u32> for StandardObserver {
806
0
    fn from(value: u32) -> Self {
807
0
        if value == 1 {
808
0
            return StandardObserver::D50;
809
0
        } else if value == 2 {
810
0
            return StandardObserver::D65;
811
0
        }
812
0
        StandardObserver::Unknown
813
0
    }
814
}
815
816
impl From<StandardObserver> for u32 {
817
0
    fn from(value: StandardObserver) -> Self {
818
0
        match value {
819
0
            StandardObserver::D50 => 1,
820
0
            StandardObserver::D65 => 2,
821
0
            StandardObserver::Unknown => 0,
822
        }
823
0
    }
824
}
825
826
#[derive(Debug, Clone, Copy)]
827
pub struct ViewingConditions {
828
    pub illuminant: Xyz,
829
    pub surround: Xyz,
830
    pub observer: StandardObserver,
831
}
832
833
#[derive(Debug, Clone, Copy)]
834
pub enum MeasurementGeometry {
835
    Unknown,
836
    /// 0°:45° or 45°:0°
837
    D45to45,
838
    /// 0°:d or d:0°
839
    D0to0,
840
}
841
842
impl From<u32> for MeasurementGeometry {
843
0
    fn from(value: u32) -> Self {
844
0
        if value == 1 {
845
0
            Self::D45to45
846
0
        } else if value == 2 {
847
0
            Self::D0to0
848
        } else {
849
0
            Self::Unknown
850
        }
851
0
    }
852
}
853
854
#[derive(Debug, Clone, Copy)]
855
pub enum StandardIlluminant {
856
    Unknown,
857
    D50,
858
    D65,
859
    D93,
860
    F2,
861
    D55,
862
    A,
863
    EquiPower,
864
    F8,
865
}
866
867
impl From<u32> for StandardIlluminant {
868
0
    fn from(value: u32) -> Self {
869
0
        match value {
870
0
            1 => StandardIlluminant::D50,
871
0
            2 => StandardIlluminant::D65,
872
0
            3 => StandardIlluminant::D93,
873
0
            4 => StandardIlluminant::F2,
874
0
            5 => StandardIlluminant::D55,
875
0
            6 => StandardIlluminant::A,
876
0
            7 => StandardIlluminant::EquiPower,
877
0
            8 => StandardIlluminant::F8,
878
0
            _ => Self::Unknown,
879
        }
880
0
    }
881
}
882
883
impl From<StandardIlluminant> for u32 {
884
0
    fn from(value: StandardIlluminant) -> Self {
885
0
        match value {
886
0
            StandardIlluminant::Unknown => 0u32,
887
0
            StandardIlluminant::D50 => 1u32,
888
0
            StandardIlluminant::D65 => 2u32,
889
0
            StandardIlluminant::D93 => 3,
890
0
            StandardIlluminant::F2 => 4,
891
0
            StandardIlluminant::D55 => 5,
892
0
            StandardIlluminant::A => 6,
893
0
            StandardIlluminant::EquiPower => 7,
894
0
            StandardIlluminant::F8 => 8,
895
        }
896
0
    }
897
}
898
899
#[derive(Debug, Clone, Copy)]
900
pub 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)]
911
pub 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)]
953
pub 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
962
impl Default for ParsingOptions {
963
0
    fn default() -> Self {
964
0
        Self {
965
0
            max_profile_size: MAX_PROFILE_SIZE,
966
0
            max_allowed_clut_size: 10_000_000,
967
0
            max_allowed_trc_size: 40_000,
968
0
        }
969
0
    }
970
}
971
972
impl ColorProfile {
973
    /// Returns profile version
974
0
    pub fn version(&self) -> ProfileVersion {
975
0
        self.version_internal
976
0
    }
977
978
0
    pub fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> {
979
0
        Self::new_from_slice_with_options(slice, Default::default())
980
0
    }
981
982
0
    pub fn new_from_slice_with_options(
983
0
        slice: &[u8],
984
0
        options: ParsingOptions,
985
0
    ) -> Result<Self, CmsError> {
986
0
        let header = ProfileHeader::new_from_slice(slice)?;
987
0
        let tags_count = header.tag_count as usize;
988
0
        if slice.len() >= options.max_profile_size {
989
0
            return Err(CmsError::InvalidProfile);
990
0
        }
991
0
        let tags_end = tags_count
992
0
            .safe_mul(TAG_SIZE)?
993
0
            .safe_add(size_of::<ProfileHeader>())?;
994
0
        if slice.len() < tags_end {
995
0
            return Err(CmsError::InvalidProfile);
996
0
        }
997
0
        let tags_slice = &slice[size_of::<ProfileHeader>()..tags_end];
998
0
        let mut profile = ColorProfile {
999
0
            rendering_intent: header.rendering_intent,
1000
0
            pcs: header.pcs,
1001
0
            profile_class: header.profile_class,
1002
0
            color_space: header.data_color_space,
1003
0
            white_point: header.illuminant.to_xyzd(),
1004
0
            version_internal: header.version,
1005
0
            creation_date_time: header.creation_date_time,
1006
0
            ..Default::default()
1007
0
        };
1008
0
        let color_space = profile.color_space;
1009
0
        for tag in tags_slice.chunks_exact(TAG_SIZE) {
1010
0
            let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
1011
0
            let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]);
1012
0
            let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize;
1013
            // Just ignore unknown tags
1014
0
            if let Ok(tag) = Tag::try_from(tag_value) {
1015
0
                match tag {
1016
                    Tag::RedXyz => {
1017
0
                        if color_space == DataColorSpace::Rgb {
1018
                            profile.red_colorant =
1019
0
                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
1020
0
                        }
1021
                    }
1022
                    Tag::GreenXyz => {
1023
0
                        if color_space == DataColorSpace::Rgb {
1024
                            profile.green_colorant =
1025
0
                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
1026
0
                        }
1027
                    }
1028
                    Tag::BlueXyz => {
1029
0
                        if color_space == DataColorSpace::Rgb {
1030
                            profile.blue_colorant =
1031
0
                                Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?;
1032
0
                        }
1033
                    }
1034
                    Tag::RedToneReproduction => {
1035
0
                        if color_space == DataColorSpace::Rgb {
1036
0
                            profile.red_trc = Self::read_trc_tag_s(
1037
0
                                slice,
1038
0
                                tag_entry as usize,
1039
0
                                tag_size,
1040
0
                                &options,
1041
0
                            )?;
1042
0
                        }
1043
                    }
1044
                    Tag::GreenToneReproduction => {
1045
0
                        if color_space == DataColorSpace::Rgb {
1046
0
                            profile.green_trc = Self::read_trc_tag_s(
1047
0
                                slice,
1048
0
                                tag_entry as usize,
1049
0
                                tag_size,
1050
0
                                &options,
1051
0
                            )?;
1052
0
                        }
1053
                    }
1054
                    Tag::BlueToneReproduction => {
1055
0
                        if color_space == DataColorSpace::Rgb {
1056
0
                            profile.blue_trc = Self::read_trc_tag_s(
1057
0
                                slice,
1058
0
                                tag_entry as usize,
1059
0
                                tag_size,
1060
0
                                &options,
1061
0
                            )?;
1062
0
                        }
1063
                    }
1064
                    Tag::GreyToneReproduction => {
1065
0
                        if color_space == DataColorSpace::Gray {
1066
0
                            profile.gray_trc = Self::read_trc_tag_s(
1067
0
                                slice,
1068
0
                                tag_entry as usize,
1069
0
                                tag_size,
1070
0
                                &options,
1071
0
                            )?;
1072
0
                        }
1073
                    }
1074
                    Tag::MediaWhitePoint => {
1075
                        profile.media_white_point =
1076
0
                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?;
1077
                    }
1078
                    Tag::Luminance => {
1079
                        profile.luminance =
1080
0
                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?;
1081
                    }
1082
                    Tag::Measurement => {
1083
                        profile.measurement =
1084
0
                            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
0
                        if (profile.profile_class == ProfileClass::InputDevice
1091
0
                            || profile.profile_class == ProfileClass::DisplayDevice)
1092
0
                            && (profile.color_space == DataColorSpace::Rgb
1093
0
                                || profile.color_space == DataColorSpace::YCbr
1094
0
                                || profile.color_space == DataColorSpace::Xyz)
1095
                        {
1096
                            profile.cicp =
1097
0
                                Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?;
1098
0
                        }
1099
                    }
1100
                    Tag::ChromaticAdaptation => {
1101
                        profile.chromatic_adaptation =
1102
0
                            Self::read_chad_tag(slice, tag_entry as usize, tag_size)?;
1103
                    }
1104
                    Tag::BlackPoint => {
1105
                        profile.black_point =
1106
0
                            Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?
1107
                    }
1108
                    Tag::DeviceToPcsLutPerceptual => {
1109
0
                        profile.lut_a_to_b_perceptual =
1110
0
                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1111
                    }
1112
                    Tag::DeviceToPcsLutColorimetric => {
1113
0
                        profile.lut_a_to_b_colorimetric =
1114
0
                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1115
                    }
1116
                    Tag::DeviceToPcsLutSaturation => {
1117
0
                        profile.lut_a_to_b_saturation =
1118
0
                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1119
                    }
1120
                    Tag::PcsToDeviceLutPerceptual => {
1121
0
                        profile.lut_b_to_a_perceptual =
1122
0
                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1123
                    }
1124
                    Tag::PcsToDeviceLutColorimetric => {
1125
0
                        profile.lut_b_to_a_colorimetric =
1126
0
                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1127
                    }
1128
                    Tag::PcsToDeviceLutSaturation => {
1129
0
                        profile.lut_b_to_a_saturation =
1130
0
                            Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1131
                    }
1132
                    Tag::Gamut => {
1133
0
                        profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?;
1134
                    }
1135
                    Tag::Copyright => {
1136
0
                        profile.copyright =
1137
0
                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1138
                    }
1139
                    Tag::ProfileDescription => {
1140
0
                        profile.description =
1141
0
                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1142
                    }
1143
                    Tag::ViewingConditionsDescription => {
1144
0
                        profile.viewing_conditions_description =
1145
0
                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1146
                    }
1147
                    Tag::DeviceModel => {
1148
0
                        profile.device_model =
1149
0
                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1150
                    }
1151
                    Tag::DeviceManufacturer => {
1152
0
                        profile.device_manufacturer =
1153
0
                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1154
                    }
1155
                    Tag::CharTarget => {
1156
0
                        profile.char_target =
1157
0
                            Self::read_string_tag(slice, tag_entry as usize, tag_size)?;
1158
                    }
1159
0
                    Tag::Chromaticity => {}
1160
                    Tag::ObserverConditions => {
1161
                        profile.viewing_conditions =
1162
0
                            Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?;
1163
                    }
1164
                    Tag::Technology => {
1165
                        profile.technology =
1166
0
                            Self::read_tech_tag(slice, tag_entry as usize, tag_size)?;
1167
                    }
1168
                    Tag::CalibrationDateTime => {
1169
                        profile.calibration_date =
1170
0
                            Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?;
1171
                    }
1172
                }
1173
0
            }
1174
        }
1175
1176
0
        Ok(profile)
1177
0
    }
1178
}
1179
1180
impl ColorProfile {
1181
    #[inline]
1182
0
    pub fn colorant_matrix(&self) -> Matrix3d {
1183
0
        Matrix3d {
1184
0
            v: [
1185
0
                [
1186
0
                    self.red_colorant.x,
1187
0
                    self.green_colorant.x,
1188
0
                    self.blue_colorant.x,
1189
0
                ],
1190
0
                [
1191
0
                    self.red_colorant.y,
1192
0
                    self.green_colorant.y,
1193
0
                    self.blue_colorant.y,
1194
0
                ],
1195
0
                [
1196
0
                    self.red_colorant.z,
1197
0
                    self.green_colorant.z,
1198
0
                    self.blue_colorant.z,
1199
0
                ],
1200
0
            ],
1201
0
        }
1202
0
    }
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
0
    pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d {
1209
0
        let red_xyz = primaries.red.to_xyzd();
1210
0
        let green_xyz = primaries.green.to_xyzd();
1211
0
        let blue_xyz = primaries.blue.to_xyzd();
1212
1213
0
        let xyz_matrix = Matrix3d {
1214
0
            v: [
1215
0
                [red_xyz.x, green_xyz.x, blue_xyz.x],
1216
0
                [red_xyz.y, green_xyz.y, blue_xyz.y],
1217
0
                [red_xyz.z, green_xyz.z, blue_xyz.z],
1218
0
            ],
1219
0
        };
1220
0
        let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd());
1221
0
        adapt_to_d50_d(colorants, white_point)
1222
0
    }
1223
1224
    /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point
1225
    /// This will nullify CICP.
1226
0
    pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) {
1227
0
        self.cicp = None;
1228
0
        let red_xyz = primaries.red.to_xyzd();
1229
0
        let green_xyz = primaries.green.to_xyzd();
1230
0
        let blue_xyz = primaries.blue.to_xyzd();
1231
1232
0
        self.chromatic_adaptation = Some(BRADFORD_D);
1233
0
        self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz)
1234
0
    }
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
0
    pub const fn update_rgb_colorimetry_triplet(
1243
0
        &mut self,
1244
0
        white_point: XyY,
1245
0
        red_xyz: Xyzd,
1246
0
        green_xyz: Xyzd,
1247
0
        blue_xyz: Xyzd,
1248
0
    ) {
1249
0
        self.cicp = None;
1250
0
        let xyz_matrix = Matrix3d {
1251
0
            v: [
1252
0
                [red_xyz.x, green_xyz.x, blue_xyz.x],
1253
0
                [red_xyz.y, green_xyz.y, blue_xyz.y],
1254
0
                [red_xyz.z, green_xyz.z, blue_xyz.z],
1255
0
            ],
1256
0
        };
1257
0
        let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd());
1258
0
        let colorants = adapt_to_d50_d(colorants, white_point);
1259
1260
0
        self.update_colorants(colorants);
1261
0
    }
1262
1263
0
    pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) {
1264
        // note: there's a transpose type of operation going on here
1265
0
        self.red_colorant.x = colorants.v[0][0];
1266
0
        self.red_colorant.y = colorants.v[1][0];
1267
0
        self.red_colorant.z = colorants.v[2][0];
1268
0
        self.green_colorant.x = colorants.v[0][1];
1269
0
        self.green_colorant.y = colorants.v[1][1];
1270
0
        self.green_colorant.z = colorants.v[2][1];
1271
0
        self.blue_colorant.x = colorants.v[0][2];
1272
0
        self.blue_colorant.y = colorants.v[1][2];
1273
0
        self.blue_colorant.z = colorants.v[2][2];
1274
0
    }
1275
1276
    /// Updates RGB triple colorimetry from CICP
1277
0
    pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool {
1278
0
        if !cicp.color_primaries.has_chromaticity()
1279
0
            || !cicp.transfer_characteristics.has_transfer_curve()
1280
        {
1281
0
            return false;
1282
0
        }
1283
0
        let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() {
1284
0
            Ok(primaries) => primaries,
1285
0
            Err(_) => return false,
1286
        };
1287
0
        let white_point: Chromaticity = match cicp.color_primaries.white_point() {
1288
0
            Ok(v) => v,
1289
0
            Err(_) => return false,
1290
        };
1291
0
        self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy);
1292
0
        self.cicp = Some(cicp);
1293
1294
0
        let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() {
1295
0
            Ok(trc) => trc,
1296
0
            Err(_) => return false,
1297
        };
1298
0
        self.green_trc = Some(red_trc.clone());
1299
0
        self.blue_trc = Some(red_trc.clone());
1300
0
        self.red_trc = Some(red_trc);
1301
0
        false
1302
0
    }
1303
1304
0
    pub const fn rgb_to_xyz(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f {
1305
0
        let xyz_inverse = xyz_matrix.inverse();
1306
0
        let s = xyz_inverse.mul_vector(wp.to_vector());
1307
0
        let mut v = xyz_matrix.mul_row_vector::<0>(s);
1308
0
        v = v.mul_row_vector::<1>(s);
1309
0
        v.mul_row_vector::<2>(s)
1310
0
    }
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
0
    pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d {
1315
0
        let xyz_inverse = xyz_matrix.inverse();
1316
0
        let s = xyz_inverse.mul_vector(wp.to_vector_d());
1317
0
        let mut v = xyz_matrix.mul_row_vector::<0>(s);
1318
0
        v = v.mul_row_vector::<1>(s);
1319
0
        v = v.mul_row_vector::<2>(s);
1320
0
        v
1321
0
    }
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
0
    pub fn rgb_to_xyz_matrix(&self) -> Matrix3d {
1331
0
        self.colorant_matrix()
1332
0
    }
1333
1334
    /// Computes transform matrix RGB -> XYZ -> RGB
1335
    /// Current profile is used as source, other as destination
1336
0
    pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d {
1337
0
        let source = self.rgb_to_xyz_matrix();
1338
0
        let dst = dest.rgb_to_xyz_matrix();
1339
0
        let dest_inverse = dst.inverse();
1340
0
        dest_inverse.mat_mul(source)
1341
0
    }
1342
1343
    /// Returns volume of colors stored in profile
1344
0
    pub fn profile_volume(&self) -> Option<f32> {
1345
0
        let red_prim = self.red_colorant;
1346
0
        let green_prim = self.green_colorant;
1347
0
        let blue_prim = self.blue_colorant;
1348
0
        let tetrahedral_vertices = Matrix3d {
1349
0
            v: [
1350
0
                [red_prim.x, red_prim.y, red_prim.z],
1351
0
                [green_prim.x, green_prim.y, green_prim.z],
1352
0
                [blue_prim.x, blue_prim.y, blue_prim.z],
1353
0
            ],
1354
0
        };
1355
0
        let det = tetrahedral_vertices.determinant()?;
1356
0
        Some((det / 6.0f64) as f32)
1357
0
    }
1358
1359
    #[allow(unused)]
1360
0
    pub(crate) fn has_device_to_pcs_lut(&self) -> bool {
1361
0
        self.lut_a_to_b_perceptual.is_some()
1362
0
            || self.lut_a_to_b_saturation.is_some()
1363
0
            || self.lut_a_to_b_colorimetric.is_some()
1364
0
    }
1365
1366
    #[allow(unused)]
1367
0
    pub(crate) fn has_pcs_to_device_lut(&self) -> bool {
1368
0
        self.lut_b_to_a_perceptual.is_some()
1369
0
            || self.lut_b_to_a_saturation.is_some()
1370
0
            || self.lut_b_to_a_colorimetric.is_some()
1371
0
    }
1372
}
1373
1374
#[cfg(test)]
1375
mod 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
}