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