/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.7.11/src/profile.rs
Line | Count | Source |
1 | | /* |
2 | | * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. |
3 | | * // |
4 | | * // Redistribution and use in source and binary forms, with or without modification, |
5 | | * // are permitted provided that the following conditions are met: |
6 | | * // |
7 | | * // 1. Redistributions of source code must retain the above copyright notice, this |
8 | | * // list of conditions and the following disclaimer. |
9 | | * // |
10 | | * // 2. Redistributions in binary form must reproduce the above copyright notice, |
11 | | * // this list of conditions and the following disclaimer in the documentation |
12 | | * // and/or other materials provided with the distribution. |
13 | | * // |
14 | | * // 3. Neither the name of the copyright holder nor the names of its |
15 | | * // contributors may be used to endorse or promote products derived from |
16 | | * // this software without specific prior written permission. |
17 | | * // |
18 | | * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
19 | | * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
20 | | * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
21 | | * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
22 | | * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
23 | | * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
24 | | * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
25 | | * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
26 | | * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
27 | | * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
28 | | */ |
29 | | use crate::chad::BRADFORD_D; |
30 | | use crate::cicp::{ |
31 | | CicpColorPrimaries, ColorPrimaries, MatrixCoefficients, TransferCharacteristics, |
32 | | }; |
33 | | use crate::dat::ColorDateTime; |
34 | | use crate::err::CmsError; |
35 | | use crate::matrix::{Matrix3f, Xyz}; |
36 | | use crate::reader::s15_fixed16_number_to_float; |
37 | | use crate::safe_math::{SafeAdd, SafeMul}; |
38 | | use crate::tag::{TAG_SIZE, Tag}; |
39 | | use crate::trc::ToneReprCurve; |
40 | | use crate::{Chromaticity, Layout, Matrix3d, Vector3d, XyY, Xyzd, adapt_to_d50_d}; |
41 | | use std::io::Read; |
42 | | |
43 | | const MAX_PROFILE_SIZE: usize = 1024 * 1024 * 10; // 10 MB max, for Fogra39 etc |
44 | | |
45 | | #[repr(u32)] |
46 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
47 | | pub enum ProfileSignature { |
48 | | Acsp, |
49 | | } |
50 | | |
51 | | impl TryFrom<u32> for ProfileSignature { |
52 | | type Error = CmsError; |
53 | | #[inline] |
54 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
55 | 0 | if value == u32::from_ne_bytes(*b"acsp").to_be() { |
56 | 0 | return Ok(ProfileSignature::Acsp); |
57 | 0 | } |
58 | 0 | Err(CmsError::InvalidProfile) |
59 | 0 | } |
60 | | } |
61 | | |
62 | | impl From<ProfileSignature> for u32 { |
63 | | #[inline] |
64 | 0 | fn from(value: ProfileSignature) -> Self { |
65 | 0 | match value { |
66 | 0 | ProfileSignature::Acsp => u32::from_ne_bytes(*b"acsp").to_be(), |
67 | | } |
68 | 0 | } |
69 | | } |
70 | | |
71 | | #[repr(u32)] |
72 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)] |
73 | | pub enum ProfileVersion { |
74 | | V2_0 = 0x02000000, |
75 | | V2_1 = 0x02100000, |
76 | | V2_2 = 0x02200000, |
77 | | V2_3 = 0x02300000, |
78 | | V2_4 = 0x02400000, |
79 | | V4_0 = 0x04000000, |
80 | | V4_1 = 0x04100000, |
81 | | V4_2 = 0x04200000, |
82 | | V4_3 = 0x04300000, |
83 | | #[default] |
84 | | V4_4 = 0x04400000, |
85 | | Unknown, |
86 | | } |
87 | | |
88 | | impl TryFrom<u32> for ProfileVersion { |
89 | | type Error = CmsError; |
90 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
91 | 0 | match value { |
92 | 0 | 0x02000000 => Ok(ProfileVersion::V2_0), |
93 | 0 | 0x02100000 => Ok(ProfileVersion::V2_1), |
94 | 0 | 0x02200000 => Ok(ProfileVersion::V2_2), |
95 | 0 | 0x02300000 => Ok(ProfileVersion::V2_3), |
96 | 0 | 0x02400000 => Ok(ProfileVersion::V2_4), |
97 | 0 | 0x04000000 => Ok(ProfileVersion::V4_0), |
98 | 0 | 0x04100000 => Ok(ProfileVersion::V4_1), |
99 | 0 | 0x04200000 => Ok(ProfileVersion::V4_2), |
100 | 0 | 0x04300000 => Ok(ProfileVersion::V4_3), |
101 | 0 | 0x04400000 => Ok(ProfileVersion::V4_3), |
102 | 0 | _ => Err(CmsError::InvalidProfile), |
103 | | } |
104 | 0 | } |
105 | | } |
106 | | |
107 | | impl From<ProfileVersion> for u32 { |
108 | 0 | fn from(value: ProfileVersion) -> Self { |
109 | 0 | match value { |
110 | 0 | ProfileVersion::V2_0 => 0x02000000, |
111 | 0 | ProfileVersion::V2_1 => 0x02100000, |
112 | 0 | ProfileVersion::V2_2 => 0x02200000, |
113 | 0 | ProfileVersion::V2_3 => 0x02300000, |
114 | 0 | ProfileVersion::V2_4 => 0x02400000, |
115 | 0 | ProfileVersion::V4_0 => 0x04000000, |
116 | 0 | ProfileVersion::V4_1 => 0x04100000, |
117 | 0 | ProfileVersion::V4_2 => 0x04200000, |
118 | 0 | ProfileVersion::V4_3 => 0x04300000, |
119 | 0 | ProfileVersion::V4_4 => 0x04400000, |
120 | 0 | ProfileVersion::Unknown => 0x02000000, |
121 | | } |
122 | 0 | } |
123 | | } |
124 | | |
125 | | #[repr(u32)] |
126 | | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Hash)] |
127 | | pub enum DataColorSpace { |
128 | | #[default] |
129 | | Xyz, |
130 | | Lab, |
131 | | Luv, |
132 | | YCbr, |
133 | | Yxy, |
134 | | Rgb, |
135 | | Gray, |
136 | | Hsv, |
137 | | Hls, |
138 | | Cmyk, |
139 | | Cmy, |
140 | | Color2, |
141 | | Color3, |
142 | | Color4, |
143 | | Color5, |
144 | | Color6, |
145 | | Color7, |
146 | | Color8, |
147 | | Color9, |
148 | | Color10, |
149 | | Color11, |
150 | | Color12, |
151 | | Color13, |
152 | | Color14, |
153 | | Color15, |
154 | | } |
155 | | |
156 | | impl DataColorSpace { |
157 | | #[inline] |
158 | 0 | pub fn check_layout(self, layout: Layout) -> Result<(), CmsError> { |
159 | 0 | let unsupported: bool = match self { |
160 | 0 | DataColorSpace::Xyz => layout != Layout::Rgb, |
161 | 0 | DataColorSpace::Lab => layout != Layout::Rgb && layout != Layout::Rgba, |
162 | 0 | DataColorSpace::Luv => layout != Layout::Rgb, |
163 | 0 | DataColorSpace::YCbr => layout != Layout::Rgb, |
164 | 0 | DataColorSpace::Yxy => layout != Layout::Rgb, |
165 | 0 | DataColorSpace::Rgb => layout != Layout::Rgb && layout != Layout::Rgba, |
166 | 0 | DataColorSpace::Gray => layout != Layout::Gray && layout != Layout::GrayAlpha, |
167 | 0 | DataColorSpace::Hsv => layout != Layout::Rgb, |
168 | 0 | DataColorSpace::Hls => layout != Layout::Rgb, |
169 | 0 | DataColorSpace::Cmyk => layout != Layout::Rgba, |
170 | 0 | DataColorSpace::Cmy => layout != Layout::Rgb, |
171 | 0 | DataColorSpace::Color2 => layout != Layout::GrayAlpha, |
172 | 0 | DataColorSpace::Color3 => layout != Layout::Rgb, |
173 | 0 | DataColorSpace::Color4 => layout != Layout::Rgba, |
174 | 0 | DataColorSpace::Color5 => layout != Layout::Inks5, |
175 | 0 | DataColorSpace::Color6 => layout != Layout::Inks6, |
176 | 0 | DataColorSpace::Color7 => layout != Layout::Inks7, |
177 | 0 | DataColorSpace::Color8 => layout != Layout::Inks8, |
178 | 0 | DataColorSpace::Color9 => layout != Layout::Inks9, |
179 | 0 | DataColorSpace::Color10 => layout != Layout::Inks10, |
180 | 0 | DataColorSpace::Color11 => layout != Layout::Inks11, |
181 | 0 | DataColorSpace::Color12 => layout != Layout::Inks12, |
182 | 0 | DataColorSpace::Color13 => layout != Layout::Inks13, |
183 | 0 | DataColorSpace::Color14 => layout != Layout::Inks14, |
184 | 0 | DataColorSpace::Color15 => layout != Layout::Inks15, |
185 | | }; |
186 | 0 | if unsupported { |
187 | 0 | Err(CmsError::InvalidLayout) |
188 | | } else { |
189 | 0 | Ok(()) |
190 | | } |
191 | 0 | } |
192 | | |
193 | 0 | pub(crate) fn is_three_channels(self) -> bool { |
194 | 0 | matches!( |
195 | 0 | self, |
196 | | DataColorSpace::Xyz |
197 | | | DataColorSpace::Lab |
198 | | | DataColorSpace::Luv |
199 | | | DataColorSpace::YCbr |
200 | | | DataColorSpace::Yxy |
201 | | | DataColorSpace::Rgb |
202 | | | DataColorSpace::Hsv |
203 | | | DataColorSpace::Hls |
204 | | | DataColorSpace::Cmy |
205 | | | DataColorSpace::Color3 |
206 | | ) |
207 | 0 | } |
208 | | } |
209 | | |
210 | | #[repr(u32)] |
211 | | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] |
212 | | pub enum ProfileClass { |
213 | | InputDevice, |
214 | | #[default] |
215 | | DisplayDevice, |
216 | | OutputDevice, |
217 | | DeviceLink, |
218 | | ColorSpace, |
219 | | Abstract, |
220 | | Named, |
221 | | } |
222 | | |
223 | | impl TryFrom<u32> for ProfileClass { |
224 | | type Error = CmsError; |
225 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
226 | 0 | if value == u32::from_ne_bytes(*b"scnr").to_be() { |
227 | 0 | return Ok(ProfileClass::InputDevice); |
228 | 0 | } else if value == u32::from_ne_bytes(*b"mntr").to_be() { |
229 | 0 | return Ok(ProfileClass::DisplayDevice); |
230 | 0 | } else if value == u32::from_ne_bytes(*b"prtr").to_be() { |
231 | 0 | return Ok(ProfileClass::OutputDevice); |
232 | 0 | } else if value == u32::from_ne_bytes(*b"link").to_be() { |
233 | 0 | return Ok(ProfileClass::DeviceLink); |
234 | 0 | } else if value == u32::from_ne_bytes(*b"spac").to_be() { |
235 | 0 | return Ok(ProfileClass::ColorSpace); |
236 | 0 | } else if value == u32::from_ne_bytes(*b"abst").to_be() { |
237 | 0 | return Ok(ProfileClass::Abstract); |
238 | 0 | } else if value == u32::from_ne_bytes(*b"nmcl").to_be() { |
239 | 0 | return Ok(ProfileClass::Named); |
240 | 0 | } |
241 | 0 | Err(CmsError::InvalidProfile) |
242 | 0 | } |
243 | | } |
244 | | |
245 | | impl From<ProfileClass> for u32 { |
246 | 0 | fn from(val: ProfileClass) -> Self { |
247 | 0 | match val { |
248 | 0 | ProfileClass::InputDevice => u32::from_ne_bytes(*b"scnr").to_be(), |
249 | 0 | ProfileClass::DisplayDevice => u32::from_ne_bytes(*b"mntr").to_be(), |
250 | 0 | ProfileClass::OutputDevice => u32::from_ne_bytes(*b"prtr").to_be(), |
251 | 0 | ProfileClass::DeviceLink => u32::from_ne_bytes(*b"link").to_be(), |
252 | 0 | ProfileClass::ColorSpace => u32::from_ne_bytes(*b"spac").to_be(), |
253 | 0 | ProfileClass::Abstract => u32::from_ne_bytes(*b"abst").to_be(), |
254 | 0 | ProfileClass::Named => u32::from_ne_bytes(*b"nmcl").to_be(), |
255 | | } |
256 | 0 | } |
257 | | } |
258 | | |
259 | | #[derive(Debug, Clone, PartialEq)] |
260 | | pub enum LutStore { |
261 | | Store8(Vec<u8>), |
262 | | Store16(Vec<u16>), |
263 | | } |
264 | | |
265 | | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] |
266 | | pub enum LutType { |
267 | | Lut8, |
268 | | Lut16, |
269 | | LutMab, |
270 | | LutMba, |
271 | | } |
272 | | |
273 | | impl TryFrom<u32> for LutType { |
274 | | type Error = CmsError; |
275 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
276 | 0 | if value == u32::from_ne_bytes(*b"mft1").to_be() { |
277 | 0 | return Ok(LutType::Lut8); |
278 | 0 | } else if value == u32::from_ne_bytes(*b"mft2").to_be() { |
279 | 0 | return Ok(LutType::Lut16); |
280 | 0 | } else if value == u32::from_ne_bytes(*b"mAB ").to_be() { |
281 | 0 | return Ok(LutType::LutMab); |
282 | 0 | } else if value == u32::from_ne_bytes(*b"mBA ").to_be() { |
283 | 0 | return Ok(LutType::LutMba); |
284 | 0 | } |
285 | 0 | Err(CmsError::InvalidProfile) |
286 | 0 | } |
287 | | } |
288 | | |
289 | | impl From<LutType> for u32 { |
290 | 0 | fn from(val: LutType) -> Self { |
291 | 0 | match val { |
292 | 0 | LutType::Lut8 => u32::from_ne_bytes(*b"mft1").to_be(), |
293 | 0 | LutType::Lut16 => u32::from_ne_bytes(*b"mft2").to_be(), |
294 | 0 | LutType::LutMab => u32::from_ne_bytes(*b"mAB ").to_be(), |
295 | 0 | LutType::LutMba => u32::from_ne_bytes(*b"mBA ").to_be(), |
296 | | } |
297 | 0 | } |
298 | | } |
299 | | |
300 | | impl TryFrom<u32> for DataColorSpace { |
301 | | type Error = CmsError; |
302 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
303 | 0 | if value == u32::from_ne_bytes(*b"XYZ ").to_be() { |
304 | 0 | return Ok(DataColorSpace::Xyz); |
305 | 0 | } else if value == u32::from_ne_bytes(*b"Lab ").to_be() { |
306 | 0 | return Ok(DataColorSpace::Lab); |
307 | 0 | } else if value == u32::from_ne_bytes(*b"Luv ").to_be() { |
308 | 0 | return Ok(DataColorSpace::Luv); |
309 | 0 | } else if value == u32::from_ne_bytes(*b"YCbr").to_be() { |
310 | 0 | return Ok(DataColorSpace::YCbr); |
311 | 0 | } else if value == u32::from_ne_bytes(*b"Yxy ").to_be() { |
312 | 0 | return Ok(DataColorSpace::Yxy); |
313 | 0 | } else if value == u32::from_ne_bytes(*b"RGB ").to_be() { |
314 | 0 | return Ok(DataColorSpace::Rgb); |
315 | 0 | } else if value == u32::from_ne_bytes(*b"GRAY").to_be() { |
316 | 0 | return Ok(DataColorSpace::Gray); |
317 | 0 | } else if value == u32::from_ne_bytes(*b"HSV ").to_be() { |
318 | 0 | return Ok(DataColorSpace::Hsv); |
319 | 0 | } else if value == u32::from_ne_bytes(*b"HLS ").to_be() { |
320 | 0 | return Ok(DataColorSpace::Hls); |
321 | 0 | } else if value == u32::from_ne_bytes(*b"CMYK").to_be() { |
322 | 0 | return Ok(DataColorSpace::Cmyk); |
323 | 0 | } else if value == u32::from_ne_bytes(*b"CMY ").to_be() { |
324 | 0 | return Ok(DataColorSpace::Cmy); |
325 | 0 | } else if value == u32::from_ne_bytes(*b"2CLR").to_be() { |
326 | 0 | return Ok(DataColorSpace::Color2); |
327 | 0 | } else if value == u32::from_ne_bytes(*b"3CLR").to_be() { |
328 | 0 | return Ok(DataColorSpace::Color3); |
329 | 0 | } else if value == u32::from_ne_bytes(*b"4CLR").to_be() { |
330 | 0 | return Ok(DataColorSpace::Color4); |
331 | 0 | } else if value == u32::from_ne_bytes(*b"5CLR").to_be() { |
332 | 0 | return Ok(DataColorSpace::Color5); |
333 | 0 | } else if value == u32::from_ne_bytes(*b"6CLR").to_be() { |
334 | 0 | return Ok(DataColorSpace::Color6); |
335 | 0 | } else if value == u32::from_ne_bytes(*b"7CLR").to_be() { |
336 | 0 | return Ok(DataColorSpace::Color7); |
337 | 0 | } else if value == u32::from_ne_bytes(*b"8CLR").to_be() { |
338 | 0 | return Ok(DataColorSpace::Color8); |
339 | 0 | } else if value == u32::from_ne_bytes(*b"9CLR").to_be() { |
340 | 0 | return Ok(DataColorSpace::Color9); |
341 | 0 | } else if value == u32::from_ne_bytes(*b"ACLR").to_be() { |
342 | 0 | return Ok(DataColorSpace::Color10); |
343 | 0 | } else if value == u32::from_ne_bytes(*b"BCLR").to_be() { |
344 | 0 | return Ok(DataColorSpace::Color11); |
345 | 0 | } else if value == u32::from_ne_bytes(*b"CCLR").to_be() { |
346 | 0 | return Ok(DataColorSpace::Color12); |
347 | 0 | } else if value == u32::from_ne_bytes(*b"DCLR").to_be() { |
348 | 0 | return Ok(DataColorSpace::Color13); |
349 | 0 | } else if value == u32::from_ne_bytes(*b"ECLR").to_be() { |
350 | 0 | return Ok(DataColorSpace::Color14); |
351 | 0 | } else if value == u32::from_ne_bytes(*b"FCLR").to_be() { |
352 | 0 | return Ok(DataColorSpace::Color15); |
353 | 0 | } |
354 | 0 | Err(CmsError::InvalidProfile) |
355 | 0 | } |
356 | | } |
357 | | |
358 | | impl From<DataColorSpace> for u32 { |
359 | 0 | fn from(val: DataColorSpace) -> Self { |
360 | 0 | match val { |
361 | 0 | DataColorSpace::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(), |
362 | 0 | DataColorSpace::Lab => u32::from_ne_bytes(*b"Lab ").to_be(), |
363 | 0 | DataColorSpace::Luv => u32::from_ne_bytes(*b"Luv ").to_be(), |
364 | 0 | DataColorSpace::YCbr => u32::from_ne_bytes(*b"YCbr").to_be(), |
365 | 0 | DataColorSpace::Yxy => u32::from_ne_bytes(*b"Yxy ").to_be(), |
366 | 0 | DataColorSpace::Rgb => u32::from_ne_bytes(*b"RGB ").to_be(), |
367 | 0 | DataColorSpace::Gray => u32::from_ne_bytes(*b"GRAY").to_be(), |
368 | 0 | DataColorSpace::Hsv => u32::from_ne_bytes(*b"HSV ").to_be(), |
369 | 0 | DataColorSpace::Hls => u32::from_ne_bytes(*b"HLS ").to_be(), |
370 | 0 | DataColorSpace::Cmyk => u32::from_ne_bytes(*b"CMYK").to_be(), |
371 | 0 | DataColorSpace::Cmy => u32::from_ne_bytes(*b"CMY ").to_be(), |
372 | 0 | DataColorSpace::Color2 => u32::from_ne_bytes(*b"2CLR").to_be(), |
373 | 0 | DataColorSpace::Color3 => u32::from_ne_bytes(*b"3CLR").to_be(), |
374 | 0 | DataColorSpace::Color4 => u32::from_ne_bytes(*b"4CLR").to_be(), |
375 | 0 | DataColorSpace::Color5 => u32::from_ne_bytes(*b"5CLR").to_be(), |
376 | 0 | DataColorSpace::Color6 => u32::from_ne_bytes(*b"6CLR").to_be(), |
377 | 0 | DataColorSpace::Color7 => u32::from_ne_bytes(*b"7CLR").to_be(), |
378 | 0 | DataColorSpace::Color8 => u32::from_ne_bytes(*b"8CLR").to_be(), |
379 | 0 | DataColorSpace::Color9 => u32::from_ne_bytes(*b"9CLR").to_be(), |
380 | 0 | DataColorSpace::Color10 => u32::from_ne_bytes(*b"ACLR").to_be(), |
381 | 0 | DataColorSpace::Color11 => u32::from_ne_bytes(*b"BCLR").to_be(), |
382 | 0 | DataColorSpace::Color12 => u32::from_ne_bytes(*b"CCLR").to_be(), |
383 | 0 | DataColorSpace::Color13 => u32::from_ne_bytes(*b"DCLR").to_be(), |
384 | 0 | DataColorSpace::Color14 => u32::from_ne_bytes(*b"ECLR").to_be(), |
385 | 0 | DataColorSpace::Color15 => u32::from_ne_bytes(*b"FCLR").to_be(), |
386 | | } |
387 | 0 | } |
388 | | } |
389 | | |
390 | | #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] |
391 | | pub enum TechnologySignatures { |
392 | | FilmScanner, |
393 | | DigitalCamera, |
394 | | ReflectiveScanner, |
395 | | InkJetPrinter, |
396 | | ThermalWaxPrinter, |
397 | | ElectrophotographicPrinter, |
398 | | ElectrostaticPrinter, |
399 | | DyeSublimationPrinter, |
400 | | PhotographicPaperPrinter, |
401 | | FilmWriter, |
402 | | VideoMonitor, |
403 | | VideoCamera, |
404 | | ProjectionTelevision, |
405 | | CathodeRayTubeDisplay, |
406 | | PassiveMatrixDisplay, |
407 | | ActiveMatrixDisplay, |
408 | | LiquidCrystalDisplay, |
409 | | OrganicLedDisplay, |
410 | | PhotoCd, |
411 | | PhotographicImageSetter, |
412 | | Gravure, |
413 | | OffsetLithography, |
414 | | Silkscreen, |
415 | | Flexography, |
416 | | MotionPictureFilmScanner, |
417 | | MotionPictureFilmRecorder, |
418 | | DigitalMotionPictureCamera, |
419 | | DigitalCinemaProjector, |
420 | | Unknown(u32), |
421 | | } |
422 | | |
423 | | impl From<u32> for TechnologySignatures { |
424 | 0 | fn from(value: u32) -> Self { |
425 | 0 | if value == u32::from_ne_bytes(*b"fscn").to_be() { |
426 | 0 | return TechnologySignatures::FilmScanner; |
427 | 0 | } else if value == u32::from_ne_bytes(*b"dcam").to_be() { |
428 | 0 | return TechnologySignatures::DigitalCamera; |
429 | 0 | } else if value == u32::from_ne_bytes(*b"rscn").to_be() { |
430 | 0 | return TechnologySignatures::ReflectiveScanner; |
431 | 0 | } else if value == u32::from_ne_bytes(*b"ijet").to_be() { |
432 | 0 | return TechnologySignatures::InkJetPrinter; |
433 | 0 | } else if value == u32::from_ne_bytes(*b"twax").to_be() { |
434 | 0 | return TechnologySignatures::ThermalWaxPrinter; |
435 | 0 | } else if value == u32::from_ne_bytes(*b"epho").to_be() { |
436 | 0 | return TechnologySignatures::ElectrophotographicPrinter; |
437 | 0 | } else if value == u32::from_ne_bytes(*b"esta").to_be() { |
438 | 0 | return TechnologySignatures::ElectrostaticPrinter; |
439 | 0 | } else if value == u32::from_ne_bytes(*b"dsub").to_be() { |
440 | 0 | return TechnologySignatures::DyeSublimationPrinter; |
441 | 0 | } else if value == u32::from_ne_bytes(*b"rpho").to_be() { |
442 | 0 | return TechnologySignatures::PhotographicPaperPrinter; |
443 | 0 | } else if value == u32::from_ne_bytes(*b"fprn").to_be() { |
444 | 0 | return TechnologySignatures::FilmWriter; |
445 | 0 | } else if value == u32::from_ne_bytes(*b"vidm").to_be() { |
446 | 0 | return TechnologySignatures::VideoMonitor; |
447 | 0 | } else if value == u32::from_ne_bytes(*b"vidc").to_be() { |
448 | 0 | return TechnologySignatures::VideoCamera; |
449 | 0 | } else if value == u32::from_ne_bytes(*b"pjtv").to_be() { |
450 | 0 | return TechnologySignatures::ProjectionTelevision; |
451 | 0 | } else if value == u32::from_ne_bytes(*b"CRT ").to_be() { |
452 | 0 | return TechnologySignatures::CathodeRayTubeDisplay; |
453 | 0 | } else if value == u32::from_ne_bytes(*b"PMD ").to_be() { |
454 | 0 | return TechnologySignatures::PassiveMatrixDisplay; |
455 | 0 | } else if value == u32::from_ne_bytes(*b"AMD ").to_be() { |
456 | 0 | return TechnologySignatures::ActiveMatrixDisplay; |
457 | 0 | } else if value == u32::from_ne_bytes(*b"LCD ").to_be() { |
458 | 0 | return TechnologySignatures::LiquidCrystalDisplay; |
459 | 0 | } else if value == u32::from_ne_bytes(*b"OLED").to_be() { |
460 | 0 | return TechnologySignatures::OrganicLedDisplay; |
461 | 0 | } else if value == u32::from_ne_bytes(*b"KPCD").to_be() { |
462 | 0 | return TechnologySignatures::PhotoCd; |
463 | 0 | } else if value == u32::from_ne_bytes(*b"imgs").to_be() { |
464 | 0 | return TechnologySignatures::PhotographicImageSetter; |
465 | 0 | } else if value == u32::from_ne_bytes(*b"grav").to_be() { |
466 | 0 | return TechnologySignatures::Gravure; |
467 | 0 | } else if value == u32::from_ne_bytes(*b"offs").to_be() { |
468 | 0 | return TechnologySignatures::OffsetLithography; |
469 | 0 | } else if value == u32::from_ne_bytes(*b"silk").to_be() { |
470 | 0 | return TechnologySignatures::Silkscreen; |
471 | 0 | } else if value == u32::from_ne_bytes(*b"flex").to_be() { |
472 | 0 | return TechnologySignatures::Flexography; |
473 | 0 | } else if value == u32::from_ne_bytes(*b"mpfs").to_be() { |
474 | 0 | return TechnologySignatures::MotionPictureFilmScanner; |
475 | 0 | } else if value == u32::from_ne_bytes(*b"mpfr").to_be() { |
476 | 0 | return TechnologySignatures::MotionPictureFilmRecorder; |
477 | 0 | } else if value == u32::from_ne_bytes(*b"dmpc").to_be() { |
478 | 0 | return TechnologySignatures::DigitalMotionPictureCamera; |
479 | 0 | } else if value == u32::from_ne_bytes(*b"dcpj").to_be() { |
480 | 0 | return TechnologySignatures::DigitalCinemaProjector; |
481 | 0 | } |
482 | 0 | TechnologySignatures::Unknown(value) |
483 | 0 | } |
484 | | } |
485 | | |
486 | | #[derive(Debug, Clone)] |
487 | | pub enum LutWarehouse { |
488 | | Lut(LutDataType), |
489 | | Multidimensional(LutMultidimensionalType), |
490 | | } |
491 | | |
492 | | impl PartialEq for LutWarehouse { |
493 | 0 | fn eq(&self, other: &Self) -> bool { |
494 | 0 | match (self, other) { |
495 | 0 | (LutWarehouse::Lut(a), LutWarehouse::Lut(b)) => a == b, |
496 | 0 | (LutWarehouse::Multidimensional(a), LutWarehouse::Multidimensional(b)) => a == b, |
497 | 0 | _ => false, // Different variants are not equal |
498 | | } |
499 | 0 | } |
500 | | } |
501 | | |
502 | | #[derive(Debug, Clone, PartialEq)] |
503 | | pub struct LutDataType { |
504 | | // used by lut8Type/lut16Type (mft2) only |
505 | | pub num_input_channels: u8, |
506 | | pub num_output_channels: u8, |
507 | | pub num_clut_grid_points: u8, |
508 | | pub matrix: Matrix3d, |
509 | | pub num_input_table_entries: u16, |
510 | | pub num_output_table_entries: u16, |
511 | | pub input_table: LutStore, |
512 | | pub clut_table: LutStore, |
513 | | pub output_table: LutStore, |
514 | | pub lut_type: LutType, |
515 | | } |
516 | | |
517 | | impl LutDataType { |
518 | 0 | pub(crate) fn has_same_kind(&self) -> bool { |
519 | 0 | matches!( |
520 | 0 | (&self.input_table, &self.clut_table, &self.output_table), |
521 | | ( |
522 | | LutStore::Store8(_), |
523 | | LutStore::Store8(_), |
524 | | LutStore::Store8(_) |
525 | | ) | ( |
526 | | LutStore::Store16(_), |
527 | | LutStore::Store16(_), |
528 | | LutStore::Store16(_) |
529 | | ) |
530 | | ) |
531 | 0 | } |
532 | | } |
533 | | |
534 | | #[derive(Debug, Clone, PartialEq)] |
535 | | pub struct LutMultidimensionalType { |
536 | | pub num_input_channels: u8, |
537 | | pub num_output_channels: u8, |
538 | | pub grid_points: [u8; 16], |
539 | | pub clut: Option<LutStore>, |
540 | | pub a_curves: Vec<ToneReprCurve>, |
541 | | pub b_curves: Vec<ToneReprCurve>, |
542 | | pub m_curves: Vec<ToneReprCurve>, |
543 | | pub matrix: Matrix3d, |
544 | | pub bias: Vector3d, |
545 | | } |
546 | | |
547 | | #[repr(u32)] |
548 | | #[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)] |
549 | | pub enum RenderingIntent { |
550 | | AbsoluteColorimetric = 3, |
551 | | Saturation = 2, |
552 | | RelativeColorimetric = 1, |
553 | | #[default] |
554 | | Perceptual = 0, |
555 | | } |
556 | | |
557 | | impl TryFrom<u32> for RenderingIntent { |
558 | | type Error = CmsError; |
559 | | |
560 | | #[inline] |
561 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
562 | 0 | match value { |
563 | 0 | 0 => Ok(RenderingIntent::Perceptual), |
564 | 0 | 1 => Ok(RenderingIntent::RelativeColorimetric), |
565 | 0 | 2 => Ok(RenderingIntent::Saturation), |
566 | 0 | 3 => Ok(RenderingIntent::AbsoluteColorimetric), |
567 | 0 | _ => Err(CmsError::InvalidRenderingIntent), |
568 | | } |
569 | 0 | } |
570 | | } |
571 | | |
572 | | impl From<RenderingIntent> for u32 { |
573 | | #[inline] |
574 | 0 | fn from(value: RenderingIntent) -> Self { |
575 | 0 | match value { |
576 | 0 | RenderingIntent::AbsoluteColorimetric => 3, |
577 | 0 | RenderingIntent::Saturation => 2, |
578 | 0 | RenderingIntent::RelativeColorimetric => 1, |
579 | 0 | RenderingIntent::Perceptual => 0, |
580 | | } |
581 | 0 | } |
582 | | } |
583 | | |
584 | | /// ICC Header |
585 | | #[repr(C)] |
586 | | #[derive(Debug, Clone, Copy)] |
587 | | pub(crate) struct ProfileHeader { |
588 | | pub size: u32, // Size of the profile (computed) |
589 | | pub cmm_type: u32, // Preferred CMM type (ignored) |
590 | | pub version: ProfileVersion, // Version (4.3 or 4.4 if CICP is included) |
591 | | pub profile_class: ProfileClass, // Display device profile |
592 | | pub data_color_space: DataColorSpace, // RGB input color space |
593 | | pub pcs: DataColorSpace, // Profile connection space |
594 | | pub creation_date_time: ColorDateTime, // Date and time |
595 | | pub signature: ProfileSignature, // Profile signature |
596 | | pub platform: u32, // Platform target (ignored) |
597 | | pub flags: u32, // Flags (not embedded, can be used independently) |
598 | | pub device_manufacturer: u32, // Device manufacturer (ignored) |
599 | | pub device_model: u32, // Device model (ignored) |
600 | | pub device_attributes: [u8; 8], // Device attributes (ignored) |
601 | | pub rendering_intent: RenderingIntent, // Relative colorimetric rendering intent |
602 | | pub illuminant: Xyz, // D50 standard illuminant X |
603 | | pub creator: u32, // Profile creator (ignored) |
604 | | pub profile_id: [u8; 16], // Profile id checksum (ignored) |
605 | | pub reserved: [u8; 28], // Reserved (ignored) |
606 | | pub tag_count: u32, // Technically not part of header, but required |
607 | | } |
608 | | |
609 | | impl ProfileHeader { |
610 | | #[allow(dead_code)] |
611 | 0 | pub(crate) fn new(size: u32) -> Self { |
612 | 0 | Self { |
613 | 0 | size, |
614 | 0 | cmm_type: 0, |
615 | 0 | version: ProfileVersion::V4_3, |
616 | 0 | profile_class: ProfileClass::DisplayDevice, |
617 | 0 | data_color_space: DataColorSpace::Rgb, |
618 | 0 | pcs: DataColorSpace::Xyz, |
619 | 0 | creation_date_time: ColorDateTime::default(), |
620 | 0 | signature: ProfileSignature::Acsp, |
621 | 0 | platform: 0, |
622 | 0 | flags: 0x00000000, |
623 | 0 | device_manufacturer: 0, |
624 | 0 | device_model: 0, |
625 | 0 | device_attributes: [0; 8], |
626 | 0 | rendering_intent: RenderingIntent::Perceptual, |
627 | 0 | illuminant: Chromaticity::D50.to_xyz(), |
628 | 0 | creator: 0, |
629 | 0 | profile_id: [0; 16], |
630 | 0 | reserved: [0; 28], |
631 | 0 | tag_count: 0, |
632 | 0 | } |
633 | 0 | } |
634 | | |
635 | | /// Creates profile from the buffer |
636 | 0 | pub(crate) fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> { |
637 | 0 | if slice.len() < size_of::<ProfileHeader>() { |
638 | 0 | return Err(CmsError::InvalidProfile); |
639 | 0 | } |
640 | 0 | let mut cursor = std::io::Cursor::new(slice); |
641 | 0 | let mut buffer = [0u8; size_of::<ProfileHeader>()]; |
642 | 0 | cursor |
643 | 0 | .read_exact(&mut buffer) |
644 | 0 | .map_err(|_| CmsError::InvalidProfile)?; |
645 | | |
646 | 0 | let header = Self { |
647 | 0 | size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()), |
648 | 0 | cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()), |
649 | 0 | version: ProfileVersion::try_from(u32::from_be_bytes( |
650 | 0 | buffer[8..12].try_into().unwrap(), |
651 | 0 | ))?, |
652 | 0 | profile_class: ProfileClass::try_from(u32::from_be_bytes( |
653 | 0 | buffer[12..16].try_into().unwrap(), |
654 | 0 | ))?, |
655 | 0 | data_color_space: DataColorSpace::try_from(u32::from_be_bytes( |
656 | 0 | buffer[16..20].try_into().unwrap(), |
657 | 0 | ))?, |
658 | 0 | pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?, |
659 | 0 | creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?, |
660 | 0 | signature: ProfileSignature::try_from(u32::from_be_bytes( |
661 | 0 | buffer[36..40].try_into().unwrap(), |
662 | 0 | ))?, |
663 | 0 | platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()), |
664 | 0 | flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()), |
665 | 0 | device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()), |
666 | 0 | device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()), |
667 | 0 | device_attributes: buffer[56..64].try_into().unwrap(), |
668 | 0 | rendering_intent: RenderingIntent::try_from(u32::from_be_bytes( |
669 | 0 | buffer[64..68].try_into().unwrap(), |
670 | 0 | ))?, |
671 | 0 | illuminant: Xyz::new( |
672 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())), |
673 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())), |
674 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())), |
675 | | ), |
676 | 0 | creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()), |
677 | 0 | profile_id: buffer[84..100].try_into().unwrap(), |
678 | 0 | reserved: buffer[100..128].try_into().unwrap(), |
679 | 0 | tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()), |
680 | | }; |
681 | 0 | Ok(header) |
682 | 0 | } |
683 | | } |
684 | | |
685 | | /// A [Coding Independent Code Point](https://en.wikipedia.org/wiki/Coding-independent_code_points). |
686 | | #[repr(C)] |
687 | | #[derive(Debug, Clone, Copy)] |
688 | | pub struct CicpProfile { |
689 | | pub color_primaries: CicpColorPrimaries, |
690 | | pub transfer_characteristics: TransferCharacteristics, |
691 | | pub matrix_coefficients: MatrixCoefficients, |
692 | | pub full_range: bool, |
693 | | } |
694 | | |
695 | | #[derive(Debug, Clone)] |
696 | | pub struct LocalizableString { |
697 | | /// An ISO 639-1 value is expected; any text w. more than two symbols will be truncated |
698 | | pub language: String, |
699 | | /// An ISO 3166-1 value is expected; any text w. more than two symbols will be truncated |
700 | | pub country: String, |
701 | | pub value: String, |
702 | | } |
703 | | |
704 | | impl LocalizableString { |
705 | | /// Creates new localizable string |
706 | | /// |
707 | | /// # Arguments |
708 | | /// |
709 | | /// * `language`: an ISO 639-1 value is expected, any text more than 2 symbols will be truncated |
710 | | /// * `country`: an ISO 3166-1 value is expected, any text more than 2 symbols will be truncated |
711 | | /// * `value`: String value |
712 | | /// |
713 | 0 | pub fn new(language: String, country: String, value: String) -> Self { |
714 | 0 | Self { |
715 | 0 | language, |
716 | 0 | country, |
717 | 0 | value, |
718 | 0 | } |
719 | 0 | } |
720 | | } |
721 | | |
722 | | #[derive(Debug, Clone)] |
723 | | pub struct DescriptionString { |
724 | | pub ascii_string: String, |
725 | | pub unicode_language_code: u32, |
726 | | pub unicode_string: String, |
727 | | pub script_code_code: i8, |
728 | | pub mac_string: String, |
729 | | } |
730 | | |
731 | | #[derive(Debug, Clone)] |
732 | | pub enum ProfileText { |
733 | | PlainString(String), |
734 | | Localizable(Vec<LocalizableString>), |
735 | | Description(DescriptionString), |
736 | | } |
737 | | |
738 | | impl ProfileText { |
739 | 0 | pub(crate) fn has_values(&self) -> bool { |
740 | 0 | match self { |
741 | 0 | ProfileText::PlainString(_) => true, |
742 | 0 | ProfileText::Localizable(lc) => !lc.is_empty(), |
743 | 0 | ProfileText::Description(_) => true, |
744 | | } |
745 | 0 | } |
746 | | } |
747 | | |
748 | | #[derive(Debug, Clone, Copy)] |
749 | | pub enum StandardObserver { |
750 | | D50, |
751 | | D65, |
752 | | Unknown, |
753 | | } |
754 | | |
755 | | impl From<u32> for StandardObserver { |
756 | 0 | fn from(value: u32) -> Self { |
757 | 0 | if value == 1 { |
758 | 0 | return StandardObserver::D50; |
759 | 0 | } else if value == 2 { |
760 | 0 | return StandardObserver::D65; |
761 | 0 | } |
762 | 0 | StandardObserver::Unknown |
763 | 0 | } |
764 | | } |
765 | | |
766 | | impl From<StandardObserver> for u32 { |
767 | 0 | fn from(value: StandardObserver) -> Self { |
768 | 0 | match value { |
769 | 0 | StandardObserver::D50 => 1, |
770 | 0 | StandardObserver::D65 => 2, |
771 | 0 | StandardObserver::Unknown => 0, |
772 | | } |
773 | 0 | } |
774 | | } |
775 | | |
776 | | #[derive(Debug, Clone, Copy)] |
777 | | pub struct ViewingConditions { |
778 | | pub illuminant: Xyz, |
779 | | pub surround: Xyz, |
780 | | pub observer: StandardObserver, |
781 | | } |
782 | | |
783 | | #[derive(Debug, Clone, Copy)] |
784 | | pub enum MeasurementGeometry { |
785 | | Unknown, |
786 | | /// 0°:45° or 45°:0° |
787 | | D45to45, |
788 | | /// 0°:d or d:0° |
789 | | D0to0, |
790 | | } |
791 | | |
792 | | impl From<u32> for MeasurementGeometry { |
793 | 0 | fn from(value: u32) -> Self { |
794 | 0 | if value == 1 { |
795 | 0 | Self::D45to45 |
796 | 0 | } else if value == 2 { |
797 | 0 | Self::D0to0 |
798 | | } else { |
799 | 0 | Self::Unknown |
800 | | } |
801 | 0 | } |
802 | | } |
803 | | |
804 | | #[derive(Debug, Clone, Copy)] |
805 | | pub enum StandardIlluminant { |
806 | | Unknown, |
807 | | D50, |
808 | | D65, |
809 | | D93, |
810 | | F2, |
811 | | D55, |
812 | | A, |
813 | | EquiPower, |
814 | | F8, |
815 | | } |
816 | | |
817 | | impl From<u32> for StandardIlluminant { |
818 | 0 | fn from(value: u32) -> Self { |
819 | 0 | match value { |
820 | 0 | 1 => StandardIlluminant::D50, |
821 | 0 | 2 => StandardIlluminant::D65, |
822 | 0 | 3 => StandardIlluminant::D93, |
823 | 0 | 4 => StandardIlluminant::F2, |
824 | 0 | 5 => StandardIlluminant::D55, |
825 | 0 | 6 => StandardIlluminant::A, |
826 | 0 | 7 => StandardIlluminant::EquiPower, |
827 | 0 | 8 => StandardIlluminant::F8, |
828 | 0 | _ => Self::Unknown, |
829 | | } |
830 | 0 | } |
831 | | } |
832 | | |
833 | | impl From<StandardIlluminant> for u32 { |
834 | 0 | fn from(value: StandardIlluminant) -> Self { |
835 | 0 | match value { |
836 | 0 | StandardIlluminant::Unknown => 0u32, |
837 | 0 | StandardIlluminant::D50 => 1u32, |
838 | 0 | StandardIlluminant::D65 => 2u32, |
839 | 0 | StandardIlluminant::D93 => 3, |
840 | 0 | StandardIlluminant::F2 => 4, |
841 | 0 | StandardIlluminant::D55 => 5, |
842 | 0 | StandardIlluminant::A => 6, |
843 | 0 | StandardIlluminant::EquiPower => 7, |
844 | 0 | StandardIlluminant::F8 => 8, |
845 | | } |
846 | 0 | } |
847 | | } |
848 | | |
849 | | #[derive(Debug, Clone, Copy)] |
850 | | pub struct Measurement { |
851 | | pub observer: StandardObserver, |
852 | | pub backing: Xyz, |
853 | | pub geometry: MeasurementGeometry, |
854 | | pub flare: f32, |
855 | | pub illuminant: StandardIlluminant, |
856 | | } |
857 | | |
858 | | /// ICC Profile representation |
859 | | #[repr(C)] |
860 | | #[derive(Debug, Clone, Default)] |
861 | | pub struct ColorProfile { |
862 | | pub pcs: DataColorSpace, |
863 | | pub color_space: DataColorSpace, |
864 | | pub profile_class: ProfileClass, |
865 | | pub rendering_intent: RenderingIntent, |
866 | | pub red_colorant: Xyzd, |
867 | | pub green_colorant: Xyzd, |
868 | | pub blue_colorant: Xyzd, |
869 | | pub white_point: Xyzd, |
870 | | pub black_point: Option<Xyzd>, |
871 | | pub media_white_point: Option<Xyzd>, |
872 | | pub luminance: Option<Xyzd>, |
873 | | pub measurement: Option<Measurement>, |
874 | | pub red_trc: Option<ToneReprCurve>, |
875 | | pub green_trc: Option<ToneReprCurve>, |
876 | | pub blue_trc: Option<ToneReprCurve>, |
877 | | pub gray_trc: Option<ToneReprCurve>, |
878 | | pub cicp: Option<CicpProfile>, |
879 | | pub chromatic_adaptation: Option<Matrix3d>, |
880 | | pub lut_a_to_b_perceptual: Option<LutWarehouse>, |
881 | | pub lut_a_to_b_colorimetric: Option<LutWarehouse>, |
882 | | pub lut_a_to_b_saturation: Option<LutWarehouse>, |
883 | | pub lut_b_to_a_perceptual: Option<LutWarehouse>, |
884 | | pub lut_b_to_a_colorimetric: Option<LutWarehouse>, |
885 | | pub lut_b_to_a_saturation: Option<LutWarehouse>, |
886 | | pub gamut: Option<LutWarehouse>, |
887 | | pub copyright: Option<ProfileText>, |
888 | | pub description: Option<ProfileText>, |
889 | | pub device_manufacturer: Option<ProfileText>, |
890 | | pub device_model: Option<ProfileText>, |
891 | | pub char_target: Option<ProfileText>, |
892 | | pub viewing_conditions: Option<ViewingConditions>, |
893 | | pub viewing_conditions_description: Option<ProfileText>, |
894 | | pub technology: Option<TechnologySignatures>, |
895 | | pub calibration_date: Option<ColorDateTime>, |
896 | | /// Version for internal and viewing purposes only. |
897 | | /// On encoding added value to profile will always be V4. |
898 | | pub(crate) version_internal: ProfileVersion, |
899 | | } |
900 | | |
901 | | #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)] |
902 | | pub struct ParsingOptions { |
903 | | // Maximum allowed profile size in bytes |
904 | | pub max_profile_size: usize, |
905 | | // Maximum allowed CLUT size in bytes |
906 | | pub max_allowed_clut_size: usize, |
907 | | // Maximum allowed TRC size in elements count |
908 | | pub max_allowed_trc_size: usize, |
909 | | } |
910 | | |
911 | | impl Default for ParsingOptions { |
912 | 0 | fn default() -> Self { |
913 | 0 | Self { |
914 | 0 | max_profile_size: MAX_PROFILE_SIZE, |
915 | 0 | max_allowed_clut_size: 10_000_000, |
916 | 0 | max_allowed_trc_size: 40_000, |
917 | 0 | } |
918 | 0 | } |
919 | | } |
920 | | |
921 | | impl ColorProfile { |
922 | | /// Returns profile version |
923 | 0 | pub fn version(&self) -> ProfileVersion { |
924 | 0 | self.version_internal |
925 | 0 | } |
926 | | |
927 | 0 | pub fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> { |
928 | 0 | Self::new_from_slice_with_options(slice, Default::default()) |
929 | 0 | } |
930 | | |
931 | 0 | pub fn new_from_slice_with_options( |
932 | 0 | slice: &[u8], |
933 | 0 | options: ParsingOptions, |
934 | 0 | ) -> Result<Self, CmsError> { |
935 | 0 | let header = ProfileHeader::new_from_slice(slice)?; |
936 | 0 | let tags_count = header.tag_count as usize; |
937 | 0 | if slice.len() >= options.max_profile_size { |
938 | 0 | return Err(CmsError::InvalidProfile); |
939 | 0 | } |
940 | 0 | let tags_end = tags_count |
941 | 0 | .safe_mul(TAG_SIZE)? |
942 | 0 | .safe_add(size_of::<ProfileHeader>())?; |
943 | 0 | if slice.len() < tags_end { |
944 | 0 | return Err(CmsError::InvalidProfile); |
945 | 0 | } |
946 | 0 | let tags_slice = &slice[size_of::<ProfileHeader>()..tags_end]; |
947 | 0 | let mut profile = ColorProfile { |
948 | 0 | rendering_intent: header.rendering_intent, |
949 | 0 | pcs: header.pcs, |
950 | 0 | profile_class: header.profile_class, |
951 | 0 | color_space: header.data_color_space, |
952 | 0 | white_point: header.illuminant.to_xyzd(), |
953 | 0 | version_internal: header.version, |
954 | 0 | ..Default::default() |
955 | 0 | }; |
956 | 0 | let color_space = profile.color_space; |
957 | 0 | for tag in tags_slice.chunks_exact(TAG_SIZE) { |
958 | 0 | let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); |
959 | 0 | let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]); |
960 | 0 | let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; |
961 | | // Just ignore unknown tags |
962 | 0 | if let Ok(tag) = Tag::try_from(tag_value) { |
963 | 0 | match tag { |
964 | | Tag::RedXyz => { |
965 | 0 | if color_space == DataColorSpace::Rgb { |
966 | | profile.red_colorant = |
967 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
968 | 0 | } |
969 | | } |
970 | | Tag::GreenXyz => { |
971 | 0 | if color_space == DataColorSpace::Rgb { |
972 | | profile.green_colorant = |
973 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
974 | 0 | } |
975 | | } |
976 | | Tag::BlueXyz => { |
977 | 0 | if color_space == DataColorSpace::Rgb { |
978 | | profile.blue_colorant = |
979 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
980 | 0 | } |
981 | | } |
982 | | Tag::RedToneReproduction => { |
983 | 0 | if color_space == DataColorSpace::Rgb { |
984 | 0 | profile.red_trc = Self::read_trc_tag_s( |
985 | 0 | slice, |
986 | 0 | tag_entry as usize, |
987 | 0 | tag_size, |
988 | 0 | &options, |
989 | 0 | )?; |
990 | 0 | } |
991 | | } |
992 | | Tag::GreenToneReproduction => { |
993 | 0 | if color_space == DataColorSpace::Rgb { |
994 | 0 | profile.green_trc = Self::read_trc_tag_s( |
995 | 0 | slice, |
996 | 0 | tag_entry as usize, |
997 | 0 | tag_size, |
998 | 0 | &options, |
999 | 0 | )?; |
1000 | 0 | } |
1001 | | } |
1002 | | Tag::BlueToneReproduction => { |
1003 | 0 | if color_space == DataColorSpace::Rgb { |
1004 | 0 | profile.blue_trc = Self::read_trc_tag_s( |
1005 | 0 | slice, |
1006 | 0 | tag_entry as usize, |
1007 | 0 | tag_size, |
1008 | 0 | &options, |
1009 | 0 | )?; |
1010 | 0 | } |
1011 | | } |
1012 | | Tag::GreyToneReproduction => { |
1013 | 0 | if color_space == DataColorSpace::Gray { |
1014 | 0 | profile.gray_trc = Self::read_trc_tag_s( |
1015 | 0 | slice, |
1016 | 0 | tag_entry as usize, |
1017 | 0 | tag_size, |
1018 | 0 | &options, |
1019 | 0 | )?; |
1020 | 0 | } |
1021 | | } |
1022 | | Tag::MediaWhitePoint => { |
1023 | | profile.media_white_point = |
1024 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; |
1025 | | } |
1026 | | Tag::Luminance => { |
1027 | | profile.luminance = |
1028 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; |
1029 | | } |
1030 | | Tag::Measurement => { |
1031 | | profile.measurement = |
1032 | 0 | Self::read_meas_tag(slice, tag_entry as usize, tag_size)?; |
1033 | | } |
1034 | | Tag::CodeIndependentPoints => { |
1035 | | // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the |
1036 | | // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces |
1037 | | // or profile classes indicated in the profile header. |
1038 | 0 | if (profile.profile_class == ProfileClass::InputDevice |
1039 | 0 | || profile.profile_class == ProfileClass::DisplayDevice) |
1040 | 0 | && (profile.color_space == DataColorSpace::Rgb |
1041 | 0 | || profile.color_space == DataColorSpace::YCbr |
1042 | 0 | || profile.color_space == DataColorSpace::Xyz) |
1043 | | { |
1044 | | profile.cicp = |
1045 | 0 | Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?; |
1046 | 0 | } |
1047 | | } |
1048 | | Tag::ChromaticAdaptation => { |
1049 | | profile.chromatic_adaptation = |
1050 | 0 | Self::read_chad_tag(slice, tag_entry as usize, tag_size)?; |
1051 | | } |
1052 | | Tag::BlackPoint => { |
1053 | | profile.black_point = |
1054 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)? |
1055 | | } |
1056 | | Tag::DeviceToPcsLutPerceptual => { |
1057 | 0 | profile.lut_a_to_b_perceptual = |
1058 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1059 | | } |
1060 | | Tag::DeviceToPcsLutColorimetric => { |
1061 | 0 | profile.lut_a_to_b_colorimetric = |
1062 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1063 | | } |
1064 | | Tag::DeviceToPcsLutSaturation => { |
1065 | 0 | profile.lut_a_to_b_saturation = |
1066 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1067 | | } |
1068 | | Tag::PcsToDeviceLutPerceptual => { |
1069 | 0 | profile.lut_b_to_a_perceptual = |
1070 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1071 | | } |
1072 | | Tag::PcsToDeviceLutColorimetric => { |
1073 | 0 | profile.lut_b_to_a_colorimetric = |
1074 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1075 | | } |
1076 | | Tag::PcsToDeviceLutSaturation => { |
1077 | 0 | profile.lut_b_to_a_saturation = |
1078 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1079 | | } |
1080 | | Tag::Gamut => { |
1081 | 0 | profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1082 | | } |
1083 | | Tag::Copyright => { |
1084 | 0 | profile.copyright = |
1085 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1086 | | } |
1087 | | Tag::ProfileDescription => { |
1088 | 0 | profile.description = |
1089 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1090 | | } |
1091 | | Tag::ViewingConditionsDescription => { |
1092 | 0 | profile.viewing_conditions_description = |
1093 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1094 | | } |
1095 | | Tag::DeviceModel => { |
1096 | 0 | profile.device_model = |
1097 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1098 | | } |
1099 | | Tag::DeviceManufacturer => { |
1100 | 0 | profile.device_manufacturer = |
1101 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1102 | | } |
1103 | | Tag::CharTarget => { |
1104 | 0 | profile.char_target = |
1105 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1106 | | } |
1107 | 0 | Tag::Chromaticity => {} |
1108 | | Tag::ObserverConditions => { |
1109 | | profile.viewing_conditions = |
1110 | 0 | Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?; |
1111 | | } |
1112 | | Tag::Technology => { |
1113 | | profile.technology = |
1114 | 0 | Self::read_tech_tag(slice, tag_entry as usize, tag_size)?; |
1115 | | } |
1116 | | Tag::CalibrationDateTime => { |
1117 | | profile.calibration_date = |
1118 | 0 | Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?; |
1119 | | } |
1120 | | } |
1121 | 0 | } |
1122 | | } |
1123 | | |
1124 | 0 | Ok(profile) |
1125 | 0 | } |
1126 | | } |
1127 | | |
1128 | | impl ColorProfile { |
1129 | | #[inline] |
1130 | 0 | pub fn colorant_matrix(&self) -> Matrix3d { |
1131 | 0 | Matrix3d { |
1132 | 0 | v: [ |
1133 | 0 | [ |
1134 | 0 | self.red_colorant.x, |
1135 | 0 | self.green_colorant.x, |
1136 | 0 | self.blue_colorant.x, |
1137 | 0 | ], |
1138 | 0 | [ |
1139 | 0 | self.red_colorant.y, |
1140 | 0 | self.green_colorant.y, |
1141 | 0 | self.blue_colorant.y, |
1142 | 0 | ], |
1143 | 0 | [ |
1144 | 0 | self.red_colorant.z, |
1145 | 0 | self.green_colorant.z, |
1146 | 0 | self.blue_colorant.z, |
1147 | 0 | ], |
1148 | 0 | ], |
1149 | 0 | } |
1150 | 0 | } |
1151 | | |
1152 | | /// Computes colorants matrix. Returns not transposed matrix. |
1153 | | /// |
1154 | | /// To work on `const` context this method does have restrictions. |
1155 | | /// If invalid values were provided it may return invalid matrix or NaNs. |
1156 | 0 | pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d { |
1157 | 0 | let red_xyz = primaries.red.to_xyzd(); |
1158 | 0 | let green_xyz = primaries.green.to_xyzd(); |
1159 | 0 | let blue_xyz = primaries.blue.to_xyzd(); |
1160 | | |
1161 | 0 | let xyz_matrix = Matrix3d { |
1162 | 0 | v: [ |
1163 | 0 | [red_xyz.x, green_xyz.x, blue_xyz.x], |
1164 | 0 | [red_xyz.y, green_xyz.y, blue_xyz.y], |
1165 | 0 | [red_xyz.z, green_xyz.z, blue_xyz.z], |
1166 | 0 | ], |
1167 | 0 | }; |
1168 | 0 | let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); |
1169 | 0 | adapt_to_d50_d(colorants, white_point) |
1170 | 0 | } |
1171 | | |
1172 | | /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point |
1173 | 0 | pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) { |
1174 | 0 | let red_xyz = primaries.red.to_xyzd(); |
1175 | 0 | let green_xyz = primaries.green.to_xyzd(); |
1176 | 0 | let blue_xyz = primaries.blue.to_xyzd(); |
1177 | | |
1178 | 0 | self.chromatic_adaptation = Some(BRADFORD_D); |
1179 | 0 | self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz) |
1180 | 0 | } |
1181 | | |
1182 | | /// Updates RGB triple colorimetry from 3 [Xyzd] and white point |
1183 | | /// |
1184 | | /// To work on `const` context this method does have restrictions. |
1185 | | /// If invalid values were provided it may return invalid matrix or NaNs. |
1186 | 0 | pub const fn update_rgb_colorimetry_triplet( |
1187 | 0 | &mut self, |
1188 | 0 | white_point: XyY, |
1189 | 0 | red_xyz: Xyzd, |
1190 | 0 | green_xyz: Xyzd, |
1191 | 0 | blue_xyz: Xyzd, |
1192 | 0 | ) { |
1193 | 0 | let xyz_matrix = Matrix3d { |
1194 | 0 | v: [ |
1195 | 0 | [red_xyz.x, green_xyz.x, blue_xyz.x], |
1196 | 0 | [red_xyz.y, green_xyz.y, blue_xyz.y], |
1197 | 0 | [red_xyz.z, green_xyz.z, blue_xyz.z], |
1198 | 0 | ], |
1199 | 0 | }; |
1200 | 0 | let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); |
1201 | 0 | let colorants = adapt_to_d50_d(colorants, white_point); |
1202 | | |
1203 | 0 | self.update_colorants(colorants); |
1204 | 0 | } |
1205 | | |
1206 | 0 | pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) { |
1207 | | // note: there's a transpose type of operation going on here |
1208 | 0 | self.red_colorant.x = colorants.v[0][0]; |
1209 | 0 | self.red_colorant.y = colorants.v[1][0]; |
1210 | 0 | self.red_colorant.z = colorants.v[2][0]; |
1211 | 0 | self.green_colorant.x = colorants.v[0][1]; |
1212 | 0 | self.green_colorant.y = colorants.v[1][1]; |
1213 | 0 | self.green_colorant.z = colorants.v[2][1]; |
1214 | 0 | self.blue_colorant.x = colorants.v[0][2]; |
1215 | 0 | self.blue_colorant.y = colorants.v[1][2]; |
1216 | 0 | self.blue_colorant.z = colorants.v[2][2]; |
1217 | 0 | } |
1218 | | |
1219 | | /// Updates RGB triple colorimetry from CICP |
1220 | 0 | pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool { |
1221 | 0 | self.cicp = Some(cicp); |
1222 | 0 | if !cicp.color_primaries.has_chromaticity() |
1223 | 0 | || !cicp.transfer_characteristics.has_transfer_curve() |
1224 | | { |
1225 | 0 | return false; |
1226 | 0 | } |
1227 | 0 | let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() { |
1228 | 0 | Ok(primaries) => primaries, |
1229 | 0 | Err(_) => return false, |
1230 | | }; |
1231 | 0 | let white_point: Chromaticity = match cicp.color_primaries.white_point() { |
1232 | 0 | Ok(v) => v, |
1233 | 0 | Err(_) => return false, |
1234 | | }; |
1235 | 0 | self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy); |
1236 | | |
1237 | 0 | let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() { |
1238 | 0 | Ok(trc) => trc, |
1239 | 0 | Err(_) => return false, |
1240 | | }; |
1241 | 0 | self.green_trc = Some(red_trc.clone()); |
1242 | 0 | self.blue_trc = Some(red_trc.clone()); |
1243 | 0 | self.red_trc = Some(red_trc); |
1244 | 0 | false |
1245 | 0 | } |
1246 | | |
1247 | 0 | pub const fn rgb_to_xyz(&self, xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { |
1248 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1249 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector()); |
1250 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1251 | 0 | v = v.mul_row_vector::<1>(s); |
1252 | 0 | v.mul_row_vector::<2>(s) |
1253 | 0 | } |
1254 | | |
1255 | | ///TODO: make primary instead of [rgb_to_xyz] in the next major version |
1256 | 0 | pub(crate) const fn rgb_to_xyz_static(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { |
1257 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1258 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector()); |
1259 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1260 | 0 | v = v.mul_row_vector::<1>(s); |
1261 | 0 | v.mul_row_vector::<2>(s) |
1262 | 0 | } |
1263 | | |
1264 | | /// If Primaries is invalid will return invalid matrix on const context. |
1265 | | /// This assumes not transposed matrix and returns not transposed matrix. |
1266 | 0 | pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d { |
1267 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1268 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector_d()); |
1269 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1270 | 0 | v = v.mul_row_vector::<1>(s); |
1271 | 0 | v = v.mul_row_vector::<2>(s); |
1272 | 0 | v |
1273 | 0 | } |
1274 | | |
1275 | 0 | pub fn rgb_to_xyz_matrix(&self) -> Matrix3d { |
1276 | 0 | let xyz_matrix = self.colorant_matrix(); |
1277 | 0 | let white_point = Chromaticity::D50.to_xyzd(); |
1278 | 0 | ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point) |
1279 | 0 | } |
1280 | | |
1281 | | /// Computes transform matrix RGB -> XYZ -> RGB |
1282 | | /// Current profile is used as source, other as destination |
1283 | 0 | pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d { |
1284 | 0 | let source = self.rgb_to_xyz_matrix(); |
1285 | 0 | let dst = dest.rgb_to_xyz_matrix(); |
1286 | 0 | let dest_inverse = dst.inverse(); |
1287 | 0 | dest_inverse.mat_mul(source) |
1288 | 0 | } |
1289 | | |
1290 | | /// Returns volume of colors stored in profile |
1291 | 0 | pub fn profile_volume(&self) -> Option<f32> { |
1292 | 0 | let red_prim = self.red_colorant; |
1293 | 0 | let green_prim = self.green_colorant; |
1294 | 0 | let blue_prim = self.blue_colorant; |
1295 | 0 | let tetrahedral_vertices = Matrix3d { |
1296 | 0 | v: [ |
1297 | 0 | [red_prim.x, red_prim.y, red_prim.z], |
1298 | 0 | [green_prim.x, green_prim.y, green_prim.z], |
1299 | 0 | [blue_prim.x, blue_prim.y, blue_prim.z], |
1300 | 0 | ], |
1301 | 0 | }; |
1302 | 0 | let det = tetrahedral_vertices.determinant()?; |
1303 | 0 | Some((det / 6.0f64) as f32) |
1304 | 0 | } |
1305 | | |
1306 | 0 | pub(crate) fn has_device_to_pcs_lut(&self) -> bool { |
1307 | 0 | self.lut_a_to_b_perceptual.is_some() |
1308 | 0 | || self.lut_a_to_b_saturation.is_some() |
1309 | 0 | || self.lut_a_to_b_colorimetric.is_some() |
1310 | 0 | } |
1311 | | |
1312 | 0 | pub(crate) fn has_pcs_to_device_lut(&self) -> bool { |
1313 | 0 | self.lut_b_to_a_perceptual.is_some() |
1314 | 0 | || self.lut_b_to_a_saturation.is_some() |
1315 | 0 | || self.lut_b_to_a_colorimetric.is_some() |
1316 | 0 | } |
1317 | | } |
1318 | | |
1319 | | #[cfg(test)] |
1320 | | mod tests { |
1321 | | use super::*; |
1322 | | use std::fs; |
1323 | | |
1324 | | #[test] |
1325 | | fn test_gray() { |
1326 | | if let Ok(gray_icc) = fs::read("./assets/Generic Gray Gamma 2.2 Profile.icc") { |
1327 | | let f_p = ColorProfile::new_from_slice(&gray_icc).unwrap(); |
1328 | | assert!(f_p.gray_trc.is_some()); |
1329 | | } |
1330 | | } |
1331 | | |
1332 | | #[test] |
1333 | | fn test_perceptual() { |
1334 | | if let Ok(srgb_perceptual_icc) = fs::read("./assets/srgb_perceptual.icc") { |
1335 | | let f_p = ColorProfile::new_from_slice(&srgb_perceptual_icc).unwrap(); |
1336 | | assert_eq!(f_p.pcs, DataColorSpace::Lab); |
1337 | | assert_eq!(f_p.color_space, DataColorSpace::Rgb); |
1338 | | assert_eq!(f_p.version(), ProfileVersion::V4_2); |
1339 | | assert!(f_p.lut_a_to_b_perceptual.is_some()); |
1340 | | assert!(f_p.lut_b_to_a_perceptual.is_some()); |
1341 | | } |
1342 | | } |
1343 | | |
1344 | | #[test] |
1345 | | fn test_us_swop_coated() { |
1346 | | if let Ok(us_swop_coated) = fs::read("./assets/us_swop_coated.icc") { |
1347 | | let f_p = ColorProfile::new_from_slice(&us_swop_coated).unwrap(); |
1348 | | assert_eq!(f_p.pcs, DataColorSpace::Lab); |
1349 | | assert_eq!(f_p.color_space, DataColorSpace::Cmyk); |
1350 | | assert_eq!(f_p.version(), ProfileVersion::V2_0); |
1351 | | |
1352 | | assert!(f_p.lut_a_to_b_perceptual.is_some()); |
1353 | | assert!(f_p.lut_b_to_a_perceptual.is_some()); |
1354 | | |
1355 | | assert!(f_p.lut_a_to_b_colorimetric.is_some()); |
1356 | | assert!(f_p.lut_b_to_a_colorimetric.is_some()); |
1357 | | |
1358 | | assert!(f_p.gamut.is_some()); |
1359 | | |
1360 | | assert!(f_p.copyright.is_some()); |
1361 | | assert!(f_p.description.is_some()); |
1362 | | } |
1363 | | } |
1364 | | |
1365 | | #[test] |
1366 | | fn test_matrix_shaper() { |
1367 | | if let Ok(matrix_shaper) = fs::read("./assets/Display P3.icc") { |
1368 | | let f_p = ColorProfile::new_from_slice(&matrix_shaper).unwrap(); |
1369 | | assert_eq!(f_p.pcs, DataColorSpace::Xyz); |
1370 | | assert_eq!(f_p.color_space, DataColorSpace::Rgb); |
1371 | | assert_eq!(f_p.version(), ProfileVersion::V4_0); |
1372 | | |
1373 | | assert!(f_p.red_trc.is_some()); |
1374 | | assert!(f_p.blue_trc.is_some()); |
1375 | | assert!(f_p.green_trc.is_some()); |
1376 | | |
1377 | | assert_ne!(f_p.red_colorant, Xyzd::default()); |
1378 | | assert_ne!(f_p.blue_colorant, Xyzd::default()); |
1379 | | assert_ne!(f_p.green_colorant, Xyzd::default()); |
1380 | | |
1381 | | assert!(f_p.copyright.is_some()); |
1382 | | assert!(f_p.description.is_some()); |
1383 | | } |
1384 | | } |
1385 | | } |