mirror of
https://github.com/edera-dev/sprout.git
synced 2025-12-19 21:20:17 +00:00
1366 lines
52 KiB
Rust
1366 lines
52 KiB
Rust
/*
|
|
* // 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<u32> for ProfileSignature {
|
|
type Error = CmsError;
|
|
#[inline]
|
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
if value == u32::from_ne_bytes(*b"acsp").to_be() {
|
|
return Ok(ProfileSignature::Acsp);
|
|
}
|
|
Err(CmsError::InvalidProfile)
|
|
}
|
|
}
|
|
|
|
impl From<ProfileSignature> 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<u32> for ProfileVersion {
|
|
type Error = CmsError;
|
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
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<ProfileVersion> 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<u32> for ProfileClass {
|
|
type Error = CmsError;
|
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
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<ProfileClass> 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<u8>),
|
|
Store16(Vec<u16>),
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
|
pub enum LutType {
|
|
Lut8,
|
|
Lut16,
|
|
LutMab,
|
|
LutMba,
|
|
}
|
|
|
|
impl TryFrom<u32> for LutType {
|
|
type Error = CmsError;
|
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
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<LutType> 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<u32> for DataColorSpace {
|
|
type Error = CmsError;
|
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
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<DataColorSpace> 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<u32> 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<LutStore>,
|
|
pub a_curves: Vec<ToneReprCurve>,
|
|
pub b_curves: Vec<ToneReprCurve>,
|
|
pub m_curves: Vec<ToneReprCurve>,
|
|
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<u32> for RenderingIntent {
|
|
type Error = CmsError;
|
|
|
|
#[inline]
|
|
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
match value {
|
|
0 => Ok(RenderingIntent::Perceptual),
|
|
1 => Ok(RenderingIntent::RelativeColorimetric),
|
|
2 => Ok(RenderingIntent::Saturation),
|
|
3 => Ok(RenderingIntent::AbsoluteColorimetric),
|
|
_ => Err(CmsError::InvalidRenderingIntent),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RenderingIntent> 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<Self, CmsError> {
|
|
if slice.len() < size_of::<ProfileHeader>() {
|
|
return Err(CmsError::InvalidProfile);
|
|
}
|
|
let mut cursor = std::io::Cursor::new(slice);
|
|
let mut buffer = [0u8; size_of::<ProfileHeader>()];
|
|
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<LocalizableString>),
|
|
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<u32> 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<u32> 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<u32> 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<StandardIlluminant> 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<Xyzd>,
|
|
pub media_white_point: Option<Xyzd>,
|
|
pub luminance: Option<Xyzd>,
|
|
pub measurement: Option<Measurement>,
|
|
pub red_trc: Option<ToneReprCurve>,
|
|
pub green_trc: Option<ToneReprCurve>,
|
|
pub blue_trc: Option<ToneReprCurve>,
|
|
pub gray_trc: Option<ToneReprCurve>,
|
|
pub cicp: Option<CicpProfile>,
|
|
pub chromatic_adaptation: Option<Matrix3d>,
|
|
pub lut_a_to_b_perceptual: Option<LutWarehouse>,
|
|
pub lut_a_to_b_colorimetric: Option<LutWarehouse>,
|
|
pub lut_a_to_b_saturation: Option<LutWarehouse>,
|
|
pub lut_b_to_a_perceptual: Option<LutWarehouse>,
|
|
pub lut_b_to_a_colorimetric: Option<LutWarehouse>,
|
|
pub lut_b_to_a_saturation: Option<LutWarehouse>,
|
|
pub gamut: Option<LutWarehouse>,
|
|
pub copyright: Option<ProfileText>,
|
|
pub description: Option<ProfileText>,
|
|
pub device_manufacturer: Option<ProfileText>,
|
|
pub device_model: Option<ProfileText>,
|
|
pub char_target: Option<ProfileText>,
|
|
pub viewing_conditions: Option<ViewingConditions>,
|
|
pub viewing_conditions_description: Option<ProfileText>,
|
|
pub technology: Option<TechnologySignatures>,
|
|
pub calibration_date: Option<ColorDateTime>,
|
|
/// 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, CmsError> {
|
|
Self::new_from_slice_with_options(slice, Default::default())
|
|
}
|
|
|
|
pub fn new_from_slice_with_options(
|
|
slice: &[u8],
|
|
options: ParsingOptions,
|
|
) -> Result<Self, CmsError> {
|
|
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::<ProfileHeader>())?;
|
|
if slice.len() < tags_end {
|
|
return Err(CmsError::InvalidProfile);
|
|
}
|
|
let tags_slice = &slice[size_of::<ProfileHeader>()..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<f32> {
|
|
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());
|
|
}
|
|
}
|
|
}
|