Skip to main content

moxcms/
reader.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 3/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 */
29use crate::err::try_vec;
30use crate::helpers::{read_matrix_3d, read_vector_3d};
31use crate::profile::LutDataType;
32use crate::safe_math::{SafeAdd, SafeMul, SafePowi};
33use crate::tag::{TAG_SIZE, TagTypeDefinition};
34use crate::{
35    CicpColorPrimaries, CicpProfile, CmsError, ColorDateTime, ColorProfile, DescriptionString,
36    LocalizableString, LutMultidimensionalType, LutStore, LutType, LutWarehouse, Matrix3d,
37    Matrix3f, MatrixCoefficients, Measurement, MeasurementGeometry, ParsingOptions, ProfileText,
38    StandardIlluminant, StandardObserver, TechnologySignatures, ToneReprCurve,
39    TransferCharacteristics, Vector3d, ViewingConditions, Xyz, Xyzd,
40};
41
42/// Produces the nearest float to `a` with a maximum error of 1/1024 which
43/// happens for large values like 0x40000040.
44#[inline]
45pub(crate) const fn s15_fixed16_number_to_float(a: i32) -> f32 {
46    a as f32 / 65536.
47}
48
49#[inline]
50pub(crate) const fn s15_fixed16_number_to_double(a: i32) -> f64 {
51    a as f64 / 65536.
52}
53
54#[inline]
55pub(crate) const fn uint16_number_to_float(a: u32) -> f32 {
56    a as f32 / 65535.
57}
58
59#[inline]
60pub(crate) const fn uint16_number_to_float_fast(a: u32) -> f32 {
61    a as f32 * (1. / 65535.)
62}
63
64// #[inline]
65// pub(crate) fn uint8_number_to_float(a: u8) -> f32 {
66//     a as f32 / 255.0
67// }
68
69#[inline]
70pub(crate) fn uint8_number_to_float_fast(a: u8) -> f32 {
71    a as f32 * (1. / 255.0)
72}
73
74fn utf16be_to_utf16(slice: &[u8]) -> Result<Vec<u16>, CmsError> {
75    let mut vec = try_vec![0u16; slice.len() / 2];
76    for (dst, chunk) in vec.iter_mut().zip(slice.chunks_exact(2)) {
77        *dst = u16::from_be_bytes([chunk[0], chunk[1]]);
78    }
79    Ok(vec)
80}
81
82/// Parse the Unicode section of a desc tag at `unicode_offset` (the byte
83/// offset right after the ASCII data). Returns `(language_code, string)`,
84/// or `Ok(None)` if truncated or the Unicode length is zero.
85fn read_desc_unicode(tag: &[u8], unicode_offset: usize) -> Result<Option<(u32, String)>, CmsError> {
86    if tag.len() < unicode_offset.safe_add(8)? {
87        return Ok(None);
88    }
89    let header = &tag[unicode_offset..unicode_offset.safe_add(8)?];
90    let language_code = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
91    let unicode_length = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize;
92    if unicode_length == 0 {
93        return Ok(Some((language_code, String::new())));
94    }
95    let byte_length = unicode_length.safe_mul(2)?;
96    let str_start = unicode_offset.safe_add(8)?;
97    if tag.len() < str_start.safe_add(byte_length)? {
98        return Ok(None);
99    }
100    let uc_data = &tag[str_start..str_start.safe_add(byte_length)?];
101    let wc = utf16be_to_utf16(uc_data)?;
102    let s = String::from_utf16_lossy(&wc)
103        .trim_end_matches('\0')
104        .to_string();
105    Ok(Some((language_code, s)))
106}
107
108impl ColorProfile {
109    pub(crate) fn read_lut_type(
110        slice: &[u8],
111        entry: usize,
112        tag_size: usize,
113    ) -> Result<LutType, CmsError> {
114        let tag_size = if tag_size == 0 { TAG_SIZE } else { tag_size };
115        let last_tag_offset = tag_size.safe_add(entry)?;
116        if last_tag_offset > slice.len() {
117            return Err(CmsError::InvalidProfile);
118        }
119        let tag = &slice[entry..last_tag_offset];
120        if tag.len() < 48 {
121            return Err(CmsError::InvalidProfile);
122        }
123        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
124        LutType::try_from(tag_type)
125    }
126
127    pub(crate) fn read_viewing_conditions(
128        slice: &[u8],
129        entry: usize,
130        tag_size: usize,
131    ) -> Result<Option<ViewingConditions>, CmsError> {
132        if tag_size < 36 {
133            return Ok(None);
134        }
135        if slice.len() < entry.safe_add(36)? {
136            return Err(CmsError::InvalidProfile);
137        }
138        let tag = &slice[entry..entry.safe_add(36)?];
139        let tag_type =
140            TagTypeDefinition::from(u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]));
141        // Ignore unknown
142        if tag_type != TagTypeDefinition::DefViewingConditions {
143            return Ok(None);
144        }
145        let illuminant_x = i32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]);
146        let illuminant_y = i32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]);
147        let illuminant_z = i32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]);
148
149        let surround_x = i32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]);
150        let surround_y = i32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]]);
151        let surround_z = i32::from_be_bytes([tag[28], tag[29], tag[30], tag[31]]);
152
153        let illuminant_type = u32::from_be_bytes([tag[32], tag[33], tag[34], tag[35]]);
154
155        let illuminant = Xyz::new(
156            s15_fixed16_number_to_float(illuminant_x),
157            s15_fixed16_number_to_float(illuminant_y),
158            s15_fixed16_number_to_float(illuminant_z),
159        );
160
161        let surround = Xyz::new(
162            s15_fixed16_number_to_float(surround_x),
163            s15_fixed16_number_to_float(surround_y),
164            s15_fixed16_number_to_float(surround_z),
165        );
166
167        let observer = StandardObserver::from(illuminant_type);
168
169        Ok(Some(ViewingConditions {
170            illuminant,
171            surround,
172            observer,
173        }))
174    }
175
176    pub(crate) fn read_string_tag(
177        slice: &[u8],
178        entry: usize,
179        tag_size: usize,
180    ) -> Result<Option<ProfileText>, CmsError> {
181        let tag_size = if tag_size == 0 { TAG_SIZE } else { tag_size };
182        if tag_size < 4 {
183            return Ok(None);
184        }
185        let last_tag_offset = tag_size.safe_add(entry)?;
186        if last_tag_offset > slice.len() {
187            return Err(CmsError::InvalidProfile);
188        }
189        let tag = &slice[entry..last_tag_offset];
190        if tag.len() < 8 {
191            return Ok(None);
192        }
193        let tag_type =
194            TagTypeDefinition::from(u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]));
195        // Ignore unknown
196        if tag_type == TagTypeDefinition::Text {
197            let sliced_from_to_end = &tag[8..tag.len()];
198            let str = String::from_utf8_lossy(sliced_from_to_end)
199                .trim_end_matches('\0')
200                .to_string();
201            return Ok(Some(ProfileText::PlainString(str)));
202        } else if tag_type == TagTypeDefinition::MultiLocalizedUnicode {
203            if tag.len() < 28 {
204                return Err(CmsError::InvalidProfile);
205            }
206            // let record_size = u32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]) as usize;
207            // // Record size is reserved to be 12.
208            // if record_size != 12 {
209            //     return Err(CmsError::InvalidIcc);
210            // }
211            let records_count = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize;
212            let primary_language_code = String::from_utf8_lossy(&[tag[16], tag[17]]).to_string();
213            let primary_country_code = String::from_utf8_lossy(&[tag[18], tag[19]]).to_string();
214            let first_string_record_length =
215                u32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]) as usize;
216            let first_record_offset =
217                u32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]]) as usize;
218
219            if tag.len() < first_record_offset.safe_add(first_string_record_length)? {
220                return Ok(None);
221            }
222
223            let resliced =
224                &tag[first_record_offset..first_record_offset + first_string_record_length];
225            let cvt = utf16be_to_utf16(resliced)?;
226            let string_record = String::from_utf16_lossy(&cvt);
227
228            let mut records = vec![LocalizableString {
229                language: primary_language_code,
230                country: primary_country_code,
231                value: string_record,
232            }];
233
234            for record in 1..records_count {
235                // Localizable header must be at least 12 bytes
236                let localizable_header_offset = if record == 1 {
237                    28
238                } else {
239                    28 + 12 * (record - 1)
240                };
241                if tag.len() < localizable_header_offset + 12 {
242                    return Err(CmsError::InvalidProfile);
243                }
244                let choked = &tag[localizable_header_offset..localizable_header_offset + 12];
245
246                let language_code = String::from_utf8_lossy(&[choked[0], choked[1]]).to_string();
247                let country_code = String::from_utf8_lossy(&[choked[2], choked[3]]).to_string();
248                let record_length =
249                    u32::from_be_bytes([choked[4], choked[5], choked[6], choked[7]]) as usize;
250                let string_offset =
251                    u32::from_be_bytes([choked[8], choked[9], choked[10], choked[11]]) as usize;
252
253                if tag.len() < string_offset.safe_add(record_length)? {
254                    return Ok(None);
255                }
256                let resliced = &tag[string_offset..string_offset + record_length];
257                let cvt = utf16be_to_utf16(resliced)?;
258                let string_record = String::from_utf16_lossy(&cvt);
259                records.push(LocalizableString {
260                    country: country_code,
261                    language: language_code,
262                    value: string_record,
263                });
264            }
265
266            return Ok(Some(ProfileText::Localizable(records)));
267        } else if tag_type == TagTypeDefinition::Description {
268            if tag.len() < 12 {
269                return Err(CmsError::InvalidProfile);
270            }
271            let ascii_length = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize;
272            let ascii_end = 12usize.safe_add(ascii_length)?;
273            if tag.len() < ascii_end {
274                return Err(CmsError::InvalidProfile);
275            }
276            let sliced = &tag[12..ascii_end];
277            let ascii_string = String::from_utf8_lossy(sliced)
278                .trim_end_matches('\0')
279                .to_string();
280
281            // Tolerate truncated desc tags — the Unicode and ScriptCode
282            // sections may be missing (common in non-conforming v4 profiles,
283            // but also seen in some v2 profiles in the wild).
284            let unicode_offset = ascii_end;
285            let (unicode_code, unicode_string) =
286                self::read_desc_unicode(tag, unicode_offset)?.unwrap_or((0, String::new()));
287
288            return Ok(Some(ProfileText::Description(DescriptionString {
289                ascii_string,
290                unicode_language_code: unicode_code,
291                unicode_string,
292                mac_string: "".to_string(),
293                script_code_code: -1,
294            })));
295        }
296        Ok(None)
297    }
298
299    fn read_lut_table_f32(table: &[u8], lut_type: LutType) -> Result<LutStore, CmsError> {
300        if lut_type == LutType::Lut16 {
301            let mut clut = try_vec![0u16; table.len() / 2];
302            for (src, dst) in table.chunks_exact(2).zip(clut.iter_mut()) {
303                *dst = u16::from_be_bytes([src[0], src[1]]);
304            }
305            Ok(LutStore::Store16(clut))
306        } else if lut_type == LutType::Lut8 {
307            let mut clut = try_vec![0u8; table.len()];
308            for (&src, dst) in table.iter().zip(clut.iter_mut()) {
309                *dst = src;
310            }
311            Ok(LutStore::Store8(clut))
312        } else {
313            unreachable!("This should never happen, report to https://github.com/awxkee/moxcms")
314        }
315    }
316
317    fn read_nested_tone_curves(
318        slice: &[u8],
319        offset: usize,
320        length: usize,
321        options: &ParsingOptions,
322    ) -> Result<Option<Vec<ToneReprCurve>>, CmsError> {
323        let mut curve_offset: usize = offset;
324        let mut curves = Vec::new();
325        for _ in 0..length {
326            if slice.len() < curve_offset.safe_add(12)? {
327                return Err(CmsError::InvalidProfile);
328            }
329            let mut tag_size = 0usize;
330            let new_curve = Self::read_trc_tag(slice, curve_offset, 0, &mut tag_size, options)?;
331            match new_curve {
332                None => return Err(CmsError::InvalidProfile),
333                Some(curve) => curves.push(curve),
334            }
335            curve_offset += tag_size;
336            // 4 byte aligned
337            if curve_offset % 4 != 0 {
338                curve_offset += 4 - curve_offset % 4;
339            }
340        }
341        Ok(Some(curves))
342    }
343
344    pub(crate) fn read_lut_abm_type(
345        slice: &[u8],
346        entry: usize,
347        tag_size: usize,
348        to_pcs: bool,
349        options: &ParsingOptions,
350    ) -> Result<Option<LutWarehouse>, CmsError> {
351        if tag_size < 48 {
352            return Ok(None);
353        }
354        let last_tag_offset = tag_size.safe_add(entry)?;
355        if last_tag_offset > slice.len() {
356            return Err(CmsError::InvalidProfile);
357        }
358        let tag = &slice[entry..last_tag_offset];
359        if tag.len() < 48 {
360            return Err(CmsError::InvalidProfile);
361        }
362        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
363        let tag_type_definition = TagTypeDefinition::from(tag_type);
364        if tag_type_definition != TagTypeDefinition::MabLut
365            && tag_type_definition != TagTypeDefinition::MbaLut
366        {
367            return Ok(None);
368        }
369        let in_channels = tag[8];
370        let out_channels = tag[9];
371        if in_channels > 4 && out_channels > 4 {
372            return Ok(None);
373        }
374        let a_curve_offset = u32::from_be_bytes([tag[28], tag[29], tag[30], tag[31]]) as usize;
375        let clut_offset = u32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]]) as usize;
376        let m_curve_offset = u32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]) as usize;
377        let matrix_offset = u32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]) as usize;
378        let b_curve_offset = u32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]) as usize;
379
380        let transform: Matrix3d;
381        let bias: Vector3d;
382        if matrix_offset != 0 {
383            let matrix_end = matrix_offset.safe_add(12 * 4)?;
384            if tag.len() < matrix_end {
385                return Err(CmsError::InvalidProfile);
386            }
387
388            let m_tag = &tag[matrix_offset..matrix_end];
389
390            bias = read_vector_3d(&m_tag[36..48])?;
391            transform = read_matrix_3d(m_tag)?;
392        } else {
393            transform = Matrix3d::IDENTITY;
394            bias = Vector3d::default();
395        }
396
397        let mut grid_points: [u8; 16] = [0; 16];
398
399        let clut_table: Option<LutStore> =
400            if clut_offset != 0 {
401                // Check if CLUT formed correctly
402                if clut_offset.safe_add(20)? > tag.len() {
403                    return Err(CmsError::InvalidProfile);
404                }
405
406                let clut_sizes_slice = &tag[clut_offset..clut_offset.safe_add(16)?];
407                for (&s, v) in clut_sizes_slice.iter().zip(grid_points.iter_mut()) {
408                    *v = s;
409                }
410
411                let mut clut_size = 1u32;
412                for &i in grid_points.iter().take(in_channels as usize) {
413                    clut_size = clut_size.safe_mul(i as u32)?;
414                }
415                clut_size = clut_size.safe_mul(out_channels as u32)?;
416
417                if clut_size == 0 {
418                    return Err(CmsError::IncorrectlyFormedLut(
419                        "Clut size was zero when it shouldn't".to_string(),
420                    ));
421                }
422
423                if clut_size > 10_000_000 {
424                    return Err(CmsError::IncorrectlyFormedLut(
425                        "Clut size exceeded 10_000_000 points what is too big".to_string(),
426                    ));
427                }
428
429                // check LUT dimensions
430                let mut grid_stride: usize = 1usize;
431                let mut last_index: usize = 0;
432                for &dim in grid_points.iter().take(in_channels as usize).rev() {
433                    let dim_usize = dim as usize;
434                    if dim_usize == 0 {
435                        return Err(CmsError::IncorrectlyFormedLut(
436                            "One of grid dimensions is zero".to_string(),
437                        ));
438                    }
439                    let l = match dim_usize
440                        .checked_sub(1)
441                        .ok_or(CmsError::OverflowingError)?
442                        .safe_mul(grid_stride)
443                        .and_then(|x| x.safe_add(last_index))
444                    {
445                        Ok(v) => v,
446                        Err(_) => {
447                            return Err(CmsError::IncorrectlyFormedLut(
448                                "Pointer size overflow on LUT dimensions".to_string(),
449                            ));
450                        }
451                    };
452                    last_index = l;
453
454                    // Multiply stride by next dimension (check for overflow)
455                    grid_stride = grid_stride.checked_mul(dim_usize).ok_or(
456                        CmsError::IncorrectlyFormedLut("Overflow on grid dimensions".to_string()),
457                    )?;
458                }
459
460                last_index = last_index.checked_mul(out_channels as usize).ok_or(
461                    CmsError::IncorrectlyFormedLut("Overflow on grid dimensions".to_string()),
462                )?;
463                if last_index >= clut_size as usize {
464                    return Err(CmsError::IncorrectlyFormedLut(format!(
465                        "Clut size should be at least {last_index}, but it was {last_index}"
466                    )));
467                }
468
469                let clut_offset20 = clut_offset.safe_add(20)?;
470
471                let clut_header = &tag[clut_offset..clut_offset20];
472                let entry_size = clut_header[16];
473                if entry_size != 1 && entry_size != 2 {
474                    return Err(CmsError::InvalidProfile);
475                }
476
477                let clut_end =
478                    clut_offset20.safe_add(clut_size.safe_mul(entry_size as u32)? as usize)?;
479
480                if tag.len() < clut_end {
481                    return Err(CmsError::InvalidProfile);
482                }
483
484                let shaped_clut_table = &tag[clut_offset20..clut_end];
485                Some(Self::read_lut_table_f32(
486                    shaped_clut_table,
487                    if entry_size == 1 {
488                        LutType::Lut8
489                    } else {
490                        LutType::Lut16
491                    },
492                )?)
493            } else {
494                None
495            };
496
497        let a_curves = if a_curve_offset == 0 {
498            Vec::new()
499        } else {
500            Self::read_nested_tone_curves(
501                tag,
502                a_curve_offset,
503                if to_pcs {
504                    in_channels as usize
505                } else {
506                    out_channels as usize
507                },
508                options,
509            )?
510            .ok_or(CmsError::InvalidProfile)?
511        };
512
513        let m_curves = if m_curve_offset == 0 {
514            Vec::new()
515        } else {
516            Self::read_nested_tone_curves(
517                tag,
518                m_curve_offset,
519                if to_pcs {
520                    out_channels as usize
521                } else {
522                    in_channels as usize
523                },
524                options,
525            )?
526            .ok_or(CmsError::InvalidProfile)?
527        };
528
529        let b_curves = if b_curve_offset == 0 {
530            Vec::new()
531        } else {
532            Self::read_nested_tone_curves(
533                tag,
534                b_curve_offset,
535                if to_pcs {
536                    out_channels as usize
537                } else {
538                    in_channels as usize
539                },
540                options,
541            )?
542            .ok_or(CmsError::InvalidProfile)?
543        };
544
545        let wh = LutWarehouse::Multidimensional(LutMultidimensionalType {
546            num_input_channels: in_channels,
547            num_output_channels: out_channels,
548            matrix: transform,
549            clut: clut_table,
550            a_curves,
551            b_curves,
552            m_curves,
553            grid_points,
554            bias,
555        });
556        Ok(Some(wh))
557    }
558
559    pub(crate) fn read_lut_a_to_b_type(
560        slice: &[u8],
561        entry: usize,
562        tag_size: usize,
563        parsing_options: &ParsingOptions,
564    ) -> Result<Option<LutWarehouse>, CmsError> {
565        if tag_size < 48 {
566            return Ok(None);
567        }
568        let last_tag_offset = tag_size.safe_add(entry)?;
569        if last_tag_offset > slice.len() {
570            return Err(CmsError::InvalidProfile);
571        }
572        let tag = &slice[entry..last_tag_offset];
573        if tag.len() < 48 {
574            return Err(CmsError::InvalidProfile);
575        }
576        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
577        let lut_type = LutType::try_from(tag_type)?;
578        assert!(lut_type == LutType::Lut8 || lut_type == LutType::Lut16);
579
580        if lut_type == LutType::Lut16 && tag.len() < 52 {
581            return Err(CmsError::InvalidProfile);
582        }
583
584        let num_input_table_entries: u16 = match lut_type {
585            LutType::Lut8 => 256,
586            LutType::Lut16 => u16::from_be_bytes([tag[48], tag[49]]),
587            _ => unreachable!(),
588        };
589        let num_output_table_entries: u16 = match lut_type {
590            LutType::Lut8 => 256,
591            LutType::Lut16 => u16::from_be_bytes([tag[50], tag[51]]),
592            _ => unreachable!(),
593        };
594
595        if !(2..=4096).contains(&num_input_table_entries)
596            || !(2..=4096).contains(&num_output_table_entries)
597        {
598            return Err(CmsError::InvalidProfile);
599        }
600
601        let input_offset: usize = match lut_type {
602            LutType::Lut8 => 48,
603            LutType::Lut16 => 52,
604            _ => unreachable!(),
605        };
606        let entry_size: usize = match lut_type {
607            LutType::Lut8 => 1,
608            LutType::Lut16 => 2,
609            _ => unreachable!(),
610        };
611
612        let in_chan = tag[8];
613        let out_chan = tag[9];
614        let is_3_to_4 = in_chan == 3 || out_chan == 4;
615        let is_4_to_3 = in_chan == 4 || out_chan == 3;
616        if !is_3_to_4 && !is_4_to_3 {
617            return Err(CmsError::InvalidProfile);
618        }
619        let grid_points = tag[10];
620        let clut_size = (grid_points as u32).safe_powi(in_chan as u32)? as usize;
621
622        if !(1..=parsing_options.max_allowed_clut_size).contains(&clut_size) {
623            return Err(CmsError::InvalidProfile);
624        }
625
626        assert!(tag.len() >= 48);
627
628        let transform = read_matrix_3d(&tag[12..48])?;
629
630        let lut_input_size = num_input_table_entries.safe_mul(in_chan as u16)? as usize;
631
632        let linearization_table_end = lut_input_size
633            .safe_mul(entry_size)?
634            .safe_add(input_offset)?;
635        if tag.len() < linearization_table_end {
636            return Err(CmsError::InvalidProfile);
637        }
638        let shaped_input_table = &tag[input_offset..linearization_table_end];
639        let linearization_table = Self::read_lut_table_f32(shaped_input_table, lut_type)?;
640
641        let clut_offset = linearization_table_end;
642
643        let clut_data_size = clut_size
644            .safe_mul(out_chan as usize)?
645            .safe_mul(entry_size)?;
646
647        if tag.len() < clut_offset.safe_add(clut_data_size)? {
648            return Err(CmsError::InvalidProfile);
649        }
650
651        let shaped_clut_table = &tag[clut_offset..clut_offset + clut_data_size];
652        let clut_table = Self::read_lut_table_f32(shaped_clut_table, lut_type)?;
653
654        let output_offset = clut_offset.safe_add(clut_data_size)?;
655
656        let output_size = (num_output_table_entries as usize).safe_mul(out_chan as usize)?;
657
658        let shaped_output = output_offset.safe_add(output_size.safe_mul(entry_size)?)?;
659        if tag.len() < shaped_output {
660            return Err(CmsError::InvalidProfile);
661        }
662
663        let shaped_output_table = &tag[output_offset..shaped_output];
664        let gamma_table = Self::read_lut_table_f32(shaped_output_table, lut_type)?;
665
666        let wh = LutWarehouse::Lut(LutDataType {
667            num_input_table_entries,
668            num_output_table_entries,
669            num_input_channels: in_chan,
670            num_output_channels: out_chan,
671            num_clut_grid_points: grid_points,
672            matrix: transform,
673            input_table: linearization_table,
674            clut_table,
675            output_table: gamma_table,
676            lut_type,
677        });
678        Ok(Some(wh))
679    }
680
681    pub(crate) fn read_lut_tag(
682        slice: &[u8],
683        tag_entry: u32,
684        tag_size: usize,
685        parsing_options: &ParsingOptions,
686    ) -> Result<Option<LutWarehouse>, CmsError> {
687        let lut_type = Self::read_lut_type(slice, tag_entry as usize, tag_size)?;
688        Ok(if lut_type == LutType::Lut8 || lut_type == LutType::Lut16 {
689            Self::read_lut_a_to_b_type(slice, tag_entry as usize, tag_size, parsing_options)?
690        } else if lut_type == LutType::LutMba || lut_type == LutType::LutMab {
691            Self::read_lut_abm_type(
692                slice,
693                tag_entry as usize,
694                tag_size,
695                lut_type == LutType::LutMab,
696                parsing_options,
697            )?
698        } else {
699            None
700        })
701    }
702
703    pub(crate) fn read_trc_tag_s(
704        slice: &[u8],
705        entry: usize,
706        tag_size: usize,
707        options: &ParsingOptions,
708    ) -> Result<Option<ToneReprCurve>, CmsError> {
709        let mut _empty = 0usize;
710        Self::read_trc_tag(slice, entry, tag_size, &mut _empty, options)
711    }
712
713    pub(crate) fn read_trc_tag(
714        slice: &[u8],
715        entry: usize,
716        tag_size: usize,
717        read_size: &mut usize,
718        options: &ParsingOptions,
719    ) -> Result<Option<ToneReprCurve>, CmsError> {
720        if slice.len() < entry.safe_add(4)? {
721            return Ok(None);
722        }
723        let small_tag = &slice[entry..entry + 4];
724        // We require always recognize tone curves.
725        let curve_type = TagTypeDefinition::from(u32::from_be_bytes([
726            small_tag[0],
727            small_tag[1],
728            small_tag[2],
729            small_tag[3],
730        ]));
731        if tag_size != 0 && tag_size < TAG_SIZE {
732            return Ok(None);
733        }
734        let last_tag_offset = if tag_size != 0 {
735            tag_size + entry
736        } else {
737            slice.len()
738        };
739        if last_tag_offset > slice.len() {
740            return Err(CmsError::MalformedTrcCurve("Data exhausted".to_string()));
741        }
742        let tag = &slice[entry..last_tag_offset];
743        if tag.len() < TAG_SIZE {
744            return Err(CmsError::MalformedTrcCurve("Data exhausted".to_string()));
745        }
746        if curve_type == TagTypeDefinition::LutToneCurve {
747            let entry_count = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize;
748            if entry_count == 0 {
749                return Ok(Some(ToneReprCurve::Lut(vec![])));
750            }
751            if entry_count > options.max_allowed_trc_size {
752                return Err(CmsError::CurveLutIsTooLarge);
753            }
754            let curve_end = entry_count.safe_mul(size_of::<u16>())?.safe_add(12)?;
755            if tag.len() < curve_end {
756                return Err(CmsError::MalformedTrcCurve(
757                    "Curve end ends to early".to_string(),
758                ));
759            }
760            let curve_sliced = &tag[12..curve_end];
761            let mut curve_values = try_vec![0u16; entry_count];
762            for (value, curve_value) in curve_sliced.chunks_exact(2).zip(curve_values.iter_mut()) {
763                let gamma_s15 = u16::from_be_bytes([value[0], value[1]]);
764                *curve_value = gamma_s15;
765            }
766            *read_size = curve_end;
767            Ok(Some(ToneReprCurve::Lut(curve_values)))
768        } else if curve_type == TagTypeDefinition::ParametricToneCurve {
769            let entry_count = u16::from_be_bytes([tag[8], tag[9]]) as usize;
770            if entry_count > 4 {
771                return Err(CmsError::MalformedTrcCurve(
772                    "Parametric curve has unknown entries count".to_string(),
773                ));
774            }
775
776            const COUNT_TO_LENGTH: [usize; 5] = [1, 3, 4, 5, 7]; //PARAMETRIC_CURVE_TYPE
777
778            if tag.len() < 12 + COUNT_TO_LENGTH[entry_count] * size_of::<u32>() {
779                return Err(CmsError::MalformedTrcCurve(
780                    "Parametric curve has unknown entries count exhaust data too early".to_string(),
781                ));
782            }
783            let curve_sliced = &tag[12..12 + COUNT_TO_LENGTH[entry_count] * size_of::<u32>()];
784            let mut params = try_vec![0f32; COUNT_TO_LENGTH[entry_count]];
785            for (value, param_value) in curve_sliced.chunks_exact(4).zip(params.iter_mut()) {
786                let parametric_value = i32::from_be_bytes([value[0], value[1], value[2], value[3]]);
787                *param_value = s15_fixed16_number_to_float(parametric_value);
788            }
789            if entry_count == 1 || entry_count == 2 {
790                // we have a type 1 or type 2 function that has a division by `a`
791                let a: f32 = params[1];
792                if a == 0.0 {
793                    return Err(CmsError::ParametricCurveZeroDivision);
794                }
795            }
796            *read_size = 12 + COUNT_TO_LENGTH[entry_count] * 4;
797            Ok(Some(ToneReprCurve::Parametric(params)))
798        } else {
799            Err(CmsError::MalformedTrcCurve(
800                "Unknown parametric curve tag".to_string(),
801            ))
802        }
803    }
804
805    pub(crate) fn read_chad_tag(
806        slice: &[u8],
807        entry: usize,
808        tag_size: usize,
809    ) -> Result<Option<Matrix3d>, CmsError> {
810        let last_tag_offset = tag_size.safe_add(entry)?;
811        if last_tag_offset > slice.len() {
812            return Err(CmsError::InvalidProfile);
813        }
814        if slice[entry..].len() < 8 {
815            return Err(CmsError::InvalidProfile);
816        }
817        if tag_size < 8 {
818            return Ok(None);
819        }
820        if (tag_size - 8) / 4 != 9 {
821            return Ok(None);
822        }
823        let tag0 = &slice[entry..entry.safe_add(8)?];
824        let c_type =
825            TagTypeDefinition::from(u32::from_be_bytes([tag0[0], tag0[1], tag0[2], tag0[3]]));
826        if c_type != TagTypeDefinition::S15Fixed16Array {
827            return Err(CmsError::InvalidProfile);
828        }
829        if slice.len() < 9 * size_of::<u32>() + 8 {
830            return Err(CmsError::InvalidProfile);
831        }
832        let tag = &slice[entry + 8..last_tag_offset];
833        if tag.len() != size_of::<Matrix3f>() {
834            return Err(CmsError::InvalidProfile);
835        }
836        let matrix = read_matrix_3d(tag)?;
837        Ok(Some(matrix))
838    }
839
840    #[inline]
841    pub(crate) fn read_tech_tag(
842        slice: &[u8],
843        entry: usize,
844        tag_size: usize,
845    ) -> Result<Option<TechnologySignatures>, CmsError> {
846        if tag_size < TAG_SIZE {
847            return Err(CmsError::InvalidProfile);
848        }
849        let last_tag_offset = tag_size.safe_add(entry)?;
850        if last_tag_offset > slice.len() {
851            return Err(CmsError::InvalidProfile);
852        }
853        let tag = &slice[entry..entry.safe_add(12)?];
854        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
855        let def = TagTypeDefinition::from(tag_type);
856        if def == TagTypeDefinition::Signature {
857            let sig = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]);
858            let tech_sig = TechnologySignatures::from(sig);
859            return Ok(Some(tech_sig));
860        }
861        Ok(None)
862    }
863
864    pub(crate) fn read_date_time_tag(
865        slice: &[u8],
866        entry: usize,
867        tag_size: usize,
868    ) -> Result<Option<ColorDateTime>, CmsError> {
869        if tag_size < 20 {
870            return Ok(None);
871        }
872        let last_tag_offset = tag_size.safe_add(entry)?;
873        if last_tag_offset > slice.len() {
874            return Err(CmsError::InvalidProfile);
875        }
876        let tag = &slice[entry..entry.safe_add(20)?];
877        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
878        let def = TagTypeDefinition::from(tag_type);
879        if def == TagTypeDefinition::DateTime {
880            let tag_value = &slice[8..20];
881            let time = ColorDateTime::new_from_slice(tag_value)?;
882            return Ok(Some(time));
883        }
884        Ok(None)
885    }
886
887    #[inline]
888    pub(crate) fn read_meas_tag(
889        slice: &[u8],
890        entry: usize,
891        tag_size: usize,
892    ) -> Result<Option<Measurement>, CmsError> {
893        if tag_size < TAG_SIZE {
894            return Ok(None);
895        }
896        let last_tag_offset = tag_size.safe_add(entry)?;
897        if last_tag_offset > slice.len() {
898            return Err(CmsError::InvalidProfile);
899        }
900        let tag = &slice[entry..entry + 12];
901        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
902        let def = TagTypeDefinition::from(tag_type);
903        if def != TagTypeDefinition::Measurement {
904            return Ok(None);
905        }
906        if 36 + entry > slice.len() {
907            return Err(CmsError::InvalidProfile);
908        }
909        let tag = &slice[entry..entry + 36];
910        let observer =
911            StandardObserver::from(u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]));
912        let q15_16_x = i32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]);
913        let q15_16_y = i32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]);
914        let q15_16_z = i32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]);
915        let x = s15_fixed16_number_to_float(q15_16_x);
916        let y = s15_fixed16_number_to_float(q15_16_y);
917        let z = s15_fixed16_number_to_float(q15_16_z);
918        let xyz = Xyz::new(x, y, z);
919        let geometry =
920            MeasurementGeometry::from(u32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]]));
921        let flare =
922            uint16_number_to_float(u32::from_be_bytes([tag[28], tag[29], tag[30], tag[31]]));
923        let illuminant =
924            StandardIlluminant::from(u32::from_be_bytes([tag[32], tag[33], tag[34], tag[35]]));
925        Ok(Some(Measurement {
926            flare,
927            illuminant,
928            geometry,
929            observer,
930            backing: xyz,
931        }))
932    }
933
934    #[inline]
935    pub(crate) fn read_xyz_tag(
936        slice: &[u8],
937        entry: usize,
938        tag_size: usize,
939    ) -> Result<Xyzd, CmsError> {
940        if tag_size < TAG_SIZE {
941            return Ok(Xyzd::default());
942        }
943        let last_tag_offset = tag_size.safe_add(entry)?;
944        if last_tag_offset > slice.len() {
945            return Err(CmsError::InvalidProfile);
946        }
947        let tag = &slice[entry..entry + 12];
948        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
949        let def = TagTypeDefinition::from(tag_type);
950        if def != TagTypeDefinition::Xyz {
951            return Ok(Xyzd::default());
952        }
953
954        let tag = &slice[entry..last_tag_offset];
955        if tag.len() < 20 {
956            return Err(CmsError::InvalidProfile);
957        }
958        let q15_16_x = i32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]);
959        let q15_16_y = i32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]);
960        let q15_16_z = i32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]);
961        let x = s15_fixed16_number_to_double(q15_16_x);
962        let y = s15_fixed16_number_to_double(q15_16_y);
963        let z = s15_fixed16_number_to_double(q15_16_z);
964        Ok(Xyzd { x, y, z })
965    }
966
967    pub(crate) fn read_cicp_tag(
968        slice: &[u8],
969        entry: usize,
970        tag_size: usize,
971    ) -> Result<Option<CicpProfile>, CmsError> {
972        if tag_size < TAG_SIZE {
973            return Ok(None);
974        }
975        let last_tag_offset = tag_size.safe_add(entry)?;
976        if last_tag_offset > slice.len() {
977            return Err(CmsError::InvalidProfile);
978        }
979        let tag = &slice[entry..last_tag_offset];
980        if tag.len() < 12 {
981            return Err(CmsError::InvalidProfile);
982        }
983        let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]);
984        let def = TagTypeDefinition::from(tag_type);
985        if def != TagTypeDefinition::Cicp {
986            return Ok(None);
987        }
988        let primaries = CicpColorPrimaries::try_from(tag[8])?;
989        let transfer_characteristics = TransferCharacteristics::try_from(tag[9])?;
990        let matrix_coefficients = MatrixCoefficients::try_from(tag[10])?;
991        let full_range = tag[11] == 1;
992        Ok(Some(CicpProfile {
993            color_primaries: primaries,
994            transfer_characteristics,
995            matrix_coefficients,
996            full_range,
997        }))
998    }
999}