/* * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. * // * // Redistribution and use in source and binary forms, with or without modification, * // are permitted provided that the following conditions are met: * // * // 1. Redistributions of source code must retain the above copyright notice, this * // list of conditions and the following disclaimer. * // * // 2. Redistributions in binary form must reproduce the above copyright notice, * // this list of conditions and the following disclaimer in the documentation * // and/or other materials provided with the distribution. * // * // 3. Neither the name of the copyright holder nor the names of its * // contributors may be used to endorse or promote products derived from * // this software without specific prior written permission. * // * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ use crate::chad::BRADFORD_D; use crate::cicp::{ CicpColorPrimaries, ColorPrimaries, MatrixCoefficients, TransferCharacteristics, }; use crate::dat::ColorDateTime; use crate::err::CmsError; use crate::matrix::{Matrix3f, Xyz}; use crate::reader::s15_fixed16_number_to_float; use crate::safe_math::{SafeAdd, SafeMul}; use crate::tag::{TAG_SIZE, Tag}; use crate::trc::ToneReprCurve; use crate::{Chromaticity, Layout, Matrix3d, Vector3d, XyY, Xyzd, adapt_to_d50_d}; use std::io::Read; const MAX_PROFILE_SIZE: usize = 1024 * 1024 * 10; // 10 MB max, for Fogra39 etc #[repr(u32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProfileSignature { Acsp, } impl TryFrom for ProfileSignature { type Error = CmsError; #[inline] fn try_from(value: u32) -> Result { if value == u32::from_ne_bytes(*b"acsp").to_be() { return Ok(ProfileSignature::Acsp); } Err(CmsError::InvalidProfile) } } impl From for u32 { #[inline] fn from(value: ProfileSignature) -> Self { match value { ProfileSignature::Acsp => u32::from_ne_bytes(*b"acsp").to_be(), } } } #[repr(u32)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)] pub enum ProfileVersion { V2_0 = 0x02000000, V2_1 = 0x02100000, V2_2 = 0x02200000, V2_3 = 0x02300000, V2_4 = 0x02400000, V4_0 = 0x04000000, V4_1 = 0x04100000, V4_2 = 0x04200000, V4_3 = 0x04300000, #[default] V4_4 = 0x04400000, Unknown, } impl TryFrom for ProfileVersion { type Error = CmsError; fn try_from(value: u32) -> Result { match value { 0x02000000 => Ok(ProfileVersion::V2_0), 0x02100000 => Ok(ProfileVersion::V2_1), 0x02200000 => Ok(ProfileVersion::V2_2), 0x02300000 => Ok(ProfileVersion::V2_3), 0x02400000 => Ok(ProfileVersion::V2_4), 0x04000000 => Ok(ProfileVersion::V4_0), 0x04100000 => Ok(ProfileVersion::V4_1), 0x04200000 => Ok(ProfileVersion::V4_2), 0x04300000 => Ok(ProfileVersion::V4_3), 0x04400000 => Ok(ProfileVersion::V4_3), _ => Err(CmsError::InvalidProfile), } } } impl From for u32 { fn from(value: ProfileVersion) -> Self { match value { ProfileVersion::V2_0 => 0x02000000, ProfileVersion::V2_1 => 0x02100000, ProfileVersion::V2_2 => 0x02200000, ProfileVersion::V2_3 => 0x02300000, ProfileVersion::V2_4 => 0x02400000, ProfileVersion::V4_0 => 0x04000000, ProfileVersion::V4_1 => 0x04100000, ProfileVersion::V4_2 => 0x04200000, ProfileVersion::V4_3 => 0x04300000, ProfileVersion::V4_4 => 0x04400000, ProfileVersion::Unknown => 0x02000000, } } } #[repr(u32)] #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Hash)] pub enum DataColorSpace { #[default] Xyz, Lab, Luv, YCbr, Yxy, Rgb, Gray, Hsv, Hls, Cmyk, Cmy, Color2, Color3, Color4, Color5, Color6, Color7, Color8, Color9, Color10, Color11, Color12, Color13, Color14, Color15, } impl DataColorSpace { #[inline] pub fn check_layout(self, layout: Layout) -> Result<(), CmsError> { let unsupported: bool = match self { DataColorSpace::Xyz => layout != Layout::Rgb, DataColorSpace::Lab => layout != Layout::Rgb, DataColorSpace::Luv => layout != Layout::Rgb, DataColorSpace::YCbr => layout != Layout::Rgb, DataColorSpace::Yxy => layout != Layout::Rgb, DataColorSpace::Rgb => layout != Layout::Rgb && layout != Layout::Rgba, DataColorSpace::Gray => layout != Layout::Gray && layout != Layout::GrayAlpha, DataColorSpace::Hsv => layout != Layout::Rgb, DataColorSpace::Hls => layout != Layout::Rgb, DataColorSpace::Cmyk => layout != Layout::Rgba, DataColorSpace::Cmy => layout != Layout::Rgb, DataColorSpace::Color2 => layout != Layout::GrayAlpha, DataColorSpace::Color3 => layout != Layout::Rgb, DataColorSpace::Color4 => layout != Layout::Rgba, DataColorSpace::Color5 => layout != Layout::Inks5, DataColorSpace::Color6 => layout != Layout::Inks6, DataColorSpace::Color7 => layout != Layout::Inks7, DataColorSpace::Color8 => layout != Layout::Inks8, DataColorSpace::Color9 => layout != Layout::Inks9, DataColorSpace::Color10 => layout != Layout::Inks10, DataColorSpace::Color11 => layout != Layout::Inks11, DataColorSpace::Color12 => layout != Layout::Inks12, DataColorSpace::Color13 => layout != Layout::Inks13, DataColorSpace::Color14 => layout != Layout::Inks14, DataColorSpace::Color15 => layout != Layout::Inks15, }; if unsupported { Err(CmsError::InvalidLayout) } else { Ok(()) } } pub(crate) fn is_three_channels(self) -> bool { matches!( self, DataColorSpace::Xyz | DataColorSpace::Lab | DataColorSpace::Luv | DataColorSpace::YCbr | DataColorSpace::Yxy | DataColorSpace::Rgb | DataColorSpace::Hsv | DataColorSpace::Hls | DataColorSpace::Cmy | DataColorSpace::Color3 ) } } #[repr(u32)] #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] pub enum ProfileClass { InputDevice, #[default] DisplayDevice, OutputDevice, DeviceLink, ColorSpace, Abstract, Named, } impl TryFrom for ProfileClass { type Error = CmsError; fn try_from(value: u32) -> Result { if value == u32::from_ne_bytes(*b"scnr").to_be() { return Ok(ProfileClass::InputDevice); } else if value == u32::from_ne_bytes(*b"mntr").to_be() { return Ok(ProfileClass::DisplayDevice); } else if value == u32::from_ne_bytes(*b"prtr").to_be() { return Ok(ProfileClass::OutputDevice); } else if value == u32::from_ne_bytes(*b"link").to_be() { return Ok(ProfileClass::DeviceLink); } else if value == u32::from_ne_bytes(*b"spac").to_be() { return Ok(ProfileClass::ColorSpace); } else if value == u32::from_ne_bytes(*b"abst").to_be() { return Ok(ProfileClass::Abstract); } else if value == u32::from_ne_bytes(*b"nmcl").to_be() { return Ok(ProfileClass::Named); } Err(CmsError::InvalidProfile) } } impl From for u32 { fn from(val: ProfileClass) -> Self { match val { ProfileClass::InputDevice => u32::from_ne_bytes(*b"scnr").to_be(), ProfileClass::DisplayDevice => u32::from_ne_bytes(*b"mntr").to_be(), ProfileClass::OutputDevice => u32::from_ne_bytes(*b"prtr").to_be(), ProfileClass::DeviceLink => u32::from_ne_bytes(*b"link").to_be(), ProfileClass::ColorSpace => u32::from_ne_bytes(*b"spac").to_be(), ProfileClass::Abstract => u32::from_ne_bytes(*b"abst").to_be(), ProfileClass::Named => u32::from_ne_bytes(*b"nmcl").to_be(), } } } #[derive(Debug, Clone)] pub enum LutStore { Store8(Vec), Store16(Vec), } #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum LutType { Lut8, Lut16, LutMab, LutMba, } impl TryFrom for LutType { type Error = CmsError; fn try_from(value: u32) -> Result { if value == u32::from_ne_bytes(*b"mft1").to_be() { return Ok(LutType::Lut8); } else if value == u32::from_ne_bytes(*b"mft2").to_be() { return Ok(LutType::Lut16); } else if value == u32::from_ne_bytes(*b"mAB ").to_be() { return Ok(LutType::LutMab); } else if value == u32::from_ne_bytes(*b"mBA ").to_be() { return Ok(LutType::LutMba); } Err(CmsError::InvalidProfile) } } impl From for u32 { fn from(val: LutType) -> Self { match val { LutType::Lut8 => u32::from_ne_bytes(*b"mft1").to_be(), LutType::Lut16 => u32::from_ne_bytes(*b"mft2").to_be(), LutType::LutMab => u32::from_ne_bytes(*b"mAB ").to_be(), LutType::LutMba => u32::from_ne_bytes(*b"mBA ").to_be(), } } } impl TryFrom for DataColorSpace { type Error = CmsError; fn try_from(value: u32) -> Result { if value == u32::from_ne_bytes(*b"XYZ ").to_be() { return Ok(DataColorSpace::Xyz); } else if value == u32::from_ne_bytes(*b"Lab ").to_be() { return Ok(DataColorSpace::Lab); } else if value == u32::from_ne_bytes(*b"Luv ").to_be() { return Ok(DataColorSpace::Luv); } else if value == u32::from_ne_bytes(*b"YCbr").to_be() { return Ok(DataColorSpace::YCbr); } else if value == u32::from_ne_bytes(*b"Yxy ").to_be() { return Ok(DataColorSpace::Yxy); } else if value == u32::from_ne_bytes(*b"RGB ").to_be() { return Ok(DataColorSpace::Rgb); } else if value == u32::from_ne_bytes(*b"GRAY").to_be() { return Ok(DataColorSpace::Gray); } else if value == u32::from_ne_bytes(*b"HSV ").to_be() { return Ok(DataColorSpace::Hsv); } else if value == u32::from_ne_bytes(*b"HLS ").to_be() { return Ok(DataColorSpace::Hls); } else if value == u32::from_ne_bytes(*b"CMYK").to_be() { return Ok(DataColorSpace::Cmyk); } else if value == u32::from_ne_bytes(*b"CMY ").to_be() { return Ok(DataColorSpace::Cmy); } else if value == u32::from_ne_bytes(*b"2CLR").to_be() { return Ok(DataColorSpace::Color2); } else if value == u32::from_ne_bytes(*b"3CLR").to_be() { return Ok(DataColorSpace::Color3); } else if value == u32::from_ne_bytes(*b"4CLR").to_be() { return Ok(DataColorSpace::Color4); } else if value == u32::from_ne_bytes(*b"5CLR").to_be() { return Ok(DataColorSpace::Color5); } else if value == u32::from_ne_bytes(*b"6CLR").to_be() { return Ok(DataColorSpace::Color6); } else if value == u32::from_ne_bytes(*b"7CLR").to_be() { return Ok(DataColorSpace::Color7); } else if value == u32::from_ne_bytes(*b"8CLR").to_be() { return Ok(DataColorSpace::Color8); } else if value == u32::from_ne_bytes(*b"9CLR").to_be() { return Ok(DataColorSpace::Color9); } else if value == u32::from_ne_bytes(*b"ACLR").to_be() { return Ok(DataColorSpace::Color10); } else if value == u32::from_ne_bytes(*b"BCLR").to_be() { return Ok(DataColorSpace::Color11); } else if value == u32::from_ne_bytes(*b"CCLR").to_be() { return Ok(DataColorSpace::Color12); } else if value == u32::from_ne_bytes(*b"DCLR").to_be() { return Ok(DataColorSpace::Color13); } else if value == u32::from_ne_bytes(*b"ECLR").to_be() { return Ok(DataColorSpace::Color14); } else if value == u32::from_ne_bytes(*b"FCLR").to_be() { return Ok(DataColorSpace::Color15); } Err(CmsError::InvalidProfile) } } impl From for u32 { fn from(val: DataColorSpace) -> Self { match val { DataColorSpace::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(), DataColorSpace::Lab => u32::from_ne_bytes(*b"Lab ").to_be(), DataColorSpace::Luv => u32::from_ne_bytes(*b"Luv ").to_be(), DataColorSpace::YCbr => u32::from_ne_bytes(*b"YCbr").to_be(), DataColorSpace::Yxy => u32::from_ne_bytes(*b"Yxy ").to_be(), DataColorSpace::Rgb => u32::from_ne_bytes(*b"RGB ").to_be(), DataColorSpace::Gray => u32::from_ne_bytes(*b"GRAY").to_be(), DataColorSpace::Hsv => u32::from_ne_bytes(*b"HSV ").to_be(), DataColorSpace::Hls => u32::from_ne_bytes(*b"HLS ").to_be(), DataColorSpace::Cmyk => u32::from_ne_bytes(*b"CMYK").to_be(), DataColorSpace::Cmy => u32::from_ne_bytes(*b"CMY ").to_be(), DataColorSpace::Color2 => u32::from_ne_bytes(*b"2CLR").to_be(), DataColorSpace::Color3 => u32::from_ne_bytes(*b"3CLR").to_be(), DataColorSpace::Color4 => u32::from_ne_bytes(*b"4CLR").to_be(), DataColorSpace::Color5 => u32::from_ne_bytes(*b"5CLR").to_be(), DataColorSpace::Color6 => u32::from_ne_bytes(*b"6CLR").to_be(), DataColorSpace::Color7 => u32::from_ne_bytes(*b"7CLR").to_be(), DataColorSpace::Color8 => u32::from_ne_bytes(*b"8CLR").to_be(), DataColorSpace::Color9 => u32::from_ne_bytes(*b"9CLR").to_be(), DataColorSpace::Color10 => u32::from_ne_bytes(*b"ACLR").to_be(), DataColorSpace::Color11 => u32::from_ne_bytes(*b"BCLR").to_be(), DataColorSpace::Color12 => u32::from_ne_bytes(*b"CCLR").to_be(), DataColorSpace::Color13 => u32::from_ne_bytes(*b"DCLR").to_be(), DataColorSpace::Color14 => u32::from_ne_bytes(*b"ECLR").to_be(), DataColorSpace::Color15 => u32::from_ne_bytes(*b"FCLR").to_be(), } } } #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub enum TechnologySignatures { FilmScanner, DigitalCamera, ReflectiveScanner, InkJetPrinter, ThermalWaxPrinter, ElectrophotographicPrinter, ElectrostaticPrinter, DyeSublimationPrinter, PhotographicPaperPrinter, FilmWriter, VideoMonitor, VideoCamera, ProjectionTelevision, CathodeRayTubeDisplay, PassiveMatrixDisplay, ActiveMatrixDisplay, LiquidCrystalDisplay, OrganicLedDisplay, PhotoCd, PhotographicImageSetter, Gravure, OffsetLithography, Silkscreen, Flexography, MotionPictureFilmScanner, MotionPictureFilmRecorder, DigitalMotionPictureCamera, DigitalCinemaProjector, Unknown(u32), } impl From for TechnologySignatures { fn from(value: u32) -> Self { if value == u32::from_ne_bytes(*b"fscn").to_be() { return TechnologySignatures::FilmScanner; } else if value == u32::from_ne_bytes(*b"dcam").to_be() { return TechnologySignatures::DigitalCamera; } else if value == u32::from_ne_bytes(*b"rscn").to_be() { return TechnologySignatures::ReflectiveScanner; } else if value == u32::from_ne_bytes(*b"ijet").to_be() { return TechnologySignatures::InkJetPrinter; } else if value == u32::from_ne_bytes(*b"twax").to_be() { return TechnologySignatures::ThermalWaxPrinter; } else if value == u32::from_ne_bytes(*b"epho").to_be() { return TechnologySignatures::ElectrophotographicPrinter; } else if value == u32::from_ne_bytes(*b"esta").to_be() { return TechnologySignatures::ElectrostaticPrinter; } else if value == u32::from_ne_bytes(*b"dsub").to_be() { return TechnologySignatures::DyeSublimationPrinter; } else if value == u32::from_ne_bytes(*b"rpho").to_be() { return TechnologySignatures::PhotographicPaperPrinter; } else if value == u32::from_ne_bytes(*b"fprn").to_be() { return TechnologySignatures::FilmWriter; } else if value == u32::from_ne_bytes(*b"vidm").to_be() { return TechnologySignatures::VideoMonitor; } else if value == u32::from_ne_bytes(*b"vidc").to_be() { return TechnologySignatures::VideoCamera; } else if value == u32::from_ne_bytes(*b"pjtv").to_be() { return TechnologySignatures::ProjectionTelevision; } else if value == u32::from_ne_bytes(*b"CRT ").to_be() { return TechnologySignatures::CathodeRayTubeDisplay; } else if value == u32::from_ne_bytes(*b"PMD ").to_be() { return TechnologySignatures::PassiveMatrixDisplay; } else if value == u32::from_ne_bytes(*b"AMD ").to_be() { return TechnologySignatures::ActiveMatrixDisplay; } else if value == u32::from_ne_bytes(*b"LCD ").to_be() { return TechnologySignatures::LiquidCrystalDisplay; } else if value == u32::from_ne_bytes(*b"OLED").to_be() { return TechnologySignatures::OrganicLedDisplay; } else if value == u32::from_ne_bytes(*b"KPCD").to_be() { return TechnologySignatures::PhotoCd; } else if value == u32::from_ne_bytes(*b"imgs").to_be() { return TechnologySignatures::PhotographicImageSetter; } else if value == u32::from_ne_bytes(*b"grav").to_be() { return TechnologySignatures::Gravure; } else if value == u32::from_ne_bytes(*b"offs").to_be() { return TechnologySignatures::OffsetLithography; } else if value == u32::from_ne_bytes(*b"silk").to_be() { return TechnologySignatures::Silkscreen; } else if value == u32::from_ne_bytes(*b"flex").to_be() { return TechnologySignatures::Flexography; } else if value == u32::from_ne_bytes(*b"mpfs").to_be() { return TechnologySignatures::MotionPictureFilmScanner; } else if value == u32::from_ne_bytes(*b"mpfr").to_be() { return TechnologySignatures::MotionPictureFilmRecorder; } else if value == u32::from_ne_bytes(*b"dmpc").to_be() { return TechnologySignatures::DigitalMotionPictureCamera; } else if value == u32::from_ne_bytes(*b"dcpj").to_be() { return TechnologySignatures::DigitalCinemaProjector; } TechnologySignatures::Unknown(value) } } #[derive(Debug, Clone)] pub enum LutWarehouse { Lut(LutDataType), Multidimensional(LutMultidimensionalType), } #[derive(Debug, Clone)] pub struct LutDataType { // used by lut8Type/lut16Type (mft2) only pub num_input_channels: u8, pub num_output_channels: u8, pub num_clut_grid_points: u8, pub matrix: Matrix3d, pub num_input_table_entries: u16, pub num_output_table_entries: u16, pub input_table: LutStore, pub clut_table: LutStore, pub output_table: LutStore, pub lut_type: LutType, } impl LutDataType { pub(crate) fn has_same_kind(&self) -> bool { matches!( (&self.input_table, &self.clut_table, &self.output_table), ( LutStore::Store8(_), LutStore::Store8(_), LutStore::Store8(_) ) | ( LutStore::Store16(_), LutStore::Store16(_), LutStore::Store16(_) ) ) } } #[derive(Debug, Clone)] pub struct LutMultidimensionalType { pub num_input_channels: u8, pub num_output_channels: u8, pub grid_points: [u8; 16], pub clut: Option, pub a_curves: Vec, pub b_curves: Vec, pub m_curves: Vec, pub matrix: Matrix3d, pub bias: Vector3d, } #[repr(u32)] #[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)] pub enum RenderingIntent { AbsoluteColorimetric = 3, Saturation = 2, RelativeColorimetric = 1, #[default] Perceptual = 0, } impl TryFrom for RenderingIntent { type Error = CmsError; #[inline] fn try_from(value: u32) -> Result { match value { 0 => Ok(RenderingIntent::Perceptual), 1 => Ok(RenderingIntent::RelativeColorimetric), 2 => Ok(RenderingIntent::Saturation), 3 => Ok(RenderingIntent::AbsoluteColorimetric), _ => Err(CmsError::InvalidRenderingIntent), } } } impl From for u32 { #[inline] fn from(value: RenderingIntent) -> Self { match value { RenderingIntent::AbsoluteColorimetric => 3, RenderingIntent::Saturation => 2, RenderingIntent::RelativeColorimetric => 1, RenderingIntent::Perceptual => 0, } } } /// ICC Header #[repr(C)] #[derive(Debug, Clone, Copy)] pub(crate) struct ProfileHeader { pub size: u32, // Size of the profile (computed) pub cmm_type: u32, // Preferred CMM type (ignored) pub version: ProfileVersion, // Version (4.3 or 4.4 if CICP is included) pub profile_class: ProfileClass, // Display device profile pub data_color_space: DataColorSpace, // RGB input color space pub pcs: DataColorSpace, // Profile connection space pub creation_date_time: ColorDateTime, // Date and time pub signature: ProfileSignature, // Profile signature pub platform: u32, // Platform target (ignored) pub flags: u32, // Flags (not embedded, can be used independently) pub device_manufacturer: u32, // Device manufacturer (ignored) pub device_model: u32, // Device model (ignored) pub device_attributes: [u8; 8], // Device attributes (ignored) pub rendering_intent: RenderingIntent, // Relative colorimetric rendering intent pub illuminant: Xyz, // D50 standard illuminant X pub creator: u32, // Profile creator (ignored) pub profile_id: [u8; 16], // Profile id checksum (ignored) pub reserved: [u8; 28], // Reserved (ignored) pub tag_count: u32, // Technically not part of header, but required } impl ProfileHeader { #[allow(dead_code)] pub(crate) fn new(size: u32) -> Self { Self { size, cmm_type: 0, version: ProfileVersion::V4_3, profile_class: ProfileClass::DisplayDevice, data_color_space: DataColorSpace::Rgb, pcs: DataColorSpace::Xyz, creation_date_time: ColorDateTime::default(), signature: ProfileSignature::Acsp, platform: 0, flags: 0x00000000, device_manufacturer: 0, device_model: 0, device_attributes: [0; 8], rendering_intent: RenderingIntent::Perceptual, illuminant: Chromaticity::D50.to_xyz(), creator: 0, profile_id: [0; 16], reserved: [0; 28], tag_count: 0, } } /// Creates profile from the buffer pub(crate) fn new_from_slice(slice: &[u8]) -> Result { if slice.len() < size_of::() { return Err(CmsError::InvalidProfile); } let mut cursor = std::io::Cursor::new(slice); let mut buffer = [0u8; size_of::()]; cursor .read_exact(&mut buffer) .map_err(|_| CmsError::InvalidProfile)?; let header = Self { size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()), cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()), version: ProfileVersion::try_from(u32::from_be_bytes( buffer[8..12].try_into().unwrap(), ))?, profile_class: ProfileClass::try_from(u32::from_be_bytes( buffer[12..16].try_into().unwrap(), ))?, data_color_space: DataColorSpace::try_from(u32::from_be_bytes( buffer[16..20].try_into().unwrap(), ))?, pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?, creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?, signature: ProfileSignature::try_from(u32::from_be_bytes( buffer[36..40].try_into().unwrap(), ))?, platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()), flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()), device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()), device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()), device_attributes: buffer[56..64].try_into().unwrap(), rendering_intent: RenderingIntent::try_from(u32::from_be_bytes( buffer[64..68].try_into().unwrap(), ))?, illuminant: Xyz::new( s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())), s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())), s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())), ), creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()), profile_id: buffer[84..100].try_into().unwrap(), reserved: buffer[100..128].try_into().unwrap(), tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()), }; Ok(header) } } /// A [Coding Independent Code Point](https://en.wikipedia.org/wiki/Coding-independent_code_points). #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct CicpProfile { pub color_primaries: CicpColorPrimaries, pub transfer_characteristics: TransferCharacteristics, pub matrix_coefficients: MatrixCoefficients, pub full_range: bool, } #[derive(Debug, Clone)] pub struct LocalizableString { /// An ISO 639-1 value is expected; any text w. more than two symbols will be truncated pub language: String, /// An ISO 3166-1 value is expected; any text w. more than two symbols will be truncated pub country: String, pub value: String, } impl LocalizableString { /// Creates new localizable string /// /// # Arguments /// /// * `language`: an ISO 639-1 value is expected, any text more than 2 symbols will be truncated /// * `country`: an ISO 3166-1 value is expected, any text more than 2 symbols will be truncated /// * `value`: String value /// pub fn new(language: String, country: String, value: String) -> Self { Self { language, country, value, } } } #[derive(Debug, Clone)] pub struct DescriptionString { pub ascii_string: String, pub unicode_language_code: u32, pub unicode_string: String, pub script_code_code: i8, pub mac_string: String, } #[derive(Debug, Clone)] pub enum ProfileText { PlainString(String), Localizable(Vec), Description(DescriptionString), } impl ProfileText { pub(crate) fn has_values(&self) -> bool { match self { ProfileText::PlainString(_) => true, ProfileText::Localizable(lc) => !lc.is_empty(), ProfileText::Description(_) => true, } } } #[derive(Debug, Clone, Copy)] pub enum StandardObserver { D50, D65, Unknown, } impl From for StandardObserver { fn from(value: u32) -> Self { if value == 1 { return StandardObserver::D50; } else if value == 2 { return StandardObserver::D65; } StandardObserver::Unknown } } #[derive(Debug, Clone, Copy)] pub struct ViewingConditions { pub illuminant: Xyz, pub surround: Xyz, pub observer: StandardObserver, } #[derive(Debug, Clone, Copy)] pub enum MeasurementGeometry { Unknown, /// 0°:45° or 45°:0° D45to45, /// 0°:d or d:0° D0to0, } impl From for MeasurementGeometry { fn from(value: u32) -> Self { if value == 1 { Self::D45to45 } else if value == 2 { Self::D0to0 } else { Self::Unknown } } } #[derive(Debug, Clone, Copy)] pub enum StandardIlluminant { Unknown, D50, D65, D93, F2, D55, A, EquiPower, F8, } impl From for StandardIlluminant { fn from(value: u32) -> Self { match value { 1 => StandardIlluminant::D50, 2 => StandardIlluminant::D65, 3 => StandardIlluminant::D93, 4 => StandardIlluminant::F2, 5 => StandardIlluminant::D55, 6 => StandardIlluminant::A, 7 => StandardIlluminant::EquiPower, 8 => StandardIlluminant::F8, _ => Self::Unknown, } } } impl From for u32 { fn from(value: StandardIlluminant) -> Self { match value { StandardIlluminant::Unknown => 0u32, StandardIlluminant::D50 => 1u32, StandardIlluminant::D65 => 2u32, StandardIlluminant::D93 => 3, StandardIlluminant::F2 => 4, StandardIlluminant::D55 => 5, StandardIlluminant::A => 6, StandardIlluminant::EquiPower => 7, StandardIlluminant::F8 => 8, } } } #[derive(Debug, Clone, Copy)] pub struct Measurement { pub observer: StandardObserver, pub backing: Xyz, pub geometry: MeasurementGeometry, pub flare: f32, pub illuminant: StandardIlluminant, } /// ICC Profile representation #[repr(C)] #[derive(Debug, Clone, Default)] pub struct ColorProfile { pub pcs: DataColorSpace, pub color_space: DataColorSpace, pub profile_class: ProfileClass, pub rendering_intent: RenderingIntent, pub red_colorant: Xyzd, pub green_colorant: Xyzd, pub blue_colorant: Xyzd, pub white_point: Xyzd, pub black_point: Option, pub media_white_point: Option, pub luminance: Option, pub measurement: Option, pub red_trc: Option, pub green_trc: Option, pub blue_trc: Option, pub gray_trc: Option, pub cicp: Option, pub chromatic_adaptation: Option, pub lut_a_to_b_perceptual: Option, pub lut_a_to_b_colorimetric: Option, pub lut_a_to_b_saturation: Option, pub lut_b_to_a_perceptual: Option, pub lut_b_to_a_colorimetric: Option, pub lut_b_to_a_saturation: Option, pub gamut: Option, pub copyright: Option, pub description: Option, pub device_manufacturer: Option, pub device_model: Option, pub char_target: Option, pub viewing_conditions: Option, pub viewing_conditions_description: Option, pub technology: Option, pub calibration_date: Option, /// Version for internal and viewing purposes only. /// On encoding added value to profile will always be V4. pub(crate) version_internal: ProfileVersion, } #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)] pub struct ParsingOptions { // Maximum allowed profile size in bytes pub max_profile_size: usize, // Maximum allowed CLUT size in bytes pub max_allowed_clut_size: usize, // Maximum allowed TRC size in elements count pub max_allowed_trc_size: usize, } impl Default for ParsingOptions { fn default() -> Self { Self { max_profile_size: MAX_PROFILE_SIZE, max_allowed_clut_size: 10_000_000, max_allowed_trc_size: 40_000, } } } impl ColorProfile { /// Returns profile version pub fn version(&self) -> ProfileVersion { self.version_internal } pub fn new_from_slice(slice: &[u8]) -> Result { Self::new_from_slice_with_options(slice, Default::default()) } pub fn new_from_slice_with_options( slice: &[u8], options: ParsingOptions, ) -> Result { let header = ProfileHeader::new_from_slice(slice)?; let tags_count = header.tag_count as usize; if slice.len() >= options.max_profile_size { return Err(CmsError::InvalidProfile); } let tags_end = tags_count .safe_mul(TAG_SIZE)? .safe_add(size_of::())?; if slice.len() < tags_end { return Err(CmsError::InvalidProfile); } let tags_slice = &slice[size_of::()..tags_end]; let mut profile = ColorProfile { rendering_intent: header.rendering_intent, pcs: header.pcs, profile_class: header.profile_class, color_space: header.data_color_space, white_point: header.illuminant.to_xyzd(), version_internal: header.version, ..Default::default() }; let color_space = profile.color_space; for tag in tags_slice.chunks_exact(TAG_SIZE) { let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]); let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; // Just ignore unknown tags if let Ok(tag) = Tag::try_from(tag_value) { match tag { Tag::RedXyz => { if color_space == DataColorSpace::Rgb { profile.red_colorant = Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; } } Tag::GreenXyz => { if color_space == DataColorSpace::Rgb { profile.green_colorant = Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; } } Tag::BlueXyz => { if color_space == DataColorSpace::Rgb { profile.blue_colorant = Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; } } Tag::RedToneReproduction => { if color_space == DataColorSpace::Rgb { profile.red_trc = Self::read_trc_tag_s( slice, tag_entry as usize, tag_size, &options, )?; } } Tag::GreenToneReproduction => { if color_space == DataColorSpace::Rgb { profile.green_trc = Self::read_trc_tag_s( slice, tag_entry as usize, tag_size, &options, )?; } } Tag::BlueToneReproduction => { if color_space == DataColorSpace::Rgb { profile.blue_trc = Self::read_trc_tag_s( slice, tag_entry as usize, tag_size, &options, )?; } } Tag::GreyToneReproduction => { if color_space == DataColorSpace::Gray { profile.gray_trc = Self::read_trc_tag_s( slice, tag_entry as usize, tag_size, &options, )?; } } Tag::MediaWhitePoint => { profile.media_white_point = Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; } Tag::Luminance => { profile.luminance = Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; } Tag::Measurement => { profile.measurement = Self::read_meas_tag(slice, tag_entry as usize, tag_size)?; } Tag::CodeIndependentPoints => { // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces // or profile classes indicated in the profile header. if (profile.profile_class == ProfileClass::InputDevice || profile.profile_class == ProfileClass::DisplayDevice) && (profile.color_space == DataColorSpace::Rgb || profile.color_space == DataColorSpace::YCbr || profile.color_space == DataColorSpace::Xyz) { profile.cicp = Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?; } } Tag::ChromaticAdaptation => { profile.chromatic_adaptation = Self::read_chad_tag(slice, tag_entry as usize, tag_size)?; } Tag::BlackPoint => { profile.black_point = Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)? } Tag::DeviceToPcsLutPerceptual => { profile.lut_a_to_b_perceptual = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; } Tag::DeviceToPcsLutColorimetric => { profile.lut_a_to_b_colorimetric = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; } Tag::DeviceToPcsLutSaturation => { profile.lut_a_to_b_saturation = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; } Tag::PcsToDeviceLutPerceptual => { profile.lut_b_to_a_perceptual = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; } Tag::PcsToDeviceLutColorimetric => { profile.lut_b_to_a_colorimetric = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; } Tag::PcsToDeviceLutSaturation => { profile.lut_b_to_a_saturation = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; } Tag::Gamut => { profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; } Tag::Copyright => { profile.copyright = Self::read_string_tag(slice, tag_entry as usize, tag_size)?; } Tag::ProfileDescription => { profile.description = Self::read_string_tag(slice, tag_entry as usize, tag_size)?; } Tag::ViewingConditionsDescription => { profile.viewing_conditions_description = Self::read_string_tag(slice, tag_entry as usize, tag_size)?; } Tag::DeviceModel => { profile.device_model = Self::read_string_tag(slice, tag_entry as usize, tag_size)?; } Tag::DeviceManufacturer => { profile.device_manufacturer = Self::read_string_tag(slice, tag_entry as usize, tag_size)?; } Tag::CharTarget => { profile.char_target = Self::read_string_tag(slice, tag_entry as usize, tag_size)?; } Tag::Chromaticity => {} Tag::ObserverConditions => { profile.viewing_conditions = Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?; } Tag::Technology => { profile.technology = Self::read_tech_tag(slice, tag_entry as usize, tag_size)?; } Tag::CalibrationDateTime => { profile.calibration_date = Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?; } } } } Ok(profile) } } impl ColorProfile { #[inline] pub fn colorant_matrix(&self) -> Matrix3d { Matrix3d { v: [ [ self.red_colorant.x, self.green_colorant.x, self.blue_colorant.x, ], [ self.red_colorant.y, self.green_colorant.y, self.blue_colorant.y, ], [ self.red_colorant.z, self.green_colorant.z, self.blue_colorant.z, ], ], } } /// Computes colorants matrix. Returns not transposed matrix. /// /// To work on `const` context this method does have restrictions. /// If invalid values were provided it may return invalid matrix or NaNs. pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d { let red_xyz = primaries.red.to_xyzd(); let green_xyz = primaries.green.to_xyzd(); let blue_xyz = primaries.blue.to_xyzd(); let xyz_matrix = Matrix3d { v: [ [red_xyz.x, green_xyz.x, blue_xyz.x], [red_xyz.y, green_xyz.y, blue_xyz.y], [red_xyz.z, green_xyz.z, blue_xyz.z], ], }; let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); adapt_to_d50_d(colorants, white_point) } /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) { let red_xyz = primaries.red.to_xyzd(); let green_xyz = primaries.green.to_xyzd(); let blue_xyz = primaries.blue.to_xyzd(); self.chromatic_adaptation = Some(BRADFORD_D); self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz) } /// Updates RGB triple colorimetry from 3 [Xyzd] and white point /// /// To work on `const` context this method does have restrictions. /// If invalid values were provided it may return invalid matrix or NaNs. pub const fn update_rgb_colorimetry_triplet( &mut self, white_point: XyY, red_xyz: Xyzd, green_xyz: Xyzd, blue_xyz: Xyzd, ) { let xyz_matrix = Matrix3d { v: [ [red_xyz.x, green_xyz.x, blue_xyz.x], [red_xyz.y, green_xyz.y, blue_xyz.y], [red_xyz.z, green_xyz.z, blue_xyz.z], ], }; let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); let colorants = adapt_to_d50_d(colorants, white_point); self.update_colorants(colorants); } pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) { // note: there's a transpose type of operation going on here self.red_colorant.x = colorants.v[0][0]; self.red_colorant.y = colorants.v[1][0]; self.red_colorant.z = colorants.v[2][0]; self.green_colorant.x = colorants.v[0][1]; self.green_colorant.y = colorants.v[1][1]; self.green_colorant.z = colorants.v[2][1]; self.blue_colorant.x = colorants.v[0][2]; self.blue_colorant.y = colorants.v[1][2]; self.blue_colorant.z = colorants.v[2][2]; } /// Updates RGB triple colorimetry from CICP pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool { self.cicp = Some(cicp); if !cicp.color_primaries.has_chromaticity() || !cicp.transfer_characteristics.has_transfer_curve() { return false; } let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() { Ok(primaries) => primaries, Err(_) => return false, }; let white_point: Chromaticity = match cicp.color_primaries.white_point() { Ok(v) => v, Err(_) => return false, }; self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy); let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() { Ok(trc) => trc, Err(_) => return false, }; self.green_trc = Some(red_trc.clone()); self.blue_trc = Some(red_trc.clone()); self.red_trc = Some(red_trc); false } pub const fn rgb_to_xyz(&self, xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { let xyz_inverse = xyz_matrix.inverse(); let s = xyz_inverse.mul_vector(wp.to_vector()); let mut v = xyz_matrix.mul_row_vector::<0>(s); v = v.mul_row_vector::<1>(s); v.mul_row_vector::<2>(s) } ///TODO: make primary instead of [rgb_to_xyz] in the next major version pub(crate) const fn rgb_to_xyz_static(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { let xyz_inverse = xyz_matrix.inverse(); let s = xyz_inverse.mul_vector(wp.to_vector()); let mut v = xyz_matrix.mul_row_vector::<0>(s); v = v.mul_row_vector::<1>(s); v.mul_row_vector::<2>(s) } /// If Primaries is invalid will return invalid matrix on const context. /// This assumes not transposed matrix and returns not transposed matrix. pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d { let xyz_inverse = xyz_matrix.inverse(); let s = xyz_inverse.mul_vector(wp.to_vector_d()); let mut v = xyz_matrix.mul_row_vector::<0>(s); v = v.mul_row_vector::<1>(s); v = v.mul_row_vector::<2>(s); v } pub fn rgb_to_xyz_matrix(&self) -> Matrix3d { let xyz_matrix = self.colorant_matrix(); let white_point = Chromaticity::D50.to_xyzd(); ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point) } /// Computes transform matrix RGB -> XYZ -> RGB /// Current profile is used as source, other as destination pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d { let source = self.rgb_to_xyz_matrix(); let dst = dest.rgb_to_xyz_matrix(); let dest_inverse = dst.inverse(); dest_inverse.mat_mul(source) } /// Returns volume of colors stored in profile pub fn profile_volume(&self) -> Option { let red_prim = self.red_colorant; let green_prim = self.green_colorant; let blue_prim = self.blue_colorant; let tetrahedral_vertices = Matrix3d { v: [ [red_prim.x, red_prim.y, red_prim.z], [green_prim.x, green_prim.y, green_prim.z], [blue_prim.x, blue_prim.y, blue_prim.z], ], }; let det = tetrahedral_vertices.determinant()?; Some((det / 6.0f64) as f32) } pub(crate) fn has_device_to_pcs_lut(&self) -> bool { self.lut_a_to_b_perceptual.is_some() || self.lut_a_to_b_saturation.is_some() || self.lut_a_to_b_colorimetric.is_some() } pub(crate) fn has_pcs_to_device_lut(&self) -> bool { self.lut_b_to_a_perceptual.is_some() || self.lut_b_to_a_saturation.is_some() || self.lut_b_to_a_colorimetric.is_some() } } #[cfg(test)] mod tests { use super::*; use std::fs; #[test] fn test_gray() { if let Ok(gray_icc) = fs::read("./assets/Generic Gray Gamma 2.2 Profile.icc") { let f_p = ColorProfile::new_from_slice(&gray_icc).unwrap(); assert!(f_p.gray_trc.is_some()); } } #[test] fn test_perceptual() { if let Ok(srgb_perceptual_icc) = fs::read("./assets/srgb_perceptual.icc") { let f_p = ColorProfile::new_from_slice(&srgb_perceptual_icc).unwrap(); assert_eq!(f_p.pcs, DataColorSpace::Lab); assert_eq!(f_p.color_space, DataColorSpace::Rgb); assert_eq!(f_p.version(), ProfileVersion::V4_2); assert!(f_p.lut_a_to_b_perceptual.is_some()); assert!(f_p.lut_b_to_a_perceptual.is_some()); } } #[test] fn test_us_swop_coated() { if let Ok(us_swop_coated) = fs::read("./assets/us_swop_coated.icc") { let f_p = ColorProfile::new_from_slice(&us_swop_coated).unwrap(); assert_eq!(f_p.pcs, DataColorSpace::Lab); assert_eq!(f_p.color_space, DataColorSpace::Cmyk); assert_eq!(f_p.version(), ProfileVersion::V2_0); assert!(f_p.lut_a_to_b_perceptual.is_some()); assert!(f_p.lut_b_to_a_perceptual.is_some()); assert!(f_p.lut_a_to_b_colorimetric.is_some()); assert!(f_p.lut_b_to_a_colorimetric.is_some()); assert!(f_p.gamut.is_some()); assert!(f_p.copyright.is_some()); assert!(f_p.description.is_some()); } } #[test] fn test_matrix_shaper() { if let Ok(matrix_shaper) = fs::read("./assets/Display P3.icc") { let f_p = ColorProfile::new_from_slice(&matrix_shaper).unwrap(); assert_eq!(f_p.pcs, DataColorSpace::Xyz); assert_eq!(f_p.color_space, DataColorSpace::Rgb); assert_eq!(f_p.version(), ProfileVersion::V4_0); assert!(f_p.red_trc.is_some()); assert!(f_p.blue_trc.is_some()); assert!(f_p.green_trc.is_some()); assert_ne!(f_p.red_colorant, Xyzd::default()); assert_ne!(f_p.blue_colorant, Xyzd::default()); assert_ne!(f_p.green_colorant, Xyzd::default()); assert!(f_p.copyright.is_some()); assert!(f_p.description.is_some()); } } }