1use 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; #[repr(u32)]
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ProfileSignature {
48 Acsp,
49}
50
51impl TryFrom<u32> for ProfileSignature {
52 type Error = CmsError;
53 #[inline]
54 fn try_from(value: u32) -> Result<Self, Self::Error> {
55 if value == u32::from_ne_bytes(*b"acsp").to_be() {
56 return Ok(ProfileSignature::Acsp);
57 }
58 Err(CmsError::InvalidProfile)
59 }
60}
61
62impl From<ProfileSignature> for u32 {
63 #[inline]
64 fn from(value: ProfileSignature) -> Self {
65 match value {
66 ProfileSignature::Acsp => u32::from_ne_bytes(*b"acsp").to_be(),
67 }
68 }
69}
70
71#[repr(u32)]
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)]
73pub enum ProfileVersion {
74 V2_0 = 0x02000000,
75 V2_1 = 0x02100000,
76 V2_2 = 0x02200000,
77 V2_3 = 0x02300000,
78 V2_4 = 0x02400000,
79 V4_0 = 0x04000000,
80 V4_1 = 0x04100000,
81 V4_2 = 0x04200000,
82 V4_3 = 0x04300000,
83 #[default]
84 V4_4 = 0x04400000,
85 Unknown,
86}
87
88impl TryFrom<u32> for ProfileVersion {
89 type Error = CmsError;
90 fn try_from(value: u32) -> Result<Self, Self::Error> {
91 match value {
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 let major = (value >> 24) & 0xFF;
109 let minor = (value >> 20) & 0x0F;
110
111 match major {
114 0 => {
115 Err(CmsError::InvalidProfile)
117 }
118 2 => {
119 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), }
127 }
128 3 => {
129 Ok(ProfileVersion::V2_4)
131 }
132 4 => {
133 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), }
141 }
142 _ => {
143 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, }
543 }
544}
545
546#[derive(Debug, Clone, PartialEq)]
547pub struct LutDataType {
548 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 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#[repr(C)]
636#[derive(Debug, Clone, Copy)]
637pub(crate) struct ProfileHeader {
638 pub size: u32, pub cmm_type: u32, pub version: ProfileVersion, pub profile_class: ProfileClass, pub data_color_space: DataColorSpace, pub pcs: DataColorSpace, pub creation_date_time: ColorDateTime, pub signature: ProfileSignature, pub platform: u32, pub flags: u32, pub device_manufacturer: u32, pub device_model: u32, pub device_attributes: [u8; 8], pub rendering_intent: RenderingIntent, pub illuminant: Xyz, pub creator: u32, pub profile_id: [u8; 16], pub reserved: [u8; 28], pub tag_count: u32, }
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 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#[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 pub language: String,
749 pub country: String,
751 pub value: String,
752}
753
754impl LocalizableString {
755 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 D45to45,
838 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#[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 pub(crate) version_internal: ProfileVersion,
950}
951
952#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)]
953pub struct ParsingOptions {
954 pub max_profile_size: usize,
956 pub max_allowed_clut_size: usize,
958 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 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 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 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 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 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 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 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 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 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 pub fn rgb_to_xyz_matrix(&self) -> Matrix3d {
1331 self.colorant_matrix()
1332 }
1333
1334 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 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 #[test]
1446 fn test_rgb_to_xyz_matrix_equals_colorant_matrix() {
1447 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 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 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 assert!(
1506 ProfileVersion::try_from(0x02020000).is_ok(),
1507 "v2.0.2 should be accepted"
1508 );
1509
1510 assert!(
1512 ProfileVersion::try_from(0x03400000).is_ok(),
1513 "v3.4 should be accepted"
1514 );
1515
1516 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 assert!(
1529 ProfileVersion::try_from(0x00000000).is_err(),
1530 "v0.0 should be rejected"
1531 );
1532
1533 assert!(
1535 ProfileVersion::try_from(0x05000000).is_err(),
1536 "v5.0 should be rejected"
1537 );
1538
1539 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 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 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 #[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 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 #[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 #[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}