/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/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 | | // First try exact match for known versions |
92 | 0 | match value { |
93 | 0 | 0x02000000 => return Ok(ProfileVersion::V2_0), |
94 | 0 | 0x02100000 => return Ok(ProfileVersion::V2_1), |
95 | 0 | 0x02200000 => return Ok(ProfileVersion::V2_2), |
96 | 0 | 0x02300000 => return Ok(ProfileVersion::V2_3), |
97 | 0 | 0x02400000 => return Ok(ProfileVersion::V2_4), |
98 | 0 | 0x04000000 => return Ok(ProfileVersion::V4_0), |
99 | 0 | 0x04100000 => return Ok(ProfileVersion::V4_1), |
100 | 0 | 0x04200000 => return Ok(ProfileVersion::V4_2), |
101 | 0 | 0x04300000 => return Ok(ProfileVersion::V4_3), |
102 | 0 | 0x04400000 => return Ok(ProfileVersion::V4_4), |
103 | 0 | _ => {} |
104 | | } |
105 | | |
106 | | // Extract major version (first byte) for range matching |
107 | | // ICC version format: major.minor.bugfix.zero in bytes [0][1][2][3] |
108 | 0 | let major = (value >> 24) & 0xFF; |
109 | 0 | let minor = (value >> 20) & 0x0F; |
110 | | |
111 | | // Accept profiles with patch versions (e.g., v2.0.2, v3.4, v4.2.9) |
112 | | // but reject invalid versions (v0.x) and unsupported versions (v5.x+ / ICC MAX) |
113 | 0 | match major { |
114 | | 0 => { |
115 | | // Version 0.x is invalid - reject |
116 | 0 | Err(CmsError::InvalidProfile) |
117 | | } |
118 | | 2 => { |
119 | | // v2.x - map to the appropriate v2 minor version or highest known |
120 | 0 | match minor { |
121 | 0 | 0 => Ok(ProfileVersion::V2_0), |
122 | 0 | 1 => Ok(ProfileVersion::V2_1), |
123 | 0 | 2 => Ok(ProfileVersion::V2_2), |
124 | 0 | 3 => Ok(ProfileVersion::V2_3), |
125 | 0 | _ => Ok(ProfileVersion::V2_4), // Higher minor versions -> v2.4 |
126 | | } |
127 | | } |
128 | | 3 => { |
129 | | // v3.x (rare but exists) - treat as v2.4 (functionally similar) |
130 | 0 | Ok(ProfileVersion::V2_4) |
131 | | } |
132 | | 4 => { |
133 | | // v4.x - map to the appropriate v4 minor version or highest known |
134 | 0 | match minor { |
135 | 0 | 0 => Ok(ProfileVersion::V4_0), |
136 | 0 | 1 => Ok(ProfileVersion::V4_1), |
137 | 0 | 2 => Ok(ProfileVersion::V4_2), |
138 | 0 | 3 => Ok(ProfileVersion::V4_3), |
139 | 0 | _ => Ok(ProfileVersion::V4_4), // Higher minor versions -> v4.4 |
140 | | } |
141 | | } |
142 | | _ => { |
143 | | // v5.x+ (ICC MAX) and other unknown versions - reject |
144 | | // ICC MAX has different white point requirements and would produce wrong colors |
145 | 0 | Err(CmsError::InvalidProfile) |
146 | | } |
147 | | } |
148 | 0 | } |
149 | | } |
150 | | |
151 | | impl From<ProfileVersion> for u32 { |
152 | 0 | fn from(value: ProfileVersion) -> Self { |
153 | 0 | match value { |
154 | 0 | ProfileVersion::V2_0 => 0x02000000, |
155 | 0 | ProfileVersion::V2_1 => 0x02100000, |
156 | 0 | ProfileVersion::V2_2 => 0x02200000, |
157 | 0 | ProfileVersion::V2_3 => 0x02300000, |
158 | 0 | ProfileVersion::V2_4 => 0x02400000, |
159 | 0 | ProfileVersion::V4_0 => 0x04000000, |
160 | 0 | ProfileVersion::V4_1 => 0x04100000, |
161 | 0 | ProfileVersion::V4_2 => 0x04200000, |
162 | 0 | ProfileVersion::V4_3 => 0x04300000, |
163 | 0 | ProfileVersion::V4_4 => 0x04400000, |
164 | 0 | ProfileVersion::Unknown => 0x02000000, |
165 | | } |
166 | 0 | } |
167 | | } |
168 | | |
169 | | #[repr(u32)] |
170 | | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Hash)] |
171 | | pub enum DataColorSpace { |
172 | | #[default] |
173 | | Xyz, |
174 | | Lab, |
175 | | Luv, |
176 | | YCbr, |
177 | | Yxy, |
178 | | Rgb, |
179 | | Gray, |
180 | | Hsv, |
181 | | Hls, |
182 | | Cmyk, |
183 | | Cmy, |
184 | | Color2, |
185 | | Color3, |
186 | | Color4, |
187 | | Color5, |
188 | | Color6, |
189 | | Color7, |
190 | | Color8, |
191 | | Color9, |
192 | | Color10, |
193 | | Color11, |
194 | | Color12, |
195 | | Color13, |
196 | | Color14, |
197 | | Color15, |
198 | | } |
199 | | |
200 | | impl DataColorSpace { |
201 | | #[inline] |
202 | 0 | pub fn check_layout(self, layout: Layout) -> Result<(), CmsError> { |
203 | 0 | let unsupported: bool = match self { |
204 | 0 | DataColorSpace::Xyz => layout != Layout::Rgb, |
205 | 0 | DataColorSpace::Lab => layout != Layout::Rgb && layout != Layout::Rgba, |
206 | 0 | DataColorSpace::Luv => layout != Layout::Rgb, |
207 | 0 | DataColorSpace::YCbr => layout != Layout::Rgb, |
208 | 0 | DataColorSpace::Yxy => layout != Layout::Rgb, |
209 | 0 | DataColorSpace::Rgb => layout != Layout::Rgb && layout != Layout::Rgba, |
210 | 0 | DataColorSpace::Gray => layout != Layout::Gray && layout != Layout::GrayAlpha, |
211 | 0 | DataColorSpace::Hsv => layout != Layout::Rgb, |
212 | 0 | DataColorSpace::Hls => layout != Layout::Rgb, |
213 | 0 | DataColorSpace::Cmyk => layout != Layout::Rgba, |
214 | 0 | DataColorSpace::Cmy => layout != Layout::Rgb, |
215 | 0 | DataColorSpace::Color2 => layout != Layout::GrayAlpha, |
216 | 0 | DataColorSpace::Color3 => layout != Layout::Rgb, |
217 | 0 | DataColorSpace::Color4 => layout != Layout::Rgba, |
218 | 0 | DataColorSpace::Color5 => layout != Layout::Inks5, |
219 | 0 | DataColorSpace::Color6 => layout != Layout::Inks6, |
220 | 0 | DataColorSpace::Color7 => layout != Layout::Inks7, |
221 | 0 | DataColorSpace::Color8 => layout != Layout::Inks8, |
222 | 0 | DataColorSpace::Color9 => layout != Layout::Inks9, |
223 | 0 | DataColorSpace::Color10 => layout != Layout::Inks10, |
224 | 0 | DataColorSpace::Color11 => layout != Layout::Inks11, |
225 | 0 | DataColorSpace::Color12 => layout != Layout::Inks12, |
226 | 0 | DataColorSpace::Color13 => layout != Layout::Inks13, |
227 | 0 | DataColorSpace::Color14 => layout != Layout::Inks14, |
228 | 0 | DataColorSpace::Color15 => layout != Layout::Inks15, |
229 | | }; |
230 | 0 | if unsupported { |
231 | 0 | Err(CmsError::InvalidLayout) |
232 | | } else { |
233 | 0 | Ok(()) |
234 | | } |
235 | 0 | } |
236 | | |
237 | 0 | pub(crate) fn is_three_channels(self) -> bool { |
238 | 0 | matches!( |
239 | 0 | self, |
240 | | DataColorSpace::Xyz |
241 | | | DataColorSpace::Lab |
242 | | | DataColorSpace::Luv |
243 | | | DataColorSpace::YCbr |
244 | | | DataColorSpace::Yxy |
245 | | | DataColorSpace::Rgb |
246 | | | DataColorSpace::Hsv |
247 | | | DataColorSpace::Hls |
248 | | | DataColorSpace::Cmy |
249 | | | DataColorSpace::Color3 |
250 | | ) |
251 | 0 | } |
252 | | } |
253 | | |
254 | | #[repr(u32)] |
255 | | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] |
256 | | pub enum ProfileClass { |
257 | | InputDevice, |
258 | | #[default] |
259 | | DisplayDevice, |
260 | | OutputDevice, |
261 | | DeviceLink, |
262 | | ColorSpace, |
263 | | Abstract, |
264 | | Named, |
265 | | } |
266 | | |
267 | | impl TryFrom<u32> for ProfileClass { |
268 | | type Error = CmsError; |
269 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
270 | 0 | if value == u32::from_ne_bytes(*b"scnr").to_be() { |
271 | 0 | return Ok(ProfileClass::InputDevice); |
272 | 0 | } else if value == u32::from_ne_bytes(*b"mntr").to_be() { |
273 | 0 | return Ok(ProfileClass::DisplayDevice); |
274 | 0 | } else if value == u32::from_ne_bytes(*b"prtr").to_be() { |
275 | 0 | return Ok(ProfileClass::OutputDevice); |
276 | 0 | } else if value == u32::from_ne_bytes(*b"link").to_be() { |
277 | 0 | return Ok(ProfileClass::DeviceLink); |
278 | 0 | } else if value == u32::from_ne_bytes(*b"spac").to_be() { |
279 | 0 | return Ok(ProfileClass::ColorSpace); |
280 | 0 | } else if value == u32::from_ne_bytes(*b"abst").to_be() { |
281 | 0 | return Ok(ProfileClass::Abstract); |
282 | 0 | } else if value == u32::from_ne_bytes(*b"nmcl").to_be() { |
283 | 0 | return Ok(ProfileClass::Named); |
284 | 0 | } |
285 | 0 | Err(CmsError::InvalidProfile) |
286 | 0 | } |
287 | | } |
288 | | |
289 | | impl From<ProfileClass> for u32 { |
290 | 0 | fn from(val: ProfileClass) -> Self { |
291 | 0 | match val { |
292 | 0 | ProfileClass::InputDevice => u32::from_ne_bytes(*b"scnr").to_be(), |
293 | 0 | ProfileClass::DisplayDevice => u32::from_ne_bytes(*b"mntr").to_be(), |
294 | 0 | ProfileClass::OutputDevice => u32::from_ne_bytes(*b"prtr").to_be(), |
295 | 0 | ProfileClass::DeviceLink => u32::from_ne_bytes(*b"link").to_be(), |
296 | 0 | ProfileClass::ColorSpace => u32::from_ne_bytes(*b"spac").to_be(), |
297 | 0 | ProfileClass::Abstract => u32::from_ne_bytes(*b"abst").to_be(), |
298 | 0 | ProfileClass::Named => u32::from_ne_bytes(*b"nmcl").to_be(), |
299 | | } |
300 | 0 | } |
301 | | } |
302 | | |
303 | | #[derive(Debug, Clone, PartialEq)] |
304 | | pub enum LutStore { |
305 | | Store8(Vec<u8>), |
306 | | Store16(Vec<u16>), |
307 | | } |
308 | | |
309 | | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] |
310 | | pub enum LutType { |
311 | | Lut8, |
312 | | Lut16, |
313 | | LutMab, |
314 | | LutMba, |
315 | | } |
316 | | |
317 | | impl TryFrom<u32> for LutType { |
318 | | type Error = CmsError; |
319 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
320 | 0 | if value == u32::from_ne_bytes(*b"mft1").to_be() { |
321 | 0 | return Ok(LutType::Lut8); |
322 | 0 | } else if value == u32::from_ne_bytes(*b"mft2").to_be() { |
323 | 0 | return Ok(LutType::Lut16); |
324 | 0 | } else if value == u32::from_ne_bytes(*b"mAB ").to_be() { |
325 | 0 | return Ok(LutType::LutMab); |
326 | 0 | } else if value == u32::from_ne_bytes(*b"mBA ").to_be() { |
327 | 0 | return Ok(LutType::LutMba); |
328 | 0 | } |
329 | 0 | Err(CmsError::InvalidProfile) |
330 | 0 | } |
331 | | } |
332 | | |
333 | | impl From<LutType> for u32 { |
334 | 0 | fn from(val: LutType) -> Self { |
335 | 0 | match val { |
336 | 0 | LutType::Lut8 => u32::from_ne_bytes(*b"mft1").to_be(), |
337 | 0 | LutType::Lut16 => u32::from_ne_bytes(*b"mft2").to_be(), |
338 | 0 | LutType::LutMab => u32::from_ne_bytes(*b"mAB ").to_be(), |
339 | 0 | LutType::LutMba => u32::from_ne_bytes(*b"mBA ").to_be(), |
340 | | } |
341 | 0 | } |
342 | | } |
343 | | |
344 | | impl TryFrom<u32> for DataColorSpace { |
345 | | type Error = CmsError; |
346 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
347 | 0 | if value == u32::from_ne_bytes(*b"XYZ ").to_be() { |
348 | 0 | return Ok(DataColorSpace::Xyz); |
349 | 0 | } else if value == u32::from_ne_bytes(*b"Lab ").to_be() { |
350 | 0 | return Ok(DataColorSpace::Lab); |
351 | 0 | } else if value == u32::from_ne_bytes(*b"Luv ").to_be() { |
352 | 0 | return Ok(DataColorSpace::Luv); |
353 | 0 | } else if value == u32::from_ne_bytes(*b"YCbr").to_be() { |
354 | 0 | return Ok(DataColorSpace::YCbr); |
355 | 0 | } else if value == u32::from_ne_bytes(*b"Yxy ").to_be() { |
356 | 0 | return Ok(DataColorSpace::Yxy); |
357 | 0 | } else if value == u32::from_ne_bytes(*b"RGB ").to_be() { |
358 | 0 | return Ok(DataColorSpace::Rgb); |
359 | 0 | } else if value == u32::from_ne_bytes(*b"GRAY").to_be() { |
360 | 0 | return Ok(DataColorSpace::Gray); |
361 | 0 | } else if value == u32::from_ne_bytes(*b"HSV ").to_be() { |
362 | 0 | return Ok(DataColorSpace::Hsv); |
363 | 0 | } else if value == u32::from_ne_bytes(*b"HLS ").to_be() { |
364 | 0 | return Ok(DataColorSpace::Hls); |
365 | 0 | } else if value == u32::from_ne_bytes(*b"CMYK").to_be() { |
366 | 0 | return Ok(DataColorSpace::Cmyk); |
367 | 0 | } else if value == u32::from_ne_bytes(*b"CMY ").to_be() { |
368 | 0 | return Ok(DataColorSpace::Cmy); |
369 | 0 | } else if value == u32::from_ne_bytes(*b"2CLR").to_be() { |
370 | 0 | return Ok(DataColorSpace::Color2); |
371 | 0 | } else if value == u32::from_ne_bytes(*b"3CLR").to_be() { |
372 | 0 | return Ok(DataColorSpace::Color3); |
373 | 0 | } else if value == u32::from_ne_bytes(*b"4CLR").to_be() { |
374 | 0 | return Ok(DataColorSpace::Color4); |
375 | 0 | } else if value == u32::from_ne_bytes(*b"5CLR").to_be() { |
376 | 0 | return Ok(DataColorSpace::Color5); |
377 | 0 | } else if value == u32::from_ne_bytes(*b"6CLR").to_be() { |
378 | 0 | return Ok(DataColorSpace::Color6); |
379 | 0 | } else if value == u32::from_ne_bytes(*b"7CLR").to_be() { |
380 | 0 | return Ok(DataColorSpace::Color7); |
381 | 0 | } else if value == u32::from_ne_bytes(*b"8CLR").to_be() { |
382 | 0 | return Ok(DataColorSpace::Color8); |
383 | 0 | } else if value == u32::from_ne_bytes(*b"9CLR").to_be() { |
384 | 0 | return Ok(DataColorSpace::Color9); |
385 | 0 | } else if value == u32::from_ne_bytes(*b"ACLR").to_be() { |
386 | 0 | return Ok(DataColorSpace::Color10); |
387 | 0 | } else if value == u32::from_ne_bytes(*b"BCLR").to_be() { |
388 | 0 | return Ok(DataColorSpace::Color11); |
389 | 0 | } else if value == u32::from_ne_bytes(*b"CCLR").to_be() { |
390 | 0 | return Ok(DataColorSpace::Color12); |
391 | 0 | } else if value == u32::from_ne_bytes(*b"DCLR").to_be() { |
392 | 0 | return Ok(DataColorSpace::Color13); |
393 | 0 | } else if value == u32::from_ne_bytes(*b"ECLR").to_be() { |
394 | 0 | return Ok(DataColorSpace::Color14); |
395 | 0 | } else if value == u32::from_ne_bytes(*b"FCLR").to_be() { |
396 | 0 | return Ok(DataColorSpace::Color15); |
397 | 0 | } |
398 | 0 | Err(CmsError::InvalidProfile) |
399 | 0 | } |
400 | | } |
401 | | |
402 | | impl From<DataColorSpace> for u32 { |
403 | 0 | fn from(val: DataColorSpace) -> Self { |
404 | 0 | match val { |
405 | 0 | DataColorSpace::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(), |
406 | 0 | DataColorSpace::Lab => u32::from_ne_bytes(*b"Lab ").to_be(), |
407 | 0 | DataColorSpace::Luv => u32::from_ne_bytes(*b"Luv ").to_be(), |
408 | 0 | DataColorSpace::YCbr => u32::from_ne_bytes(*b"YCbr").to_be(), |
409 | 0 | DataColorSpace::Yxy => u32::from_ne_bytes(*b"Yxy ").to_be(), |
410 | 0 | DataColorSpace::Rgb => u32::from_ne_bytes(*b"RGB ").to_be(), |
411 | 0 | DataColorSpace::Gray => u32::from_ne_bytes(*b"GRAY").to_be(), |
412 | 0 | DataColorSpace::Hsv => u32::from_ne_bytes(*b"HSV ").to_be(), |
413 | 0 | DataColorSpace::Hls => u32::from_ne_bytes(*b"HLS ").to_be(), |
414 | 0 | DataColorSpace::Cmyk => u32::from_ne_bytes(*b"CMYK").to_be(), |
415 | 0 | DataColorSpace::Cmy => u32::from_ne_bytes(*b"CMY ").to_be(), |
416 | 0 | DataColorSpace::Color2 => u32::from_ne_bytes(*b"2CLR").to_be(), |
417 | 0 | DataColorSpace::Color3 => u32::from_ne_bytes(*b"3CLR").to_be(), |
418 | 0 | DataColorSpace::Color4 => u32::from_ne_bytes(*b"4CLR").to_be(), |
419 | 0 | DataColorSpace::Color5 => u32::from_ne_bytes(*b"5CLR").to_be(), |
420 | 0 | DataColorSpace::Color6 => u32::from_ne_bytes(*b"6CLR").to_be(), |
421 | 0 | DataColorSpace::Color7 => u32::from_ne_bytes(*b"7CLR").to_be(), |
422 | 0 | DataColorSpace::Color8 => u32::from_ne_bytes(*b"8CLR").to_be(), |
423 | 0 | DataColorSpace::Color9 => u32::from_ne_bytes(*b"9CLR").to_be(), |
424 | 0 | DataColorSpace::Color10 => u32::from_ne_bytes(*b"ACLR").to_be(), |
425 | 0 | DataColorSpace::Color11 => u32::from_ne_bytes(*b"BCLR").to_be(), |
426 | 0 | DataColorSpace::Color12 => u32::from_ne_bytes(*b"CCLR").to_be(), |
427 | 0 | DataColorSpace::Color13 => u32::from_ne_bytes(*b"DCLR").to_be(), |
428 | 0 | DataColorSpace::Color14 => u32::from_ne_bytes(*b"ECLR").to_be(), |
429 | 0 | DataColorSpace::Color15 => u32::from_ne_bytes(*b"FCLR").to_be(), |
430 | | } |
431 | 0 | } |
432 | | } |
433 | | |
434 | | #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] |
435 | | pub enum TechnologySignatures { |
436 | | FilmScanner, |
437 | | DigitalCamera, |
438 | | ReflectiveScanner, |
439 | | InkJetPrinter, |
440 | | ThermalWaxPrinter, |
441 | | ElectrophotographicPrinter, |
442 | | ElectrostaticPrinter, |
443 | | DyeSublimationPrinter, |
444 | | PhotographicPaperPrinter, |
445 | | FilmWriter, |
446 | | VideoMonitor, |
447 | | VideoCamera, |
448 | | ProjectionTelevision, |
449 | | CathodeRayTubeDisplay, |
450 | | PassiveMatrixDisplay, |
451 | | ActiveMatrixDisplay, |
452 | | LiquidCrystalDisplay, |
453 | | OrganicLedDisplay, |
454 | | PhotoCd, |
455 | | PhotographicImageSetter, |
456 | | Gravure, |
457 | | OffsetLithography, |
458 | | Silkscreen, |
459 | | Flexography, |
460 | | MotionPictureFilmScanner, |
461 | | MotionPictureFilmRecorder, |
462 | | DigitalMotionPictureCamera, |
463 | | DigitalCinemaProjector, |
464 | | Unknown(u32), |
465 | | } |
466 | | |
467 | | impl From<u32> for TechnologySignatures { |
468 | 0 | fn from(value: u32) -> Self { |
469 | 0 | if value == u32::from_ne_bytes(*b"fscn").to_be() { |
470 | 0 | return TechnologySignatures::FilmScanner; |
471 | 0 | } else if value == u32::from_ne_bytes(*b"dcam").to_be() { |
472 | 0 | return TechnologySignatures::DigitalCamera; |
473 | 0 | } else if value == u32::from_ne_bytes(*b"rscn").to_be() { |
474 | 0 | return TechnologySignatures::ReflectiveScanner; |
475 | 0 | } else if value == u32::from_ne_bytes(*b"ijet").to_be() { |
476 | 0 | return TechnologySignatures::InkJetPrinter; |
477 | 0 | } else if value == u32::from_ne_bytes(*b"twax").to_be() { |
478 | 0 | return TechnologySignatures::ThermalWaxPrinter; |
479 | 0 | } else if value == u32::from_ne_bytes(*b"epho").to_be() { |
480 | 0 | return TechnologySignatures::ElectrophotographicPrinter; |
481 | 0 | } else if value == u32::from_ne_bytes(*b"esta").to_be() { |
482 | 0 | return TechnologySignatures::ElectrostaticPrinter; |
483 | 0 | } else if value == u32::from_ne_bytes(*b"dsub").to_be() { |
484 | 0 | return TechnologySignatures::DyeSublimationPrinter; |
485 | 0 | } else if value == u32::from_ne_bytes(*b"rpho").to_be() { |
486 | 0 | return TechnologySignatures::PhotographicPaperPrinter; |
487 | 0 | } else if value == u32::from_ne_bytes(*b"fprn").to_be() { |
488 | 0 | return TechnologySignatures::FilmWriter; |
489 | 0 | } else if value == u32::from_ne_bytes(*b"vidm").to_be() { |
490 | 0 | return TechnologySignatures::VideoMonitor; |
491 | 0 | } else if value == u32::from_ne_bytes(*b"vidc").to_be() { |
492 | 0 | return TechnologySignatures::VideoCamera; |
493 | 0 | } else if value == u32::from_ne_bytes(*b"pjtv").to_be() { |
494 | 0 | return TechnologySignatures::ProjectionTelevision; |
495 | 0 | } else if value == u32::from_ne_bytes(*b"CRT ").to_be() { |
496 | 0 | return TechnologySignatures::CathodeRayTubeDisplay; |
497 | 0 | } else if value == u32::from_ne_bytes(*b"PMD ").to_be() { |
498 | 0 | return TechnologySignatures::PassiveMatrixDisplay; |
499 | 0 | } else if value == u32::from_ne_bytes(*b"AMD ").to_be() { |
500 | 0 | return TechnologySignatures::ActiveMatrixDisplay; |
501 | 0 | } else if value == u32::from_ne_bytes(*b"LCD ").to_be() { |
502 | 0 | return TechnologySignatures::LiquidCrystalDisplay; |
503 | 0 | } else if value == u32::from_ne_bytes(*b"OLED").to_be() { |
504 | 0 | return TechnologySignatures::OrganicLedDisplay; |
505 | 0 | } else if value == u32::from_ne_bytes(*b"KPCD").to_be() { |
506 | 0 | return TechnologySignatures::PhotoCd; |
507 | 0 | } else if value == u32::from_ne_bytes(*b"imgs").to_be() { |
508 | 0 | return TechnologySignatures::PhotographicImageSetter; |
509 | 0 | } else if value == u32::from_ne_bytes(*b"grav").to_be() { |
510 | 0 | return TechnologySignatures::Gravure; |
511 | 0 | } else if value == u32::from_ne_bytes(*b"offs").to_be() { |
512 | 0 | return TechnologySignatures::OffsetLithography; |
513 | 0 | } else if value == u32::from_ne_bytes(*b"silk").to_be() { |
514 | 0 | return TechnologySignatures::Silkscreen; |
515 | 0 | } else if value == u32::from_ne_bytes(*b"flex").to_be() { |
516 | 0 | return TechnologySignatures::Flexography; |
517 | 0 | } else if value == u32::from_ne_bytes(*b"mpfs").to_be() { |
518 | 0 | return TechnologySignatures::MotionPictureFilmScanner; |
519 | 0 | } else if value == u32::from_ne_bytes(*b"mpfr").to_be() { |
520 | 0 | return TechnologySignatures::MotionPictureFilmRecorder; |
521 | 0 | } else if value == u32::from_ne_bytes(*b"dmpc").to_be() { |
522 | 0 | return TechnologySignatures::DigitalMotionPictureCamera; |
523 | 0 | } else if value == u32::from_ne_bytes(*b"dcpj").to_be() { |
524 | 0 | return TechnologySignatures::DigitalCinemaProjector; |
525 | 0 | } |
526 | 0 | TechnologySignatures::Unknown(value) |
527 | 0 | } |
528 | | } |
529 | | |
530 | | #[derive(Debug, Clone)] |
531 | | pub enum LutWarehouse { |
532 | | Lut(LutDataType), |
533 | | Multidimensional(LutMultidimensionalType), |
534 | | } |
535 | | |
536 | | impl PartialEq for LutWarehouse { |
537 | 0 | fn eq(&self, other: &Self) -> bool { |
538 | 0 | match (self, other) { |
539 | 0 | (LutWarehouse::Lut(a), LutWarehouse::Lut(b)) => a == b, |
540 | 0 | (LutWarehouse::Multidimensional(a), LutWarehouse::Multidimensional(b)) => a == b, |
541 | 0 | _ => false, // Different variants are not equal |
542 | | } |
543 | 0 | } |
544 | | } |
545 | | |
546 | | #[derive(Debug, Clone, PartialEq)] |
547 | | pub struct LutDataType { |
548 | | // used by lut8Type/lut16Type (mft2) only |
549 | | pub num_input_channels: u8, |
550 | | pub num_output_channels: u8, |
551 | | pub num_clut_grid_points: u8, |
552 | | pub matrix: Matrix3d, |
553 | | pub num_input_table_entries: u16, |
554 | | pub num_output_table_entries: u16, |
555 | | pub input_table: LutStore, |
556 | | pub clut_table: LutStore, |
557 | | pub output_table: LutStore, |
558 | | pub lut_type: LutType, |
559 | | } |
560 | | |
561 | | impl LutDataType { |
562 | 0 | pub(crate) fn has_same_kind(&self) -> bool { |
563 | 0 | matches!( |
564 | 0 | (&self.input_table, &self.clut_table, &self.output_table), |
565 | | ( |
566 | | LutStore::Store8(_), |
567 | | LutStore::Store8(_), |
568 | | LutStore::Store8(_) |
569 | | ) | ( |
570 | | LutStore::Store16(_), |
571 | | LutStore::Store16(_), |
572 | | LutStore::Store16(_) |
573 | | ) |
574 | | ) |
575 | 0 | } |
576 | | } |
577 | | |
578 | | #[derive(Debug, Clone, PartialEq)] |
579 | | pub struct LutMultidimensionalType { |
580 | | pub num_input_channels: u8, |
581 | | pub num_output_channels: u8, |
582 | | pub grid_points: [u8; 16], |
583 | | pub clut: Option<LutStore>, |
584 | | pub a_curves: Vec<ToneReprCurve>, |
585 | | pub b_curves: Vec<ToneReprCurve>, |
586 | | pub m_curves: Vec<ToneReprCurve>, |
587 | | pub matrix: Matrix3d, |
588 | | pub bias: Vector3d, |
589 | | } |
590 | | |
591 | | #[repr(u32)] |
592 | | #[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)] |
593 | | pub enum RenderingIntent { |
594 | | AbsoluteColorimetric = 3, |
595 | | Saturation = 2, |
596 | | RelativeColorimetric = 1, |
597 | | #[default] |
598 | | Perceptual = 0, |
599 | | } |
600 | | |
601 | | impl TryFrom<u32> for RenderingIntent { |
602 | | type Error = CmsError; |
603 | | |
604 | | #[inline] |
605 | 0 | fn try_from(value: u32) -> Result<Self, Self::Error> { |
606 | | // Rendering intent is a big-endian u32 at bytes 64-67 with valid |
607 | | // values 0-3. Non-conforming profiles (e.g. old Linotype "Lino" |
608 | | // v2.1 profiles with byte-swapped values) may have invalid values. |
609 | | // Default to Perceptual rather than rejecting the entire profile, |
610 | | // since this field is advisory — moxcms uses TransformOptions for |
611 | | // actual LUT selection. |
612 | 0 | match value { |
613 | 0 | 0 => Ok(RenderingIntent::Perceptual), |
614 | 0 | 1 => Ok(RenderingIntent::RelativeColorimetric), |
615 | 0 | 2 => Ok(RenderingIntent::Saturation), |
616 | 0 | 3 => Ok(RenderingIntent::AbsoluteColorimetric), |
617 | 0 | _ => Ok(RenderingIntent::Perceptual), |
618 | | } |
619 | 0 | } |
620 | | } |
621 | | |
622 | | impl From<RenderingIntent> for u32 { |
623 | | #[inline] |
624 | 0 | fn from(value: RenderingIntent) -> Self { |
625 | 0 | match value { |
626 | 0 | RenderingIntent::AbsoluteColorimetric => 3, |
627 | 0 | RenderingIntent::Saturation => 2, |
628 | 0 | RenderingIntent::RelativeColorimetric => 1, |
629 | 0 | RenderingIntent::Perceptual => 0, |
630 | | } |
631 | 0 | } |
632 | | } |
633 | | |
634 | | /// ICC Header |
635 | | #[repr(C)] |
636 | | #[derive(Debug, Clone, Copy)] |
637 | | pub(crate) struct ProfileHeader { |
638 | | pub size: u32, // Size of the profile (computed) |
639 | | pub cmm_type: u32, // Preferred CMM type (ignored) |
640 | | pub version: ProfileVersion, // Version (4.3 or 4.4 if CICP is included) |
641 | | pub profile_class: ProfileClass, // Display device profile |
642 | | pub data_color_space: DataColorSpace, // RGB input color space |
643 | | pub pcs: DataColorSpace, // Profile connection space |
644 | | pub creation_date_time: ColorDateTime, // Date and time |
645 | | pub signature: ProfileSignature, // Profile signature |
646 | | pub platform: u32, // Platform target (ignored) |
647 | | pub flags: u32, // Flags (not embedded, can be used independently) |
648 | | pub device_manufacturer: u32, // Device manufacturer (ignored) |
649 | | pub device_model: u32, // Device model (ignored) |
650 | | pub device_attributes: [u8; 8], // Device attributes (ignored) |
651 | | pub rendering_intent: RenderingIntent, // Relative colorimetric rendering intent |
652 | | pub illuminant: Xyz, // D50 standard illuminant X |
653 | | pub creator: u32, // Profile creator (ignored) |
654 | | pub profile_id: [u8; 16], // Profile id checksum (ignored) |
655 | | pub reserved: [u8; 28], // Reserved (ignored) |
656 | | pub tag_count: u32, // Technically not part of header, but required |
657 | | } |
658 | | |
659 | | impl ProfileHeader { |
660 | | #[allow(dead_code)] |
661 | 0 | pub(crate) fn new(size: u32) -> Self { |
662 | 0 | Self { |
663 | 0 | size, |
664 | 0 | cmm_type: 0, |
665 | 0 | version: ProfileVersion::V4_3, |
666 | 0 | profile_class: ProfileClass::DisplayDevice, |
667 | 0 | data_color_space: DataColorSpace::Rgb, |
668 | 0 | pcs: DataColorSpace::Xyz, |
669 | 0 | creation_date_time: ColorDateTime::default(), |
670 | 0 | signature: ProfileSignature::Acsp, |
671 | 0 | platform: 0, |
672 | 0 | flags: 0x00000000, |
673 | 0 | device_manufacturer: 0, |
674 | 0 | device_model: 0, |
675 | 0 | device_attributes: [0; 8], |
676 | 0 | rendering_intent: RenderingIntent::Perceptual, |
677 | 0 | illuminant: Chromaticity::D50.to_xyz(), |
678 | 0 | creator: 0, |
679 | 0 | profile_id: [0; 16], |
680 | 0 | reserved: [0; 28], |
681 | 0 | tag_count: 0, |
682 | 0 | } |
683 | 0 | } |
684 | | |
685 | | /// Creates profile from the buffer |
686 | 0 | pub(crate) fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> { |
687 | 0 | if slice.len() < size_of::<ProfileHeader>() { |
688 | 0 | return Err(CmsError::InvalidProfile); |
689 | 0 | } |
690 | 0 | let mut cursor = std::io::Cursor::new(slice); |
691 | 0 | let mut buffer = [0u8; size_of::<ProfileHeader>()]; |
692 | 0 | cursor |
693 | 0 | .read_exact(&mut buffer) |
694 | 0 | .map_err(|_| CmsError::InvalidProfile)?; |
695 | | |
696 | 0 | let header = Self { |
697 | 0 | size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()), |
698 | 0 | cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()), |
699 | 0 | version: ProfileVersion::try_from(u32::from_be_bytes( |
700 | 0 | buffer[8..12].try_into().unwrap(), |
701 | 0 | ))?, |
702 | 0 | profile_class: ProfileClass::try_from(u32::from_be_bytes( |
703 | 0 | buffer[12..16].try_into().unwrap(), |
704 | 0 | ))?, |
705 | 0 | data_color_space: DataColorSpace::try_from(u32::from_be_bytes( |
706 | 0 | buffer[16..20].try_into().unwrap(), |
707 | 0 | ))?, |
708 | 0 | pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?, |
709 | 0 | creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?, |
710 | 0 | signature: ProfileSignature::try_from(u32::from_be_bytes( |
711 | 0 | buffer[36..40].try_into().unwrap(), |
712 | 0 | ))?, |
713 | 0 | platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()), |
714 | 0 | flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()), |
715 | 0 | device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()), |
716 | 0 | device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()), |
717 | 0 | device_attributes: buffer[56..64].try_into().unwrap(), |
718 | 0 | rendering_intent: RenderingIntent::try_from(u32::from_be_bytes( |
719 | 0 | buffer[64..68].try_into().unwrap(), |
720 | 0 | ))?, |
721 | 0 | illuminant: Xyz::new( |
722 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())), |
723 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())), |
724 | 0 | s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())), |
725 | | ), |
726 | 0 | creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()), |
727 | 0 | profile_id: buffer[84..100].try_into().unwrap(), |
728 | 0 | reserved: buffer[100..128].try_into().unwrap(), |
729 | 0 | tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()), |
730 | | }; |
731 | 0 | Ok(header) |
732 | 0 | } |
733 | | } |
734 | | |
735 | | /// A [Coding Independent Code Point](https://en.wikipedia.org/wiki/Coding-independent_code_points). |
736 | | #[repr(C)] |
737 | | #[derive(Debug, Clone, Copy)] |
738 | | pub struct CicpProfile { |
739 | | pub color_primaries: CicpColorPrimaries, |
740 | | pub transfer_characteristics: TransferCharacteristics, |
741 | | pub matrix_coefficients: MatrixCoefficients, |
742 | | pub full_range: bool, |
743 | | } |
744 | | |
745 | | #[derive(Debug, Clone)] |
746 | | pub struct LocalizableString { |
747 | | /// An ISO 639-1 value is expected; any text w. more than two symbols will be truncated |
748 | | pub language: String, |
749 | | /// An ISO 3166-1 value is expected; any text w. more than two symbols will be truncated |
750 | | pub country: String, |
751 | | pub value: String, |
752 | | } |
753 | | |
754 | | impl LocalizableString { |
755 | | /// Creates new localizable string |
756 | | /// |
757 | | /// # Arguments |
758 | | /// |
759 | | /// * `language`: an ISO 639-1 value is expected, any text more than 2 symbols will be truncated |
760 | | /// * `country`: an ISO 3166-1 value is expected, any text more than 2 symbols will be truncated |
761 | | /// * `value`: String value |
762 | | /// |
763 | 0 | pub fn new(language: String, country: String, value: String) -> Self { |
764 | 0 | Self { |
765 | 0 | language, |
766 | 0 | country, |
767 | 0 | value, |
768 | 0 | } |
769 | 0 | } |
770 | | } |
771 | | |
772 | | #[derive(Debug, Clone)] |
773 | | pub struct DescriptionString { |
774 | | pub ascii_string: String, |
775 | | pub unicode_language_code: u32, |
776 | | pub unicode_string: String, |
777 | | pub script_code_code: i8, |
778 | | pub mac_string: String, |
779 | | } |
780 | | |
781 | | #[derive(Debug, Clone)] |
782 | | pub enum ProfileText { |
783 | | PlainString(String), |
784 | | Localizable(Vec<LocalizableString>), |
785 | | Description(DescriptionString), |
786 | | } |
787 | | |
788 | | impl ProfileText { |
789 | 0 | pub(crate) fn has_values(&self) -> bool { |
790 | 0 | match self { |
791 | 0 | ProfileText::PlainString(_) => true, |
792 | 0 | ProfileText::Localizable(lc) => !lc.is_empty(), |
793 | 0 | ProfileText::Description(_) => true, |
794 | | } |
795 | 0 | } |
796 | | } |
797 | | |
798 | | #[derive(Debug, Clone, Copy)] |
799 | | pub enum StandardObserver { |
800 | | D50, |
801 | | D65, |
802 | | Unknown, |
803 | | } |
804 | | |
805 | | impl From<u32> for StandardObserver { |
806 | 0 | fn from(value: u32) -> Self { |
807 | 0 | if value == 1 { |
808 | 0 | return StandardObserver::D50; |
809 | 0 | } else if value == 2 { |
810 | 0 | return StandardObserver::D65; |
811 | 0 | } |
812 | 0 | StandardObserver::Unknown |
813 | 0 | } |
814 | | } |
815 | | |
816 | | impl From<StandardObserver> for u32 { |
817 | 0 | fn from(value: StandardObserver) -> Self { |
818 | 0 | match value { |
819 | 0 | StandardObserver::D50 => 1, |
820 | 0 | StandardObserver::D65 => 2, |
821 | 0 | StandardObserver::Unknown => 0, |
822 | | } |
823 | 0 | } |
824 | | } |
825 | | |
826 | | #[derive(Debug, Clone, Copy)] |
827 | | pub struct ViewingConditions { |
828 | | pub illuminant: Xyz, |
829 | | pub surround: Xyz, |
830 | | pub observer: StandardObserver, |
831 | | } |
832 | | |
833 | | #[derive(Debug, Clone, Copy)] |
834 | | pub enum MeasurementGeometry { |
835 | | Unknown, |
836 | | /// 0°:45° or 45°:0° |
837 | | D45to45, |
838 | | /// 0°:d or d:0° |
839 | | D0to0, |
840 | | } |
841 | | |
842 | | impl From<u32> for MeasurementGeometry { |
843 | 0 | fn from(value: u32) -> Self { |
844 | 0 | if value == 1 { |
845 | 0 | Self::D45to45 |
846 | 0 | } else if value == 2 { |
847 | 0 | Self::D0to0 |
848 | | } else { |
849 | 0 | Self::Unknown |
850 | | } |
851 | 0 | } |
852 | | } |
853 | | |
854 | | #[derive(Debug, Clone, Copy)] |
855 | | pub enum StandardIlluminant { |
856 | | Unknown, |
857 | | D50, |
858 | | D65, |
859 | | D93, |
860 | | F2, |
861 | | D55, |
862 | | A, |
863 | | EquiPower, |
864 | | F8, |
865 | | } |
866 | | |
867 | | impl From<u32> for StandardIlluminant { |
868 | 0 | fn from(value: u32) -> Self { |
869 | 0 | match value { |
870 | 0 | 1 => StandardIlluminant::D50, |
871 | 0 | 2 => StandardIlluminant::D65, |
872 | 0 | 3 => StandardIlluminant::D93, |
873 | 0 | 4 => StandardIlluminant::F2, |
874 | 0 | 5 => StandardIlluminant::D55, |
875 | 0 | 6 => StandardIlluminant::A, |
876 | 0 | 7 => StandardIlluminant::EquiPower, |
877 | 0 | 8 => StandardIlluminant::F8, |
878 | 0 | _ => Self::Unknown, |
879 | | } |
880 | 0 | } |
881 | | } |
882 | | |
883 | | impl From<StandardIlluminant> for u32 { |
884 | 0 | fn from(value: StandardIlluminant) -> Self { |
885 | 0 | match value { |
886 | 0 | StandardIlluminant::Unknown => 0u32, |
887 | 0 | StandardIlluminant::D50 => 1u32, |
888 | 0 | StandardIlluminant::D65 => 2u32, |
889 | 0 | StandardIlluminant::D93 => 3, |
890 | 0 | StandardIlluminant::F2 => 4, |
891 | 0 | StandardIlluminant::D55 => 5, |
892 | 0 | StandardIlluminant::A => 6, |
893 | 0 | StandardIlluminant::EquiPower => 7, |
894 | 0 | StandardIlluminant::F8 => 8, |
895 | | } |
896 | 0 | } |
897 | | } |
898 | | |
899 | | #[derive(Debug, Clone, Copy)] |
900 | | pub struct Measurement { |
901 | | pub observer: StandardObserver, |
902 | | pub backing: Xyz, |
903 | | pub geometry: MeasurementGeometry, |
904 | | pub flare: f32, |
905 | | pub illuminant: StandardIlluminant, |
906 | | } |
907 | | |
908 | | /// ICC Profile representation |
909 | | #[repr(C)] |
910 | | #[derive(Debug, Clone, Default)] |
911 | | pub struct ColorProfile { |
912 | | pub pcs: DataColorSpace, |
913 | | pub color_space: DataColorSpace, |
914 | | pub profile_class: ProfileClass, |
915 | | pub rendering_intent: RenderingIntent, |
916 | | pub red_colorant: Xyzd, |
917 | | pub green_colorant: Xyzd, |
918 | | pub blue_colorant: Xyzd, |
919 | | pub white_point: Xyzd, |
920 | | pub black_point: Option<Xyzd>, |
921 | | pub media_white_point: Option<Xyzd>, |
922 | | pub luminance: Option<Xyzd>, |
923 | | pub measurement: Option<Measurement>, |
924 | | pub red_trc: Option<ToneReprCurve>, |
925 | | pub green_trc: Option<ToneReprCurve>, |
926 | | pub blue_trc: Option<ToneReprCurve>, |
927 | | pub gray_trc: Option<ToneReprCurve>, |
928 | | pub cicp: Option<CicpProfile>, |
929 | | pub chromatic_adaptation: Option<Matrix3d>, |
930 | | pub lut_a_to_b_perceptual: Option<LutWarehouse>, |
931 | | pub lut_a_to_b_colorimetric: Option<LutWarehouse>, |
932 | | pub lut_a_to_b_saturation: Option<LutWarehouse>, |
933 | | pub lut_b_to_a_perceptual: Option<LutWarehouse>, |
934 | | pub lut_b_to_a_colorimetric: Option<LutWarehouse>, |
935 | | pub lut_b_to_a_saturation: Option<LutWarehouse>, |
936 | | pub gamut: Option<LutWarehouse>, |
937 | | pub copyright: Option<ProfileText>, |
938 | | pub description: Option<ProfileText>, |
939 | | pub device_manufacturer: Option<ProfileText>, |
940 | | pub device_model: Option<ProfileText>, |
941 | | pub char_target: Option<ProfileText>, |
942 | | pub viewing_conditions: Option<ViewingConditions>, |
943 | | pub viewing_conditions_description: Option<ProfileText>, |
944 | | pub technology: Option<TechnologySignatures>, |
945 | | pub calibration_date: Option<ColorDateTime>, |
946 | | pub creation_date_time: ColorDateTime, |
947 | | /// Version for internal and viewing purposes only. |
948 | | /// On encoding this is computable property which will set at least V4. |
949 | | pub(crate) version_internal: ProfileVersion, |
950 | | } |
951 | | |
952 | | #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)] |
953 | | pub struct ParsingOptions { |
954 | | // Maximum allowed profile size in bytes |
955 | | pub max_profile_size: usize, |
956 | | // Maximum allowed CLUT size in bytes |
957 | | pub max_allowed_clut_size: usize, |
958 | | // Maximum allowed TRC size in elements count |
959 | | pub max_allowed_trc_size: usize, |
960 | | } |
961 | | |
962 | | impl Default for ParsingOptions { |
963 | 0 | fn default() -> Self { |
964 | 0 | Self { |
965 | 0 | max_profile_size: MAX_PROFILE_SIZE, |
966 | 0 | max_allowed_clut_size: 10_000_000, |
967 | 0 | max_allowed_trc_size: 40_000, |
968 | 0 | } |
969 | 0 | } |
970 | | } |
971 | | |
972 | | impl ColorProfile { |
973 | | /// Returns profile version |
974 | 0 | pub fn version(&self) -> ProfileVersion { |
975 | 0 | self.version_internal |
976 | 0 | } |
977 | | |
978 | 0 | pub fn new_from_slice(slice: &[u8]) -> Result<Self, CmsError> { |
979 | 0 | Self::new_from_slice_with_options(slice, Default::default()) |
980 | 0 | } |
981 | | |
982 | 0 | pub fn new_from_slice_with_options( |
983 | 0 | slice: &[u8], |
984 | 0 | options: ParsingOptions, |
985 | 0 | ) -> Result<Self, CmsError> { |
986 | 0 | let header = ProfileHeader::new_from_slice(slice)?; |
987 | 0 | let tags_count = header.tag_count as usize; |
988 | 0 | if slice.len() >= options.max_profile_size { |
989 | 0 | return Err(CmsError::InvalidProfile); |
990 | 0 | } |
991 | 0 | let tags_end = tags_count |
992 | 0 | .safe_mul(TAG_SIZE)? |
993 | 0 | .safe_add(size_of::<ProfileHeader>())?; |
994 | 0 | if slice.len() < tags_end { |
995 | 0 | return Err(CmsError::InvalidProfile); |
996 | 0 | } |
997 | 0 | let tags_slice = &slice[size_of::<ProfileHeader>()..tags_end]; |
998 | 0 | let mut profile = ColorProfile { |
999 | 0 | rendering_intent: header.rendering_intent, |
1000 | 0 | pcs: header.pcs, |
1001 | 0 | profile_class: header.profile_class, |
1002 | 0 | color_space: header.data_color_space, |
1003 | 0 | white_point: header.illuminant.to_xyzd(), |
1004 | 0 | version_internal: header.version, |
1005 | 0 | creation_date_time: header.creation_date_time, |
1006 | 0 | ..Default::default() |
1007 | 0 | }; |
1008 | 0 | let color_space = profile.color_space; |
1009 | 0 | for tag in tags_slice.chunks_exact(TAG_SIZE) { |
1010 | 0 | let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); |
1011 | 0 | let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]); |
1012 | 0 | let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; |
1013 | | // Just ignore unknown tags |
1014 | 0 | if let Ok(tag) = Tag::try_from(tag_value) { |
1015 | 0 | match tag { |
1016 | | Tag::RedXyz => { |
1017 | 0 | if color_space == DataColorSpace::Rgb { |
1018 | | profile.red_colorant = |
1019 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
1020 | 0 | } |
1021 | | } |
1022 | | Tag::GreenXyz => { |
1023 | 0 | if color_space == DataColorSpace::Rgb { |
1024 | | profile.green_colorant = |
1025 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
1026 | 0 | } |
1027 | | } |
1028 | | Tag::BlueXyz => { |
1029 | 0 | if color_space == DataColorSpace::Rgb { |
1030 | | profile.blue_colorant = |
1031 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; |
1032 | 0 | } |
1033 | | } |
1034 | | Tag::RedToneReproduction => { |
1035 | 0 | if color_space == DataColorSpace::Rgb { |
1036 | 0 | profile.red_trc = Self::read_trc_tag_s( |
1037 | 0 | slice, |
1038 | 0 | tag_entry as usize, |
1039 | 0 | tag_size, |
1040 | 0 | &options, |
1041 | 0 | )?; |
1042 | 0 | } |
1043 | | } |
1044 | | Tag::GreenToneReproduction => { |
1045 | 0 | if color_space == DataColorSpace::Rgb { |
1046 | 0 | profile.green_trc = Self::read_trc_tag_s( |
1047 | 0 | slice, |
1048 | 0 | tag_entry as usize, |
1049 | 0 | tag_size, |
1050 | 0 | &options, |
1051 | 0 | )?; |
1052 | 0 | } |
1053 | | } |
1054 | | Tag::BlueToneReproduction => { |
1055 | 0 | if color_space == DataColorSpace::Rgb { |
1056 | 0 | profile.blue_trc = Self::read_trc_tag_s( |
1057 | 0 | slice, |
1058 | 0 | tag_entry as usize, |
1059 | 0 | tag_size, |
1060 | 0 | &options, |
1061 | 0 | )?; |
1062 | 0 | } |
1063 | | } |
1064 | | Tag::GreyToneReproduction => { |
1065 | 0 | if color_space == DataColorSpace::Gray { |
1066 | 0 | profile.gray_trc = Self::read_trc_tag_s( |
1067 | 0 | slice, |
1068 | 0 | tag_entry as usize, |
1069 | 0 | tag_size, |
1070 | 0 | &options, |
1071 | 0 | )?; |
1072 | 0 | } |
1073 | | } |
1074 | | Tag::MediaWhitePoint => { |
1075 | | profile.media_white_point = |
1076 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; |
1077 | | } |
1078 | | Tag::Luminance => { |
1079 | | profile.luminance = |
1080 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; |
1081 | | } |
1082 | | Tag::Measurement => { |
1083 | | profile.measurement = |
1084 | 0 | Self::read_meas_tag(slice, tag_entry as usize, tag_size)?; |
1085 | | } |
1086 | | Tag::CodeIndependentPoints => { |
1087 | | // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the |
1088 | | // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces |
1089 | | // or profile classes indicated in the profile header. |
1090 | 0 | if (profile.profile_class == ProfileClass::InputDevice |
1091 | 0 | || profile.profile_class == ProfileClass::DisplayDevice) |
1092 | 0 | && (profile.color_space == DataColorSpace::Rgb |
1093 | 0 | || profile.color_space == DataColorSpace::YCbr |
1094 | 0 | || profile.color_space == DataColorSpace::Xyz) |
1095 | | { |
1096 | | profile.cicp = |
1097 | 0 | Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?; |
1098 | 0 | } |
1099 | | } |
1100 | | Tag::ChromaticAdaptation => { |
1101 | | profile.chromatic_adaptation = |
1102 | 0 | Self::read_chad_tag(slice, tag_entry as usize, tag_size)?; |
1103 | | } |
1104 | | Tag::BlackPoint => { |
1105 | | profile.black_point = |
1106 | 0 | Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)? |
1107 | | } |
1108 | | Tag::DeviceToPcsLutPerceptual => { |
1109 | 0 | profile.lut_a_to_b_perceptual = |
1110 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1111 | | } |
1112 | | Tag::DeviceToPcsLutColorimetric => { |
1113 | 0 | profile.lut_a_to_b_colorimetric = |
1114 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1115 | | } |
1116 | | Tag::DeviceToPcsLutSaturation => { |
1117 | 0 | profile.lut_a_to_b_saturation = |
1118 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1119 | | } |
1120 | | Tag::PcsToDeviceLutPerceptual => { |
1121 | 0 | profile.lut_b_to_a_perceptual = |
1122 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1123 | | } |
1124 | | Tag::PcsToDeviceLutColorimetric => { |
1125 | 0 | profile.lut_b_to_a_colorimetric = |
1126 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1127 | | } |
1128 | | Tag::PcsToDeviceLutSaturation => { |
1129 | 0 | profile.lut_b_to_a_saturation = |
1130 | 0 | Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1131 | | } |
1132 | | Tag::Gamut => { |
1133 | 0 | profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; |
1134 | | } |
1135 | | Tag::Copyright => { |
1136 | 0 | profile.copyright = |
1137 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1138 | | } |
1139 | | Tag::ProfileDescription => { |
1140 | 0 | profile.description = |
1141 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1142 | | } |
1143 | | Tag::ViewingConditionsDescription => { |
1144 | 0 | profile.viewing_conditions_description = |
1145 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1146 | | } |
1147 | | Tag::DeviceModel => { |
1148 | 0 | profile.device_model = |
1149 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1150 | | } |
1151 | | Tag::DeviceManufacturer => { |
1152 | 0 | profile.device_manufacturer = |
1153 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1154 | | } |
1155 | | Tag::CharTarget => { |
1156 | 0 | profile.char_target = |
1157 | 0 | Self::read_string_tag(slice, tag_entry as usize, tag_size)?; |
1158 | | } |
1159 | 0 | Tag::Chromaticity => {} |
1160 | | Tag::ObserverConditions => { |
1161 | | profile.viewing_conditions = |
1162 | 0 | Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?; |
1163 | | } |
1164 | | Tag::Technology => { |
1165 | | profile.technology = |
1166 | 0 | Self::read_tech_tag(slice, tag_entry as usize, tag_size)?; |
1167 | | } |
1168 | | Tag::CalibrationDateTime => { |
1169 | | profile.calibration_date = |
1170 | 0 | Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?; |
1171 | | } |
1172 | | } |
1173 | 0 | } |
1174 | | } |
1175 | | |
1176 | 0 | Ok(profile) |
1177 | 0 | } |
1178 | | } |
1179 | | |
1180 | | impl ColorProfile { |
1181 | | #[inline] |
1182 | 0 | pub fn colorant_matrix(&self) -> Matrix3d { |
1183 | 0 | Matrix3d { |
1184 | 0 | v: [ |
1185 | 0 | [ |
1186 | 0 | self.red_colorant.x, |
1187 | 0 | self.green_colorant.x, |
1188 | 0 | self.blue_colorant.x, |
1189 | 0 | ], |
1190 | 0 | [ |
1191 | 0 | self.red_colorant.y, |
1192 | 0 | self.green_colorant.y, |
1193 | 0 | self.blue_colorant.y, |
1194 | 0 | ], |
1195 | 0 | [ |
1196 | 0 | self.red_colorant.z, |
1197 | 0 | self.green_colorant.z, |
1198 | 0 | self.blue_colorant.z, |
1199 | 0 | ], |
1200 | 0 | ], |
1201 | 0 | } |
1202 | 0 | } |
1203 | | |
1204 | | /// Computes colorants matrix. Returns not transposed matrix. |
1205 | | /// |
1206 | | /// To work on `const` context this method does have restrictions. |
1207 | | /// If invalid values were provided it may return invalid matrix or NaNs. |
1208 | 0 | pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d { |
1209 | 0 | let red_xyz = primaries.red.to_xyzd(); |
1210 | 0 | let green_xyz = primaries.green.to_xyzd(); |
1211 | 0 | let blue_xyz = primaries.blue.to_xyzd(); |
1212 | | |
1213 | 0 | let xyz_matrix = Matrix3d { |
1214 | 0 | v: [ |
1215 | 0 | [red_xyz.x, green_xyz.x, blue_xyz.x], |
1216 | 0 | [red_xyz.y, green_xyz.y, blue_xyz.y], |
1217 | 0 | [red_xyz.z, green_xyz.z, blue_xyz.z], |
1218 | 0 | ], |
1219 | 0 | }; |
1220 | 0 | let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); |
1221 | 0 | adapt_to_d50_d(colorants, white_point) |
1222 | 0 | } |
1223 | | |
1224 | | /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point |
1225 | | /// This will nullify CICP. |
1226 | 0 | pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) { |
1227 | 0 | self.cicp = None; |
1228 | 0 | let red_xyz = primaries.red.to_xyzd(); |
1229 | 0 | let green_xyz = primaries.green.to_xyzd(); |
1230 | 0 | let blue_xyz = primaries.blue.to_xyzd(); |
1231 | | |
1232 | 0 | self.chromatic_adaptation = Some(BRADFORD_D); |
1233 | 0 | self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz) |
1234 | 0 | } |
1235 | | |
1236 | | /// Updates RGB triple colorimetry from 3 [Xyzd] and white point |
1237 | | /// |
1238 | | /// To work on `const` context this method does have restrictions. |
1239 | | /// If invalid values were provided it may return invalid matrix or NaNs. |
1240 | | /// |
1241 | | /// This will void CICP tag. |
1242 | 0 | pub const fn update_rgb_colorimetry_triplet( |
1243 | 0 | &mut self, |
1244 | 0 | white_point: XyY, |
1245 | 0 | red_xyz: Xyzd, |
1246 | 0 | green_xyz: Xyzd, |
1247 | 0 | blue_xyz: Xyzd, |
1248 | 0 | ) { |
1249 | 0 | self.cicp = None; |
1250 | 0 | let xyz_matrix = Matrix3d { |
1251 | 0 | v: [ |
1252 | 0 | [red_xyz.x, green_xyz.x, blue_xyz.x], |
1253 | 0 | [red_xyz.y, green_xyz.y, blue_xyz.y], |
1254 | 0 | [red_xyz.z, green_xyz.z, blue_xyz.z], |
1255 | 0 | ], |
1256 | 0 | }; |
1257 | 0 | let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); |
1258 | 0 | let colorants = adapt_to_d50_d(colorants, white_point); |
1259 | | |
1260 | 0 | self.update_colorants(colorants); |
1261 | 0 | } |
1262 | | |
1263 | 0 | pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) { |
1264 | | // note: there's a transpose type of operation going on here |
1265 | 0 | self.red_colorant.x = colorants.v[0][0]; |
1266 | 0 | self.red_colorant.y = colorants.v[1][0]; |
1267 | 0 | self.red_colorant.z = colorants.v[2][0]; |
1268 | 0 | self.green_colorant.x = colorants.v[0][1]; |
1269 | 0 | self.green_colorant.y = colorants.v[1][1]; |
1270 | 0 | self.green_colorant.z = colorants.v[2][1]; |
1271 | 0 | self.blue_colorant.x = colorants.v[0][2]; |
1272 | 0 | self.blue_colorant.y = colorants.v[1][2]; |
1273 | 0 | self.blue_colorant.z = colorants.v[2][2]; |
1274 | 0 | } |
1275 | | |
1276 | | /// Updates RGB triple colorimetry from CICP |
1277 | 0 | pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool { |
1278 | 0 | if !cicp.color_primaries.has_chromaticity() |
1279 | 0 | || !cicp.transfer_characteristics.has_transfer_curve() |
1280 | | { |
1281 | 0 | return false; |
1282 | 0 | } |
1283 | 0 | let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() { |
1284 | 0 | Ok(primaries) => primaries, |
1285 | 0 | Err(_) => return false, |
1286 | | }; |
1287 | 0 | let white_point: Chromaticity = match cicp.color_primaries.white_point() { |
1288 | 0 | Ok(v) => v, |
1289 | 0 | Err(_) => return false, |
1290 | | }; |
1291 | 0 | self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy); |
1292 | 0 | self.cicp = Some(cicp); |
1293 | | |
1294 | 0 | let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() { |
1295 | 0 | Ok(trc) => trc, |
1296 | 0 | Err(_) => return false, |
1297 | | }; |
1298 | 0 | self.green_trc = Some(red_trc.clone()); |
1299 | 0 | self.blue_trc = Some(red_trc.clone()); |
1300 | 0 | self.red_trc = Some(red_trc); |
1301 | 0 | false |
1302 | 0 | } |
1303 | | |
1304 | 0 | pub const fn rgb_to_xyz(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { |
1305 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1306 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector()); |
1307 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1308 | 0 | v = v.mul_row_vector::<1>(s); |
1309 | 0 | v.mul_row_vector::<2>(s) |
1310 | 0 | } |
1311 | | |
1312 | | /// If Primaries is invalid will return invalid matrix on const context. |
1313 | | /// This assumes not transposed matrix and returns not transposed matrix. |
1314 | 0 | pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d { |
1315 | 0 | let xyz_inverse = xyz_matrix.inverse(); |
1316 | 0 | let s = xyz_inverse.mul_vector(wp.to_vector_d()); |
1317 | 0 | let mut v = xyz_matrix.mul_row_vector::<0>(s); |
1318 | 0 | v = v.mul_row_vector::<1>(s); |
1319 | 0 | v = v.mul_row_vector::<2>(s); |
1320 | 0 | v |
1321 | 0 | } |
1322 | | |
1323 | | /// Returns the RGB to XYZ transformation matrix. |
1324 | | /// |
1325 | | /// Per ICC.1:2022-05 Section F.3, the computational model is: |
1326 | | /// connection = colorantMatrix × linear_rgb |
1327 | | /// |
1328 | | /// The colorant tags (rXYZ, gXYZ, bXYZ) are used directly as matrix columns. |
1329 | | /// This matches skcms and lcms2 behavior. |
1330 | 0 | pub fn rgb_to_xyz_matrix(&self) -> Matrix3d { |
1331 | 0 | self.colorant_matrix() |
1332 | 0 | } |
1333 | | |
1334 | | /// Computes transform matrix RGB -> XYZ -> RGB |
1335 | | /// Current profile is used as source, other as destination |
1336 | 0 | pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d { |
1337 | 0 | let source = self.rgb_to_xyz_matrix(); |
1338 | 0 | let dst = dest.rgb_to_xyz_matrix(); |
1339 | 0 | let dest_inverse = dst.inverse(); |
1340 | 0 | dest_inverse.mat_mul(source) |
1341 | 0 | } |
1342 | | |
1343 | | /// Returns volume of colors stored in profile |
1344 | 0 | pub fn profile_volume(&self) -> Option<f32> { |
1345 | 0 | let red_prim = self.red_colorant; |
1346 | 0 | let green_prim = self.green_colorant; |
1347 | 0 | let blue_prim = self.blue_colorant; |
1348 | 0 | let tetrahedral_vertices = Matrix3d { |
1349 | 0 | v: [ |
1350 | 0 | [red_prim.x, red_prim.y, red_prim.z], |
1351 | 0 | [green_prim.x, green_prim.y, green_prim.z], |
1352 | 0 | [blue_prim.x, blue_prim.y, blue_prim.z], |
1353 | 0 | ], |
1354 | 0 | }; |
1355 | 0 | let det = tetrahedral_vertices.determinant()?; |
1356 | 0 | Some((det / 6.0f64) as f32) |
1357 | 0 | } |
1358 | | |
1359 | | #[allow(unused)] |
1360 | 0 | pub(crate) fn has_device_to_pcs_lut(&self) -> bool { |
1361 | 0 | self.lut_a_to_b_perceptual.is_some() |
1362 | 0 | || self.lut_a_to_b_saturation.is_some() |
1363 | 0 | || self.lut_a_to_b_colorimetric.is_some() |
1364 | 0 | } |
1365 | | |
1366 | | #[allow(unused)] |
1367 | 0 | pub(crate) fn has_pcs_to_device_lut(&self) -> bool { |
1368 | 0 | self.lut_b_to_a_perceptual.is_some() |
1369 | 0 | || self.lut_b_to_a_saturation.is_some() |
1370 | 0 | || self.lut_b_to_a_colorimetric.is_some() |
1371 | 0 | } |
1372 | | } |
1373 | | |
1374 | | #[cfg(test)] |
1375 | | mod tests { |
1376 | | use super::*; |
1377 | | use std::fs; |
1378 | | |
1379 | | #[test] |
1380 | | fn test_gray() { |
1381 | | if let Ok(gray_icc) = fs::read("./assets/Generic Gray Gamma 2.2 Profile.icc") { |
1382 | | let f_p = ColorProfile::new_from_slice(&gray_icc).unwrap(); |
1383 | | assert!(f_p.gray_trc.is_some()); |
1384 | | } |
1385 | | } |
1386 | | |
1387 | | #[test] |
1388 | | fn test_perceptual() { |
1389 | | if let Ok(srgb_perceptual_icc) = fs::read("./assets/srgb_perceptual.icc") { |
1390 | | let f_p = ColorProfile::new_from_slice(&srgb_perceptual_icc).unwrap(); |
1391 | | assert_eq!(f_p.pcs, DataColorSpace::Lab); |
1392 | | assert_eq!(f_p.color_space, DataColorSpace::Rgb); |
1393 | | assert_eq!(f_p.version(), ProfileVersion::V4_2); |
1394 | | assert!(f_p.lut_a_to_b_perceptual.is_some()); |
1395 | | assert!(f_p.lut_b_to_a_perceptual.is_some()); |
1396 | | } |
1397 | | } |
1398 | | |
1399 | | #[test] |
1400 | | fn test_us_swop_coated() { |
1401 | | if let Ok(us_swop_coated) = fs::read("./assets/us_swop_coated.icc") { |
1402 | | let f_p = ColorProfile::new_from_slice(&us_swop_coated).unwrap(); |
1403 | | assert_eq!(f_p.pcs, DataColorSpace::Lab); |
1404 | | assert_eq!(f_p.color_space, DataColorSpace::Cmyk); |
1405 | | assert_eq!(f_p.version(), ProfileVersion::V2_0); |
1406 | | |
1407 | | assert!(f_p.lut_a_to_b_perceptual.is_some()); |
1408 | | assert!(f_p.lut_b_to_a_perceptual.is_some()); |
1409 | | |
1410 | | assert!(f_p.lut_a_to_b_colorimetric.is_some()); |
1411 | | assert!(f_p.lut_b_to_a_colorimetric.is_some()); |
1412 | | |
1413 | | assert!(f_p.gamut.is_some()); |
1414 | | |
1415 | | assert!(f_p.copyright.is_some()); |
1416 | | assert!(f_p.description.is_some()); |
1417 | | } |
1418 | | } |
1419 | | |
1420 | | #[test] |
1421 | | fn test_matrix_shaper() { |
1422 | | if let Ok(matrix_shaper) = fs::read("./assets/Display P3.icc") { |
1423 | | let f_p = ColorProfile::new_from_slice(&matrix_shaper).unwrap(); |
1424 | | assert_eq!(f_p.pcs, DataColorSpace::Xyz); |
1425 | | assert_eq!(f_p.color_space, DataColorSpace::Rgb); |
1426 | | assert_eq!(f_p.version(), ProfileVersion::V4_0); |
1427 | | |
1428 | | assert!(f_p.red_trc.is_some()); |
1429 | | assert!(f_p.blue_trc.is_some()); |
1430 | | assert!(f_p.green_trc.is_some()); |
1431 | | |
1432 | | assert_ne!(f_p.red_colorant, Xyzd::default()); |
1433 | | assert_ne!(f_p.blue_colorant, Xyzd::default()); |
1434 | | assert_ne!(f_p.green_colorant, Xyzd::default()); |
1435 | | |
1436 | | assert!(f_p.copyright.is_some()); |
1437 | | assert!(f_p.description.is_some()); |
1438 | | } |
1439 | | } |
1440 | | |
1441 | | /// Verify rgb_to_xyz_matrix returns colorant_matrix directly per ICC.1:2022-05 F.3. |
1442 | | /// |
1443 | | /// SM245B.icc is a V2 Samsung monitor profile with D65 colorants and no CHAD tag. |
1444 | | /// Source: https://skia.googlesource.com/skcms/+/refs/heads/main/profiles/misc/SM245B.icc |
1445 | | #[test] |
1446 | | fn test_rgb_to_xyz_matrix_equals_colorant_matrix() { |
1447 | | // Test with SM245B.icc (D65 colorants, no CHAD tag) |
1448 | | if let Ok(icc_data) = fs::read("./assets/SM245B.icc") { |
1449 | | if let Ok(profile) = ColorProfile::new_from_slice(&icc_data) { |
1450 | | let rgb_to_xyz = profile.rgb_to_xyz_matrix(); |
1451 | | let colorants = profile.colorant_matrix(); |
1452 | | |
1453 | | for i in 0..3 { |
1454 | | for j in 0..3 { |
1455 | | assert!( |
1456 | | (rgb_to_xyz.v[i][j] - colorants.v[i][j]).abs() < 1e-10, |
1457 | | "rgb_to_xyz_matrix should equal colorant_matrix at [{i}][{j}]" |
1458 | | ); |
1459 | | } |
1460 | | } |
1461 | | } |
1462 | | } |
1463 | | |
1464 | | // Also verify with sRGB |
1465 | | let srgb = ColorProfile::new_srgb(); |
1466 | | let rgb_to_xyz = srgb.rgb_to_xyz_matrix(); |
1467 | | let colorants = srgb.colorant_matrix(); |
1468 | | |
1469 | | for i in 0..3 { |
1470 | | for j in 0..3 { |
1471 | | assert!( |
1472 | | (rgb_to_xyz.v[i][j] - colorants.v[i][j]).abs() < 1e-10, |
1473 | | "sRGB: rgb_to_xyz_matrix should equal colorant_matrix at [{i}][{j}]" |
1474 | | ); |
1475 | | } |
1476 | | } |
1477 | | } |
1478 | | |
1479 | | #[test] |
1480 | | fn test_profile_version_parsing_standard() { |
1481 | | // Standard versions should work |
1482 | | assert_eq!( |
1483 | | ProfileVersion::try_from(0x02000000).unwrap(), |
1484 | | ProfileVersion::V2_0 |
1485 | | ); |
1486 | | assert_eq!( |
1487 | | ProfileVersion::try_from(0x02400000).unwrap(), |
1488 | | ProfileVersion::V2_4 |
1489 | | ); |
1490 | | assert_eq!( |
1491 | | ProfileVersion::try_from(0x04000000).unwrap(), |
1492 | | ProfileVersion::V4_0 |
1493 | | ); |
1494 | | assert_eq!( |
1495 | | ProfileVersion::try_from(0x04400000).unwrap(), |
1496 | | ProfileVersion::V4_4 |
1497 | | ); |
1498 | | } |
1499 | | |
1500 | | #[test] |
1501 | | fn test_profile_version_parsing_patch_versions() { |
1502 | | // Patch versions found in real ICC profiles should be accepted |
1503 | | |
1504 | | // v2.0.2 (SM245B.icc) - minor bugfix version |
1505 | | assert!( |
1506 | | ProfileVersion::try_from(0x02020000).is_ok(), |
1507 | | "v2.0.2 should be accepted" |
1508 | | ); |
1509 | | |
1510 | | // v3.4 (ibm-t61.icc, new.icc) - intermediate version |
1511 | | assert!( |
1512 | | ProfileVersion::try_from(0x03400000).is_ok(), |
1513 | | "v3.4 should be accepted" |
1514 | | ); |
1515 | | |
1516 | | // v4.2.9 (lcms_samsung_syncmaster.icc) - patch version |
1517 | | assert!( |
1518 | | ProfileVersion::try_from(0x04290000).is_ok(), |
1519 | | "v4.2.9 should be accepted" |
1520 | | ); |
1521 | | } |
1522 | | |
1523 | | #[test] |
1524 | | fn test_profile_version_parsing_rejected() { |
1525 | | // Invalid and unsupported versions should be rejected |
1526 | | |
1527 | | // v0.0 - invalid version (no such ICC spec exists) |
1528 | | assert!( |
1529 | | ProfileVersion::try_from(0x00000000).is_err(), |
1530 | | "v0.0 should be rejected" |
1531 | | ); |
1532 | | |
1533 | | // v5.0 (iccMAX) - reject because it has different white point requirements |
1534 | | assert!( |
1535 | | ProfileVersion::try_from(0x05000000).is_err(), |
1536 | | "v5.0 should be rejected" |
1537 | | ); |
1538 | | |
1539 | | // v6.0 - future/unknown version |
1540 | | assert!( |
1541 | | ProfileVersion::try_from(0x06000000).is_err(), |
1542 | | "v6.0 should be rejected" |
1543 | | ); |
1544 | | } |
1545 | | |
1546 | | #[test] |
1547 | | fn test_profile_version_v4_4_mapping() { |
1548 | | // V4.4 should map to V4_4, not V4_3 (regression test for typo) |
1549 | | assert_eq!( |
1550 | | ProfileVersion::try_from(0x04400000).unwrap(), |
1551 | | ProfileVersion::V4_4 |
1552 | | ); |
1553 | | } |
1554 | | |
1555 | | #[test] |
1556 | | fn test_rendering_intent_invalid_defaults_to_perceptual() { |
1557 | | // Valid values are 0-3. Invalid values default to Perceptual |
1558 | | // rather than rejecting the profile. |
1559 | | assert_eq!( |
1560 | | RenderingIntent::try_from(0x01000000).unwrap(), |
1561 | | RenderingIntent::Perceptual |
1562 | | ); |
1563 | | assert_eq!( |
1564 | | RenderingIntent::try_from(0x04000000).unwrap(), |
1565 | | RenderingIntent::Perceptual |
1566 | | ); |
1567 | | assert_eq!( |
1568 | | RenderingIntent::try_from(0xFFFFFFFF).unwrap(), |
1569 | | RenderingIntent::Perceptual |
1570 | | ); |
1571 | | } |
1572 | | |
1573 | | /// Parse a profile with a non-conforming rendering intent value. |
1574 | | /// Synthesized from SM245B.icc with bytes 64-67 set to 0x01000000 |
1575 | | /// (byte-swapped, as found in old Linotype "Lino" profiles). |
1576 | | /// The invalid value should default to Perceptual per our policy. |
1577 | | #[test] |
1578 | | fn test_invalid_rendering_intent_defaults_to_perceptual() { |
1579 | | let icc_data = |
1580 | | fs::read("./assets/swapped_intent.icc").expect("swapped_intent.icc test asset"); |
1581 | | let profile = ColorProfile::new_from_slice(&icc_data) |
1582 | | .expect("Profile with invalid rendering intent should parse"); |
1583 | | assert_eq!(profile.rendering_intent, RenderingIntent::Perceptual); |
1584 | | // Verify the rest of the profile parsed correctly |
1585 | | assert_eq!(profile.color_space, DataColorSpace::Rgb); |
1586 | | assert!(profile.red_trc.is_some()); |
1587 | | assert!(profile.green_trc.is_some()); |
1588 | | assert!(profile.blue_trc.is_some()); |
1589 | | let dst = ColorProfile::new_srgb(); |
1590 | | let transform = profile |
1591 | | .create_transform_8bit(Layout::Rgba, &dst, Layout::Rgba, Default::default()) |
1592 | | .expect("Should create transform from profile with defaulted intent"); |
1593 | | let src = [128u8, 128, 128, 255]; |
1594 | | let mut out = [0u8; 4]; |
1595 | | transform.transform(&src, &mut out).unwrap(); |
1596 | | assert_eq!(out[3], 255, "Alpha should be preserved"); |
1597 | | } |
1598 | | |
1599 | | /// v4 profile with correct mluc description tag should parse as |
1600 | | /// Localizable (regression: ensure desc-tolerance doesn't break mluc). |
1601 | | #[test] |
1602 | | fn test_v4_mluc_description_parses_as_localizable() { |
1603 | | let icc_data = fs::read("./assets/Display P3.icc").expect("Display P3.icc test asset"); |
1604 | | let profile = |
1605 | | ColorProfile::new_from_slice(&icc_data).expect("Display P3 profile should parse"); |
1606 | | assert_eq!(profile.version(), ProfileVersion::V4_0); |
1607 | | let desc = profile |
1608 | | .description |
1609 | | .clone() |
1610 | | .expect("description should be present"); |
1611 | | match desc { |
1612 | | super::ProfileText::Localizable(records) => { |
1613 | | assert!(!records.is_empty(), "mluc should have at least one record"); |
1614 | | assert!( |
1615 | | records[0].value.contains("Display P3"), |
1616 | | "mluc should contain 'Display P3', got: {}", |
1617 | | records[0].value |
1618 | | ); |
1619 | | } |
1620 | | other => panic!("v4 mluc should parse as Localizable, got {:?}", other), |
1621 | | } |
1622 | | } |
1623 | | |
1624 | | /// v4 profile with non-conforming truncated desc tag should parse. |
1625 | | /// Synthesized from Display P3.icc with the mluc tag replaced by a |
1626 | | /// minimal desc tag (ASCII only, no Unicode/ScriptCode sections). |
1627 | | #[test] |
1628 | | fn test_v4_truncated_desc_tag() { |
1629 | | let icc_data = |
1630 | | fs::read("./assets/truncated_desc_v4.icc").expect("truncated_desc_v4.icc test asset"); |
1631 | | let profile = ColorProfile::new_from_slice(&icc_data) |
1632 | | .expect("v4 profile with truncated desc should parse"); |
1633 | | assert_eq!(profile.version(), ProfileVersion::V4_0); |
1634 | | assert_eq!(profile.color_space, DataColorSpace::Rgb); |
1635 | | let desc = profile |
1636 | | .description |
1637 | | .clone() |
1638 | | .expect("description should be present"); |
1639 | | match desc { |
1640 | | ProfileText::Description(d) => { |
1641 | | assert!( |
1642 | | d.ascii_string.contains("Display P3"), |
1643 | | "desc should contain 'Display P3', got: {}", |
1644 | | d.ascii_string |
1645 | | ); |
1646 | | } |
1647 | | other => panic!( |
1648 | | "v4 truncated desc should parse as Description, got {:?}", |
1649 | | other |
1650 | | ), |
1651 | | } |
1652 | | let dst = ColorProfile::new_srgb(); |
1653 | | let transform = profile |
1654 | | .create_transform_8bit(Layout::Rgba, &dst, Layout::Rgba, Default::default()) |
1655 | | .expect("Should create transform from v4 profile with truncated desc"); |
1656 | | let src = [128u8, 128, 128, 255]; |
1657 | | let mut out = [0u8; 4]; |
1658 | | transform.transform(&src, &mut out).unwrap(); |
1659 | | assert_eq!(out[3], 255, "Alpha should be preserved"); |
1660 | | } |
1661 | | } |