1use alloc::vec::Vec;
4
5use crate::error::bail;
6use crate::j2c::{ComponentData, Header};
7use crate::jp2::cdef::ChannelType;
8use crate::jp2::colr::{CieLab, EnumeratedColorspace};
9use crate::jp2::icc::ICCMetadata;
10use crate::jp2::{self, DecodedImage, ImageBoxes};
11use crate::math::{self, dispatch, f32x8, Level, Simd, SIMD_WIDTH};
12use crate::{
13 checked_decode_sample_count, try_reserve_decode_elements, ColorError, DecodeSettings,
14 DecodingError, FormatError, Result, ValidationError, DEFAULT_MAX_DECODE_BYTES,
15};
16
17mod allocation;
18#[cfg(test)]
19mod boundary_tests;
20mod postprocess;
21pub(crate) use postprocess::{resolve_palette_indices, validate_and_reorder_channels};
22
23pub(crate) fn resolve_alpha_and_color_space(
24 boxes: &ImageBoxes,
25 header: &Header<'_>,
26 settings: &DecodeSettings,
27 retained_baseline_bytes: usize,
28) -> Result<(ColorSpace, bool, bool)> {
29 let mut num_components = header.component_infos.len();
30
31 if settings.resolve_palette_indices {
34 if let Some(palette_box) = &boxes.palette {
35 num_components = palette_box.columns.len();
36 }
37 }
38
39 let mut has_alpha = false;
40
41 if let Some(cdef) = &boxes.channel_definition {
42 has_alpha = cdef.channel_definitions.iter().any(|definition| {
43 matches!(
44 definition.channel_type,
45 ChannelType::Opacity | ChannelType::PremultipliedOpacity
46 )
47 });
48 }
49
50 let mut color_space = if !settings.resolve_palette_indices && boxes.palette.is_some() {
53 has_alpha = false;
54 ColorSpace::Gray
55 } else {
56 let retained_container_bytes =
57 crate::image::retained_container_metadata_bytes(header, boxes)?
58 .checked_add(retained_baseline_bytes)
59 .ok_or(ValidationError::ImageTooLarge)?;
60 if retained_container_bytes > DEFAULT_MAX_DECODE_BYTES {
61 return Err(ValidationError::ImageTooLarge.into());
62 }
63 get_color_space(boxes, num_components, retained_container_bytes)?
64 };
65
66 let actual_num_components = header.component_infos.len();
67 let mut used_lenient_metadata_recovery = false;
68
69 if boxes.palette.is_none()
71 && actual_num_components != usize::from(color_space.num_channels() + u16::from(has_alpha))
72 {
73 if !settings.strict
74 && actual_num_components == usize::from(color_space.num_channels()) + 1
75 && !has_alpha
76 {
77 has_alpha = true;
80 used_lenient_metadata_recovery = true;
81 } else {
82 if actual_num_components == 1 || (actual_num_components == 2 && has_alpha) {
84 color_space = ColorSpace::Gray;
85 } else if actual_num_components == 3 {
86 color_space = ColorSpace::RGB;
87 } else if actual_num_components == 4 {
88 if has_alpha {
89 color_space = ColorSpace::RGB;
90 } else {
91 color_space = ColorSpace::CMYK;
92 }
93 } else {
94 color_space = ColorSpace::Unknown {
95 num_channels: u16::try_from(actual_num_components)
96 .map_err(|_| ValidationError::TooManyChannels)?,
97 };
98 }
99 }
100 }
101
102 Ok((color_space, has_alpha, used_lenient_metadata_recovery))
103}
104
105#[derive(Debug)]
107pub enum ColorSpace {
108 Gray,
110 RGB,
112 CMYK,
114 Unknown {
116 num_channels: u16,
118 },
119 Icc {
121 profile: Vec<u8>,
123 num_channels: u16,
125 },
126}
127
128impl ColorSpace {
129 #[must_use]
131 pub fn num_channels(&self) -> u16 {
132 match self {
133 Self::Gray => 1,
134 Self::RGB => 3,
135 Self::CMYK => 4,
136 Self::Unknown { num_channels } => *num_channels,
137 Self::Icc {
138 num_channels: num_components,
139 ..
140 } => *num_components,
141 }
142 }
143}
144
145pub struct Bitmap {
147 pub color_space: ColorSpace,
149 pub data: Vec<u8>,
158 pub has_alpha: bool,
160 pub width: u32,
162 pub height: u32,
164 pub original_bit_depth: u8,
167}
168
169pub struct RawBitmap {
178 pub data: Vec<u8>,
180 pub width: u32,
182 pub height: u32,
184 pub bit_depth: u8,
186 pub signed: bool,
191 pub component_signed: Vec<bool>,
193 pub num_components: u16,
195 pub bytes_per_sample: u8,
197}
198
199pub struct NativeComponentPlane {
201 pub(crate) data: Vec<u8>,
202 pub(crate) dimensions: (u32, u32),
203 pub(crate) bit_depth: u8,
204 pub(crate) signed: bool,
205 pub(crate) sampling: (u8, u8),
206 pub(crate) bytes_per_sample: u8,
207}
208
209#[doc(hidden)]
211pub type NativeComponentPlaneParts = (Vec<u8>, (u32, u32), u8, bool, (u8, u8), u8);
212
213impl NativeComponentPlane {
214 #[must_use]
216 pub fn data(&self) -> &[u8] {
217 &self.data
218 }
219
220 crate::__j2k_component_plane_metadata_accessors!();
221
222 #[must_use]
224 pub fn bytes_per_sample(&self) -> u8 {
225 self.bytes_per_sample
226 }
227
228 #[doc(hidden)]
230 #[must_use]
231 pub fn allocated_bytes(&self) -> usize {
232 self.data.capacity()
233 }
234
235 #[doc(hidden)]
237 #[must_use]
238 pub fn into_parts(self) -> NativeComponentPlaneParts {
239 (
240 self.data,
241 self.dimensions,
242 self.bit_depth,
243 self.signed,
244 self.sampling,
245 self.bytes_per_sample,
246 )
247 }
248}
249
250pub struct DecodedNativeComponents {
252 pub(crate) dimensions: (u32, u32),
253 pub(crate) color_space: ColorSpace,
254 pub(crate) has_alpha: bool,
255 pub(crate) planes: Vec<NativeComponentPlane>,
256}
257
258impl DecodedNativeComponents {
259 #[must_use]
261 pub fn dimensions(&self) -> (u32, u32) {
262 self.dimensions
263 }
264
265 #[must_use]
267 pub fn color_space(&self) -> &ColorSpace {
268 &self.color_space
269 }
270
271 #[must_use]
273 pub fn has_alpha(&self) -> bool {
274 self.has_alpha
275 }
276
277 #[must_use]
279 pub fn planes(&self) -> &[NativeComponentPlane] {
280 &self.planes
281 }
282
283 #[doc(hidden)]
285 #[must_use]
286 pub fn allocated_bytes(&self) -> Option<usize> {
287 let mut bytes = self
288 .planes
289 .capacity()
290 .checked_mul(core::mem::size_of::<NativeComponentPlane>())?;
291 for plane in &self.planes {
292 bytes = bytes.checked_add(plane.allocated_bytes())?;
293 }
294 if let ColorSpace::Icc { profile, .. } = &self.color_space {
295 bytes = bytes.checked_add(profile.capacity())?;
296 }
297 Some(bytes)
298 }
299
300 #[doc(hidden)]
302 #[must_use]
303 pub fn into_parts(self) -> ((u32, u32), ColorSpace, bool, Vec<NativeComponentPlane>) {
304 (
305 self.dimensions,
306 self.color_space,
307 self.has_alpha,
308 self.planes,
309 )
310 }
311}
312
313pub struct ComponentPlane<'a> {
315 pub(crate) samples: &'a [f32],
316 pub(crate) dimensions: (u32, u32),
317 pub(crate) bit_depth: u8,
318 pub(crate) signed: bool,
319 pub(crate) sampling: (u8, u8),
320}
321
322#[doc(hidden)]
324pub type ComponentPlaneParts<'a> = (&'a [f32], (u32, u32), u8, bool, (u8, u8));
325
326impl<'a> ComponentPlane<'a> {
327 #[must_use]
329 pub fn samples(&self) -> &'a [f32] {
330 self.samples
331 }
332
333 crate::__j2k_component_plane_metadata_accessors!();
334
335 #[doc(hidden)]
337 #[must_use]
338 pub fn into_parts(self) -> ComponentPlaneParts<'a> {
339 (
340 self.samples,
341 self.dimensions,
342 self.bit_depth,
343 self.signed,
344 self.sampling,
345 )
346 }
347}
348
349pub struct DecodedComponents<'a> {
351 pub(crate) dimensions: (u32, u32),
352 pub(crate) color_space: ColorSpace,
353 pub(crate) has_alpha: bool,
354 pub(crate) planes: Vec<ComponentPlane<'a>>,
355 pub(crate) live_bytes: usize,
356}
357
358impl<'a> DecodedComponents<'a> {
359 #[must_use]
361 pub fn dimensions(&self) -> (u32, u32) {
362 self.dimensions
363 }
364
365 #[must_use]
367 pub fn color_space(&self) -> &ColorSpace {
368 &self.color_space
369 }
370
371 #[must_use]
373 pub fn has_alpha(&self) -> bool {
374 self.has_alpha
375 }
376
377 #[must_use]
379 pub fn planes(&self) -> &[ComponentPlane<'a>] {
380 &self.planes
381 }
382
383 #[doc(hidden)]
389 #[must_use]
390 pub fn live_bytes(&self) -> usize {
391 self.live_bytes
392 }
393
394 #[doc(hidden)]
396 #[must_use]
397 pub fn into_parts(self) -> ((u32, u32), ColorSpace, bool, Vec<ComponentPlane<'a>>) {
398 (
399 self.dimensions,
400 self.color_space,
401 self.has_alpha,
402 self.planes,
403 )
404 }
405}
406
407pub(crate) fn validate_interleaved_output_buffer(
408 image: &DecodedImage<'_, '_>,
409 buf: &[u8],
410) -> Result<()> {
411 let required_len = interleaved_output_len(image)?;
412 if buf.len() < required_len {
413 bail!(DecodingError::OutputBufferTooSmall);
414 }
415 Ok(())
416}
417
418fn interleaved_output_len(image: &DecodedImage<'_, '_>) -> Result<usize> {
419 let Some(first) = image.decoded_components.first() else {
420 bail!(DecodingError::CodeBlockDecodeFailure);
421 };
422 first
423 .container
424 .truncated()
425 .len()
426 .checked_mul(image.decoded_components.len())
427 .ok_or(ValidationError::ImageTooLarge.into())
428}
429
430#[expect(
431 clippy::cast_possible_truncation,
432 clippy::cast_sign_loss,
433 clippy::cast_precision_loss,
434 reason = "pixel samples are rounded and intentionally quantized to the stable 8-bit output format"
435)]
436pub(crate) fn interleave_and_convert(
437 image: &mut DecodedImage<'_, '_>,
438 buf: &mut [u8],
439) -> Result<()> {
440 let components = &mut *image.decoded_components;
441 let num_components = components.len();
442
443 let mut all_same_bit_depth = Some(components[0].bit_depth);
444
445 for component in components.iter().skip(1) {
446 if Some(component.bit_depth) != all_same_bit_depth {
447 all_same_bit_depth = None;
448 }
449 }
450
451 let max_len = components[0].container.truncated().len();
452
453 let mut output_iter = buf.iter_mut();
454
455 if all_same_bit_depth == Some(8) && num_components <= 4 {
456 match num_components {
458 1 => {
460 for (output, input) in output_iter.zip(
461 components[0]
462 .container
463 .iter()
464 .map(|v| math::round_f32(*v) as u8),
465 ) {
466 *output = input;
467 }
468 }
469 2 => {
471 let c0 = &components[0];
472 let c1 = &components[1];
473
474 let c0 = &c0.container[..max_len];
475 let c1 = &c1.container[..max_len];
476
477 for i in 0..max_len {
478 *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
479 *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
480 }
481 }
482 3 => {
484 let c0 = &components[0];
485 let c1 = &components[1];
486 let c2 = &components[2];
487
488 let c0 = &c0.container[..max_len];
489 let c1 = &c1.container[..max_len];
490 let c2 = &c2.container[..max_len];
491
492 for i in 0..max_len {
493 *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
494 *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
495 *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
496 }
497 }
498 4 => {
500 let c0 = &components[0];
501 let c1 = &components[1];
502 let c2 = &components[2];
503 let c3 = &components[3];
504
505 let c0 = &c0.container[..max_len];
506 let c1 = &c1.container[..max_len];
507 let c2 = &c2.container[..max_len];
508 let c3 = &c3.container[..max_len];
509
510 for i in 0..max_len {
511 *output_iter.next().unwrap() = math::round_f32(c0[i]) as u8;
512 *output_iter.next().unwrap() = math::round_f32(c1[i]) as u8;
513 *output_iter.next().unwrap() = math::round_f32(c2[i]) as u8;
514 *output_iter.next().unwrap() = math::round_f32(c3[i]) as u8;
515 }
516 }
517 _ => bail!(ValidationError::TooManyChannels),
518 }
519 } else {
520 let mul_factor = ((1 << 8) - 1) as f32;
522
523 for sample in 0..max_len {
524 for channel in components.iter() {
525 *output_iter.next().unwrap() = math::round_f32(
526 (channel.container[sample]
527 / ((1_u64 << u32::from(channel.bit_depth)) - 1) as f32)
528 * mul_factor,
529 ) as u8;
530 }
531 }
532 }
533
534 Ok(())
535}
536
537#[expect(
538 clippy::cast_possible_truncation,
539 clippy::cast_sign_loss,
540 clippy::cast_precision_loss,
541 reason = "region samples use the same stable rounded 8-bit quantization as full-image decode"
542)]
543pub(crate) fn interleave_and_convert_region(
544 image: &mut DecodedImage<'_, '_>,
545 image_width: usize,
546 roi: (u32, u32, u32, u32),
547 buf: &mut [u8],
548) {
549 let components = &mut *image.decoded_components;
550 let num_components = components.len();
551 let (x, y, width, height) = roi;
552 let mut output_iter = buf.iter_mut();
553
554 let mut all_same_bit_depth = Some(components[0].bit_depth);
555 for component in components.iter().skip(1) {
556 if Some(component.bit_depth) != all_same_bit_depth {
557 all_same_bit_depth = None;
558 }
559 }
560
561 if all_same_bit_depth == Some(8) && num_components <= 4 {
562 for row in y as usize..(y + height) as usize {
563 let row_base = row * image_width;
564 for col in x as usize..(x + width) as usize {
565 let idx = row_base + col;
566 for component in components.iter() {
567 *output_iter.next().unwrap() = math::round_f32(component.container[idx]) as u8;
568 }
569 }
570 }
571 } else {
572 let mul_factor = ((1 << 8) - 1) as f32;
573 for row in y as usize..(y + height) as usize {
574 let row_base = row * image_width;
575 for col in x as usize..(x + width) as usize {
576 let idx = row_base + col;
577 for component in components.iter() {
578 *output_iter.next().unwrap() = math::round_f32(
579 (component.container[idx]
580 / ((1_u64 << u32::from(component.bit_depth)) - 1) as f32)
581 * mul_factor,
582 ) as u8;
583 }
584 }
585 }
586 }
587}
588
589pub(crate) fn native_component_plane_dimensions(
590 reference_dimensions: (u32, u32),
591 sampling: (u8, u8),
592 sample_count: usize,
593) -> Result<(u32, u32)> {
594 let reference_sample_count =
595 checked_decode_sample_count(reference_dimensions.0, reference_dimensions.1)?;
596 if sample_count == reference_sample_count {
597 return Ok(reference_dimensions);
598 }
599
600 let (x_rsiz, y_rsiz) = sampling;
601 if x_rsiz == 0 || y_rsiz == 0 {
602 bail!(DecodingError::CodeBlockDecodeFailure);
603 }
604 let sampled_dimensions = (
605 reference_dimensions.0.div_ceil(u32::from(x_rsiz)),
606 reference_dimensions.1.div_ceil(u32::from(y_rsiz)),
607 );
608 let sampled_sample_count =
609 checked_decode_sample_count(sampled_dimensions.0, sampled_dimensions.1)?;
610 if sample_count == sampled_sample_count {
611 return Ok(sampled_dimensions);
612 }
613
614 bail!(DecodingError::CodeBlockDecodeFailure)
615}
616
617pub(crate) fn convert_color_space(image: &mut DecodedImage<'_, '_>, bit_depth: u8) -> Result<()> {
618 if let Some(jp2::colr::ColorSpace::Enumerated(e)) = &image
619 .boxes
620 .primary_color_specification()
621 .map(|i| &i.color_space)
622 {
623 match e {
624 EnumeratedColorspace::Sycc => {
625 dispatch!(Level::new(), simd => {
626 sycc_to_rgb(simd, image.decoded_components, bit_depth)
627 })?;
628 }
629 EnumeratedColorspace::CieLab(cielab) => {
630 dispatch!(Level::new(), simd => {
631 cielab_to_rgb(simd, image.decoded_components, bit_depth, cielab)
632 })?;
633 }
634 _ => {}
635 }
636 }
637
638 Ok(())
639}
640
641fn get_color_space(
642 boxes: &ImageBoxes,
643 num_components: usize,
644 retained_container_bytes: usize,
645) -> Result<ColorSpace> {
646 let cs = match boxes
647 .primary_color_specification()
648 .map_or(&jp2::colr::ColorSpace::Unknown, |specification| {
649 &specification.color_space
650 }) {
651 jp2::colr::ColorSpace::Enumerated(e) => {
652 match e {
653 EnumeratedColorspace::Cmyk => ColorSpace::CMYK,
654 EnumeratedColorspace::Srgb
655 | EnumeratedColorspace::EsRgb
656 | EnumeratedColorspace::Sycc => ColorSpace::RGB,
657 EnumeratedColorspace::RommRgb => {
658 ColorSpace::Icc {
660 profile: try_clone_color_profile(
661 include_bytes!("../assets/ProPhoto-v2-micro.icc"),
662 retained_container_bytes,
663 )?,
664 num_channels: 3,
665 }
666 }
667 EnumeratedColorspace::Greyscale => ColorSpace::Gray,
668 EnumeratedColorspace::CieLab(_) => ColorSpace::Icc {
669 profile: try_clone_color_profile(
670 include_bytes!("../assets/LAB.icc"),
671 retained_container_bytes,
672 )?,
673 num_channels: 3,
674 },
675 _ => bail!(FormatError::Unsupported),
676 }
677 }
678 jp2::colr::ColorSpace::Icc(icc) => {
679 if let Some(metadata) = ICCMetadata::from_data(icc) {
680 ColorSpace::Icc {
681 profile: try_clone_color_profile(icc, retained_container_bytes)?,
682 num_channels: u16::from(metadata.color_space.num_components()),
683 }
684 } else {
685 ColorSpace::RGB
690 }
691 }
692 jp2::colr::ColorSpace::Unknown => match num_components {
693 1 => ColorSpace::Gray,
694 3 => ColorSpace::RGB,
695 4 => ColorSpace::CMYK,
696 _ => ColorSpace::Unknown {
697 num_channels: u16::try_from(num_components).unwrap_or(u16::MAX),
698 },
699 },
700 };
701
702 Ok(cs)
703}
704
705fn try_clone_color_profile(profile: &[u8], retained_bytes: usize) -> Result<Vec<u8>> {
706 checked_color_profile_peak(retained_bytes, profile.len(), DEFAULT_MAX_DECODE_BYTES)?;
707 let mut cloned = Vec::new();
708 try_reserve_decode_elements(&mut cloned, profile.len())?;
709 checked_color_profile_peak(retained_bytes, cloned.capacity(), DEFAULT_MAX_DECODE_BYTES)?;
710 cloned.extend_from_slice(profile);
711 Ok(cloned)
712}
713
714fn checked_color_profile_peak(
715 retained_bytes: usize,
716 profile_bytes: usize,
717 cap: usize,
718) -> Result<usize> {
719 let peak = retained_bytes
720 .checked_add(profile_bytes)
721 .ok_or(ValidationError::ImageTooLarge)?;
722 if peak > cap {
723 return Err(ValidationError::ImageTooLarge.into());
724 }
725 Ok(peak)
726}
727
728#[expect(
729 clippy::cast_possible_truncation,
730 reason = "Rust's saturating float-to-integer conversion is retained before rejecting negative indices"
731)]
732fn palette_index(sample: f32) -> Result<usize> {
733 let rounded = math::round_f32(sample) as i64;
734 usize::try_from(rounded).map_err(|_| ColorError::PaletteResolutionFailed.into())
735}
736
737fn sign_extend_palette_value(raw: u64, bit_depth: u8) -> i64 {
738 if bit_depth == 0 {
739 return raw.cast_signed();
740 }
741 if bit_depth >= 64 {
742 return raw.cast_signed();
743 }
744
745 let mask = (1_u64 << bit_depth) - 1;
746 let value = raw & mask;
747 let shift = 64 - u32::from(bit_depth);
748 (value << shift).cast_signed() >> shift
749}
750
751fn clamped_power_of_two_u32(exponent: u8) -> u32 {
752 if u32::from(exponent) >= u32::BITS {
753 u32::MAX
754 } else {
755 1_u32 << exponent
756 }
757}
758
759fn clamped_add_u32(left: u32, right: u32) -> u32 {
760 if right > u32::MAX - left {
761 u32::MAX
762 } else {
763 left + right
764 }
765}
766
767fn max_value_for_bit_depth(bit_depth: u8) -> u32 {
768 if u32::from(bit_depth) >= u32::BITS {
769 u32::MAX
770 } else {
771 (1_u32 << bit_depth) - 1
772 }
773}
774
775#[expect(
776 clippy::cast_precision_loss,
777 reason = "OpenJPEG-compatible CIE Lab scaling intentionally uses f32 arithmetic"
778)]
779#[inline]
780pub(crate) fn cielab_to_rgb<S: Simd>(
781 simd: S,
782 components: &mut [ComponentData],
783 bit_depth: u8,
784 lab: &CieLab,
785) -> Result<()> {
786 let (head, _) = components
787 .split_at_mut_checked(3)
788 .ok_or(ColorError::LabConversionFailed)?;
789
790 let [l, a, b] = head else {
791 bail!(ColorError::LabConversionFailed);
792 };
793
794 let prec0 = l.bit_depth;
795 let prec1 = a.bit_depth;
796 let prec2 = b.bit_depth;
797
798 if prec0 < 4 || prec1 < 4 || prec2 < 4 {
800 bail!(ColorError::LabConversionFailed);
801 }
802
803 let rl = lab.rl.unwrap_or(100);
804 let ra = lab.ra.unwrap_or(170);
805 let rb = lab.rb.unwrap_or(200);
806 let ol = lab.ol.unwrap_or(0);
807 let a_shift = bit_depth
808 .checked_sub(1)
809 .ok_or(ColorError::LabConversionFailed)?;
810 let b_high_shift = bit_depth
811 .checked_sub(2)
812 .ok_or(ColorError::LabConversionFailed)?;
813 let b_low_shift = bit_depth
814 .checked_sub(3)
815 .ok_or(ColorError::LabConversionFailed)?;
816 let default_a_offset = clamped_power_of_two_u32(a_shift);
817 let default_b_offset = clamped_add_u32(
818 clamped_power_of_two_u32(b_high_shift),
819 clamped_power_of_two_u32(b_low_shift),
820 );
821 let oa = lab.oa.unwrap_or(default_a_offset);
822 let ob = lab.ob.unwrap_or(default_b_offset);
823
824 let min_l = -(rl as f32 * ol as f32) / ((1_u64 << u32::from(prec0)) - 1) as f32;
826 let max_l = min_l + rl as f32;
827 let min_a = -(ra as f32 * oa as f32) / ((1_u64 << u32::from(prec1)) - 1) as f32;
828 let max_a = min_a + ra as f32;
829 let min_b = -(rb as f32 * ob as f32) / ((1_u64 << u32::from(prec2)) - 1) as f32;
830 let max_b = min_b + rb as f32;
831
832 let bit_max = max_value_for_bit_depth(bit_depth);
833
834 let divisor_l = ((1_u64 << u32::from(prec0)) - 1) as f32;
838 let divisor_a = ((1_u64 << u32::from(prec1)) - 1) as f32;
839 let divisor_b = ((1_u64 << u32::from(prec2)) - 1) as f32;
840
841 let scale_l_final = bit_max as f32 / 100.0;
842 let scale_ab_final = bit_max as f32 / 255.0;
843
844 let l_offset = min_l * scale_l_final;
845 let l_scale = (max_l - min_l) / divisor_l * scale_l_final;
846 let a_offset = (min_a + 128.0) * scale_ab_final;
847 let a_scale = (max_a - min_a) / divisor_a * scale_ab_final;
848 let b_offset = (min_b + 128.0) * scale_ab_final;
849 let b_scale = (max_b - min_b) / divisor_b * scale_ab_final;
850
851 let l_offset_v = f32x8::splat(simd, l_offset);
852 let l_scale_v = f32x8::splat(simd, l_scale);
853 let a_offset_v = f32x8::splat(simd, a_offset);
854 let a_scale_v = f32x8::splat(simd, a_scale);
855 let b_offset_v = f32x8::splat(simd, b_offset);
856 let b_scale_v = f32x8::splat(simd, b_scale);
857
858 for ((l_chunk, a_chunk), b_chunk) in l
862 .container
863 .chunks_exact_mut(SIMD_WIDTH)
864 .zip(a.container.chunks_exact_mut(SIMD_WIDTH))
865 .zip(b.container.chunks_exact_mut(SIMD_WIDTH))
866 {
867 let l_v = f32x8::from_slice(simd, l_chunk);
868 let a_v = f32x8::from_slice(simd, a_chunk);
869 let b_v = f32x8::from_slice(simd, b_chunk);
870
871 l_v.mul_add(l_scale_v, l_offset_v).store(l_chunk);
872 a_v.mul_add(a_scale_v, a_offset_v).store(a_chunk);
873 b_v.mul_add(b_scale_v, b_offset_v).store(b_chunk);
874 }
875
876 l.integer_container = None;
880 a.integer_container = None;
881 b.integer_container = None;
882
883 Ok(())
884}
885
886#[expect(
887 clippy::cast_precision_loss,
888 reason = "JPEG 2000 sYCC conversion intentionally uses f32 SIMD arithmetic"
889)]
890#[inline]
891fn sycc_to_rgb<S: Simd>(simd: S, components: &mut [ComponentData], bit_depth: u8) -> Result<()> {
892 let offset = (1_u64 << (u32::from(bit_depth) - 1)) as f32;
893 let max_value = ((1_u64 << u32::from(bit_depth)) - 1) as f32;
894
895 let (head, _) = components
896 .split_at_mut_checked(3)
897 .ok_or(ColorError::SyccConversionFailed)?;
898
899 let [luma, blue_chroma, red_chroma] = head else {
900 bail!(ColorError::SyccConversionFailed);
901 };
902
903 let offset_v = f32x8::splat(simd, offset);
904 let max_v = f32x8::splat(simd, max_value);
905 let zero_v = f32x8::splat(simd, 0.0);
906 let red_chroma_to_red = f32x8::splat(simd, 1.402);
907 let blue_chroma_to_green = f32x8::splat(simd, -0.344_136);
908 let red_chroma_to_green = f32x8::splat(simd, -0.714_136);
909 let blue_chroma_to_blue = f32x8::splat(simd, 1.772);
910
911 for ((luma_chunk, blue_chroma_chunk), red_chroma_chunk) in luma
912 .container
913 .chunks_exact_mut(SIMD_WIDTH)
914 .zip(blue_chroma.container.chunks_exact_mut(SIMD_WIDTH))
915 .zip(red_chroma.container.chunks_exact_mut(SIMD_WIDTH))
916 {
917 let luma_values = f32x8::from_slice(simd, luma_chunk);
918 let blue_chroma_values = f32x8::from_slice(simd, blue_chroma_chunk) - offset_v;
919 let red_chroma_values = f32x8::from_slice(simd, red_chroma_chunk) - offset_v;
920
921 let red = red_chroma_values.mul_add(red_chroma_to_red, luma_values);
923 let green = red_chroma_values.mul_add(
925 red_chroma_to_green,
926 blue_chroma_values.mul_add(blue_chroma_to_green, luma_values),
927 );
928 let blue = blue_chroma_values.mul_add(blue_chroma_to_blue, luma_values);
930
931 red.min(max_v).max(zero_v).store(luma_chunk);
932 green.min(max_v).max(zero_v).store(blue_chroma_chunk);
933 blue.min(max_v).max(zero_v).store(red_chroma_chunk);
934 }
935
936 luma.integer_container = None;
937 blue_chroma.integer_container = None;
938 red_chroma.integer_container = None;
939
940 Ok(())
941}
942
943#[cfg(test)]
944mod tests {
945 use super::{
946 checked_color_profile_peak, clamped_add_u32, clamped_power_of_two_u32,
947 max_value_for_bit_depth, palette_index, sycc_to_rgb, ColorSpace, ComponentPlane,
948 DecodedComponents, DecodedNativeComponents, NativeComponentPlane,
949 };
950 use crate::j2c::ComponentData;
951 use crate::math::{dispatch, Level, SimdBuffer, SIMD_WIDTH};
952 use alloc::{vec, vec::Vec};
953 use core::mem::size_of;
954
955 #[test]
956 fn lab_integer_scaling_preserves_clamped_boundaries() {
957 assert_eq!(clamped_power_of_two_u32(31), 1_u32 << 31);
958 assert_eq!(clamped_power_of_two_u32(32), u32::MAX);
959 assert_eq!(clamped_add_u32(u32::MAX, 1), u32::MAX);
960 assert_eq!(max_value_for_bit_depth(31), (1_u32 << 31) - 1);
961 assert_eq!(max_value_for_bit_depth(32), u32::MAX);
962 }
963
964 #[test]
965 fn sycc_conversion_discards_pretransform_integer_shadows() {
966 let component = |value: u8| ComponentData {
967 container: SimdBuffer::<SIMD_WIDTH>::new(vec![f32::from(value); SIMD_WIDTH]),
968 integer_container: Some(vec![i64::from(value); SIMD_WIDTH]),
969 bit_depth: 8,
970 signed: false,
971 };
972 let mut components = vec![component(128), component(128), component(128)];
973
974 dispatch!(Level::new(), simd => sycc_to_rgb(simd, &mut components, 8))
975 .expect("sYCC conversion");
976
977 assert!(
978 components
979 .iter()
980 .all(|component| component.integer_container.is_none()),
981 "native packing must not reuse pre-transform exact samples"
982 );
983 }
984
985 #[test]
986 fn retained_color_profile_peak_accepts_exact_cap_and_rejects_one_over() {
987 assert_eq!(
988 checked_color_profile_peak(7, 5, 12).expect("exact ICC clone peak"),
989 12
990 );
991 assert!(checked_color_profile_peak(8, 5, 12).is_err());
992 }
993
994 #[test]
995 fn palette_indices_reject_negative_samples_without_wrapping() {
996 assert!(palette_index(-1.0).is_err());
997 assert_eq!(palette_index(2.4).expect("valid palette index"), 2);
998 }
999
1000 #[test]
1001 fn native_component_handoff_preserves_owned_capacities() {
1002 let mut data = Vec::with_capacity(9);
1003 data.push(3);
1004 let mut planes = Vec::with_capacity(4);
1005 planes.push(NativeComponentPlane {
1006 data,
1007 dimensions: (1, 1),
1008 bit_depth: 8,
1009 signed: false,
1010 sampling: (1, 1),
1011 bytes_per_sample: 1,
1012 });
1013 let mut profile = Vec::with_capacity(7);
1014 profile.push(1);
1015 let decoded = DecodedNativeComponents {
1016 dimensions: (1, 1),
1017 color_space: ColorSpace::Icc {
1018 profile,
1019 num_channels: 1,
1020 },
1021 has_alpha: false,
1022 planes,
1023 };
1024 let expected = decoded.planes.capacity() * size_of::<NativeComponentPlane>()
1025 + decoded.planes[0].data.capacity()
1026 + match &decoded.color_space {
1027 ColorSpace::Icc { profile, .. } => profile.capacity(),
1028 _ => 0,
1029 };
1030 let plane_owner_capacity = decoded.planes.capacity();
1031 let data_capacity = decoded.planes[0].data.capacity();
1032 let profile_capacity = match &decoded.color_space {
1033 ColorSpace::Icc { profile, .. } => profile.capacity(),
1034 _ => 0,
1035 };
1036 assert_eq!(decoded.allocated_bytes(), Some(expected));
1037
1038 let (_, color_space, _, planes) = decoded.into_parts();
1039 assert_eq!(planes.capacity(), plane_owner_capacity);
1040 assert_eq!(planes[0].allocated_bytes(), data_capacity);
1041 assert!(matches!(
1042 color_space,
1043 ColorSpace::Icc { profile, .. } if profile.capacity() == profile_capacity
1044 ));
1045 }
1046
1047 #[test]
1048 fn borrowed_component_handoff_preserves_metadata_capacities() {
1049 let samples = [2.0_f32];
1050 let mut planes = Vec::with_capacity(3);
1051 planes.push(ComponentPlane {
1052 samples: &samples,
1053 dimensions: (1, 1),
1054 bit_depth: 8,
1055 signed: false,
1056 sampling: (1, 1),
1057 });
1058 let mut profile = Vec::with_capacity(5);
1059 profile.push(1);
1060 let decoded = DecodedComponents {
1061 dimensions: (1, 1),
1062 color_space: ColorSpace::Icc {
1063 profile,
1064 num_channels: 1,
1065 },
1066 has_alpha: false,
1067 planes,
1068 live_bytes: 123,
1069 };
1070 let plane_owner_capacity = decoded.planes.capacity();
1071 let profile_capacity = match &decoded.color_space {
1072 ColorSpace::Icc { profile, .. } => profile.capacity(),
1073 _ => 0,
1074 };
1075
1076 assert_eq!(decoded.live_bytes(), 123);
1077 let (_, color_space, _, planes) = decoded.into_parts();
1078 assert_eq!(planes.capacity(), plane_owner_capacity);
1079 assert!(core::ptr::eq(
1080 planes[0].samples().as_ptr(),
1081 samples.as_ptr()
1082 ));
1083 assert!(matches!(
1084 color_space,
1085 ColorSpace::Icc { profile, .. } if profile.capacity() == profile_capacity
1086 ));
1087 }
1088}