Skip to main content

pdfluent_jpeg2000/
lib.rs

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