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