Skip to main content

dicom_toolkit_jpeg2000/
lib.rs

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