Skip to main content

j2k_native/
color.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use alloc::vec::Vec;
4
5use crate::error::bail;
6use crate::j2c::{ComponentData, Header};
7use crate::jp2::cdef::ChannelType;
8use crate::jp2::colr::{CieLab, EnumeratedColorspace};
9use crate::jp2::icc::ICCMetadata;
10use crate::jp2::{self, DecodedImage, ImageBoxes};
11use crate::math::{self, dispatch, f32x8, Level, Simd, SIMD_WIDTH};
12use crate::{
13    checked_decode_sample_count, try_reserve_decode_elements, ColorError, DecodeSettings,
14    DecodingError, FormatError, Result, ValidationError, DEFAULT_MAX_DECODE_BYTES,
15};
16
17mod allocation;
18#[cfg(test)]
19mod boundary_tests;
20mod postprocess;
21pub(crate) use postprocess::{resolve_palette_indices, validate_and_reorder_channels};
22
23pub(crate) fn resolve_alpha_and_color_space(
24    boxes: &ImageBoxes,
25    header: &Header<'_>,
26    settings: &DecodeSettings,
27    retained_baseline_bytes: usize,
28) -> Result<(ColorSpace, bool)> {
29    let mut num_components = header.component_infos.len();
30
31    // Override number of components with what is actually in the palette box
32    // in case we resolve them.
33    if settings.resolve_palette_indices {
34        if let Some(palette_box) = &boxes.palette {
35            num_components = palette_box.columns.len();
36        }
37    }
38
39    let mut has_alpha = false;
40
41    if let Some(cdef) = &boxes.channel_definition {
42        has_alpha = cdef.channel_definitions.iter().any(|definition| {
43            matches!(
44                definition.channel_type,
45                ChannelType::Opacity | ChannelType::PremultipliedOpacity
46            )
47        });
48    }
49
50    // If palette indices remain unresolved, the exposed samples are indices;
51    // avoid cloning an ICC profile that would be discarded immediately.
52    let mut color_space = if !settings.resolve_palette_indices && boxes.palette.is_some() {
53        has_alpha = false;
54        ColorSpace::Gray
55    } else {
56        let retained_container_bytes =
57            crate::image::retained_container_metadata_bytes(header, boxes)?
58                .checked_add(retained_baseline_bytes)
59                .ok_or(ValidationError::ImageTooLarge)?;
60        if retained_container_bytes > DEFAULT_MAX_DECODE_BYTES {
61            return Err(ValidationError::ImageTooLarge.into());
62        }
63        get_color_space(boxes, num_components, retained_container_bytes)?
64    };
65
66    let actual_num_components = header.component_infos.len();
67
68    // Validate the number of channels.
69    if boxes.palette.is_none()
70        && actual_num_components != usize::from(color_space.num_channels() + u16::from(has_alpha))
71    {
72        if !settings.strict
73            && actual_num_components == usize::from(color_space.num_channels()) + 1
74            && !has_alpha
75        {
76            // See OPENJPEG test case orb-blue10-lin-j2k. Assume that we have an
77            // alpha channel in this case.
78            has_alpha = true;
79        } else {
80            // Color space is invalid, attempt to repair.
81            if actual_num_components == 1 || (actual_num_components == 2 && has_alpha) {
82                color_space = ColorSpace::Gray;
83            } else if actual_num_components == 3 {
84                color_space = ColorSpace::RGB;
85            } else if actual_num_components == 4 {
86                if has_alpha {
87                    color_space = ColorSpace::RGB;
88                } else {
89                    color_space = ColorSpace::CMYK;
90                }
91            } else {
92                color_space = ColorSpace::Unknown {
93                    num_channels: u16::try_from(actual_num_components)
94                        .map_err(|_| ValidationError::TooManyChannels)?,
95                };
96            }
97        }
98    }
99
100    Ok((color_space, has_alpha))
101}
102
103/// The color space of the image.
104#[derive(Debug)]
105pub enum ColorSpace {
106    /// A grayscale image.
107    Gray,
108    /// An RGB image.
109    RGB,
110    /// A CMYK image.
111    CMYK,
112    /// An unknown color space.
113    Unknown {
114        /// The number of channels of the color space.
115        num_channels: u16,
116    },
117    /// An image based on an ICC profile.
118    Icc {
119        /// The raw data of the ICC profile.
120        profile: Vec<u8>,
121        /// The number of channels used by the ICC profile.
122        num_channels: u16,
123    },
124}
125
126impl ColorSpace {
127    /// Return the number of expected channels for the color space.
128    #[must_use]
129    pub fn num_channels(&self) -> u16 {
130        match self {
131            Self::Gray => 1,
132            Self::RGB => 3,
133            Self::CMYK => 4,
134            Self::Unknown { num_channels } => *num_channels,
135            Self::Icc {
136                num_channels: num_components,
137                ..
138            } => *num_components,
139        }
140    }
141}
142
143/// A bitmap storing the decoded result of the image.
144pub struct Bitmap {
145    /// The color space of the image.
146    pub color_space: ColorSpace,
147    /// The raw pixel data of the image. The result will always be in
148    /// 8-bit (in case the original image had a different bit-depth, this
149    /// decode path scales it to 8-bit).
150    ///
151    /// The size is guaranteed to equal
152    /// `width * height * (num_channels + (if has_alpha { 1 } else { 0 })`.
153    /// Pixels are interleaved on a per-channel basis, the alpha channel always
154    /// appearing as the last channel, if available.
155    pub data: Vec<u8>,
156    /// Whether the image has an alpha channel.
157    pub has_alpha: bool,
158    /// The width of the image.
159    pub width: u32,
160    /// The height of the image.
161    pub height: u32,
162    /// The original bit depth of the image. You usually don't need to do anything
163    /// with this parameter, it just exists for informational purposes.
164    pub original_bit_depth: u8,
165}
166
167/// Raw decoded pixel data at native bit depth (no 8-bit scaling).
168///
169/// For bit depths ≤ 8, `data` contains one byte per sample.
170/// For bit depths > 8 (e.g., 12 or 16), `data` contains two bytes per sample
171/// in little-endian byte order (`u16` LE).
172///
173/// Samples are interleaved: for a 3-component image, the layout is
174/// `[R0, G0, B0, R1, G1, B1, ...]`.
175pub struct RawBitmap {
176    /// The raw pixel data at native bit depth.
177    pub data: Vec<u8>,
178    /// The width of the image in pixels.
179    pub width: u32,
180    /// The height of the image in pixels.
181    pub height: u32,
182    /// The original bit depth per sample (e.g., 8, 12, 16).
183    pub bit_depth: u8,
184    /// Whether every component in this packed bitmap is signed.
185    ///
186    /// Use [`Self::component_signed`] for per-component signedness when
187    /// handling arbitrary JPEG 2000 component metadata.
188    pub signed: bool,
189    /// Per-component signedness in codestream/component order.
190    pub component_signed: Vec<bool>,
191    /// The number of components (e.g., 1 for grayscale, 3 for RGB).
192    pub num_components: u16,
193    /// Bytes per sample in the packed little-endian native representation.
194    pub bytes_per_sample: u8,
195}
196
197/// One owned decoded component plane at native bit depth.
198pub struct NativeComponentPlane {
199    pub(crate) data: Vec<u8>,
200    pub(crate) dimensions: (u32, u32),
201    pub(crate) bit_depth: u8,
202    pub(crate) signed: bool,
203    pub(crate) sampling: (u8, u8),
204    pub(crate) bytes_per_sample: u8,
205}
206
207/// Allocation-free facade adapter representation of an owned native component plane.
208#[doc(hidden)]
209pub type NativeComponentPlaneParts = (Vec<u8>, (u32, u32), u8, bool, (u8, u8), u8);
210
211impl NativeComponentPlane {
212    /// Packed little-endian sample bytes for this component in row-major order.
213    #[must_use]
214    pub fn data(&self) -> &[u8] {
215        &self.data
216    }
217
218    crate::__j2k_component_plane_metadata_accessors!();
219
220    /// Bytes used for each packed little-endian sample in [`Self::data`].
221    #[must_use]
222    pub fn bytes_per_sample(&self) -> u8 {
223        self.bytes_per_sample
224    }
225
226    /// Return the byte capacity owned by this plane.
227    #[doc(hidden)]
228    #[must_use]
229    pub fn allocated_bytes(&self) -> usize {
230        self.data.capacity()
231    }
232
233    /// Consume this plane into allocation-free facade adapter parts.
234    #[doc(hidden)]
235    #[must_use]
236    pub fn into_parts(self) -> NativeComponentPlaneParts {
237        (
238            self.data,
239            self.dimensions,
240            self.bit_depth,
241            self.signed,
242            self.sampling,
243            self.bytes_per_sample,
244        )
245    }
246}
247
248/// Owned decoded native-bit-depth component planes for an image.
249pub struct DecodedNativeComponents {
250    pub(crate) dimensions: (u32, u32),
251    pub(crate) color_space: ColorSpace,
252    pub(crate) has_alpha: bool,
253    pub(crate) planes: Vec<NativeComponentPlane>,
254}
255
256impl DecodedNativeComponents {
257    /// Dimensions of the decoded image represented by these planes.
258    #[must_use]
259    pub fn dimensions(&self) -> (u32, u32) {
260        self.dimensions
261    }
262
263    /// Color space after JPEG 2000 color conversion has been applied.
264    #[must_use]
265    pub fn color_space(&self) -> &ColorSpace {
266        &self.color_space
267    }
268
269    /// Whether the decoded image has an alpha channel.
270    #[must_use]
271    pub fn has_alpha(&self) -> bool {
272        self.has_alpha
273    }
274
275    /// Decoded component planes in display order.
276    #[must_use]
277    pub fn planes(&self) -> &[NativeComponentPlane] {
278        &self.planes
279    }
280
281    /// Return the actual heap capacity retained by this owned result.
282    #[doc(hidden)]
283    #[must_use]
284    pub fn allocated_bytes(&self) -> Option<usize> {
285        let mut bytes = self
286            .planes
287            .capacity()
288            .checked_mul(core::mem::size_of::<NativeComponentPlane>())?;
289        for plane in &self.planes {
290            bytes = bytes.checked_add(plane.allocated_bytes())?;
291        }
292        if let ColorSpace::Icc { profile, .. } = &self.color_space {
293            bytes = bytes.checked_add(profile.capacity())?;
294        }
295        Some(bytes)
296    }
297
298    /// Consume this result into allocation-free facade adapter parts.
299    #[doc(hidden)]
300    #[must_use]
301    pub fn into_parts(self) -> ((u32, u32), ColorSpace, bool, Vec<NativeComponentPlane>) {
302        (
303            self.dimensions,
304            self.color_space,
305            self.has_alpha,
306            self.planes,
307        )
308    }
309}
310
311/// A borrowed decoded component plane.
312pub struct ComponentPlane<'a> {
313    pub(crate) samples: &'a [f32],
314    pub(crate) dimensions: (u32, u32),
315    pub(crate) bit_depth: u8,
316    pub(crate) signed: bool,
317    pub(crate) sampling: (u8, u8),
318}
319
320/// Allocation-free facade adapter representation of a borrowed component plane.
321#[doc(hidden)]
322pub type ComponentPlaneParts<'a> = (&'a [f32], (u32, u32), u8, bool, (u8, u8));
323
324impl<'a> ComponentPlane<'a> {
325    /// Component samples in row-major order.
326    #[must_use]
327    pub fn samples(&self) -> &'a [f32] {
328        self.samples
329    }
330
331    crate::__j2k_component_plane_metadata_accessors!();
332
333    /// Consume this borrowed plane into allocation-free facade adapter parts.
334    #[doc(hidden)]
335    #[must_use]
336    pub fn into_parts(self) -> ComponentPlaneParts<'a> {
337        (
338            self.samples,
339            self.dimensions,
340            self.bit_depth,
341            self.signed,
342            self.sampling,
343        )
344    }
345}
346
347/// Borrowed decoded component planes for an image.
348pub struct DecodedComponents<'a> {
349    pub(crate) dimensions: (u32, u32),
350    pub(crate) color_space: ColorSpace,
351    pub(crate) has_alpha: bool,
352    pub(crate) planes: Vec<ComponentPlane<'a>>,
353    pub(crate) live_bytes: usize,
354}
355
356impl<'a> DecodedComponents<'a> {
357    /// Dimensions of the decoded image represented by these planes.
358    #[must_use]
359    pub fn dimensions(&self) -> (u32, u32) {
360        self.dimensions
361    }
362
363    /// Color space after JPEG 2000 color conversion has been applied.
364    #[must_use]
365    pub fn color_space(&self) -> &ColorSpace {
366        &self.color_space
367    }
368
369    /// Whether the decoded image has an alpha channel.
370    #[must_use]
371    pub fn has_alpha(&self) -> bool {
372        self.has_alpha
373    }
374
375    /// Borrowed decoded component planes in display order.
376    #[must_use]
377    pub fn planes(&self) -> &[ComponentPlane<'a>] {
378        &self.planes
379    }
380
381    /// Return the retained heap capacity that remains live with this borrowed result.
382    ///
383    /// This includes the decoder-context component owners backing the borrowed
384    /// sample slices, SIMD padding, exact-integer shadow buffers, the component
385    /// metadata vector, and an owned ICC profile when present.
386    #[doc(hidden)]
387    #[must_use]
388    pub fn live_bytes(&self) -> usize {
389        self.live_bytes
390    }
391
392    /// Consume this result into allocation-free facade adapter parts.
393    #[doc(hidden)]
394    #[must_use]
395    pub fn into_parts(self) -> ((u32, u32), ColorSpace, bool, Vec<ComponentPlane<'a>>) {
396        (
397            self.dimensions,
398            self.color_space,
399            self.has_alpha,
400            self.planes,
401        )
402    }
403}
404
405pub(crate) fn validate_interleaved_output_buffer(
406    image: &DecodedImage<'_, '_>,
407    buf: &[u8],
408) -> Result<()> {
409    let required_len = interleaved_output_len(image)?;
410    if buf.len() < required_len {
411        bail!(DecodingError::OutputBufferTooSmall);
412    }
413    Ok(())
414}
415
416fn interleaved_output_len(image: &DecodedImage<'_, '_>) -> Result<usize> {
417    let Some(first) = image.decoded_components.first() else {
418        bail!(DecodingError::CodeBlockDecodeFailure);
419    };
420    first
421        .container
422        .truncated()
423        .len()
424        .checked_mul(image.decoded_components.len())
425        .ok_or(ValidationError::ImageTooLarge.into())
426}
427
428#[expect(
429    clippy::cast_possible_truncation,
430    clippy::cast_sign_loss,
431    clippy::cast_precision_loss,
432    reason = "pixel samples are rounded and intentionally quantized to the stable 8-bit output format"
433)]
434pub(crate) fn interleave_and_convert(
435    image: &mut DecodedImage<'_, '_>,
436    buf: &mut [u8],
437) -> Result<()> {
438    let components = &mut *image.decoded_components;
439    let num_components = components.len();
440
441    let mut all_same_bit_depth = Some(components[0].bit_depth);
442
443    for component in components.iter().skip(1) {
444        if Some(component.bit_depth) != all_same_bit_depth {
445            all_same_bit_depth = None;
446        }
447    }
448
449    let max_len = components[0].container.truncated().len();
450
451    let mut output_iter = buf.iter_mut();
452
453    if all_same_bit_depth == Some(8) && num_components <= 4 {
454        // Fast path for the common case.
455        match num_components {
456            // Gray-scale.
457            1 => {
458                for (output, input) in output_iter.zip(
459                    components[0]
460                        .container
461                        .iter()
462                        .map(|v| math::round_f32(*v) as u8),
463                ) {
464                    *output = input;
465                }
466            }
467            // Gray-scale with alpha.
468            2 => {
469                let c0 = &components[0];
470                let c1 = &components[1];
471
472                let c0 = &c0.container[..max_len];
473                let c1 = &c1.container[..max_len];
474
475                for i in 0..max_len {
476                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
477                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
478                }
479            }
480            // RGB
481            3 => {
482                let c0 = &components[0];
483                let c1 = &components[1];
484                let c2 = &components[2];
485
486                let c0 = &c0.container[..max_len];
487                let c1 = &c1.container[..max_len];
488                let c2 = &c2.container[..max_len];
489
490                for i in 0..max_len {
491                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
492                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
493                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
494                }
495            }
496            // RGBA or CMYK.
497            4 => {
498                let c0 = &components[0];
499                let c1 = &components[1];
500                let c2 = &components[2];
501                let c3 = &components[3];
502
503                let c0 = &c0.container[..max_len];
504                let c1 = &c1.container[..max_len];
505                let c2 = &c2.container[..max_len];
506                let c3 = &c3.container[..max_len];
507
508                for i in 0..max_len {
509                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
510                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
511                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
512                    *output_iter.next().unwrap() = math::round_f32(c3[i]) as u8;
513                }
514            }
515            _ => bail!(ValidationError::TooManyChannels),
516        }
517    } else {
518        // Slow path that also requires us to scale to 8 bit.
519        let mul_factor = ((1 << 8) - 1) as f32;
520
521        for sample in 0..max_len {
522            for channel in components.iter() {
523                *output_iter.next().unwrap() = math::round_f32(
524                    (channel.container[sample]
525                        / ((1_u64 << u32::from(channel.bit_depth)) - 1) as f32)
526                        * mul_factor,
527                ) as u8;
528            }
529        }
530    }
531
532    Ok(())
533}
534
535#[expect(
536    clippy::cast_possible_truncation,
537    clippy::cast_sign_loss,
538    clippy::cast_precision_loss,
539    reason = "region samples use the same stable rounded 8-bit quantization as full-image decode"
540)]
541pub(crate) fn interleave_and_convert_region(
542    image: &mut DecodedImage<'_, '_>,
543    image_width: usize,
544    roi: (u32, u32, u32, u32),
545    buf: &mut [u8],
546) {
547    let components = &mut *image.decoded_components;
548    let num_components = components.len();
549    let (x, y, width, height) = roi;
550    let mut output_iter = buf.iter_mut();
551
552    let mut all_same_bit_depth = Some(components[0].bit_depth);
553    for component in components.iter().skip(1) {
554        if Some(component.bit_depth) != all_same_bit_depth {
555            all_same_bit_depth = None;
556        }
557    }
558
559    if all_same_bit_depth == Some(8) && num_components <= 4 {
560        for row in y as usize..(y + height) as usize {
561            let row_base = row * image_width;
562            for col in x as usize..(x + width) as usize {
563                let idx = row_base + col;
564                for component in components.iter() {
565                    *output_iter.next().unwrap() = math::round_f32(component.container[idx]) as u8;
566                }
567            }
568        }
569    } else {
570        let mul_factor = ((1 << 8) - 1) as f32;
571        for row in y as usize..(y + height) as usize {
572            let row_base = row * image_width;
573            for col in x as usize..(x + width) as usize {
574                let idx = row_base + col;
575                for component in components.iter() {
576                    *output_iter.next().unwrap() = math::round_f32(
577                        (component.container[idx]
578                            / ((1_u64 << u32::from(component.bit_depth)) - 1) as f32)
579                            * mul_factor,
580                    ) as u8;
581                }
582            }
583        }
584    }
585}
586
587pub(crate) fn native_component_plane_dimensions(
588    reference_dimensions: (u32, u32),
589    sampling: (u8, u8),
590    sample_count: usize,
591) -> Result<(u32, u32)> {
592    let reference_sample_count =
593        checked_decode_sample_count(reference_dimensions.0, reference_dimensions.1)?;
594    if sample_count == reference_sample_count {
595        return Ok(reference_dimensions);
596    }
597
598    let (x_rsiz, y_rsiz) = sampling;
599    if x_rsiz == 0 || y_rsiz == 0 {
600        bail!(DecodingError::CodeBlockDecodeFailure);
601    }
602    let sampled_dimensions = (
603        reference_dimensions.0.div_ceil(u32::from(x_rsiz)),
604        reference_dimensions.1.div_ceil(u32::from(y_rsiz)),
605    );
606    let sampled_sample_count =
607        checked_decode_sample_count(sampled_dimensions.0, sampled_dimensions.1)?;
608    if sample_count == sampled_sample_count {
609        return Ok(sampled_dimensions);
610    }
611
612    bail!(DecodingError::CodeBlockDecodeFailure)
613}
614
615pub(crate) fn convert_color_space(image: &mut DecodedImage<'_, '_>, bit_depth: u8) -> Result<()> {
616    if let Some(jp2::colr::ColorSpace::Enumerated(e)) = &image
617        .boxes
618        .primary_color_specification()
619        .map(|i| &i.color_space)
620    {
621        match e {
622            EnumeratedColorspace::Sycc => {
623                dispatch!(Level::new(), simd => {
624                    sycc_to_rgb(simd, image.decoded_components, bit_depth)
625                })?;
626            }
627            EnumeratedColorspace::CieLab(cielab) => {
628                dispatch!(Level::new(), simd => {
629                    cielab_to_rgb(simd, image.decoded_components, bit_depth, cielab)
630                })?;
631            }
632            _ => {}
633        }
634    }
635
636    Ok(())
637}
638
639fn get_color_space(
640    boxes: &ImageBoxes,
641    num_components: usize,
642    retained_container_bytes: usize,
643) -> Result<ColorSpace> {
644    let cs = match boxes
645        .primary_color_specification()
646        .map_or(&jp2::colr::ColorSpace::Unknown, |specification| {
647            &specification.color_space
648        }) {
649        jp2::colr::ColorSpace::Enumerated(e) => {
650            match e {
651                EnumeratedColorspace::Cmyk => ColorSpace::CMYK,
652                EnumeratedColorspace::Srgb
653                | EnumeratedColorspace::EsRgb
654                | EnumeratedColorspace::Sycc => ColorSpace::RGB,
655                EnumeratedColorspace::RommRgb => {
656                    // Use an ICC profile to process the RommRGB color space.
657                    ColorSpace::Icc {
658                        profile: try_clone_color_profile(
659                            include_bytes!("../assets/ProPhoto-v2-micro.icc"),
660                            retained_container_bytes,
661                        )?,
662                        num_channels: 3,
663                    }
664                }
665                EnumeratedColorspace::Greyscale => ColorSpace::Gray,
666                EnumeratedColorspace::CieLab(_) => ColorSpace::Icc {
667                    profile: try_clone_color_profile(
668                        include_bytes!("../assets/LAB.icc"),
669                        retained_container_bytes,
670                    )?,
671                    num_channels: 3,
672                },
673                _ => bail!(FormatError::Unsupported),
674            }
675        }
676        jp2::colr::ColorSpace::Icc(icc) => {
677            if let Some(metadata) = ICCMetadata::from_data(icc) {
678                ColorSpace::Icc {
679                    profile: try_clone_color_profile(icc, retained_container_bytes)?,
680                    num_channels: u16::from(metadata.color_space.num_components()),
681                }
682            } else {
683                // See OPENJPEG test orb-blue10-lin-jp2.jp2. They seem to
684                // assume RGB in this case (even though the image has 4
685                // components with no opacity channel, they assume RGBA instead
686                // of CMYK).
687                ColorSpace::RGB
688            }
689        }
690        jp2::colr::ColorSpace::Unknown => match num_components {
691            1 => ColorSpace::Gray,
692            3 => ColorSpace::RGB,
693            4 => ColorSpace::CMYK,
694            _ => ColorSpace::Unknown {
695                num_channels: u16::try_from(num_components).unwrap_or(u16::MAX),
696            },
697        },
698    };
699
700    Ok(cs)
701}
702
703fn try_clone_color_profile(profile: &[u8], retained_bytes: usize) -> Result<Vec<u8>> {
704    checked_color_profile_peak(retained_bytes, profile.len(), DEFAULT_MAX_DECODE_BYTES)?;
705    let mut cloned = Vec::new();
706    try_reserve_decode_elements(&mut cloned, profile.len())?;
707    checked_color_profile_peak(retained_bytes, cloned.capacity(), DEFAULT_MAX_DECODE_BYTES)?;
708    cloned.extend_from_slice(profile);
709    Ok(cloned)
710}
711
712fn checked_color_profile_peak(
713    retained_bytes: usize,
714    profile_bytes: usize,
715    cap: usize,
716) -> Result<usize> {
717    let peak = retained_bytes
718        .checked_add(profile_bytes)
719        .ok_or(ValidationError::ImageTooLarge)?;
720    if peak > cap {
721        return Err(ValidationError::ImageTooLarge.into());
722    }
723    Ok(peak)
724}
725
726#[expect(
727    clippy::cast_possible_truncation,
728    reason = "Rust's saturating float-to-integer conversion is retained before rejecting negative indices"
729)]
730fn palette_index(sample: f32) -> Result<usize> {
731    let rounded = math::round_f32(sample) as i64;
732    usize::try_from(rounded).map_err(|_| ColorError::PaletteResolutionFailed.into())
733}
734
735fn sign_extend_palette_value(raw: u64, bit_depth: u8) -> i64 {
736    if bit_depth == 0 {
737        return raw.cast_signed();
738    }
739    if bit_depth >= 64 {
740        return raw.cast_signed();
741    }
742
743    let mask = (1_u64 << bit_depth) - 1;
744    let value = raw & mask;
745    let shift = 64 - u32::from(bit_depth);
746    (value << shift).cast_signed() >> shift
747}
748
749fn clamped_power_of_two_u32(exponent: u8) -> u32 {
750    if u32::from(exponent) >= u32::BITS {
751        u32::MAX
752    } else {
753        1_u32 << exponent
754    }
755}
756
757fn clamped_add_u32(left: u32, right: u32) -> u32 {
758    if right > u32::MAX - left {
759        u32::MAX
760    } else {
761        left + right
762    }
763}
764
765fn max_value_for_bit_depth(bit_depth: u8) -> u32 {
766    if u32::from(bit_depth) >= u32::BITS {
767        u32::MAX
768    } else {
769        (1_u32 << bit_depth) - 1
770    }
771}
772
773#[expect(
774    clippy::cast_precision_loss,
775    reason = "OpenJPEG-compatible CIE Lab scaling intentionally uses f32 arithmetic"
776)]
777#[inline]
778pub(crate) fn cielab_to_rgb<S: Simd>(
779    simd: S,
780    components: &mut [ComponentData],
781    bit_depth: u8,
782    lab: &CieLab,
783) -> Result<()> {
784    let (head, _) = components
785        .split_at_mut_checked(3)
786        .ok_or(ColorError::LabConversionFailed)?;
787
788    let [l, a, b] = head else {
789        bail!(ColorError::LabConversionFailed);
790    };
791
792    let prec0 = l.bit_depth;
793    let prec1 = a.bit_depth;
794    let prec2 = b.bit_depth;
795
796    // Prevent underflows/divisions by zero further below.
797    if prec0 < 4 || prec1 < 4 || prec2 < 4 {
798        bail!(ColorError::LabConversionFailed);
799    }
800
801    let rl = lab.rl.unwrap_or(100);
802    let ra = lab.ra.unwrap_or(170);
803    let rb = lab.rb.unwrap_or(200);
804    let ol = lab.ol.unwrap_or(0);
805    let a_shift = bit_depth
806        .checked_sub(1)
807        .ok_or(ColorError::LabConversionFailed)?;
808    let b_high_shift = bit_depth
809        .checked_sub(2)
810        .ok_or(ColorError::LabConversionFailed)?;
811    let b_low_shift = bit_depth
812        .checked_sub(3)
813        .ok_or(ColorError::LabConversionFailed)?;
814    let default_a_offset = clamped_power_of_two_u32(a_shift);
815    let default_b_offset = clamped_add_u32(
816        clamped_power_of_two_u32(b_high_shift),
817        clamped_power_of_two_u32(b_low_shift),
818    );
819    let oa = lab.oa.unwrap_or(default_a_offset);
820    let ob = lab.ob.unwrap_or(default_b_offset);
821
822    // Copied from OpenJPEG.
823    let min_l = -(rl as f32 * ol as f32) / ((1_u64 << u32::from(prec0)) - 1) as f32;
824    let max_l = min_l + rl as f32;
825    let min_a = -(ra as f32 * oa as f32) / ((1_u64 << u32::from(prec1)) - 1) as f32;
826    let max_a = min_a + ra as f32;
827    let min_b = -(rb as f32 * ob as f32) / ((1_u64 << u32::from(prec2)) - 1) as f32;
828    let max_b = min_b + rb as f32;
829
830    let bit_max = max_value_for_bit_depth(bit_depth);
831
832    // Note that we are not doing the actual conversion with the ICC profile yet,
833    // just decoding the raw LAB values.
834    // We leave applying the ICC profile to the user.
835    let divisor_l = ((1_u64 << u32::from(prec0)) - 1) as f32;
836    let divisor_a = ((1_u64 << u32::from(prec1)) - 1) as f32;
837    let divisor_b = ((1_u64 << u32::from(prec2)) - 1) as f32;
838
839    let scale_l_final = bit_max as f32 / 100.0;
840    let scale_ab_final = bit_max as f32 / 255.0;
841
842    let l_offset = min_l * scale_l_final;
843    let l_scale = (max_l - min_l) / divisor_l * scale_l_final;
844    let a_offset = (min_a + 128.0) * scale_ab_final;
845    let a_scale = (max_a - min_a) / divisor_a * scale_ab_final;
846    let b_offset = (min_b + 128.0) * scale_ab_final;
847    let b_scale = (max_b - min_b) / divisor_b * scale_ab_final;
848
849    let l_offset_v = f32x8::splat(simd, l_offset);
850    let l_scale_v = f32x8::splat(simd, l_scale);
851    let a_offset_v = f32x8::splat(simd, a_offset);
852    let a_scale_v = f32x8::splat(simd, a_scale);
853    let b_offset_v = f32x8::splat(simd, b_offset);
854    let b_scale_v = f32x8::splat(simd, b_scale);
855
856    // Note that we are not doing the actual conversion with the ICC profile yet,
857    // just decoding the raw LAB values.
858    // We leave applying the ICC profile to the user.
859    for ((l_chunk, a_chunk), b_chunk) in l
860        .container
861        .chunks_exact_mut(SIMD_WIDTH)
862        .zip(a.container.chunks_exact_mut(SIMD_WIDTH))
863        .zip(b.container.chunks_exact_mut(SIMD_WIDTH))
864    {
865        let l_v = f32x8::from_slice(simd, l_chunk);
866        let a_v = f32x8::from_slice(simd, a_chunk);
867        let b_v = f32x8::from_slice(simd, b_chunk);
868
869        l_v.mul_add(l_scale_v, l_offset_v).store(l_chunk);
870        a_v.mul_add(a_scale_v, a_offset_v).store(a_chunk);
871        b_v.mul_add(b_scale_v, b_offset_v).store(b_chunk);
872    }
873
874    // The color transform replaced the source samples. Any exact-integer
875    // shadow describes the pre-transform component values and must not win
876    // over the converted f32 planes during native output packing.
877    l.integer_container = None;
878    a.integer_container = None;
879    b.integer_container = None;
880
881    Ok(())
882}
883
884#[expect(
885    clippy::cast_precision_loss,
886    reason = "JPEG 2000 sYCC conversion intentionally uses f32 SIMD arithmetic"
887)]
888#[inline]
889fn sycc_to_rgb<S: Simd>(simd: S, components: &mut [ComponentData], bit_depth: u8) -> Result<()> {
890    let offset = (1_u64 << (u32::from(bit_depth) - 1)) as f32;
891    let max_value = ((1_u64 << u32::from(bit_depth)) - 1) as f32;
892
893    let (head, _) = components
894        .split_at_mut_checked(3)
895        .ok_or(ColorError::SyccConversionFailed)?;
896
897    let [luma, blue_chroma, red_chroma] = head else {
898        bail!(ColorError::SyccConversionFailed);
899    };
900
901    let offset_v = f32x8::splat(simd, offset);
902    let max_v = f32x8::splat(simd, max_value);
903    let zero_v = f32x8::splat(simd, 0.0);
904    let red_chroma_to_red = f32x8::splat(simd, 1.402);
905    let blue_chroma_to_green = f32x8::splat(simd, -0.344_136);
906    let red_chroma_to_green = f32x8::splat(simd, -0.714_136);
907    let blue_chroma_to_blue = f32x8::splat(simd, 1.772);
908
909    for ((luma_chunk, blue_chroma_chunk), red_chroma_chunk) in luma
910        .container
911        .chunks_exact_mut(SIMD_WIDTH)
912        .zip(blue_chroma.container.chunks_exact_mut(SIMD_WIDTH))
913        .zip(red_chroma.container.chunks_exact_mut(SIMD_WIDTH))
914    {
915        let luma_values = f32x8::from_slice(simd, luma_chunk);
916        let blue_chroma_values = f32x8::from_slice(simd, blue_chroma_chunk) - offset_v;
917        let red_chroma_values = f32x8::from_slice(simd, red_chroma_chunk) - offset_v;
918
919        // r = y + 1.402 * cr
920        let red = red_chroma_values.mul_add(red_chroma_to_red, luma_values);
921        // g = y - 0.344136 * cb - 0.714136 * cr
922        let green = red_chroma_values.mul_add(
923            red_chroma_to_green,
924            blue_chroma_values.mul_add(blue_chroma_to_green, luma_values),
925        );
926        // b = y + 1.772 * cb
927        let blue = blue_chroma_values.mul_add(blue_chroma_to_blue, luma_values);
928
929        red.min(max_v).max(zero_v).store(luma_chunk);
930        green.min(max_v).max(zero_v).store(blue_chroma_chunk);
931        blue.min(max_v).max(zero_v).store(red_chroma_chunk);
932    }
933
934    luma.integer_container = None;
935    blue_chroma.integer_container = None;
936    red_chroma.integer_container = None;
937
938    Ok(())
939}
940
941#[cfg(test)]
942mod tests {
943    use super::{
944        checked_color_profile_peak, clamped_add_u32, clamped_power_of_two_u32,
945        max_value_for_bit_depth, palette_index, sycc_to_rgb, ColorSpace, ComponentPlane,
946        DecodedComponents, DecodedNativeComponents, NativeComponentPlane,
947    };
948    use crate::j2c::ComponentData;
949    use crate::math::{dispatch, Level, SimdBuffer, SIMD_WIDTH};
950    use alloc::{vec, vec::Vec};
951    use core::mem::size_of;
952
953    #[test]
954    fn lab_integer_scaling_preserves_clamped_boundaries() {
955        assert_eq!(clamped_power_of_two_u32(31), 1_u32 << 31);
956        assert_eq!(clamped_power_of_two_u32(32), u32::MAX);
957        assert_eq!(clamped_add_u32(u32::MAX, 1), u32::MAX);
958        assert_eq!(max_value_for_bit_depth(31), (1_u32 << 31) - 1);
959        assert_eq!(max_value_for_bit_depth(32), u32::MAX);
960    }
961
962    #[test]
963    fn sycc_conversion_discards_pretransform_integer_shadows() {
964        let component = |value: u8| ComponentData {
965            container: SimdBuffer::<SIMD_WIDTH>::new(vec![f32::from(value); SIMD_WIDTH]),
966            integer_container: Some(vec![i64::from(value); SIMD_WIDTH]),
967            bit_depth: 8,
968            signed: false,
969        };
970        let mut components = vec![component(128), component(128), component(128)];
971
972        dispatch!(Level::new(), simd => sycc_to_rgb(simd, &mut components, 8))
973            .expect("sYCC conversion");
974
975        assert!(
976            components
977                .iter()
978                .all(|component| component.integer_container.is_none()),
979            "native packing must not reuse pre-transform exact samples"
980        );
981    }
982
983    #[test]
984    fn retained_color_profile_peak_accepts_exact_cap_and_rejects_one_over() {
985        assert_eq!(
986            checked_color_profile_peak(7, 5, 12).expect("exact ICC clone peak"),
987            12
988        );
989        assert!(checked_color_profile_peak(8, 5, 12).is_err());
990    }
991
992    #[test]
993    fn palette_indices_reject_negative_samples_without_wrapping() {
994        assert!(palette_index(-1.0).is_err());
995        assert_eq!(palette_index(2.4).expect("valid palette index"), 2);
996    }
997
998    #[test]
999    fn native_component_handoff_preserves_owned_capacities() {
1000        let mut data = Vec::with_capacity(9);
1001        data.push(3);
1002        let mut planes = Vec::with_capacity(4);
1003        planes.push(NativeComponentPlane {
1004            data,
1005            dimensions: (1, 1),
1006            bit_depth: 8,
1007            signed: false,
1008            sampling: (1, 1),
1009            bytes_per_sample: 1,
1010        });
1011        let mut profile = Vec::with_capacity(7);
1012        profile.push(1);
1013        let decoded = DecodedNativeComponents {
1014            dimensions: (1, 1),
1015            color_space: ColorSpace::Icc {
1016                profile,
1017                num_channels: 1,
1018            },
1019            has_alpha: false,
1020            planes,
1021        };
1022        let expected = decoded.planes.capacity() * size_of::<NativeComponentPlane>()
1023            + decoded.planes[0].data.capacity()
1024            + match &decoded.color_space {
1025                ColorSpace::Icc { profile, .. } => profile.capacity(),
1026                _ => 0,
1027            };
1028        let plane_owner_capacity = decoded.planes.capacity();
1029        let data_capacity = decoded.planes[0].data.capacity();
1030        let profile_capacity = match &decoded.color_space {
1031            ColorSpace::Icc { profile, .. } => profile.capacity(),
1032            _ => 0,
1033        };
1034        assert_eq!(decoded.allocated_bytes(), Some(expected));
1035
1036        let (_, color_space, _, planes) = decoded.into_parts();
1037        assert_eq!(planes.capacity(), plane_owner_capacity);
1038        assert_eq!(planes[0].allocated_bytes(), data_capacity);
1039        assert!(matches!(
1040            color_space,
1041            ColorSpace::Icc { profile, .. } if profile.capacity() == profile_capacity
1042        ));
1043    }
1044
1045    #[test]
1046    fn borrowed_component_handoff_preserves_metadata_capacities() {
1047        let samples = [2.0_f32];
1048        let mut planes = Vec::with_capacity(3);
1049        planes.push(ComponentPlane {
1050            samples: &samples,
1051            dimensions: (1, 1),
1052            bit_depth: 8,
1053            signed: false,
1054            sampling: (1, 1),
1055        });
1056        let mut profile = Vec::with_capacity(5);
1057        profile.push(1);
1058        let decoded = DecodedComponents {
1059            dimensions: (1, 1),
1060            color_space: ColorSpace::Icc {
1061                profile,
1062                num_channels: 1,
1063            },
1064            has_alpha: false,
1065            planes,
1066            live_bytes: 123,
1067        };
1068        let plane_owner_capacity = decoded.planes.capacity();
1069        let profile_capacity = match &decoded.color_space {
1070            ColorSpace::Icc { profile, .. } => profile.capacity(),
1071            _ => 0,
1072        };
1073
1074        assert_eq!(decoded.live_bytes(), 123);
1075        let (_, color_space, _, planes) = decoded.into_parts();
1076        assert_eq!(planes.capacity(), plane_owner_capacity);
1077        assert!(core::ptr::eq(
1078            planes[0].samples().as_ptr(),
1079            samples.as_ptr()
1080        ));
1081        assert!(matches!(
1082            color_space,
1083            ColorSpace::Icc { profile, .. } if profile.capacity() == profile_capacity
1084        ));
1085    }
1086}