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 decoded image components, which can be inspected directly or packed
14into a simple 8-bit image with either greyscale, RGB, CMYK or an ICC-based color
15space.
16
17# Example
18```rust,no_run
19use pdfluent_jpeg2000::{DecodeSettings, DecoderContext, Image};
20
21let data = std::fs::read("image.jp2").unwrap();
22let image = Image::new(&data, &DecodeSettings::default()).unwrap();
23
24println!(
25    "{}x{} image in {:?} with alpha={}",
26    image.width(),
27    image.height(),
28    image.color_space(),
29    image.has_alpha(),
30);
31
32let mut ctx = DecoderContext::default();
33let decoded = image.decode(&mut ctx).unwrap();
34let bitmap = decoded.data_u8();
35```
36
37If you want to see a more comprehensive example, please take a look
38at the example in [GitHub](https://github.com/LaurenzV/hayro/blob/main/hayro-jpeg2000/examples/png.rs),
39which shows you the main steps needed to convert a JPEG2000 image into PNG for example.
40
41# Testing
42The decoder has been tested against 20.000+ images scraped from random PDFs
43on the internet and also passes a large part of the `OpenJPEG` test suite. So you
44can expect the crate to perform decently in terms of decoding correctness.
45
46# Performance
47A decent amount of effort has already been put into optimizing this crate
48(both in terms of raw performance but also memory allocations). However, there
49are some more important optimizations that have not been implemented yet, so
50there is definitely still room for improvement (and I am planning on implementing
51them eventually).
52
53Overall, you should expect this crate to have worse performance than `OpenJPEG`,
54but the difference gap should not be too large.
55
56# Safety
57By default, the crate has the `simd` feature enabled, which uses the
58[`fearless_simd`](https://github.com/linebender/fearless_simd) crate to accelerate
59important parts of the pipeline. If you want to eliminate any usage of unsafe
60in this crate as well as its dependencies, you can simply disable this
61feature, at the cost of worse decoding performance. Unsafe code is forbidden
62via a crate-level attribute.
63
64The crate is `no_std` compatible but requires an allocator to be available.
65*/
66
67#![cfg_attr(not(feature = "std"), no_std)]
68#![forbid(unsafe_code)]
69#![forbid(missing_docs)]
70
71extern crate alloc;
72
73use alloc::vec;
74use alloc::vec::Vec;
75
76use crate::error::{bail, err};
77use crate::j2c::Header;
78use crate::jp2::ImageBoxes;
79use crate::jp2::cdef::{ChannelAssociation, ChannelType};
80use crate::jp2::cmap::ComponentMappingType;
81use crate::jp2::colr::{CieLab, EnumeratedColorspace};
82use crate::jp2::icc::ICCMetadata;
83
84pub mod error;
85#[macro_use]
86pub(crate) mod log;
87pub(crate) mod math;
88
89use crate::math::{Level, SIMD_WIDTH, Simd, dispatch, f32x8};
90pub use error::{
91    ColorError, DecodeError, DecodingError, FormatError, MarkerError, Result, TileError,
92    ValidationError,
93};
94pub use j2c::{ComponentData, DecoderContext};
95pub use jp2::DecodedImage;
96
97#[cfg(feature = "image")]
98pub mod integration;
99mod j2c;
100mod jp2;
101pub(crate) mod reader;
102
103/// JP2 signature box: 00 00 00 0C 6A 50 20 20
104pub(crate) const JP2_MAGIC: &[u8] = b"\x00\x00\x00\x0C\x6A\x50\x20\x20";
105/// Codestream signature: FF 4F FF 51 (SOC + SIZ markers)
106pub(crate) const CODESTREAM_MAGIC: &[u8] = b"\xFF\x4F\xFF\x51";
107
108/// Settings to apply during decoding.
109#[derive(Debug, Copy, Clone)]
110pub struct DecodeSettings {
111    /// Whether palette indices should be resolved.
112    ///
113    /// JPEG2000 images can be stored in two different ways. First, by storing
114    /// RGB values (depending on the color space) for each pixel. Secondly, by
115    /// only storing a single index for each channel, and then resolving the
116    /// actual color using the index.
117    ///
118    /// If you disable this option, in case you have an image with palette
119    /// indices, they will not be resolved, but instead a grayscale image
120    /// will be returned, with each pixel value corresponding to the palette
121    /// index of the location.
122    pub resolve_palette_indices: bool,
123    /// Whether strict mode should be enabled when decoding.
124    ///
125    /// It is recommended to leave this flag disabled, unless you have a
126    /// specific reason not to.
127    pub strict: bool,
128    /// A hint for the target resolution that the image should be decoded at.
129    pub target_resolution: Option<(u32, u32)>,
130}
131
132impl Default for DecodeSettings {
133    fn default() -> Self {
134        Self {
135            resolve_palette_indices: true,
136            strict: false,
137            target_resolution: None,
138        }
139    }
140}
141
142/// A JPEG2000 image or codestream.
143pub struct Image<'a> {
144    /// The codestream containing the data to decode.
145    pub(crate) codestream: &'a [u8],
146    /// The header of the J2C codestream.
147    pub(crate) header: Header<'a>,
148    /// The JP2 boxes of the image. In the case of a raw codestream, we
149    /// will synthesize the necessary boxes.
150    pub(crate) boxes: ImageBoxes,
151    /// Settings that should be applied during decoding.
152    pub(crate) settings: DecodeSettings,
153    /// Whether the image has an alpha channel.
154    pub(crate) has_alpha: bool,
155    /// The color space of the image.
156    pub(crate) color_space: ColorSpace,
157}
158
159impl<'a> Image<'a> {
160    /// Try to create a new JPEG2000 image from the given data.
161    pub fn new(data: &'a [u8], settings: &DecodeSettings) -> Result<Self> {
162        if data.starts_with(JP2_MAGIC) {
163            jp2::parse(data, *settings)
164        } else if data.starts_with(CODESTREAM_MAGIC) {
165            j2c::parse(data, settings)
166        } else {
167            err!(FormatError::InvalidSignature)
168        }
169    }
170
171    /// Whether the image has an alpha channel.
172    pub fn has_alpha(&self) -> bool {
173        self.has_alpha
174    }
175
176    /// The color space of the image.
177    pub fn color_space(&self) -> &ColorSpace {
178        &self.color_space
179    }
180
181    /// The width of the image.
182    pub fn width(&self) -> u32 {
183        self.header.size_data.image_width()
184    }
185
186    /// The height of the image.
187    pub fn height(&self) -> u32 {
188        self.header.size_data.image_height()
189    }
190
191    /// The original bit depth of the image. You usually don't need to do anything
192    /// with this parameter, it just exists for informational purposes.
193    pub fn original_bit_depth(&self) -> u8 {
194        // Note that this only works if all components have the same precision.
195        self.header.component_infos[0].size_info.precision
196    }
197
198    /// Decode the image and return its decoded components.
199    pub fn decode<'b>(
200        &'a self,
201        decoder_context: &'b mut DecoderContext<'a>,
202    ) -> Result<DecodedImage<'b>> {
203        let settings = &self.settings;
204        j2c::decode(self.codestream, &self.header, decoder_context)?;
205        let mut decoded_image = DecodedImage {
206            decoded_components: &mut decoder_context.channel_data,
207            boxes: self.boxes.clone(),
208        };
209
210        // Resolve palette indices.
211        if settings.resolve_palette_indices {
212            let components = core::mem::take(decoded_image.decoded_components);
213            *decoded_image.decoded_components =
214                resolve_palette_indices(components, &decoded_image.boxes)?;
215        }
216
217        if let Some(cdef) = &decoded_image.boxes.channel_definition {
218            // Sort by the channel association. Note that this will only work if
219            // each component is referenced only once.
220            let mut components = decoded_image
221                .decoded_components
222                .iter()
223                .cloned()
224                .zip(
225                    cdef.channel_definitions
226                        .iter()
227                        .map(|c| match c._association {
228                            ChannelAssociation::WholeImage => u16::MAX,
229                            ChannelAssociation::Colour(c) => c,
230                        }),
231                )
232                .collect::<Vec<_>>();
233            components.sort_by_key(|c1| c1.1);
234            *decoded_image.decoded_components = components.into_iter().map(|c| c.0).collect();
235        }
236
237        // Note that this is only valid if all images have the same bit depth.
238        let bit_depth = decoded_image.decoded_components[0].bit_depth;
239        convert_color_space(&mut decoded_image, bit_depth)?;
240
241        Ok(decoded_image)
242    }
243}
244
245pub(crate) fn resolve_alpha_and_color_space(
246    boxes: &ImageBoxes,
247    header: &Header<'_>,
248    settings: &DecodeSettings,
249) -> Result<(ColorSpace, bool)> {
250    let mut num_components = header.component_infos.len();
251
252    // Override number of components with what is actually in the palette box
253    // in case we resolve them.
254    if settings.resolve_palette_indices
255        && let Some(palette_box) = &boxes.palette
256    {
257        num_components = palette_box.columns.len();
258    }
259
260    let mut has_alpha = false;
261
262    if let Some(cdef) = &boxes.channel_definition {
263        let last = cdef.channel_definitions.last().unwrap();
264        has_alpha = last.channel_type == ChannelType::Opacity;
265    }
266
267    let mut color_space = get_color_space(boxes, num_components)?;
268
269    // If we didn't resolve palette indices, we need to assume grayscale image.
270    if !settings.resolve_palette_indices && boxes.palette.is_some() {
271        has_alpha = false;
272        color_space = ColorSpace::Gray;
273    }
274
275    let actual_num_components = header.component_infos.len();
276
277    // Validate the number of channels.
278    if boxes.palette.is_none()
279        && actual_num_components
280            != (color_space.num_channels() + if has_alpha { 1 } else { 0 }) as usize
281    {
282        if !settings.strict
283            && actual_num_components == color_space.num_channels() as usize + 1
284            && !has_alpha
285        {
286            // See OPENJPEG test case orb-blue10-lin-j2k. Assume that we have an
287            // alpha channel in this case.
288            has_alpha = true;
289        } else {
290            // Color space is invalid, attempt to repair.
291            if actual_num_components == 1 || (actual_num_components == 2 && has_alpha) {
292                color_space = ColorSpace::Gray;
293            } else if actual_num_components == 3 {
294                color_space = ColorSpace::RGB;
295            } else if actual_num_components == 4 {
296                if has_alpha {
297                    color_space = ColorSpace::RGB;
298                } else {
299                    color_space = ColorSpace::CMYK;
300                }
301            } else {
302                bail!(ValidationError::TooManyChannels);
303            }
304        }
305    }
306
307    Ok((color_space, has_alpha))
308}
309
310/// The color space of the image.
311#[derive(Debug, Clone)]
312pub enum ColorSpace {
313    /// A grayscale image.
314    Gray,
315    /// An RGB image.
316    RGB,
317    /// A CMYK image.
318    CMYK,
319    /// An unknown color space.
320    Unknown {
321        /// The number of channels of the color space.
322        num_channels: u8,
323    },
324    /// An image based on an ICC profile.
325    Icc {
326        /// The raw data of the ICC profile.
327        profile: Vec<u8>,
328        /// The number of channels used by the ICC profile.
329        num_channels: u8,
330    },
331}
332
333impl ColorSpace {
334    /// Return the number of expected channels for the color space.
335    pub fn num_channels(&self) -> u8 {
336        match self {
337            Self::Gray => 1,
338            Self::RGB => 3,
339            Self::CMYK => 4,
340            Self::Unknown { num_channels } => *num_channels,
341            Self::Icc {
342                num_channels: num_components,
343                ..
344            } => *num_components,
345        }
346    }
347}
348
349/// A bitmap storing the decoded result of the image.
350pub struct Bitmap {
351    /// The color space of the image.
352    pub color_space: ColorSpace,
353    /// The raw pixel data of the image. The result will always be in
354    /// 8-bit (in case the original image had a different bit-depth,
355    /// hayro-jpeg2000 always scales to 8-bit).
356    ///
357    /// The size is guaranteed to equal
358    /// `width * height * (num_channels + (if has_alpha { 1 } else { 0 })`.
359    /// Pixels are interleaved on a per-channel basis, the alpha channel always
360    /// appearing as the last channel, if available.
361    pub data: Vec<u8>,
362    /// Whether the image has an alpha channel.
363    pub has_alpha: bool,
364    /// The width of the image.
365    pub width: u32,
366    /// The height of the image.
367    pub height: u32,
368    /// The original bit depth of the image. You usually don't need to do anything
369    /// with this parameter, it just exists for informational purposes.
370    pub original_bit_depth: u8,
371}
372
373impl DecodedImage<'_> {
374    /// The decoded components of the image.
375    pub fn components(&self) -> &[ComponentData] {
376        self.decoded_components
377    }
378
379    /// Return the decoded image as interleaved unsigned 8-bit sample data.
380    pub fn data_u8(&self) -> Vec<u8> {
381        let components = self.components();
382        let buffer_size = components[0].samples().len() * components.len();
383        let mut buf = vec![0; buffer_size];
384        self.store_u8_into(&mut buf);
385        buf
386    }
387
388    /// Store the decoded image as interleaved unsigned 8-bit sample data into `buf`.
389    ///
390    /// The buffer must have the correct size.
391    pub fn store_u8_into(&self, buf: &mut [u8]) {
392        interleave_and_convert(self, buf);
393    }
394}
395
396fn interleave_and_convert(image: &DecodedImage<'_>, buf: &mut [u8]) {
397    let components = &*image.decoded_components;
398    let num_components = components.len();
399
400    let mut all_same_bit_depth = Some(components[0].bit_depth);
401
402    for component in components.iter().skip(1) {
403        if Some(component.bit_depth) != all_same_bit_depth {
404            all_same_bit_depth = None;
405        }
406    }
407
408    let max_len = components[0].container.truncated().len();
409
410    let mut output_iter = buf.iter_mut();
411
412    if all_same_bit_depth == Some(8) && num_components <= 4 {
413        // Fast path for the common case.
414        match num_components {
415            // Gray-scale.
416            1 => {
417                for (output, input) in output_iter.zip(
418                    components[0]
419                        .container
420                        .iter()
421                        .map(|v| math::round_f32(*v) as u8),
422                ) {
423                    *output = input;
424                }
425            }
426            // Gray-scale with alpha.
427            2 => {
428                let c0 = &components[0];
429                let c1 = &components[1];
430
431                let c0 = &c0.container[..max_len];
432                let c1 = &c1.container[..max_len];
433
434                for i in 0..max_len {
435                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
436                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
437                }
438            }
439            // RGB
440            3 => {
441                let c0 = &components[0];
442                let c1 = &components[1];
443                let c2 = &components[2];
444
445                let c0 = &c0.container[..max_len];
446                let c1 = &c1.container[..max_len];
447                let c2 = &c2.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                }
454            }
455            // RGBA or CMYK.
456            4 => {
457                let c0 = &components[0];
458                let c1 = &components[1];
459                let c2 = &components[2];
460                let c3 = &components[3];
461
462                let c0 = &c0.container[..max_len];
463                let c1 = &c1.container[..max_len];
464                let c2 = &c2.container[..max_len];
465                let c3 = &c3.container[..max_len];
466
467                for i in 0..max_len {
468                    *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
469                    *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
470                    *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
471                    *output_iter.next().unwrap() = math::round_f32(c3[i]) as u8;
472                }
473            }
474            _ => unreachable!(),
475        }
476    } else {
477        // Slow path that also requires us to scale to 8 bit.
478        let mul_factor = ((1 << 8) - 1) as f32;
479
480        for sample in 0..max_len {
481            for channel in components.iter() {
482                *output_iter.next().unwrap() = math::round_f32(
483                    (channel.container[sample] / ((1_u32 << channel.bit_depth) - 1) as f32)
484                        * mul_factor,
485                ) as u8;
486            }
487        }
488    }
489}
490
491fn convert_color_space(image: &mut DecodedImage<'_>, bit_depth: u8) -> Result<()> {
492    if let Some(jp2::colr::ColorSpace::Enumerated(e)) = &image
493        .boxes
494        .color_specification
495        .as_ref()
496        .map(|i| &i.color_space)
497    {
498        match e {
499            EnumeratedColorspace::Sycc => {
500                dispatch!(Level::new(), simd => {
501                    sycc_to_rgb(simd, image.decoded_components, bit_depth)
502                })?;
503            }
504            EnumeratedColorspace::CieLab(cielab) => {
505                dispatch!(Level::new(), simd => {
506                    cielab_to_rgb(simd, image.decoded_components, bit_depth, cielab)
507                })?;
508            }
509            EnumeratedColorspace::Ycck => {
510                // YCCK: channels 0-2 are YCbCr, channel 3 is K (black).
511                // Convert YCbCr → RGB using the same transform as SyCC, then
512                // invert channels 0-2 to obtain CMY (C = max−R, M = max−G, Y = max−B).
513                // K (channel 3) stays in standard JP2 convention (0 = no ink).
514                // After this transform all four channels are in DeviceCMYK convention.
515                dispatch!(Level::new(), simd => {
516                    sycc_to_rgb(simd, image.decoded_components, bit_depth)
517                })?;
518                // Invert YCbCr→RGB result into CMY: C = max−R, M = max−G, Y = max−B.
519                let max_val = ((1_u32 << bit_depth) - 1) as f32;
520                for comp in image.decoded_components.iter_mut().take(3) {
521                    for v in comp.container.iter_mut() {
522                        *v = max_val - *v;
523                    }
524                }
525            }
526            _ => {}
527        }
528    }
529
530    Ok(())
531}
532
533fn get_color_space(boxes: &ImageBoxes, num_components: usize) -> Result<ColorSpace> {
534    let cs = match boxes
535        .color_specification
536        .as_ref()
537        .map(|c| &c.color_space)
538        .unwrap_or(&jp2::colr::ColorSpace::Unknown)
539    {
540        jp2::colr::ColorSpace::Enumerated(e) => {
541            match e {
542                EnumeratedColorspace::Cmyk => ColorSpace::CMYK,
543                // YCCK: YCbCr + K channels.  The YCbCr channels are converted to
544                // RGB and then inverted to CMY in convert_color_space(); K is kept
545                // as-is.  The result is DeviceCMYK, so map to CMYK here.
546                EnumeratedColorspace::Ycck => ColorSpace::CMYK,
547                EnumeratedColorspace::Srgb => ColorSpace::RGB,
548                EnumeratedColorspace::RommRgb => {
549                    // Use an ICC profile to process the RommRGB color space.
550                    ColorSpace::Icc {
551                        profile: include_bytes!("../assets/ProPhoto-v2-micro.icc").to_vec(),
552                        num_channels: 3,
553                    }
554                }
555                EnumeratedColorspace::EsRgb => ColorSpace::RGB,
556                EnumeratedColorspace::Greyscale => ColorSpace::Gray,
557                EnumeratedColorspace::Sycc => ColorSpace::RGB,
558                EnumeratedColorspace::CieLab(_) => ColorSpace::Icc {
559                    profile: include_bytes!("../assets/LAB.icc").to_vec(),
560                    num_channels: 3,
561                },
562                _ => bail!(FormatError::Unsupported),
563            }
564        }
565        jp2::colr::ColorSpace::Icc(icc) => {
566            if let Some(metadata) = ICCMetadata::from_data(icc) {
567                ColorSpace::Icc {
568                    profile: icc.clone(),
569                    num_channels: metadata.color_space.num_components(),
570                }
571            } else {
572                // See OPENJPEG test orb-blue10-lin-jp2.jp2. They seem to
573                // assume RGB in this case (even though the image has 4
574                // components with no opacity channel, they assume RGBA instead
575                // of CMYK).
576                ColorSpace::RGB
577            }
578        }
579        jp2::colr::ColorSpace::Unknown => match num_components {
580            1 => ColorSpace::Gray,
581            3 => ColorSpace::RGB,
582            4 => ColorSpace::CMYK,
583            _ => ColorSpace::Unknown {
584                num_channels: num_components as u8,
585            },
586        },
587    };
588
589    Ok(cs)
590}
591
592fn resolve_palette_indices(
593    components: Vec<ComponentData>,
594    boxes: &ImageBoxes,
595) -> Result<Vec<ComponentData>> {
596    let Some(palette) = boxes.palette.as_ref() else {
597        // Nothing to resolve.
598        return Ok(components);
599    };
600
601    let mapping = boxes.component_mapping.as_ref().unwrap();
602    let mut resolved = Vec::with_capacity(mapping.entries.len());
603
604    for entry in &mapping.entries {
605        let component_idx = entry.component_index as usize;
606        let component = components
607            .get(component_idx)
608            .ok_or(ColorError::PaletteResolutionFailed)?;
609
610        match entry.mapping_type {
611            ComponentMappingType::Direct => resolved.push(component.clone()),
612            ComponentMappingType::Palette { column } => {
613                let column_idx = column as usize;
614                let column_info = palette
615                    .columns
616                    .get(column_idx)
617                    .ok_or(ColorError::PaletteResolutionFailed)?;
618
619                let mut mapped =
620                    Vec::with_capacity(component.container.truncated().len() + SIMD_WIDTH);
621
622                for &sample in component.container.truncated() {
623                    let index = math::round_f32(sample) as i64;
624                    let value = palette
625                        .map(index as usize, column_idx)
626                        .ok_or(ColorError::PaletteResolutionFailed)?;
627                    mapped.push(value as f32);
628                }
629
630                resolved.push(ComponentData {
631                    container: math::SimdBuffer::new(mapped),
632                    bit_depth: column_info.bit_depth,
633                });
634            }
635        }
636    }
637
638    Ok(resolved)
639}
640
641#[inline(always)]
642fn cielab_to_rgb<S: Simd>(
643    simd: S,
644    components: &mut [ComponentData],
645    bit_depth: u8,
646    lab: &CieLab,
647) -> Result<()> {
648    let (head, _) = components
649        .split_at_mut_checked(3)
650        .ok_or(ColorError::LabConversionFailed)?;
651
652    let [l, a, b] = head else {
653        unreachable!();
654    };
655
656    let prec0 = l.bit_depth;
657    let prec1 = a.bit_depth;
658    let prec2 = b.bit_depth;
659
660    // Prevent underflows/divisions by zero further below.
661    if prec0 < 4 || prec1 < 4 || prec2 < 4 {
662        bail!(ColorError::LabConversionFailed);
663    }
664
665    let rl = lab.rl.unwrap_or(100);
666    let ra = lab.ra.unwrap_or(170);
667    let rb = lab.rb.unwrap_or(200);
668    let ol = lab.ol.unwrap_or(0);
669    let oa = lab.oa.unwrap_or(1 << (bit_depth - 1));
670    let ob = lab
671        .ob
672        .unwrap_or((1 << (bit_depth - 2)) + (1 << (bit_depth - 3)));
673
674    // Copied from OpenJPEG.
675    let min_l = -(rl as f32 * ol as f32) / ((1 << prec0) - 1) as f32;
676    let max_l = min_l + rl as f32;
677    let min_a = -(ra as f32 * oa as f32) / ((1 << prec1) - 1) as f32;
678    let max_a = min_a + ra as f32;
679    let min_b = -(rb as f32 * ob as f32) / ((1 << prec2) - 1) as f32;
680    let max_b = min_b + rb as f32;
681
682    let bit_max = (1_u32 << bit_depth) - 1;
683
684    // Note that we are not doing the actual conversion with the ICC profile yet,
685    // just decoding the raw LAB values.
686    // We leave applying the ICC profile to the user.
687    let divisor_l = ((1 << prec0) - 1) as f32;
688    let divisor_a = ((1 << prec1) - 1) as f32;
689    let divisor_b = ((1 << prec2) - 1) as f32;
690
691    let scale_l_final = bit_max as f32 / 100.0;
692    let scale_ab_final = bit_max as f32 / 255.0;
693
694    let l_offset = min_l * scale_l_final;
695    let l_scale = (max_l - min_l) / divisor_l * scale_l_final;
696    let a_offset = (min_a + 128.0) * scale_ab_final;
697    let a_scale = (max_a - min_a) / divisor_a * scale_ab_final;
698    let b_offset = (min_b + 128.0) * scale_ab_final;
699    let b_scale = (max_b - min_b) / divisor_b * scale_ab_final;
700
701    let l_offset_v = f32x8::splat(simd, l_offset);
702    let l_scale_v = f32x8::splat(simd, l_scale);
703    let a_offset_v = f32x8::splat(simd, a_offset);
704    let a_scale_v = f32x8::splat(simd, a_scale);
705    let b_offset_v = f32x8::splat(simd, b_offset);
706    let b_scale_v = f32x8::splat(simd, b_scale);
707
708    // Note that we are not doing the actual conversion with the ICC profile yet,
709    // just decoding the raw LAB values.
710    // We leave applying the ICC profile to the user.
711    for ((l_chunk, a_chunk), b_chunk) in l
712        .container
713        .chunks_exact_mut(SIMD_WIDTH)
714        .zip(a.container.chunks_exact_mut(SIMD_WIDTH))
715        .zip(b.container.chunks_exact_mut(SIMD_WIDTH))
716    {
717        let l_v = f32x8::from_slice(simd, l_chunk);
718        let a_v = f32x8::from_slice(simd, a_chunk);
719        let b_v = f32x8::from_slice(simd, b_chunk);
720
721        l_v.mul_add(l_scale_v, l_offset_v).store(l_chunk);
722        a_v.mul_add(a_scale_v, a_offset_v).store(a_chunk);
723        b_v.mul_add(b_scale_v, b_offset_v).store(b_chunk);
724    }
725
726    Ok(())
727}
728
729#[inline(always)]
730fn sycc_to_rgb<S: Simd>(simd: S, components: &mut [ComponentData], bit_depth: u8) -> Result<()> {
731    let offset = (1_u32 << (bit_depth as u32 - 1)) as f32;
732    let max_value = ((1_u32 << bit_depth as u32) - 1) as f32;
733
734    let (head, _) = components
735        .split_at_mut_checked(3)
736        .ok_or(ColorError::SyccConversionFailed)?;
737
738    let [y, cb, cr] = head else {
739        unreachable!();
740    };
741
742    let offset_v = f32x8::splat(simd, offset);
743    let max_v = f32x8::splat(simd, max_value);
744    let zero_v = f32x8::splat(simd, 0.0);
745    let cr_to_r = f32x8::splat(simd, 1.402);
746    let cb_to_g = f32x8::splat(simd, -0.344136);
747    let cr_to_g = f32x8::splat(simd, -0.714136);
748    let cb_to_b = f32x8::splat(simd, 1.772);
749
750    for ((y_chunk, cb_chunk), cr_chunk) in y
751        .container
752        .chunks_exact_mut(SIMD_WIDTH)
753        .zip(cb.container.chunks_exact_mut(SIMD_WIDTH))
754        .zip(cr.container.chunks_exact_mut(SIMD_WIDTH))
755    {
756        let y_v = f32x8::from_slice(simd, y_chunk);
757        let cb_v = f32x8::from_slice(simd, cb_chunk) - offset_v;
758        let cr_v = f32x8::from_slice(simd, cr_chunk) - offset_v;
759
760        // r = y + 1.402 * cr
761        let r = cr_v.mul_add(cr_to_r, y_v);
762        // g = y - 0.344136 * cb - 0.714136 * cr
763        let g = cr_v.mul_add(cr_to_g, cb_v.mul_add(cb_to_g, y_v));
764        // b = y + 1.772 * cb
765        let b = cb_v.mul_add(cb_to_b, y_v);
766
767        r.min(max_v).max(zero_v).store(y_chunk);
768        g.min(max_v).max(zero_v).store(cb_chunk);
769        b.min(max_v).max(zero_v).store(cr_chunk);
770    }
771
772    Ok(())
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778
779    // Minimal valid JPEG 2000 raw codestream (J2C) for a 2×2 greyscale image.
780    //
781    // Layout:
782    //   SOC  (FF 4F)
783    //   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)
784    //   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)
785    //   QCD  (FF 5C): Lqcd=4, Sqcd=0 (NoQuantization), 1 step-size byte (0x80)
786    //   SOT  (FF 90): codestream tail starts here; not parsed by Image::new()
787    //
788    // After parsing the header, Image::new() stores the tail (&[0xFF, 0x90]) as
789    // codestream data. This constant covers header parsing only; append
790    // MINIMAL_J2C_TILE (see `full_j2c`) to run a decode as well.
791    #[rustfmt::skip]
792    const MINIMAL_J2C: &[u8] = &[
793        // SOC
794        0xFF, 0x4F,
795
796        // SIZ marker (FF 51) — Lsiz = 41 (includes itself, excludes marker)
797        0xFF, 0x51,
798        0x00, 0x29,              // Lsiz = 41
799        0x00, 0x00,              // Rsiz = 0 (no profile)
800        0x00, 0x00, 0x00, 0x02,  // Xsiz = 2
801        0x00, 0x00, 0x00, 0x02,  // Ysiz = 2
802        0x00, 0x00, 0x00, 0x00,  // XOsiz = 0
803        0x00, 0x00, 0x00, 0x00,  // YOsiz = 0
804        0x00, 0x00, 0x00, 0x02,  // XTsiz = 2 (tile covers whole image)
805        0x00, 0x00, 0x00, 0x02,  // YTsiz = 2
806        0x00, 0x00, 0x00, 0x00,  // XTOsiz = 0
807        0x00, 0x00, 0x00, 0x00,  // YTOsiz = 0
808        0x00, 0x01,              // Csiz = 1 component
809        0x07, 0x01, 0x01,        // Component 0: Ssiz=7 (8-bit unsigned), XRsiz=1, YRsiz=1
810
811        // COD marker (FF 52) — Lcod = 12
812        0xFF, 0x52,
813        0x00, 0x0C,              // Lcod = 12
814        0x00,                    // Scod = 0 (no precincts)
815        0x00,                    // progression order = 0 (LRCP)
816        0x00, 0x01,              // num_layers = 1
817        0x00,                    // MCT = 0 (no multi-component transform)
818        0x00,                    // num_decomposition_levels = 0
819        0x00,                    // code_block_width = 0 (+2 = 2)
820        0x00,                    // code_block_height = 0 (+2 = 2)
821        0x00,                    // code_block_style = 0
822        0x01,                    // transformation = 1 (reversible 5/3 wavelet)
823
824        // QCD marker (FF 5C) — Lqcd = 4
825        0xFF, 0x5C,
826        0x00, 0x04,              // Lqcd = 4 (includes itself + Sqcd + 1 step-size byte)
827        0x00,                    // Sqcd = 0 (NoQuantization, guard_bits=0)
828        0x80,                    // step-size[0]: exponent = 0x80 >> 3 = 16
829
830        // SOT (Start Of Tile) — reader.tail() returns from here
831        0xFF, 0x90,
832    ];
833
834    // `MINIMAL_J2C` plus a complete single tile-part, so the whole decode path
835    // runs. The tile carries one empty packet, which is legal: every code-block
836    // contributes nothing and all coefficients stay at zero.
837    //
838    //   SOT (FF 90): Lsot=10, Isot=0, Psot=15, TPsot=0, TNsot=1
839    //   SOD (FF 93)
840    //   packet header 0x00 — leading bit 0 means "zero-length packet"
841    //   EOC (FF D9)
842    #[rustfmt::skip]
843    const MINIMAL_J2C_TILE: &[u8] = &[
844        0x00, 0x0A,              // Lsot = 10
845        0x00, 0x00,              // Isot = 0 (first tile)
846        0x00, 0x00, 0x00, 0x0F,  // Psot = 15 (SOT marker .. end of tile-part data)
847        0x00,                    // TPsot = 0 (first tile-part)
848        0x01,                    // TNsot = 1 (one tile-part in total)
849
850        0xFF, 0x93,              // SOD
851
852        0x00,                    // one empty packet
853
854        0xFF, 0xD9,              // EOC
855    ];
856
857    fn full_j2c() -> Vec<u8> {
858        let mut data = MINIMAL_J2C.to_vec();
859        data.extend_from_slice(MINIMAL_J2C_TILE);
860        data
861    }
862
863    #[test]
864    fn new_minimal_j2c_succeeds() {
865        assert!(Image::new(MINIMAL_J2C, &DecodeSettings::default()).is_ok());
866    }
867
868    #[test]
869    fn new_minimal_j2c_dimensions() {
870        let image = Image::new(MINIMAL_J2C, &DecodeSettings::default()).expect("J2C should parse");
871        assert_eq!(image.width(), 2);
872        assert_eq!(image.height(), 2);
873    }
874
875    #[test]
876    fn new_minimal_j2c_is_greyscale() {
877        let image = Image::new(MINIMAL_J2C, &DecodeSettings::default()).expect("J2C should parse");
878        assert_eq!(image.color_space().num_channels(), 1);
879    }
880
881    #[test]
882    fn new_minimal_j2c_no_alpha() {
883        let image = Image::new(MINIMAL_J2C, &DecodeSettings::default()).expect("J2C should parse");
884        assert!(!image.has_alpha());
885    }
886
887    #[test]
888    fn new_invalid_signature_returns_error() {
889        assert!(Image::new(b"\x00\x00\x00\x00", &DecodeSettings::default()).is_err());
890    }
891
892    /// Return `MINIMAL_J2C` with the four SIZ dimension fields replaced.
893    ///
894    /// Byte offsets inside the SIZ segment: Xsiz@8, Ysiz@12, XTsiz@24, YTsiz@28.
895    fn j2c_with_grid(xsiz: u32, ysiz: u32, xtsiz: u32, ytsiz: u32) -> Vec<u8> {
896        let mut data = MINIMAL_J2C.to_vec();
897        for (offset, value) in [(8, xsiz), (12, ysiz), (24, xtsiz), (28, ytsiz)] {
898            data[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
899        }
900        data
901    }
902
903    /// The decode API round-trips through a caller-owned [`DecoderContext`]
904    /// since upstream 0.4.0. This is the shape both of our in-tree consumers
905    /// (`pdf-syntax::filter::jpx` and `pdfluent-lopdf::Object::decode_jpx`) use,
906    /// so a signature change upstream fails here first.
907    #[test]
908    fn decode_minimal_j2c_round_trip() {
909        let data = full_j2c();
910        let image = Image::new(&data, &DecodeSettings::default()).expect("J2C should parse");
911        let mut ctx = DecoderContext::default();
912        let decoded = image.decode(&mut ctx).expect("2x2 greyscale should decode");
913        assert_eq!(decoded.components().len(), 1);
914        assert_eq!(decoded.data_u8().len(), 2 * 2);
915    }
916
917    // Regression for upstream a7e3aace (#1355), which replaced the hardcoded
918    // 60000-pixel dimension cap with checked allocation arithmetic. `num_tiles()`
919    // used to multiply the two SIZ tile counts unchecked. A grid of u32::MAX by
920    // u32::MAX one-pixel tiles wraps that product; the wrapped value is also what
921    // the per-tile index is later validated against, so the failure is a wrong
922    // answer rather than a crash. Must be rejected as ImageTooLarge.
923    #[test]
924    fn siz_with_overflowing_tile_count_is_rejected() {
925        let data = j2c_with_grid(u32::MAX, u32::MAX, 1, 1);
926        match Image::new(&data, &DecodeSettings::default()) {
927            Err(DecodeError::Validation(ValidationError::ImageTooLarge)) => {}
928            Err(other) => panic!("expected ImageTooLarge, got {other:?}"),
929            Ok(_) => panic!("a u32::MAX x u32::MAX tile grid must not be accepted"),
930        }
931    }
932
933    // A tile grid that does not overflow must still be accepted, so the guard
934    // above cannot be satisfied by rejecting everything.
935    #[test]
936    fn siz_with_large_but_valid_tile_count_is_accepted() {
937        // 2^16 x 2^16 = 2^32 exactly? No: 65535 x 65535 = 4_294_836_225 < u32::MAX.
938        let data = j2c_with_grid(65535, 65535, 1, 1);
939        assert!(
940            Image::new(&data, &DecodeSettings::default()).is_ok(),
941            "65535x65535 one-pixel tiles fit in u32 and must be accepted"
942        );
943    }
944
945    // Regression for upstream ff7e2e40 (#1237): a caller-supplied target
946    // resolution of zero reached `image_width() / target_width` and divided by
947    // zero. Must return a header, not panic.
948    #[test]
949    fn zero_target_resolution_does_not_panic() {
950        let settings = DecodeSettings {
951            resolve_palette_indices: false,
952            strict: false,
953            target_resolution: Some((0, 0)),
954        };
955        let image = Image::new(MINIMAL_J2C, &settings).expect("J2C should parse");
956        assert_eq!(image.width(), 2);
957        assert_eq!(image.height(), 2);
958    }
959
960    fn lab_component(samples: &[f32], bit_depth: u8) -> ComponentData {
961        ComponentData {
962            container: math::SimdBuffer::new(samples.to_vec()),
963            bit_depth,
964        }
965    }
966
967    fn cielab_b_channel(lab: &CieLab) -> Vec<f32> {
968        let mut components = [
969            lab_component(&[10.0, 200.0, 128.0, 255.0], 8),
970            lab_component(&[10.0, 200.0, 128.0, 255.0], 8),
971            lab_component(&[10.0, 200.0, 128.0, 255.0], 8),
972        ];
973        dispatch!(Level::new(), simd => {
974            cielab_to_rgb(simd, &mut components, 8, lab)
975        })
976        .expect("LAB conversion should succeed for 8-bit components");
977        components[2].samples().to_vec()
978    }
979
980    // Regression for upstream c2df2014 (#1313): the b* range was read from
981    // `lab.ra` instead of `lab.rb`, so every JP2 carrying an explicit Rb decoded
982    // its b* channel against the a* range. One character, silently wrong colour.
983    //
984    // Two CieLab values that differ only in `rb` must produce different b*
985    // output. With the bug they are identical, because `rb` is never read.
986    #[test]
987    fn cielab_b_range_comes_from_rb_not_ra() {
988        let base = CieLab {
989            rl: Some(100),
990            ol: Some(0),
991            ra: Some(170),
992            oa: Some(128),
993            rb: Some(200),
994            ob: Some(96),
995        };
996        let widened = CieLab {
997            rb: Some(400),
998            ..base
999        };
1000
1001        let from_base = cielab_b_channel(&base);
1002        let from_widened = cielab_b_channel(&widened);
1003
1004        assert_ne!(
1005            from_base, from_widened,
1006            "changing rb must change the b* channel; if it does not, rb is being ignored"
1007        );
1008    }
1009
1010    // The a* channel must NOT move when only `rb` changes -- otherwise the test
1011    // above could pass on a mutation that simply swapped the two fields.
1012    #[test]
1013    fn cielab_a_range_is_unaffected_by_rb() {
1014        let base = CieLab {
1015            rl: Some(100),
1016            ol: Some(0),
1017            ra: Some(170),
1018            oa: Some(128),
1019            rb: Some(200),
1020            ob: Some(96),
1021        };
1022        let widened = CieLab {
1023            rb: Some(400),
1024            ..base
1025        };
1026
1027        let a_of = |lab: &CieLab| {
1028            let mut components = [
1029                lab_component(&[10.0, 200.0, 128.0, 255.0], 8),
1030                lab_component(&[10.0, 200.0, 128.0, 255.0], 8),
1031                lab_component(&[10.0, 200.0, 128.0, 255.0], 8),
1032            ];
1033            dispatch!(Level::new(), simd => {
1034                cielab_to_rgb(simd, &mut components, 8, lab)
1035            })
1036            .expect("LAB conversion should succeed for 8-bit components");
1037            components[1].samples().to_vec()
1038        };
1039
1040        assert_eq!(a_of(&base), a_of(&widened));
1041    }
1042}