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