hayro_jpeg2000/
lib.rs

1/*!
2A memory-safe, pure-Rust JPEG 2000 decoder.
3
4`hayro-jpeg2000` can decode both raw JPEG 2000 codestreams (`.j2c`) and images wrapped
5inside the JP2 container format. The decoder supports the vast majority of features
6defined in the JPEG2000 core coding system (ISO/IEC 15444-1) as well as some color
7spaces from the extensions (ISO/IEC 15444-2). There are still some missing pieces
8for some "obscure" features(like for example support for progression order
9changes in tile-parts), but all features that actually commonly appear in real-life
10images should be supported (if not, please open an issue!).
11
12The decoder abstracts away most of the internal complexity of JPEG2000
13and yields a simple 8-bit image with either greyscale, RGB, CMYK or an ICC-based
14color space, which can then be processed further according to your needs.
15
16# Example
17```rust,no_run
18use hayro_jpeg2000::{Image, DecodeSettings};
19
20let data = std::fs::read("image.jp2").unwrap();
21let image = Image::new(&data, &DecodeSettings::default()).unwrap();
22
23println!(
24    "{}x{} image in {:?} with alpha={}",
25    image.width(),
26    image.height(),
27    image.color_space(),
28    image.has_alpha(),
29);
30
31let bitmap = image.decode().unwrap();
32```
33
34If you want to see a more comprehensive example, please take a look
35at the example in [GitHub](https://github.com/LaurenzV/hayro/blob/main/hayro-jpeg2000/examples/png.rs),
36which shows you the main steps needed to convert a JPEG2000 image into PNG for example.
37
38# Testing
39The decoder has been tested against 20.000+ images scraped from random PDFs
40on the internet and also passes a large part of the `OpenJPEG` test suite. So you
41can expect the crate to perform decently in terms of decoding correctness.
42
43# Performance
44A decent amount of effort has already been put into optimizing this crate
45(both in terms of raw performance but also memory allocations). However, there
46are some more important optimizations that have not been implemented yet, so
47there is definitely still room for improvement (and I am planning on implementing
48them eventually).
49
50Overall, you should expect this crate to have worse performance than `OpenJPEG`,
51but the difference gap should not be too large.
52
53# Safety
54By default, the crate has the `simd` feature enabled, which uses the
55[`fearless_simd`](https://github.com/linebender/fearless_simd) crate to accelerate
56important parts of the pipeline. If you want to eliminate any usage of unsafe
57in this crate as well as its dependencies, you can simply disable this
58feature, at the cost of worse decoding performance. Unsafe code is forbidden
59via a crate-level attribute.
60
61The crate is `no_std` compatible but requires an allocator to be available.
62*/
63
64#![cfg_attr(not(feature = "std"), no_std)]
65#![forbid(unsafe_code)]
66#![forbid(missing_docs)]
67
68extern crate alloc;
69
70use alloc::vec;
71use alloc::vec::Vec;
72
73use crate::error::{bail, err};
74use crate::j2c::{ComponentData, DecodedCodestream, Header};
75use crate::jp2::cdef::{ChannelAssociation, ChannelType};
76use crate::jp2::cmap::ComponentMappingType;
77use crate::jp2::colr::{CieLab, EnumeratedColorspace};
78use crate::jp2::icc::ICCMetadata;
79use crate::jp2::{DecodedImage, ImageBoxes};
80
81pub mod error;
82#[macro_use]
83pub(crate) mod log;
84pub(crate) mod math;
85
86use crate::math::{Level, SIMD_WIDTH, Simd, dispatch, f32x8};
87pub use error::{
88    ColorError, DecodeError, DecodingError, FormatError, MarkerError, Result, TileError,
89    ValidationError,
90};
91
92#[cfg(feature = "image")]
93pub mod integration;
94mod j2c;
95mod jp2;
96pub(crate) mod reader;
97
98/// JP2 signature box: 00 00 00 0C 6A 50 20 20
99pub(crate) const JP2_MAGIC: &[u8] = b"\x00\x00\x00\x0C\x6A\x50\x20\x20";
100/// Codestream signature: FF 4F FF 51 (SOC + SIZ markers)
101pub(crate) const CODESTREAM_MAGIC: &[u8] = b"\xFF\x4F\xFF\x51";
102
103/// Settings to apply during decoding.
104#[derive(Debug, Copy, Clone)]
105pub struct DecodeSettings {
106    /// Whether palette indices should be resolved.
107    ///
108    /// JPEG2000 images can be stored in two different ways. First, by storing
109    /// RGB values (depending on the color space) for each pixel. Secondly, by
110    /// only storing a single index for each channel, and then resolving the
111    /// actual color using the index.
112    ///
113    /// If you disable this option, in case you have an image with palette
114    /// indices, they will not be resolved, but instead a grayscale image
115    /// will be returned, with each pixel value corresponding to the palette
116    /// index of the location.
117    pub resolve_palette_indices: bool,
118    /// Whether strict mode should be enabled when decoding.
119    ///
120    /// It is recommended to leave this flag disabled, unless you have a
121    /// specific reason not to.
122    pub strict: bool,
123    /// A hint for the target resolution that the image should be decoded at.
124    pub target_resolution: Option<(u32, u32)>,
125}
126
127impl Default for DecodeSettings {
128    fn default() -> Self {
129        Self {
130            resolve_palette_indices: true,
131            strict: false,
132            target_resolution: None,
133        }
134    }
135}
136
137/// A JPEG2000 image or codestream.
138pub struct Image<'a> {
139    /// The codestream containing the data to decode.
140    pub(crate) codestream: &'a [u8],
141    /// The header of the J2C codestream.
142    pub(crate) header: Header<'a>,
143    /// The JP2 boxes of the image. In the case of a raw codestream, we
144    /// will synthesize the necessary boxes.
145    pub(crate) boxes: ImageBoxes,
146    /// Settings that should be applied during decoding.
147    pub(crate) settings: DecodeSettings,
148    /// Whether the image has an alpha channel.
149    pub(crate) has_alpha: bool,
150    /// The color space of the image.
151    pub(crate) color_space: ColorSpace,
152}
153
154impl<'a> Image<'a> {
155    /// Try to create a new JPEG2000 image from the given data.
156    pub fn new(data: &'a [u8], settings: &DecodeSettings) -> Result<Self> {
157        if data.starts_with(JP2_MAGIC) {
158            jp2::parse(data, *settings)
159        } else if data.starts_with(CODESTREAM_MAGIC) {
160            j2c::parse(data, settings)
161        } else {
162            err!(FormatError::InvalidSignature)
163        }
164    }
165
166    /// Whether the image has an alpha channel.
167    pub fn has_alpha(&self) -> bool {
168        self.has_alpha
169    }
170
171    /// The color space of the image.
172    pub fn color_space(&self) -> &ColorSpace {
173        &self.color_space
174    }
175
176    /// The width of the image.
177    pub fn width(&self) -> u32 {
178        self.header.size_data.image_width()
179    }
180
181    /// The height of the image.
182    pub fn height(&self) -> u32 {
183        self.header.size_data.image_height()
184    }
185
186    /// The original bit depth of the image. You usually don't need to do anything
187    /// with this parameter, it just exists for informational purposes.
188    pub fn original_bit_depth(&self) -> u8 {
189        // Note that this only works if all components have the same precision.
190        self.header.component_infos[0].size_info.precision
191    }
192
193    /// Decode the image.
194    pub fn decode(&self) -> Result<Vec<u8>> {
195        let buffer_size = self.width() as usize
196            * self.height() as usize
197            * (self.color_space.num_channels() as usize + if self.has_alpha { 1 } else { 0 });
198        let mut buf = vec![0; buffer_size];
199        self.decode_into(&mut buf)?;
200
201        Ok(buf)
202    }
203
204    /// Decode the image into the given buffer. The buffer must have the correct
205    /// size.
206    pub(crate) fn decode_into(&self, buf: &mut [u8]) -> Result<()> {
207        let settings = &self.settings;
208        let mut decoded_image =
209            j2c::decode(self.codestream, &self.header).map(move |data| DecodedImage {
210                decoded: DecodedCodestream { components: data },
211                boxes: self.boxes.clone(),
212            })?;
213
214        // Resolve palette indices.
215        if settings.resolve_palette_indices {
216            decoded_image.decoded.components =
217                resolve_palette_indices(decoded_image.decoded.components, &decoded_image.boxes)?;
218        }
219
220        if let Some(cdef) = &decoded_image.boxes.channel_definition {
221            // Sort by the channel association. Note that this will only work if
222            // each component is referenced only once.
223            let mut components = decoded_image
224                .decoded
225                .components
226                .into_iter()
227                .zip(
228                    cdef.channel_definitions
229                        .iter()
230                        .map(|c| match c._association {
231                            ChannelAssociation::WholeImage => u16::MAX,
232                            ChannelAssociation::Colour(c) => c,
233                        }),
234                )
235                .collect::<Vec<_>>();
236            components.sort_by(|c1, c2| c1.1.cmp(&c2.1));
237            decoded_image.decoded.components = components.into_iter().map(|c| c.0).collect();
238        }
239
240        // Note that this is only valid if all images have the same bit depth.
241        let bit_depth = decoded_image.decoded.components[0].bit_depth;
242        convert_color_space(&mut decoded_image, bit_depth)?;
243
244        interleave_and_convert(decoded_image, buf);
245
246        Ok(())
247    }
248}
249
250pub(crate) fn resolve_alpha_and_color_space(
251    boxes: &ImageBoxes,
252    header: &Header<'_>,
253    settings: &DecodeSettings,
254) -> Result<(ColorSpace, bool)> {
255    let mut num_components = header.component_infos.len();
256
257    // Override number of components with what is actually in the palette box
258    // in case we resolve them.
259    if settings.resolve_palette_indices
260        && let Some(palette_box) = &boxes.palette
261    {
262        num_components = palette_box.columns.len();
263    }
264
265    let mut has_alpha = false;
266
267    if let Some(cdef) = &boxes.channel_definition {
268        let last = cdef.channel_definitions.last().unwrap();
269        has_alpha = last.channel_type == ChannelType::Opacity;
270    }
271
272    let mut color_space = get_color_space(boxes, num_components)?;
273
274    // If we didn't resolve palette indices, we need to assume grayscale image.
275    if !settings.resolve_palette_indices && boxes.palette.is_some() {
276        has_alpha = false;
277        color_space = ColorSpace::Gray;
278    }
279
280    let actual_num_components = header.component_infos.len();
281
282    // Validate the number of channels.
283    if boxes.palette.is_none()
284        && actual_num_components
285            != (color_space.num_channels() + if has_alpha { 1 } else { 0 }) as usize
286    {
287        if !settings.strict
288            && actual_num_components == color_space.num_channels() as usize + 1
289            && !has_alpha
290        {
291            // See OPENJPEG test case orb-blue10-lin-j2k. Assume that we have an
292            // alpha channel in this case.
293            has_alpha = true;
294        } else {
295            // Color space is invalid, attempt to repair.
296            if actual_num_components == 1 || (actual_num_components == 2 && has_alpha) {
297                color_space = ColorSpace::Gray;
298            } else if actual_num_components == 3 {
299                color_space = ColorSpace::RGB;
300            } else if actual_num_components == 4 {
301                if has_alpha {
302                    color_space = ColorSpace::RGB;
303                } else {
304                    color_space = ColorSpace::CMYK;
305                }
306            } else {
307                bail!(ValidationError::TooManyChannels);
308            }
309        }
310    }
311
312    Ok((color_space, has_alpha))
313}
314
315/// The color space of the image.
316#[derive(Debug, Clone)]
317pub enum ColorSpace {
318    /// A grayscale image.
319    Gray,
320    /// An RGB image.
321    RGB,
322    /// A CMYK image.
323    CMYK,
324    /// An unknown color space.
325    Unknown {
326        /// The number of channels of the color space.
327        num_channels: u8,
328    },
329    /// An image based on an ICC profile.
330    Icc {
331        /// The raw data of the ICC profile.
332        profile: Vec<u8>,
333        /// The number of channels used by the ICC profile.
334        num_channels: u8,
335    },
336}
337
338impl ColorSpace {
339    /// Return the number of expected channels for the color space.
340    pub fn num_channels(&self) -> u8 {
341        match self {
342            Self::Gray => 1,
343            Self::RGB => 3,
344            Self::CMYK => 4,
345            Self::Unknown { num_channels } => *num_channels,
346            Self::Icc {
347                num_channels: num_components,
348                ..
349            } => *num_components,
350        }
351    }
352}
353
354/// A bitmap storing the decoded result of the image.
355pub struct Bitmap {
356    /// The color space of the image.
357    pub color_space: ColorSpace,
358    /// The raw pixel data of the image. The result will always be in
359    /// 8-bit (in case the original image had a different bit-depth,
360    /// hayro-jpeg2000 always scales to 8-bit).
361    ///
362    /// The size is guaranteed to equal
363    /// `width * height * (num_channels + (if has_alpha { 1 } else { 0 })`.
364    /// Pixels are interleaved on a per-channel basis, the alpha channel always
365    /// appearing as the last channel, if available.
366    pub data: Vec<u8>,
367    /// Whether the image has an alpha channel.
368    pub has_alpha: bool,
369    /// The width of the image.
370    pub width: u32,
371    /// The height of the image.
372    pub height: u32,
373    /// The original bit depth of the image. You usually don't need to do anything
374    /// with this parameter, it just exists for informational purposes.
375    pub original_bit_depth: u8,
376}
377
378fn interleave_and_convert(image: DecodedImage, buf: &mut [u8]) {
379    let mut components = image.decoded.components;
380    let num_components = components.len();
381
382    let mut all_same_bit_depth = Some(components[0].bit_depth);
383
384    for component in components.iter().skip(1) {
385        if Some(component.bit_depth) != all_same_bit_depth {
386            all_same_bit_depth = None;
387        }
388    }
389
390    let max_len = components[0].container.truncated().len();
391
392    let mut output_iter = buf.iter_mut();
393
394    if all_same_bit_depth == Some(8) && num_components <= 4 {
395        // Fast path for the common case.
396        match num_components {
397            // Gray-scale.
398            1 => {
399                for (output, input) in output_iter.zip(
400                    components[0]
401                        .container
402                        .iter()
403                        .map(|v| math::round_f32(*v) as u8),
404                ) {
405                    *output = input;
406                }
407            }
408            // Gray-scale with alpha.
409            2 => {
410                let c1 = components.pop().unwrap();
411                let c0 = components.pop().unwrap();
412
413                let c0 = &c0.container[..max_len];
414                let c1 = &c1.container[..max_len];
415
416                for i in 0..max_len {
417                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
418                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
419                }
420            }
421            // RGB
422            3 => {
423                let c2 = components.pop().unwrap();
424                let c1 = components.pop().unwrap();
425                let c0 = components.pop().unwrap();
426
427                let c0 = &c0.container[..max_len];
428                let c1 = &c1.container[..max_len];
429                let c2 = &c2.container[..max_len];
430
431                for i in 0..max_len {
432                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
433                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
434                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
435                }
436            }
437            // RGBA or CMYK.
438            4 => {
439                let c3 = components.pop().unwrap();
440                let c2 = components.pop().unwrap();
441                let c1 = components.pop().unwrap();
442                let c0 = components.pop().unwrap();
443
444                let c0 = &c0.container[..max_len];
445                let c1 = &c1.container[..max_len];
446                let c2 = &c2.container[..max_len];
447                let c3 = &c3.container[..max_len];
448
449                for i in 0..max_len {
450                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
451                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
452                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
453                    *output_iter.next().unwrap() = math::round_f32(c3[i]) as u8;
454                }
455            }
456            _ => unreachable!(),
457        }
458    } else {
459        // Slow path that also requires us to scale to 8 bit.
460        let mul_factor = ((1 << 8) - 1) as f32;
461
462        for sample in 0..max_len {
463            for channel in components.iter() {
464                *output_iter.next().unwrap() = math::round_f32(
465                    (channel.container[sample] / ((1_u32 << channel.bit_depth) - 1) as f32)
466                        * mul_factor,
467                ) as u8;
468            }
469        }
470    }
471}
472
473fn convert_color_space(image: &mut DecodedImage, bit_depth: u8) -> Result<()> {
474    if let Some(jp2::colr::ColorSpace::Enumerated(e)) = &image
475        .boxes
476        .color_specification
477        .as_ref()
478        .map(|i| &i.color_space)
479    {
480        match e {
481            EnumeratedColorspace::Sycc => {
482                dispatch!(Level::new(), simd => {
483                    sycc_to_rgb(simd, &mut image.decoded.components, bit_depth)
484                })?;
485            }
486            EnumeratedColorspace::CieLab(cielab) => {
487                dispatch!(Level::new(), simd => {
488                    cielab_to_rgb(simd, &mut image.decoded.components, bit_depth, cielab)
489                })?;
490            }
491            _ => {}
492        }
493    }
494
495    Ok(())
496}
497
498fn get_color_space(boxes: &ImageBoxes, num_components: usize) -> Result<ColorSpace> {
499    let cs = match boxes
500        .color_specification
501        .as_ref()
502        .map(|c| &c.color_space)
503        .unwrap_or(&jp2::colr::ColorSpace::Unknown)
504    {
505        jp2::colr::ColorSpace::Enumerated(e) => {
506            match e {
507                EnumeratedColorspace::Cmyk => ColorSpace::CMYK,
508                EnumeratedColorspace::Srgb => ColorSpace::RGB,
509                EnumeratedColorspace::RommRgb => {
510                    // Use an ICC profile to process the RommRGB color space.
511                    ColorSpace::Icc {
512                        profile: include_bytes!("../assets/ISO22028-2_ROMM-RGB.icc").to_vec(),
513                        num_channels: 3,
514                    }
515                }
516                EnumeratedColorspace::EsRgb => ColorSpace::RGB,
517                EnumeratedColorspace::Greyscale => ColorSpace::Gray,
518                EnumeratedColorspace::Sycc => ColorSpace::RGB,
519                EnumeratedColorspace::CieLab(_) => ColorSpace::Icc {
520                    profile: include_bytes!("../assets/LAB.icc").to_vec(),
521                    num_channels: 3,
522                },
523                _ => bail!(FormatError::Unsupported),
524            }
525        }
526        jp2::colr::ColorSpace::Icc(icc) => {
527            if let Some(metadata) = ICCMetadata::from_data(icc) {
528                ColorSpace::Icc {
529                    profile: icc.clone(),
530                    num_channels: metadata.color_space.num_components(),
531                }
532            } else {
533                // See OPENJPEG test orb-blue10-lin-jp2.jp2. They seem to
534                // assume RGB in this case (even though the image has 4
535                // components with no opacity channel, they assume RGBA instead
536                // of CMYK).
537                ColorSpace::RGB
538            }
539        }
540        jp2::colr::ColorSpace::Unknown => match num_components {
541            1 => ColorSpace::Gray,
542            3 => ColorSpace::RGB,
543            4 => ColorSpace::CMYK,
544            _ => ColorSpace::Unknown {
545                num_channels: num_components as u8,
546            },
547        },
548    };
549
550    Ok(cs)
551}
552
553fn resolve_palette_indices(
554    components: Vec<ComponentData>,
555    boxes: &ImageBoxes,
556) -> Result<Vec<ComponentData>> {
557    let Some(palette) = boxes.palette.as_ref() else {
558        // Nothing to resolve.
559        return Ok(components);
560    };
561
562    let mapping = boxes.component_mapping.as_ref().unwrap();
563    let mut resolved = Vec::with_capacity(mapping.entries.len());
564
565    for entry in &mapping.entries {
566        let component_idx = entry.component_index as usize;
567        let component = components
568            .get(component_idx)
569            .ok_or(ColorError::PaletteResolutionFailed)?;
570
571        match entry.mapping_type {
572            ComponentMappingType::Direct => resolved.push(component.clone()),
573            ComponentMappingType::Palette { column } => {
574                let column_idx = column as usize;
575                let column_info = palette
576                    .columns
577                    .get(column_idx)
578                    .ok_or(ColorError::PaletteResolutionFailed)?;
579
580                let mut mapped =
581                    Vec::with_capacity(component.container.truncated().len() + SIMD_WIDTH);
582
583                for &sample in component.container.truncated() {
584                    let index = math::round_f32(sample) as i64;
585                    let value = palette
586                        .map(index as usize, column_idx)
587                        .ok_or(ColorError::PaletteResolutionFailed)?;
588                    mapped.push(value as f32);
589                }
590
591                resolved.push(ComponentData {
592                    container: math::SimdBuffer::new(mapped),
593                    bit_depth: column_info.bit_depth,
594                });
595            }
596        }
597    }
598
599    Ok(resolved)
600}
601
602#[inline(always)]
603fn cielab_to_rgb<S: Simd>(
604    simd: S,
605    components: &mut [ComponentData],
606    bit_depth: u8,
607    lab: &CieLab,
608) -> Result<()> {
609    let (head, _) = components
610        .split_at_mut_checked(3)
611        .ok_or(ColorError::LabConversionFailed)?;
612
613    let [l, a, b] = head else {
614        unreachable!();
615    };
616
617    let prec0 = l.bit_depth;
618    let prec1 = a.bit_depth;
619    let prec2 = b.bit_depth;
620
621    // Prevent underflows/divisions by zero further below.
622    if prec0 < 4 || prec1 < 4 || prec2 < 4 {
623        bail!(ColorError::LabConversionFailed);
624    }
625
626    let rl = lab.rl.unwrap_or(100);
627    let ra = lab.ra.unwrap_or(170);
628    let rb = lab.ra.unwrap_or(200);
629    let ol = lab.ol.unwrap_or(0);
630    let oa = lab.oa.unwrap_or(1 << (bit_depth - 1));
631    let ob = lab
632        .ob
633        .unwrap_or((1 << (bit_depth - 2)) + (1 << (bit_depth - 3)));
634
635    // Copied from OpenJPEG.
636    let min_l = -(rl as f32 * ol as f32) / ((1 << prec0) - 1) as f32;
637    let max_l = min_l + rl as f32;
638    let min_a = -(ra as f32 * oa as f32) / ((1 << prec1) - 1) as f32;
639    let max_a = min_a + ra as f32;
640    let min_b = -(rb as f32 * ob as f32) / ((1 << prec2) - 1) as f32;
641    let max_b = min_b + rb as f32;
642
643    let bit_max = (1_u32 << bit_depth) - 1;
644
645    // Note that we are not doing the actual conversion with the ICC profile yet,
646    // just decoding the raw LAB values.
647    // We leave applying the ICC profile to the user.
648    let divisor_l = ((1 << prec0) - 1) as f32;
649    let divisor_a = ((1 << prec1) - 1) as f32;
650    let divisor_b = ((1 << prec2) - 1) as f32;
651
652    let scale_l_final = bit_max as f32 / 100.0;
653    let scale_ab_final = bit_max as f32 / 255.0;
654
655    let l_offset = min_l * scale_l_final;
656    let l_scale = (max_l - min_l) / divisor_l * scale_l_final;
657    let a_offset = (min_a + 128.0) * scale_ab_final;
658    let a_scale = (max_a - min_a) / divisor_a * scale_ab_final;
659    let b_offset = (min_b + 128.0) * scale_ab_final;
660    let b_scale = (max_b - min_b) / divisor_b * scale_ab_final;
661
662    let l_offset_v = f32x8::splat(simd, l_offset);
663    let l_scale_v = f32x8::splat(simd, l_scale);
664    let a_offset_v = f32x8::splat(simd, a_offset);
665    let a_scale_v = f32x8::splat(simd, a_scale);
666    let b_offset_v = f32x8::splat(simd, b_offset);
667    let b_scale_v = f32x8::splat(simd, b_scale);
668
669    // Note that we are not doing the actual conversion with the ICC profile yet,
670    // just decoding the raw LAB values.
671    // We leave applying the ICC profile to the user.
672    for ((l_chunk, a_chunk), b_chunk) in l
673        .container
674        .chunks_exact_mut(SIMD_WIDTH)
675        .zip(a.container.chunks_exact_mut(SIMD_WIDTH))
676        .zip(b.container.chunks_exact_mut(SIMD_WIDTH))
677    {
678        let l_v = f32x8::from_slice(simd, l_chunk);
679        let a_v = f32x8::from_slice(simd, a_chunk);
680        let b_v = f32x8::from_slice(simd, b_chunk);
681
682        l_v.mul_add(l_scale_v, l_offset_v).store(l_chunk);
683        a_v.mul_add(a_scale_v, a_offset_v).store(a_chunk);
684        b_v.mul_add(b_scale_v, b_offset_v).store(b_chunk);
685    }
686
687    Ok(())
688}
689
690#[inline(always)]
691fn sycc_to_rgb<S: Simd>(simd: S, components: &mut [ComponentData], bit_depth: u8) -> Result<()> {
692    let offset = (1_u32 << (bit_depth as u32 - 1)) as f32;
693    let max_value = ((1_u32 << bit_depth as u32) - 1) as f32;
694
695    let (head, _) = components
696        .split_at_mut_checked(3)
697        .ok_or(ColorError::SyccConversionFailed)?;
698
699    let [y, cb, cr] = head else {
700        unreachable!();
701    };
702
703    let offset_v = f32x8::splat(simd, offset);
704    let max_v = f32x8::splat(simd, max_value);
705    let zero_v = f32x8::splat(simd, 0.0);
706    let cr_to_r = f32x8::splat(simd, 1.402);
707    let cb_to_g = f32x8::splat(simd, -0.344136);
708    let cr_to_g = f32x8::splat(simd, -0.714136);
709    let cb_to_b = f32x8::splat(simd, 1.772);
710
711    for ((y_chunk, cb_chunk), cr_chunk) in y
712        .container
713        .chunks_exact_mut(SIMD_WIDTH)
714        .zip(cb.container.chunks_exact_mut(SIMD_WIDTH))
715        .zip(cr.container.chunks_exact_mut(SIMD_WIDTH))
716    {
717        let y_v = f32x8::from_slice(simd, y_chunk);
718        let cb_v = f32x8::from_slice(simd, cb_chunk) - offset_v;
719        let cr_v = f32x8::from_slice(simd, cr_chunk) - offset_v;
720
721        // r = y + 1.402 * cr
722        let r = cr_v.mul_add(cr_to_r, y_v);
723        // g = y - 0.344136 * cb - 0.714136 * cr
724        let g = cr_v.mul_add(cr_to_g, cb_v.mul_add(cb_to_g, y_v));
725        // b = y + 1.772 * cb
726        let b = cb_v.mul_add(cb_to_b, y_v);
727
728        r.min(max_v).max(zero_v).store(y_chunk);
729        g.min(max_v).max(zero_v).store(cb_chunk);
730        b.min(max_v).max(zero_v).store(cr_chunk);
731    }
732
733    Ok(())
734}