/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.7.9/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, |
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)] |
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 | | #[derive(Debug, Clone)] |
493 | | pub struct LutDataType { |
494 | | // used by lut8Type/lut16Type (mft2) only |
495 | | pub num_input_channels: u8, |
496 | | pub num_output_channels: u8, |
497 | | pub num_clut_grid_points: u8, |
498 | | pub matrix: Matrix3d, |
499 | | pub num_input_table_entries: u16, |
500 | | pub num_output_table_entries: u16, |
501 | | pub input_table: LutStore, |
502 | | pub clut_table: LutStore, |
503 | | pub output_table: LutStore, |
504 | | pub lut_type: LutType, |
505 | | } |
506 | | |
507 | | impl LutDataType { |
508 | 0 | pub(crate) fn has_same_kind(&self) -> bool { |
509 | 0 | matches!( |
510 | 0 | (&self.input_table, &self.clut_table, &self.output_table), |
511 | | ( |
512 | | LutStore::Store8(_), |
513 | | LutStore::Store8(_), |
514 | | LutStore::Store8(_) |
515 | | ) | ( |
516 | | LutStore::Store16(_), |
517 | | LutStore::Store16(_), |
518 | | LutStore::Store16(_) |
519 | | ) |
520 | | ) |
521 | 0 | } |
522 | | } |
523 | | |
524 | | #[derive(Debug, Clone)] |
525 | | pub struct LutMultidimensionalType { |
526 | | pub num_input_channels: u8, |
527 | | pub num_output_channels: u8, |
528 | | pub grid_points: [u8; 16], |
529 | | pub clut: Option<LutStore>, |
530 | | pub a_curves: Vec<ToneReprCurve>, |
531 | | pub b_curves: Vec<ToneReprCurve>, |
532 | | pub m_curves: Vec<ToneReprCurve>, |
533 | | pub matrix: Matrix3d, |
534 | | pub bias: Vector3d, |
535 | | } |
536 | | |
537 | | #[repr(u32)] |
538 | | #[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)] |
539 | | pub enum RenderingIntent { |
540 | | AbsoluteColorimetric = 3, |
541 | | Saturation = 2, |
542 | | RelativeColorimetric = 1, |
543 | | #[default] |
544 | | Perceptual = 0, |
545 | | } |
546 | | |
547 | | impl TryFrom<u32> for RenderingIntent { |
548 | | type Error = CmsError; |
549 | | |
550 | | #[inline] |
551 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
552 | 0 | match value { |
553 | 0 | 0 => Ok(RenderingIntent::Perceptual), |
554 | 0 | 1 => Ok(RenderingIntent::RelativeColorimetric), |
555 | 0 | 2 => Ok(RenderingIntent::Saturation), |
556 | 0 | 3 => Ok(RenderingIntent::AbsoluteColorimetric), |
557 | 0 | _ => Err(CmsError::InvalidRenderingIntent), |
558 | | } |
559 | 0 | } |
560 | | } |
561 | | |
562 | | impl From<RenderingIntent> for u32 { |
563 | | #[inline] |
564 | 0 | fn from(value: RenderingIntent) -> Self { |
565 | 0 | match value { |
566 | 0 | RenderingIntent::AbsoluteColorimetric => 3, |
567 | 0 | RenderingIntent::Saturation => 2, |
568 | 0 | RenderingIntent::RelativeColorimetric => 1, |
569 | 0 | RenderingIntent::Perceptual => 0, |
570 | | } |
571 | 0 | } |
572 | | } |
573 | | |
574 | | /// ICC Header |
575 | | #[repr(C)] |
576 | | #[derive(Debug, Clone, Copy)] |
577 | | pub(crate) struct ProfileHeader { |
578 | | pub size: u32, // Size of the profile (computed) |
579 | | pub cmm_type: u32, // Preferred CMM type (ignored) |
580 | | pub version: ProfileVersion, // Version (4.3 or 4.4 if CICP is included) |
581 | | pub profile_class: ProfileClass, // Display device profile |
582 | | pub data_color_space: DataColorSpace, // RGB input color space |
583 | | pub pcs: DataColorSpace, // Profile connection space |
584 | | pub creation_date_time: ColorDateTime, // Date and time |
585 | | pub signature: ProfileSignature, // Profile signature |
586 | | pub platform: u32, // Platform target (ignored) |
587 | | pub flags: u32, // Flags (not embedded, can be used independently) |
588 | | pub device_manufacturer: u32, // Device manufacturer (ignored) |
589 | | pub device_model: u32, // Device model (ignored) |
590 | | pub device_attributes: [u8; 8], // Device attributes (ignored) |
591 | | pub rendering_intent: RenderingIntent, // Relative colorimetric rendering intent |
592 | | pub illuminant: Xyz, // D50 standard illuminant X |
593 | | pub creator: u32, // Profile creator (ignored) |
594 | | pub profile_id: [u8; 16], // Profile id checksum (ignored) |
595 | | pub reserved: [u8; 28], // Reserved (ignored) |
596 | | pub tag_count: u32, // Technically not part of header, but required |
597 | | } |
598 | | |
599 | | impl ProfileHeader { |
600 | | #[allow(dead_code)] |
601 | 0 | pub(crate) fn new(size: u32) -> Self { |
602 | 0 | Self { |
603 | 0 | size, |
604 | 0 | cmm_type: 0, |
605 | 0 | version: ProfileVersion::V4_3, |
606 | 0 | profile_class: ProfileClass::DisplayDevice, |
607 | 0 | data_color_space: DataColorSpace::Rgb, |
608 | 0 | pcs: DataColorSpace::Xyz, |
609 | 0 | creation_date_time: ColorDateTime::default(), |
610 | 0 | signature: ProfileSignature::Acsp, |
611 | 0 | platform: 0, |
612 | 0 | flags: 0x00000000, |
613 | 0 | device_manufacturer: 0, |
614 | 0 | device_model: 0, |
615 | 0 | device_attributes: [0; 8], |
616 | 0 | rendering_intent: RenderingIntent::Perceptual, |
617 | 0 | illuminant: Chromaticity::D50.to_xyz(), |
618 | 0 | creator: 0, |
619 | 0 | profile_id: [0; 16], |
620 | 0 | reserved: [0; 28], |
621 | 0 | tag_count: 0, |
622 | 0 | } |
623 | 0 | } |
624 | | |
625 | | /// Creates profile from the buffer |
626 | 0 | pub(crate) fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> { |
627 | 0 | if slice.len() < size_of::<ProfileHeader>() { |
628 | 0 | return Err(CmsError::InvalidProfile); |
629 | 0 | } |
630 | 0 | let mut cursor = std::io::Cursor::new(slice); |
631 | 0 | let mut buffer = [0u8; size_of::<ProfileHeader>()]; |
632 | 0 | cursor |
633 | 0 | .read_exact(&mut buffer) |
634 | 0 | .map_err(|_| CmsError::InvalidProfile)?; |
635 | | |
636 | 0 | let header = Self { |
637 | 0 | size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()), |
638 | 0 | cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()), |
639 | 0 | version: ProfileVersion::try_from(u32::from_be_bytes( |
640 | 0 | buffer[8..12].try_into().unwrap(), |
641 | 0 | ))?, |
642 | 0 | profile_class: ProfileClass::try_from(u32::from_be_bytes( |
643 | 0 | buffer[12..16].try_into().unwrap(), |
644 | 0 | ))?, |
645 | 0 | data_color_space: DataColorSpace::try_from(u32::from_be_bytes( |
646 | 0 | buffer[16..20].try_into().unwrap(), |
647 | 0 | ))?, |
648 | 0 | pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?, |
649 | 0 | creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?, |
650 | 0 | signature: ProfileSignature::try_from(u32::from_be_bytes( |
651 | 0 | buffer[36..40].try_into().unwrap(), |
652 | 0 | ))?, |
653 | 0 | platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()), |
654 | 0 | flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()), |
655 | 0 | device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()), |
656 | 0 | device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()), |
657 | 0 | device_attributes: buffer[56..64].try_into().unwrap(), |
658 | 0 | rendering_intent: RenderingIntent::try_from(u32::from_be_bytes( |
659 | 0 | buffer[64..68].try_into().unwrap(), |
660 | 0 | ))?, |
661 | 0 | illuminant: Xyz::new( |
662 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())), |
663 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())), |
664 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())), |
665 | | ), |
666 | 0 | creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()), |
667 | 0 | profile_id: buffer[84..100].try_into().unwrap(), |
668 | 0 | reserved: buffer[100..128].try_into().unwrap(), |
669 | 0 | tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()), |
670 | | }; |
671 | 0 | Ok(header) |
672 | 0 | } |
673 | | } |
674 | | |
675 | | /// A [Coding Independent Code Point](https://en.wikipedia.org/wiki/Coding-independent_code_points). |
676 | | #[repr(C)] |
677 | | #[derive(Debug, Clone, Copy)] |
678 | | pub struct CicpProfile { |
679 | | pub color_primaries: CicpColorPrimaries, |
680 | | pub transfer_characteristics: TransferCharacteristics, |
681 | | pub matrix_coefficients: MatrixCoefficients, |
682 | | pub full_range: bool, |
683 | | } |
684 | | |
685 | | #[derive(Debug, Clone)] |
686 | | pub struct LocalizableString { |
687 | | /// An ISO 639-1 value is expected; any text w. more than two symbols will be truncated |
688 | | pub language: String, |
689 | | /// An ISO 3166-1 value is expected; any text w. more than two symbols will be truncated |
690 | | pub country: String, |
691 | | pub value: String, |
692 | | } |
693 | | |
694 | | impl LocalizableString { |
695 | | /// Creates new localizable string |
696 | | /// |
697 | | /// # Arguments |
698 | | /// |
699 | | /// * `language`: an ISO 639-1 value is expected, any text more than 2 symbols will be truncated |
700 | | /// * `country`: an ISO 3166-1 value is expected, any text more than 2 symbols will be truncated |
701 | | /// * `value`: String value |
702 | | /// |
703 | 0 | pub fn new(language: String, country: String, value: String) -> Self { |
704 | 0 | Self { |
705 | 0 | language, |
706 | 0 | country, |
707 | 0 | value, |
708 | 0 | } |
709 | 0 | } |
710 | | } |
711 | | |
712 | | #[derive(Debug, Clone)] |
713 | | pub struct DescriptionString { |
714 | | pub ascii_string: String, |
715 | | pub unicode_language_code: u32, |
716 | | pub unicode_string: String, |
717 | | pub script_code_code: i8, |
718 | | pub mac_string: String, |
719 | | } |
720 | | |
721 | | #[derive(Debug, Clone)] |
722 | | pub enum ProfileText { |
723 | | PlainString(String), |
724 | | Localizable(Vec<LocalizableString>), |
725 | | Description(DescriptionString), |
726 | | } |
727 | | |
728 | | impl ProfileText { |
729 | 0 | pub(crate) fn has_values(&self) -> bool { |
730 | 0 | match self { |
731 | 0 | ProfileText::PlainString(_) => true, |
732 | 0 | ProfileText::Localizable(lc) => !lc.is_empty(), |
733 | 0 | ProfileText::Description(_) => true, |
734 | | } |
735 | 0 | } |
736 | | } |
737 | | |
738 | | #[derive(Debug, Clone, Copy)] |
739 | | pub enum StandardObserver { |
740 | | D50, |
741 | | D65, |
742 | | Unknown, |
743 | | } |
744 | | |
745 | | impl From<u32> for StandardObserver { |
746 | 0 | fn from(value: u32) -> Self { |
747 | 0 | if value == 1 { |
748 | 0 | return StandardObserver::D50; |
749 | 0 | } else if value == 2 { |
750 | 0 | return StandardObserver::D65; |
751 | 0 | } |
752 | 0 | StandardObserver::Unknown |
753 | 0 | } |
754 | | } |
755 | | |
756 | | #[derive(Debug, Clone, Copy)] |
757 | | pub struct ViewingConditions { |
758 | | pub illuminant: Xyz, |
759 | | pub surround: Xyz, |
760 | | pub observer: StandardObserver, |
761 | | } |
762 | | |
763 | | #[derive(Debug, Clone, Copy)] |
764 | | pub enum MeasurementGeometry { |
765 | | Unknown, |
766 | | /// 0°:45° or 45°:0° |
767 | | D45to45, |
768 | | /// 0°:d or d:0° |
769 | | D0to0, |
770 | | } |
771 | | |
772 | | impl From<u32> for MeasurementGeometry { |
773 | 0 | fn from(value: u32) -> Self { |
774 | 0 | if value == 1 { |
775 | 0 | Self::D45to45 |
776 | 0 | } else if value == 2 { |
777 | 0 | Self::D0to0 |
778 | | } else { |
779 | 0 | Self::Unknown |
780 | | } |
781 | 0 | } |
782 | | } |
783 | | |
784 | | #[derive(Debug, Clone, Copy)] |
785 | | pub enum StandardIlluminant { |
786 | | Unknown, |
787 | | D50, |
788 | | D65, |
789 | | D93, |
790 | | F2, |
791 | | D55, |
792 | | A, |
793 | | EquiPower, |
794 | | F8, |
795 | | } |
796 | | |
797 | | impl From<u32> for StandardIlluminant { |
798 | 0 | fn from(value: u32) -> Self { |
799 | 0 | match value { |
800 | 0 | 1 => StandardIlluminant::D50, |
801 | 0 | 2 => StandardIlluminant::D65, |
802 | 0 | 3 => StandardIlluminant::D93, |
803 | 0 | 4 => StandardIlluminant::F2, |
804 | 0 | 5 => StandardIlluminant::D55, |
805 | 0 | 6 => StandardIlluminant::A, |
806 | 0 | 7 => StandardIlluminant::EquiPower, |
807 | 0 | 8 => StandardIlluminant::F8, |
808 | 0 | _ => Self::Unknown, |
809 | | } |
810 | 0 | } |
811 | | } |
812 | | |
813 | | impl From<StandardIlluminant> for u32 { |
814 | 0 | fn from(value: StandardIlluminant) -> Self { |
815 | 0 | match value { |
816 | 0 | StandardIlluminant::Unknown => 0u32, |
817 | 0 | StandardIlluminant::D50 => 1u32, |
818 | 0 | StandardIlluminant::D65 => 2u32, |
819 | 0 | StandardIlluminant::D93 => 3, |
820 | 0 | StandardIlluminant::F2 => 4, |
821 | 0 | StandardIlluminant::D55 => 5, |
822 | 0 | StandardIlluminant::A => 6, |
823 | 0 | StandardIlluminant::EquiPower => 7, |
824 | 0 | StandardIlluminant::F8 => 8, |
825 | | } |
826 | 0 | } |
827 | | } |
828 | | |
829 | | #[derive(Debug, Clone, Copy)] |
830 | | pub struct Measurement { |
831 | | pub observer: StandardObserver, |
832 | | pub backing: Xyz, |
833 | | pub geometry: MeasurementGeometry, |
834 | | pub flare: f32, |
835 | | pub illuminant: StandardIlluminant, |
836 | | } |
837 | | |
838 | | /// ICC Profile representation |
839 | | #[repr(C)] |
840 | | #[derive(Debug, Clone, Default)] |
841 | | pub struct ColorProfile { |
842 | | pub pcs: DataColorSpace, |
843 | | pub color_space: DataColorSpace, |
844 | | pub profile_class: ProfileClass, |
845 | | pub rendering_intent: RenderingIntent, |
846 | | pub red_colorant: Xyzd, |
847 | | pub green_colorant: Xyzd, |
848 | | pub blue_colorant: Xyzd, |
849 | | pub white_point: Xyzd, |
850 | | pub black_point: Option<Xyzd>, |
851 | | pub media_white_point: Option<Xyzd>, |
852 | | pub luminance: Option<Xyzd>, |
853 | | pub measurement: Option<Measurement>, |
854 | | pub red_trc: Option<ToneReprCurve>, |
855 | | pub green_trc: Option<ToneReprCurve>, |
856 | | pub blue_trc: Option<ToneReprCurve>, |
857 | | pub gray_trc: Option<ToneReprCurve>, |
858 | | pub cicp: Option<CicpProfile>, |
859 | | pub chromatic_adaptation: Option<Matrix3d>, |
860 | | pub lut_a_to_b_perceptual: Option<LutWarehouse>, |
861 | | pub lut_a_to_b_colorimetric: Option<LutWarehouse>, |
862 | | pub lut_a_to_b_saturation: Option<LutWarehouse>, |
863 | | pub lut_b_to_a_perceptual: Option<LutWarehouse>, |
864 | | pub lut_b_to_a_colorimetric: Option<LutWarehouse>, |
865 | | pub lut_b_to_a_saturation: Option<LutWarehouse>, |
866 | | pub gamut: Option<LutWarehouse>, |
867 | | pub copyright: Option<ProfileText>, |
868 | | pub description: Option<ProfileText>, |
869 | | pub device_manufacturer: Option<ProfileText>, |
870 | | pub device_model: Option<ProfileText>, |
871 | | pub char_target: Option<ProfileText>, |
872 | | pub viewing_conditions: Option<ViewingConditions>, |
873 | | pub viewing_conditions_description: Option<ProfileText>, |
874 | | pub technology: Option<TechnologySignatures>, |
875 | | pub calibration_date: Option<ColorDateTime>, |
876 | | /// Version for internal and viewing purposes only. |
877 | | /// On encoding added value to profile will always be V4. |
878 | | pub(crate) version_internal: ProfileVersion, |
879 | | } |
880 | | |
881 | | #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)] |
882 | | pub struct ParsingOptions { |
883 | | // Maximum allowed profile size in bytes |
884 | | pub max_profile_size: usize, |
885 | | // Maximum allowed CLUT size in bytes |
886 | | pub max_allowed_clut_size: usize, |
887 | | // Maximum allowed TRC size in elements count |
888 | | pub max_allowed_trc_size: usize, |
889 | | } |
890 | | |
891 | | impl Default for ParsingOptions { |
892 | 0 | fn default() -> Self { |
893 | 0 | Self { |
894 | 0 | max_profile_size: MAX_PROFILE_SIZE, |
895 | 0 | max_allowed_clut_size: 10_000_000, |
896 | 0 | max_allowed_trc_size: 40_000, |
897 | 0 | } |
898 | 0 | } |
899 | | } |
900 | | |
901 | | impl ColorProfile { |
902 | | /// Returns profile version |
903 | 0 | pub fn version(&self) -> ProfileVersion { |
904 | 0 | self.version_internal |
905 | 0 | } |
906 | | |
907 | 0 | pub fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> { |
908 | 0 | Self::new_from_slice_with_options(slice, Default::default()) |
909 | 0 | } |
910 | | |
911 | 0 | pub fn new_from_slice_with_options( |
912 | 0 | slice: &[u8], |
913 | 0 | options: ParsingOptions, |
914 | 0 | ) -> Result<Self, CmsError> { |
915 | 0 | let header = ProfileHeader::new_from_slice(slice)?; |
916 | 0 | let tags_count = header.tag_count as usize; |
917 | 0 | if slice.len() >= options.max_profile_size { |
918 | 0 | return Err(CmsError::InvalidProfile); |
919 | 0 | } |
920 | 0 | let tags_end = tags_count |
921 | 0 | .safe_mul(TAG_SIZE)? |
922 | 0 | .safe_add(size_of::<ProfileHeader>())?; |
923 | 0 | if slice.len() < tags_end { |
924 | 0 | return Err(CmsError::InvalidProfile); |
925 | 0 | } |
926 | 0 | let tags_slice = &slice[size_of::<ProfileHeader>()..tags_end]; |
927 | 0 | let mut profile = ColorProfile { |
928 | 0 | rendering_intent: header.rendering_intent, |
929 | 0 | pcs: header.pcs, |
930 | 0 | profile_class: header.profile_class, |
931 | 0 | color_space: header.data_color_space, |
932 | 0 | white_point: header.illuminant.to_xyzd(), |
933 | 0 | version_internal: header.version, |
934 | 0 | ..Default::default() |
935 | 0 | }; |
936 | 0 | let color_space = profile.color_space; |
937 | 0 | for tag in tags_slice.chunks_exact(TAG_SIZE) { |
938 | 0 | let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); |
939 | 0 | let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]); |
940 | 0 | let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; |
941 | | // Just ignore unknown tags |
942 | 0 | if let Ok(tag) = Tag::try_from(tag_value) { |
943 | 0 | match tag { |
944 | | Tag::RedXyz => { |
945 | 0 | if color_space == DataColorSpace::Rgb { |
946 | | profile.red_colorant = |
947 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
948 | 0 | } |
949 | | } |
950 | | Tag::GreenXyz => { |
951 | 0 | if color_space == DataColorSpace::Rgb { |
952 | | profile.green_colorant = |
953 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
954 | 0 | } |
955 | | } |
956 | | Tag::BlueXyz => { |
957 | 0 | if color_space == DataColorSpace::Rgb { |
958 | | profile.blue_colorant = |
959 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
960 | 0 | } |
961 | | } |
962 | | Tag::RedToneReproduction => { |
963 | 0 | if color_space == DataColorSpace::Rgb { |
964 | 0 | profile.red_trc = Self::read_trc_tag_s( |
965 | 0 | slice, |
966 | 0 | tag_entry as usize, |
967 | 0 | tag_size, |
968 | 0 | &options, |
969 | 0 | )?; |
970 | 0 | } |
971 | | } |
972 | | Tag::GreenToneReproduction => { |
973 | 0 | if color_space == DataColorSpace::Rgb { |
974 | 0 | profile.green_trc = Self::read_trc_tag_s( |
975 | 0 | slice, |
976 | 0 | tag_entry as usize, |
977 | 0 | tag_size, |
978 | 0 | &options, |
979 | 0 | )?; |
980 | 0 | } |
981 | | } |
982 | | Tag::BlueToneReproduction => { |
983 | 0 | if color_space == DataColorSpace::Rgb { |
984 | 0 | profile.blue_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::GreyToneReproduction => { |
993 | 0 | if color_space == DataColorSpace::Gray { |
994 | 0 | profile.gray_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::MediaWhitePoint => { |
1003 | | profile.media_white_point = |
1004 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; |
1005 | | } |
1006 | | Tag::Luminance => { |
1007 | | profile.luminance = |
1008 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; |
1009 | | } |
1010 | | Tag::Measurement => { |
1011 | | profile.measurement = |
1012 | 0 | Self::read_meas_tag(slice, tag_entry as usize, tag_size)?; |
1013 | | } |
1014 | | Tag::CodeIndependentPoints => { |
1015 | | // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the |
1016 | | // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces |
1017 | | // or profile classes indicated in the profile header. |
1018 | 0 | if (profile.profile_class == ProfileClass::InputDevice |
1019 | 0 | || profile.profile_class == ProfileClass::DisplayDevice) |
1020 | 0 | && (profile.color_space == DataColorSpace::Rgb |
1021 | 0 | || profile.color_space == DataColorSpace::YCbr |
1022 | 0 | || profile.color_space == DataColorSpace::Xyz) |
1023 | | { |
1024 | | profile.cicp = |
1025 | 0 | Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?; |
1026 | 0 | } |
1027 | | } |
1028 | | Tag::ChromaticAdaptation => { |
1029 | | profile.chromatic_adaptation = |
1030 | 0 | Self::read_chad_tag(slice, tag_entry as usize, tag_size)?; |
1031 | | } |
1032 | | Tag::BlackPoint => { |
1033 | | profile.black_point = |
1034 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)? |
1035 | | } |
1036 | | Tag::DeviceToPcsLutPerceptual => { |
1037 | 0 | profile.lut_a_to_b_perceptual = |
1038 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1039 | | } |
1040 | | Tag::DeviceToPcsLutColorimetric => { |
1041 | 0 | profile.lut_a_to_b_colorimetric = |
1042 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1043 | | } |
1044 | | Tag::DeviceToPcsLutSaturation => { |
1045 | 0 | profile.lut_a_to_b_saturation = |
1046 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1047 | | } |
1048 | | Tag::PcsToDeviceLutPerceptual => { |
1049 | 0 | profile.lut_b_to_a_perceptual = |
1050 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1051 | | } |
1052 | | Tag::PcsToDeviceLutColorimetric => { |
1053 | 0 | profile.lut_b_to_a_colorimetric = |
1054 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1055 | | } |
1056 | | Tag::PcsToDeviceLutSaturation => { |
1057 | 0 | profile.lut_b_to_a_saturation = |
1058 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1059 | | } |
1060 | | Tag::Gamut => { |
1061 | 0 | profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1062 | | } |
1063 | | Tag::Copyright => { |
1064 | 0 | profile.copyright = |
1065 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1066 | | } |
1067 | | Tag::ProfileDescription => { |
1068 | 0 | profile.description = |
1069 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1070 | | } |
1071 | | Tag::ViewingConditionsDescription => { |
1072 | 0 | profile.viewing_conditions_description = |
1073 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1074 | | } |
1075 | | Tag::DeviceModel => { |
1076 | 0 | profile.device_model = |
1077 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1078 | | } |
1079 | | Tag::DeviceManufacturer => { |
1080 | 0 | profile.device_manufacturer = |
1081 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1082 | | } |
1083 | | Tag::CharTarget => { |
1084 | 0 | profile.char_target = |
1085 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1086 | | } |
1087 | 0 | Tag::Chromaticity => {} |
1088 | | Tag::ObserverConditions => { |
1089 | | profile.viewing_conditions = |
1090 | 0 | Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?; |
1091 | | } |
1092 | | Tag::Technology => { |
1093 | | profile.technology = |
1094 | 0 | Self::read_tech_tag(slice, tag_entry as usize, tag_size)?; |
1095 | | } |
1096 | | Tag::CalibrationDateTime => { |
1097 | | profile.calibration_date = |
1098 | 0 | Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?; |
1099 | | } |
1100 | | } |
1101 | 0 | } |
1102 | | } |
1103 | | |
1104 | 0 | Ok(profile) |
1105 | 0 | } |
1106 | | } |
1107 | | |
1108 | | impl ColorProfile { |
1109 | | #[inline] |
1110 | 0 | pub fn colorant_matrix(&self) -> Matrix3d { |
1111 | 0 | Matrix3d { |
1112 | 0 | v: [ |
1113 | 0 | [ |
1114 | 0 | self.red_colorant.x, |
1115 | 0 | self.green_colorant.x, |
1116 | 0 | self.blue_colorant.x, |
1117 | 0 | ], |
1118 | 0 | [ |
1119 | 0 | self.red_colorant.y, |
1120 | 0 | self.green_colorant.y, |
1121 | 0 | self.blue_colorant.y, |
1122 | 0 | ], |
1123 | 0 | [ |
1124 | 0 | self.red_colorant.z, |
1125 | 0 | self.green_colorant.z, |
1126 | 0 | self.blue_colorant.z, |
1127 | 0 | ], |
1128 | 0 | ], |
1129 | 0 | } |
1130 | 0 | } |
1131 | | |
1132 | | /// Computes colorants matrix. Returns not transposed matrix. |
1133 | | /// |
1134 | | /// To work on `const` context this method does have restrictions. |
1135 | | /// If invalid values were provided it may return invalid matrix or NaNs. |
1136 | 0 | pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d { |
1137 | 0 | let red_xyz = primaries.red.to_xyzd(); |
1138 | 0 | let green_xyz = primaries.green.to_xyzd(); |
1139 | 0 | let blue_xyz = primaries.blue.to_xyzd(); |
1140 | | |
1141 | 0 | let xyz_matrix = Matrix3d { |
1142 | 0 | v: [ |
1143 | 0 | [red_xyz.x, green_xyz.x, blue_xyz.x], |
1144 | 0 | [red_xyz.y, green_xyz.y, blue_xyz.y], |
1145 | 0 | [red_xyz.z, green_xyz.z, blue_xyz.z], |
1146 | 0 | ], |
1147 | 0 | }; |
1148 | 0 | let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); |
1149 | 0 | adapt_to_d50_d(colorants, white_point) |
1150 | 0 | } |
1151 | | |
1152 | | /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point |
1153 | 0 | pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) { |
1154 | 0 | let red_xyz = primaries.red.to_xyzd(); |
1155 | 0 | let green_xyz = primaries.green.to_xyzd(); |
1156 | 0 | let blue_xyz = primaries.blue.to_xyzd(); |
1157 | | |
1158 | 0 | self.chromatic_adaptation = Some(BRADFORD_D); |
1159 | 0 | self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz) |
1160 | 0 | } |
1161 | | |
1162 | | /// Updates RGB triple colorimetry from 3 [Xyzd] and white point |
1163 | | /// |
1164 | | /// To work on `const` context this method does have restrictions. |
1165 | | /// If invalid values were provided it may return invalid matrix or NaNs. |
1166 | 0 | pub const fn update_rgb_colorimetry_triplet( |
1167 | 0 | &mut self, |
1168 | 0 | white_point: XyY, |
1169 | 0 | red_xyz: Xyzd, |
1170 | 0 | green_xyz: Xyzd, |
1171 | 0 | blue_xyz: Xyzd, |
1172 | 0 | ) { |
1173 | 0 | let xyz_matrix = Matrix3d { |
1174 | 0 | v: [ |
1175 | 0 | [red_xyz.x, green_xyz.x, blue_xyz.x], |
1176 | 0 | [red_xyz.y, green_xyz.y, blue_xyz.y], |
1177 | 0 | [red_xyz.z, green_xyz.z, blue_xyz.z], |
1178 | 0 | ], |
1179 | 0 | }; |
1180 | 0 | let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); |
1181 | 0 | let colorants = adapt_to_d50_d(colorants, white_point); |
1182 | | |
1183 | 0 | self.update_colorants(colorants); |
1184 | 0 | } |
1185 | | |
1186 | 0 | pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) { |
1187 | | // note: there's a transpose type of operation going on here |
1188 | 0 | self.red_colorant.x = colorants.v[0][0]; |
1189 | 0 | self.red_colorant.y = colorants.v[1][0]; |
1190 | 0 | self.red_colorant.z = colorants.v[2][0]; |
1191 | 0 | self.green_colorant.x = colorants.v[0][1]; |
1192 | 0 | self.green_colorant.y = colorants.v[1][1]; |
1193 | 0 | self.green_colorant.z = colorants.v[2][1]; |
1194 | 0 | self.blue_colorant.x = colorants.v[0][2]; |
1195 | 0 | self.blue_colorant.y = colorants.v[1][2]; |
1196 | 0 | self.blue_colorant.z = colorants.v[2][2]; |
1197 | 0 | } |
1198 | | |
1199 | | /// Updates RGB triple colorimetry from CICP |
1200 | 0 | pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool { |
1201 | 0 | self.cicp = Some(cicp); |
1202 | 0 | if !cicp.color_primaries.has_chromaticity() |
1203 | 0 | || !cicp.transfer_characteristics.has_transfer_curve() |
1204 | | { |
1205 | 0 | return false; |
1206 | 0 | } |
1207 | 0 | let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() { |
1208 | 0 | Ok(primaries) => primaries, |
1209 | 0 | Err(_) => return false, |
1210 | | }; |
1211 | 0 | let white_point: Chromaticity = match cicp.color_primaries.white_point() { |
1212 | 0 | Ok(v) => v, |
1213 | 0 | Err(_) => return false, |
1214 | | }; |
1215 | 0 | self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy); |
1216 | | |
1217 | 0 | let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() { |
1218 | 0 | Ok(trc) => trc, |
1219 | 0 | Err(_) => return false, |
1220 | | }; |
1221 | 0 | self.green_trc = Some(red_trc.clone()); |
1222 | 0 | self.blue_trc = Some(red_trc.clone()); |
1223 | 0 | self.red_trc = Some(red_trc); |
1224 | 0 | false |
1225 | 0 | } |
1226 | | |
1227 | 0 | pub const fn rgb_to_xyz(&self, xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { |
1228 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1229 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector()); |
1230 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1231 | 0 | v = v.mul_row_vector::<1>(s); |
1232 | 0 | v.mul_row_vector::<2>(s) |
1233 | 0 | } |
1234 | | |
1235 | | ///TODO: make primary instead of [rgb_to_xyz] in the next major version |
1236 | 0 | pub(crate) const fn rgb_to_xyz_static(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { |
1237 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1238 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector()); |
1239 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1240 | 0 | v = v.mul_row_vector::<1>(s); |
1241 | 0 | v.mul_row_vector::<2>(s) |
1242 | 0 | } |
1243 | | |
1244 | | /// If Primaries is invalid will return invalid matrix on const context. |
1245 | | /// This assumes not transposed matrix and returns not transposed matrix. |
1246 | 0 | pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d { |
1247 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1248 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector_d()); |
1249 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1250 | 0 | v = v.mul_row_vector::<1>(s); |
1251 | 0 | v = v.mul_row_vector::<2>(s); |
1252 | 0 | v |
1253 | 0 | } |
1254 | | |
1255 | 0 | pub fn rgb_to_xyz_matrix(&self) -> Matrix3d { |
1256 | 0 | let xyz_matrix = self.colorant_matrix(); |
1257 | 0 | let white_point = Chromaticity::D50.to_xyzd(); |
1258 | 0 | ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point) |
1259 | 0 | } |
1260 | | |
1261 | | /// Computes transform matrix RGB -> XYZ -> RGB |
1262 | | /// Current profile is used as source, other as destination |
1263 | 0 | pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d { |
1264 | 0 | let source = self.rgb_to_xyz_matrix(); |
1265 | 0 | let dst = dest.rgb_to_xyz_matrix(); |
1266 | 0 | let dest_inverse = dst.inverse(); |
1267 | 0 | dest_inverse.mat_mul(source) |
1268 | 0 | } |
1269 | | |
1270 | | /// Returns volume of colors stored in profile |
1271 | 0 | pub fn profile_volume(&self) -> Option<f32> { |
1272 | 0 | let red_prim = self.red_colorant; |
1273 | 0 | let green_prim = self.green_colorant; |
1274 | 0 | let blue_prim = self.blue_colorant; |
1275 | 0 | let tetrahedral_vertices = Matrix3d { |
1276 | 0 | v: [ |
1277 | 0 | [red_prim.x, red_prim.y, red_prim.z], |
1278 | 0 | [green_prim.x, green_prim.y, green_prim.z], |
1279 | 0 | [blue_prim.x, blue_prim.y, blue_prim.z], |
1280 | 0 | ], |
1281 | 0 | }; |
1282 | 0 | let det = tetrahedral_vertices.determinant()?; |
1283 | 0 | Some((det / 6.0f64) as f32) |
1284 | 0 | } |
1285 | | |
1286 | 0 | pub(crate) fn has_device_to_pcs_lut(&self) -> bool { |
1287 | 0 | self.lut_a_to_b_perceptual.is_some() |
1288 | 0 | || self.lut_a_to_b_saturation.is_some() |
1289 | 0 | || self.lut_a_to_b_colorimetric.is_some() |
1290 | 0 | } |
1291 | | |
1292 | 0 | pub(crate) fn has_pcs_to_device_lut(&self) -> bool { |
1293 | 0 | self.lut_b_to_a_perceptual.is_some() |
1294 | 0 | || self.lut_b_to_a_saturation.is_some() |
1295 | 0 | || self.lut_b_to_a_colorimetric.is_some() |
1296 | 0 | } |
1297 | | } |
1298 | | |
1299 | | #[cfg(test)] |
1300 | | mod tests { |
1301 | | use super::*; |
1302 | | use std::fs; |
1303 | | |
1304 | | #[test] |
1305 | | fn test_gray() { |
1306 | | if let Ok(gray_icc) = fs::read("./assets/Generic Gray Gamma 2.2 Profile.icc") { |
1307 | | let f_p = ColorProfile::new_from_slice(&gray_icc).unwrap(); |
1308 | | assert!(f_p.gray_trc.is_some()); |
1309 | | } |
1310 | | } |
1311 | | |
1312 | | #[test] |
1313 | | fn test_perceptual() { |
1314 | | if let Ok(srgb_perceptual_icc) = fs::read("./assets/srgb_perceptual.icc") { |
1315 | | let f_p = ColorProfile::new_from_slice(&srgb_perceptual_icc).unwrap(); |
1316 | | assert_eq!(f_p.pcs, DataColorSpace::Lab); |
1317 | | assert_eq!(f_p.color_space, DataColorSpace::Rgb); |
1318 | | assert_eq!(f_p.version(), ProfileVersion::V4_2); |
1319 | | assert!(f_p.lut_a_to_b_perceptual.is_some()); |
1320 | | assert!(f_p.lut_b_to_a_perceptual.is_some()); |
1321 | | } |
1322 | | } |
1323 | | |
1324 | | #[test] |
1325 | | fn test_us_swop_coated() { |
1326 | | if let Ok(us_swop_coated) = fs::read("./assets/us_swop_coated.icc") { |
1327 | | let f_p = ColorProfile::new_from_slice(&us_swop_coated).unwrap(); |
1328 | | assert_eq!(f_p.pcs, DataColorSpace::Lab); |
1329 | | assert_eq!(f_p.color_space, DataColorSpace::Cmyk); |
1330 | | assert_eq!(f_p.version(), ProfileVersion::V2_0); |
1331 | | |
1332 | | assert!(f_p.lut_a_to_b_perceptual.is_some()); |
1333 | | assert!(f_p.lut_b_to_a_perceptual.is_some()); |
1334 | | |
1335 | | assert!(f_p.lut_a_to_b_colorimetric.is_some()); |
1336 | | assert!(f_p.lut_b_to_a_colorimetric.is_some()); |
1337 | | |
1338 | | assert!(f_p.gamut.is_some()); |
1339 | | |
1340 | | assert!(f_p.copyright.is_some()); |
1341 | | assert!(f_p.description.is_some()); |
1342 | | } |
1343 | | } |
1344 | | |
1345 | | #[test] |
1346 | | fn test_matrix_shaper() { |
1347 | | if let Ok(matrix_shaper) = fs::read("./assets/Display P3.icc") { |
1348 | | let f_p = ColorProfile::new_from_slice(&matrix_shaper).unwrap(); |
1349 | | assert_eq!(f_p.pcs, DataColorSpace::Xyz); |
1350 | | assert_eq!(f_p.color_space, DataColorSpace::Rgb); |
1351 | | assert_eq!(f_p.version(), ProfileVersion::V4_0); |
1352 | | |
1353 | | assert!(f_p.red_trc.is_some()); |
1354 | | assert!(f_p.blue_trc.is_some()); |
1355 | | assert!(f_p.green_trc.is_some()); |
1356 | | |
1357 | | assert_ne!(f_p.red_colorant, Xyzd::default()); |
1358 | | assert_ne!(f_p.blue_colorant, Xyzd::default()); |
1359 | | assert_ne!(f_p.green_colorant, Xyzd::default()); |
1360 | | |
1361 | | assert!(f_p.copyright.is_some()); |
1362 | | assert!(f_p.description.is_some()); |
1363 | | } |
1364 | | } |
1365 | | } |