1use alloc::borrow::Cow;
62use alloc::vec;
63use alloc::vec::Vec;
64
65use crate::convert::ConvertPlan;
66use crate::converter::RowConverter;
67use crate::negotiate::{ConvertIntent, best_match};
68use crate::policy::{AlphaPolicy, ConvertOptions};
69use crate::{
70 AlphaMode, ChannelLayout, ChannelType, ColorModel, ConvertError, PixelBuffer, PixelDescriptor,
71 PixelSliceMut,
72};
73use whereat::{At, ResultAtExt};
74
75fn assert_not_cmyk(desc: &PixelDescriptor) {
80 assert!(
81 desc.color_model() != ColorModel::Cmyk,
82 "CMYK pixel data cannot be processed by zenpixels-convert. \
83 Use a CMS (e.g., moxcms) with an ICC profile for CMYK↔RGB conversion."
84 );
85}
86
87#[derive(Clone, Debug)]
89pub struct Adapted<'a> {
90 pub data: Cow<'a, [u8]>,
92 pub descriptor: PixelDescriptor,
94 pub width: u32,
96 pub rows: u32,
98}
99
100#[track_caller]
122pub fn adapt_for_encode<'a>(
123 data: &'a [u8],
124 descriptor: PixelDescriptor,
125 width: u32,
126 rows: u32,
127 stride: usize,
128 supported: &[PixelDescriptor],
129) -> Result<Adapted<'a>, At<ConvertError>> {
130 adapt_for_encode_with_intent(
131 data,
132 descriptor,
133 width,
134 rows,
135 stride,
136 supported,
137 ConvertIntent::Fastest,
138 )
139}
140
141#[track_caller]
145pub fn adapt_for_encode_with_intent<'a>(
146 data: &'a [u8],
147 descriptor: PixelDescriptor,
148 width: u32,
149 rows: u32,
150 stride: usize,
151 supported: &[PixelDescriptor],
152 intent: ConvertIntent,
153) -> Result<Adapted<'a>, At<ConvertError>> {
154 assert_not_cmyk(&descriptor);
155 if supported.is_empty() {
156 return Err(whereat::at!(ConvertError::EmptyFormatList));
157 }
158
159 if supported.contains(&descriptor) {
161 return Ok(Adapted {
162 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
163 descriptor,
164 width,
165 rows,
166 });
167 }
168
169 for &target in supported {
174 if descriptor.channel_type() == target.channel_type()
175 && descriptor.layout() == target.layout()
176 && descriptor.alpha() == target.alpha()
177 && descriptor.primaries == target.primaries
178 && descriptor.signal_range == target.signal_range
179 {
180 return Ok(Adapted {
181 data: contiguous_from_strided(
182 data,
183 width,
184 rows,
185 stride,
186 descriptor.bytes_per_pixel(),
187 ),
188 descriptor: target,
189 width,
190 rows,
191 });
192 }
193 }
194
195 let target = best_match(descriptor, supported, intent)
197 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
198
199 let mut converter = RowConverter::new(descriptor, target).at()?;
200
201 let src_bpp = descriptor.bytes_per_pixel();
202 let dst_bpp = target.bytes_per_pixel();
203 let dst_stride = (width as usize) * dst_bpp;
204 let mut output = vec![0u8; dst_stride * rows as usize];
205
206 for y in 0..rows {
207 let src_start = y as usize * stride;
208 let src_end = src_start + (width as usize * src_bpp);
209 let dst_start = y as usize * dst_stride;
210 let dst_end = dst_start + dst_stride;
211 converter.convert_row(
212 &data[src_start..src_end],
213 &mut output[dst_start..dst_end],
214 width,
215 );
216 }
217
218 Ok(Adapted {
219 data: Cow::Owned(output),
220 descriptor: target,
221 width,
222 rows,
223 })
224}
225
226#[track_caller]
230pub fn convert_buffer(
231 src: &[u8],
232 width: u32,
233 rows: u32,
234 from: PixelDescriptor,
235 to: PixelDescriptor,
236) -> Result<Vec<u8>, At<ConvertError>> {
237 assert_not_cmyk(&from);
238 assert_not_cmyk(&to);
239 if from == to {
240 return Ok(src.to_vec());
241 }
242
243 let mut converter = RowConverter::new(from, to).at()?;
244 let src_bpp = from.bytes_per_pixel();
245 let dst_bpp = to.bytes_per_pixel();
246 let src_stride = (width as usize) * src_bpp;
247 let dst_stride = (width as usize) * dst_bpp;
248 let mut output = vec![0u8; dst_stride * rows as usize];
249
250 for y in 0..rows {
251 let src_start = y as usize * src_stride;
252 let src_end = src_start + src_stride;
253 let dst_start = y as usize * dst_stride;
254 let dst_end = dst_start + dst_stride;
255 converter.convert_row(
256 &src[src_start..src_end],
257 &mut output[dst_start..dst_end],
258 width,
259 );
260 }
261
262 Ok(output)
263}
264
265#[track_caller]
282pub(crate) fn convert_buffer_with_anchor(
283 src: &[u8],
284 width: u32,
285 rows: u32,
286 src_stride: usize,
287 from: PixelDescriptor,
288 to: PixelDescriptor,
289 anchor: zenpixels::hdr::DiffuseWhite,
290) -> Result<PixelBuffer, At<ConvertError>> {
291 let mut buf = PixelBuffer::try_new(width, rows, to)
295 .map_err(|_| whereat::at!(ConvertError::AllocationFailed))?;
296 let dst_stride = buf.stride();
297 {
298 let mut slice = buf.as_slice_mut();
299 convert_into_with_anchor(
300 src,
301 width,
302 rows,
303 src_stride,
304 from,
305 to,
306 anchor,
307 slice.as_strided_bytes_mut(),
308 dst_stride,
309 )?;
310 }
311 Ok(buf)
312}
313
314#[track_caller]
321#[allow(clippy::too_many_arguments)] pub(crate) fn convert_into_with_anchor(
323 src: &[u8],
324 width: u32,
325 rows: u32,
326 src_stride: usize,
327 from: PixelDescriptor,
328 to: PixelDescriptor,
329 anchor: zenpixels::hdr::DiffuseWhite,
330 dst: &mut [u8],
331 dst_stride: usize,
332) -> Result<(), At<ConvertError>> {
333 assert_not_cmyk(&from);
334 assert_not_cmyk(&to);
335
336 let dst_row = (width as usize) * to.bytes_per_pixel();
337 if rows > 0 {
338 let needed = (rows as usize - 1) * dst_stride + dst_row;
342 if dst_stride < dst_row || dst.len() < needed {
343 return Err(whereat::at!(ConvertError::BufferSize {
344 expected: needed.max(dst_row),
345 actual: dst.len().min(dst_stride),
346 }));
347 }
348 }
349
350 let plan = ConvertPlan::new(from, to).at()?.with_pq_anchor(anchor);
353 let mut converter = RowConverter::from_plan(plan);
354 let src_row = (width as usize) * from.bytes_per_pixel();
355
356 for y in 0..rows as usize {
357 let src_start = y * src_stride;
358 let dst_start = y * dst_stride;
359 converter.convert_row(
360 &src[src_start..src_start + src_row],
361 &mut dst[dst_start..dst_start + dst_row],
362 width,
363 );
364 }
365
366 Ok(())
367}
368
369pub fn try_adapt_in_place(
409 buf: &mut PixelBuffer,
410 target: PixelDescriptor,
411) -> Result<(), At<ConvertError>> {
412 let src = buf.descriptor();
413 let no_path = || {
414 Err(whereat::at!(ConvertError::NoPath {
415 from: src,
416 to: target
417 }))
418 };
419
420 if src.format == target.format {
422 buf.transform_in_place(|px| {
423 rewrap(px.bytes, px.width, px.rows, px.stride, target, px.color)
424 });
425 return Ok(());
426 }
427
428 if src.bytes_per_pixel() == target.bytes_per_pixel() {
431 let swappable = src.channel_type() == ChannelType::U8
432 && target.channel_type() == ChannelType::U8
433 && matches!(
434 (src.layout(), target.layout()),
435 (ChannelLayout::Rgba, ChannelLayout::Bgra)
436 | (ChannelLayout::Bgra, ChannelLayout::Rgba)
437 );
438 if !swappable {
439 return no_path();
440 }
441 buf.transform_in_place(|px| {
442 let width = px.width as usize;
443 let rows = px.rows as usize;
444 let _ = garb::bytes::rgba_to_bgra_inplace_strided(px.bytes, width, rows, px.stride);
447 rewrap(px.bytes, px.width, px.rows, px.stride, target, px.color)
448 });
449 return Ok(());
450 }
451
452 if !matches!(
457 src.alpha,
458 Some(AlphaMode::Undefined) | Some(AlphaMode::Opaque)
459 ) {
460 return no_path();
461 }
462 if src.channel_type() != target.channel_type() {
463 return no_path();
464 }
465 let map: &'static [usize] = match (src.layout(), target.layout()) {
468 (ChannelLayout::Rgba, ChannelLayout::Rgb) => &[0, 1, 2],
469 (ChannelLayout::Bgra, ChannelLayout::Rgb) => &[2, 1, 0],
472 (ChannelLayout::GrayAlpha, ChannelLayout::Gray) => &[0],
473 _ => return no_path(),
474 };
475
476 let in_bpp = src.bytes_per_pixel();
477 let out_bpp = target.bytes_per_pixel();
478 let elem = src.bytes_per_channel();
479
480 buf.transform_in_place(|px| drop_lane_impl(px, target, map, in_bpp, out_bpp, elem));
481 Ok(())
482}
483
484fn drop_lane_impl<'a>(
488 px: zenpixels::InPlacePixels<'a>,
489 target: PixelDescriptor,
490 map: &'static [usize],
491 in_bpp: usize,
492 out_bpp: usize,
493 elem: usize,
494) -> PixelSliceMut<'a> {
495 let width = px.width as usize;
496 let out_stride = px.stride - (px.stride % out_bpp);
505 for y in 0..px.rows as usize {
506 let sbase = y * px.stride;
507 let dbase = y * out_stride;
508 for x in 0..width {
509 let s = sbase + x * in_bpp;
510 let mut tmp = [0u8; 16];
511 tmp[..in_bpp].copy_from_slice(&px.bytes[s..s + in_bpp]);
512 let d = dbase + x * out_bpp;
513 for (k, &c) in map.iter().enumerate() {
514 px.bytes[d + k * elem..d + (k + 1) * elem]
515 .copy_from_slice(&tmp[c * elem..(c + 1) * elem]);
516 }
517 }
518 }
519 rewrap(px.bytes, px.width, px.rows, out_stride, target, px.color)
520}
521
522fn rewrap<'a>(
525 bytes: &'a mut [u8],
526 width: u32,
527 rows: u32,
528 stride: usize,
529 descriptor: PixelDescriptor,
530 color: Option<alloc::sync::Arc<zenpixels::ColorContext>>,
531) -> PixelSliceMut<'a> {
532 let out = PixelSliceMut::new(bytes, width, rows, stride, descriptor)
533 .expect("in-place adaptation geometry is always valid");
534 match color {
535 Some(c) => out.with_color_context(c),
536 None => out,
537 }
538}
539
540#[track_caller]
546pub fn adapt_for_encode_explicit<'a>(
547 data: &'a [u8],
548 descriptor: PixelDescriptor,
549 width: u32,
550 rows: u32,
551 stride: usize,
552 supported: &[PixelDescriptor],
553 options: &ConvertOptions,
554) -> Result<Adapted<'a>, At<ConvertError>> {
555 assert_not_cmyk(&descriptor);
556 if supported.is_empty() {
557 return Err(whereat::at!(ConvertError::EmptyFormatList));
558 }
559
560 if supported.contains(&descriptor) {
562 return Ok(Adapted {
563 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
564 descriptor,
565 width,
566 rows,
567 });
568 }
569
570 for &target in supported {
572 if descriptor.channel_type() == target.channel_type()
573 && descriptor.layout() == target.layout()
574 && descriptor.alpha() == target.alpha()
575 && descriptor.primaries == target.primaries
576 && descriptor.signal_range == target.signal_range
577 {
578 return Ok(Adapted {
579 data: contiguous_from_strided(
580 data,
581 width,
582 rows,
583 stride,
584 descriptor.bytes_per_pixel(),
585 ),
586 descriptor: target,
587 width,
588 rows,
589 });
590 }
591 }
592
593 let target = best_match(descriptor, supported, ConvertIntent::Fastest)
595 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
596
597 let plan = ConvertPlan::new_explicit(descriptor, target, options).at()?;
599
600 let drops_alpha = descriptor.alpha().is_some() && target.alpha().is_none();
602 if drops_alpha && options.alpha_policy == AlphaPolicy::DiscardIfOpaque {
603 let src_bpp = descriptor.bytes_per_pixel();
604 if !is_fully_opaque(data, width, rows, stride, src_bpp, &descriptor) {
605 return Err(whereat::at!(ConvertError::AlphaNotOpaque));
606 }
607 }
608
609 let mut converter = RowConverter::from_plan(plan);
610 let src_bpp = descriptor.bytes_per_pixel();
611 let dst_bpp = target.bytes_per_pixel();
612 let dst_stride = (width as usize) * dst_bpp;
613 let mut output = vec![0u8; dst_stride * rows as usize];
614
615 for y in 0..rows {
616 let src_start = y as usize * stride;
617 let src_end = src_start + (width as usize * src_bpp);
618 let dst_start = y as usize * dst_stride;
619 let dst_end = dst_start + dst_stride;
620 converter.convert_row(
621 &data[src_start..src_end],
622 &mut output[dst_start..dst_end],
623 width,
624 );
625 }
626
627 Ok(Adapted {
628 data: Cow::Owned(output),
629 descriptor: target,
630 width,
631 rows,
632 })
633}
634
635fn is_fully_opaque(
637 data: &[u8],
638 width: u32,
639 rows: u32,
640 stride: usize,
641 bpp: usize,
642 desc: &PixelDescriptor,
643) -> bool {
644 if desc.alpha().is_none() {
645 return true;
646 }
647 let cs = desc.channel_type().byte_size();
648 let alpha_offset = (desc.layout().channels() - 1) * cs;
649 for y in 0..rows {
650 let row_start = y as usize * stride;
651 for x in 0..width as usize {
652 let off = row_start + x * bpp + alpha_offset;
653 match desc.channel_type() {
654 crate::ChannelType::U8 => {
655 if data[off] != 255 {
656 return false;
657 }
658 }
659 crate::ChannelType::U16 => {
660 let v = u16::from_ne_bytes([data[off], data[off + 1]]);
661 if v != 65535 {
662 return false;
663 }
664 }
665 crate::ChannelType::F32 => {
666 let v = f32::from_ne_bytes([
667 data[off],
668 data[off + 1],
669 data[off + 2],
670 data[off + 3],
671 ]);
672 if v < 1.0 {
673 return false;
674 }
675 }
676 _ => return false,
677 }
678 }
679 }
680 true
681}
682
683fn contiguous_from_strided<'a>(
685 data: &'a [u8],
686 width: u32,
687 rows: u32,
688 stride: usize,
689 bpp: usize,
690) -> Cow<'a, [u8]> {
691 let row_bytes = width as usize * bpp;
692 if stride == row_bytes {
693 let total = row_bytes * rows as usize;
695 Cow::Borrowed(&data[..total])
696 } else {
697 let mut packed = Vec::with_capacity(row_bytes * rows as usize);
699 for y in 0..rows as usize {
700 let start = y * stride;
701 packed.extend_from_slice(&data[start..start + row_bytes]);
702 }
703 Cow::Owned(packed)
704 }
705}
706
707#[cfg(test)]
708mod anchor_tests {
709 use super::convert_buffer_with_anchor;
713 use crate::{PixelDescriptor, TransferFunction};
714 use alloc::vec;
715 use alloc::vec::Vec;
716 use zenpixels::hdr::DiffuseWhite;
717
718 fn pq_oetf(x: f64) -> f64 {
720 if x <= 0.0 {
721 return 0.0;
722 }
723 let m1 = 2610.0 / 16384.0;
724 let m2 = 2523.0 / 4096.0 * 128.0;
725 let c1 = 3424.0 / 4096.0;
726 let c2 = 2413.0 / 4096.0 * 32.0;
727 let c3 = 2392.0 / 4096.0 * 32.0;
728 let xp = x.powf(m1);
729 ((c1 + c2 * xp) / (1.0 + c3 * xp)).powf(m2)
730 }
731
732 fn gray_rgb_f32(values: &[f32]) -> Vec<u8> {
734 let mut v = Vec::with_capacity(values.len() * 12);
735 for &g in values {
736 for _ in 0..3 {
737 v.extend_from_slice(&g.to_ne_bytes());
738 }
739 }
740 v
741 }
742
743 fn gray_rgba_f32(pixels: &[(f32, f32)]) -> Vec<u8> {
745 let mut v = Vec::with_capacity(pixels.len() * 16);
746 for &(g, a) in pixels {
747 for _ in 0..3 {
748 v.extend_from_slice(&g.to_ne_bytes());
749 }
750 v.extend_from_slice(&a.to_ne_bytes());
751 }
752 v
753 }
754
755 fn packed_stride(width: usize, desc: PixelDescriptor) -> usize {
757 width * desc.bytes_per_pixel()
758 }
759
760 fn pq16_target() -> (PixelDescriptor, PixelDescriptor) {
761 let target = PixelDescriptor::RGB16_BT2100_PQ;
762 let lin = PixelDescriptor::RGBF32_LINEAR.with_primaries(target.primaries);
764 (lin, target)
765 }
766
767 #[test]
768 fn anchor_pq16_encode_matches_st2084_oracle() {
769 let values = [0.001f32, 0.1, 1.0, 2.0, 49.0];
770 let (lin, target) = pq16_target();
771 let out = convert_buffer_with_anchor(
772 &gray_rgb_f32(&values),
773 values.len() as u32,
774 1,
775 packed_stride(values.len(), lin),
776 lin,
777 target,
778 DiffuseWhite::BT2408,
779 )
780 .unwrap();
781 let codes: &[u16] = bytemuck::cast_slice(out.as_slice().as_strided_bytes());
782 for (i, &v) in values.iter().enumerate() {
783 let got = i64::from(codes[i * 3]);
784 let want = (pq_oetf(f64::from(v) * 203.0 / 10_000.0) * 65535.0).round() as i64;
786 assert!(
787 (got - want).abs() <= 1,
788 "@203 at {v}: got {got} want {want}"
789 );
790 }
791 }
792
793 #[test]
794 fn anchor_changes_pq_output_in_kernel() {
795 let (lin, target) = pq16_target();
796 let src = gray_rgb_f32(&[1.0]);
797 let enc = |w: DiffuseWhite| {
798 let o = convert_buffer_with_anchor(&src, 1, 1, packed_stride(1, lin), lin, target, w)
799 .unwrap();
800 let ob = o.as_slice().as_strided_bytes();
801 i64::from(u16::from_ne_bytes([ob[0], ob[1]]))
802 };
803 let c100 = enc(DiffuseWhite::new(100.0));
804 let c203 = enc(DiffuseWhite::BT2408);
805 assert_ne!(c100, c203);
808 let want100 = (pq_oetf(100.0 / 10_000.0) * 65535.0).round() as i64;
809 assert!(
810 (c100 - want100).abs() <= 1,
811 "@100: got {c100} want {want100}"
812 );
813 }
814
815 #[test]
816 fn anchor_pq16_decode_divides_and_roundtrips() {
817 let values = [0.05f32, 0.2, 1.0, 5.0];
820 let (lin, target) = pq16_target();
821 let pq = convert_buffer_with_anchor(
822 &gray_rgb_f32(&values),
823 values.len() as u32,
824 1,
825 packed_stride(values.len(), lin),
826 lin,
827 target,
828 DiffuseWhite::BT2408,
829 )
830 .unwrap();
831 let back = convert_buffer_with_anchor(
832 pq.as_slice().as_strided_bytes(),
833 values.len() as u32,
834 1,
835 pq.stride(),
836 target,
837 lin,
838 DiffuseWhite::BT2408,
839 )
840 .unwrap();
841 let backf: &[f32] = bytemuck::cast_slice(back.as_slice().as_strided_bytes());
842 for (i, &v) in values.iter().enumerate() {
843 let got = backf[i * 3];
844 let rel = ((f64::from(got) - f64::from(v)) / f64::from(v)).abs();
845 assert!(rel < 0.02, "roundtrip @203 at {v}: got {got} (rel {rel})");
846 }
847 }
848
849 #[test]
850 fn anchor_threads_through_f32_pq_slice_kernel() {
851 let values = [0.1f32, 1.0, 4.0];
854 let target = PixelDescriptor::RGB16_BT2100_PQ;
855 let lin = PixelDescriptor::RGBF32_LINEAR.with_primaries(target.primaries);
856 let pqf32 = lin.with_transfer(TransferFunction::Pq);
857 let out = convert_buffer_with_anchor(
858 &gray_rgb_f32(&values),
859 values.len() as u32,
860 1,
861 packed_stride(values.len(), lin),
862 lin,
863 pqf32,
864 DiffuseWhite::BT2408,
865 )
866 .unwrap();
867 let encoded: &[f32] = bytemuck::cast_slice(out.as_slice().as_strided_bytes());
868 for (i, &v) in values.iter().enumerate() {
869 let got = f64::from(encoded[i * 3]);
870 let want = pq_oetf(f64::from(v) * 203.0 / 10_000.0);
871 assert!(
872 (got - want).abs() < 1e-3,
873 "f32 PQ @203 at {v}: got {got} want {want}"
874 );
875 }
876 }
877
878 #[test]
879 fn no_anchor_default_is_unscaled() {
880 let (lin, target) = pq16_target();
883 let out = convert_buffer_with_anchor(
884 &gray_rgb_f32(&[1.0]),
885 1,
886 1,
887 packed_stride(1, lin),
888 lin,
889 target,
890 DiffuseWhite::new(10_000.0),
891 )
892 .unwrap();
893 let ob = out.as_slice().as_strided_bytes();
894 assert_eq!(u16::from_ne_bytes([ob[0], ob[1]]), 65535);
895 }
896
897 #[test]
898 fn anchor_preserves_alpha_through_rgba_pq16() {
899 let rgb_pq = PixelDescriptor::RGB16_BT2100_PQ;
903 let src = PixelDescriptor::RGBAF32_LINEAR.with_primaries(rgb_pq.primaries);
904 let target = PixelDescriptor::RGBA16
905 .with_transfer(TransferFunction::Pq)
906 .with_primaries(rgb_pq.primaries);
907 let pixels = [(1.0f32, 0.5f32), (2.0, 0.25)];
908 let out = convert_buffer_with_anchor(
909 &gray_rgba_f32(&pixels),
910 pixels.len() as u32,
911 1,
912 packed_stride(pixels.len(), src),
913 src,
914 target,
915 DiffuseWhite::BT2408,
916 )
917 .unwrap();
918 let codes: &[u16] = bytemuck::cast_slice(out.as_slice().as_strided_bytes());
919 for (i, &(g, a)) in pixels.iter().enumerate() {
920 let r = i64::from(codes[i * 4]);
921 let want_rgb = (pq_oetf(f64::from(g) * 203.0 / 10_000.0) * 65535.0).round() as i64;
922 assert!(
923 (r - want_rgb).abs() <= 1,
924 "rgb @203 at {g}: got {r} want {want_rgb}"
925 );
926 let alpha = codes[i * 4 + 3];
928 let want_a = (f64::from(a) * 65535.0).round() as u16;
929 assert_eq!(
930 alpha, want_a,
931 "alpha must pass through linearly: got {alpha} want {want_a}"
932 );
933 }
934 }
935
936 #[test]
937 fn anchor_honors_source_stride() {
938 let (lin, target) = pq16_target();
941 let row_vals = [0.1f32, 1.0, 3.0];
942 let width = row_vals.len() as u32;
943 let rows = 2u32;
944 let row = packed_stride(row_vals.len(), lin);
945 let stride = row + 2 * 12; let mut packed = gray_rgb_f32(&row_vals);
948 packed.extend_from_slice(&gray_rgb_f32(&row_vals));
949 let want = convert_buffer_with_anchor(
950 &packed,
951 width,
952 rows,
953 row,
954 lin,
955 target,
956 DiffuseWhite::BT2408,
957 )
958 .unwrap();
959
960 let mut strided = vec![0u8; stride * rows as usize];
961 for y in 0..rows as usize {
962 let s = y * stride;
963 strided[s..s + row].copy_from_slice(&gray_rgb_f32(&row_vals));
964 for b in strided[s + row..s + stride].chunks_exact_mut(4) {
965 b.copy_from_slice(&999.0f32.to_ne_bytes());
966 }
967 }
968 let got = convert_buffer_with_anchor(
969 &strided,
970 width,
971 rows,
972 stride,
973 lin,
974 target,
975 DiffuseWhite::BT2408,
976 )
977 .unwrap();
978 assert_eq!(
979 got.as_slice().as_strided_bytes(),
980 want.as_slice().as_strided_bytes(),
981 "strided source must convert identically to packed"
982 );
983 }
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989 use zenpixels::descriptor::{ColorPrimaries, SignalRange};
990 use zenpixels::policy::{AlphaPolicy, DepthPolicy};
991
992 fn test_rgb8_data() -> Vec<u8> {
994 vec![255, 0, 0, 0, 255, 0]
995 }
996
997 fn buf_from(bytes: &[u8], w: u32, h: u32, desc: PixelDescriptor) -> zenpixels::PixelBuffer {
1000 zenpixels::PixelBuffer::from_vec(bytes.to_vec(), w, h, desc).unwrap()
1001 }
1002
1003 #[test]
1004 fn in_place_bgra_to_rgba_swaps_bytes_and_updates_buffer() {
1005 let mut buf = buf_from(
1007 &[10u8, 20, 30, 255, 40, 50, 60, 128],
1008 2,
1009 1,
1010 PixelDescriptor::BGRA8_SRGB,
1011 );
1012 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB)
1013 .expect("4bpp B<->R swap is in-place");
1014 assert_eq!(buf.descriptor(), PixelDescriptor::RGBA8_SRGB);
1015 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 255, 60, 50, 40, 128]);
1016 }
1017
1018 #[test]
1019 fn in_place_rgba_to_bgra_roundtrips() {
1020 let original = [1u8, 2, 3, 4, 5, 6, 7, 8];
1021 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGBA8_SRGB);
1022 try_adapt_in_place(&mut buf, PixelDescriptor::BGRA8_SRGB).expect("to bgra");
1023 assert_eq!(buf.as_slice().row(0), &[3u8, 2, 1, 4, 7, 6, 5, 8]);
1024 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB).expect("back to rgba");
1025 assert_eq!(buf.as_slice().row(0), &original[..]);
1026 }
1027
1028 #[test]
1029 fn in_place_swap_respects_stride_padding() {
1030 let mut buf =
1033 zenpixels::PixelBuffer::new_simd_aligned(1, 2, PixelDescriptor::BGRA8_SRGB, 16);
1034 assert_eq!(buf.stride(), 16, "fixture must be strided");
1035 {
1036 let mut view = buf.as_slice_mut();
1037 view.row_mut(0).copy_from_slice(&[10, 20, 30, 255]);
1038 view.row_mut(1).copy_from_slice(&[40, 50, 60, 128]);
1039 let backing = view.as_strided_bytes_mut();
1040 backing[4..16].fill(0xAA);
1041 backing[20..32].fill(0xBB);
1042 }
1043 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB).expect("strided swap");
1044 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 255]);
1045 assert_eq!(buf.as_slice().row(1), &[60u8, 50, 40, 128]);
1046 let view = buf.as_slice();
1047 let backing = view.as_strided_bytes();
1048 assert!(
1049 backing[4..16].iter().all(|&b| b == 0xAA),
1050 "row-0 padding must be untouched"
1051 );
1052 assert!(
1053 backing[20..28].iter().all(|&b| b == 0xBB),
1054 "row-1 padding must be untouched"
1055 );
1056 }
1057
1058 #[test]
1059 fn in_place_metadata_retag_moves_no_bytes() {
1060 let original = [1u8, 2, 3, 4, 5, 6];
1061 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGB8);
1062 let target = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
1063 try_adapt_in_place(&mut buf, target).expect("same-format retag");
1064 assert_eq!(buf.descriptor(), target);
1065 assert_eq!(buf.as_slice().row(0), &original[..]);
1066 }
1067
1068 #[test]
1069 fn in_place_rejects_live_alpha_drop_and_depth_changes_unchanged() {
1070 let original = [1u8, 2, 3, 4, 5, 6, 7, 8];
1073 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGBA8_SRGB);
1074 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB)
1075 .expect_err("straight-alpha drop is not contract-exact");
1076 assert_eq!(buf.descriptor(), PixelDescriptor::RGBA8_SRGB);
1077 assert_eq!(buf.as_slice().row(0), &original[..]);
1078
1079 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA16_SRGB)
1081 .expect_err("depth change cannot be in-place");
1082 assert_eq!(buf.as_slice().row(0), &original[..]);
1083 }
1084
1085 #[test]
1086 fn in_place_rgbx_to_rgb_compacts_and_buffer_adopts_geometry() {
1087 let mut buf = buf_from(
1092 &[
1093 1u8, 2, 3, 0xEE, 4, 5, 6, 0xEE, 7, 8, 9, 0xEE, 10, 11, 12, 0xEE, ],
1096 2,
1097 2,
1098 PixelDescriptor::RGBX8_SRGB,
1099 );
1100 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB)
1101 .expect("padding drop is contract-exact");
1102 assert_eq!(buf.descriptor(), PixelDescriptor::RGB8_SRGB);
1103 assert_eq!(buf.stride(), 6);
1104 assert_eq!(buf.as_slice().row(0), &[1u8, 2, 3, 4, 5, 6]);
1105 assert_eq!(buf.as_slice().row(1), &[7u8, 8, 9, 10, 11, 12]);
1106 }
1107
1108 #[test]
1109 fn drop_lane_impl_keeps_divisible_stride_rows_in_place() {
1110 let mut bytes = [
1115 1u8, 2, 3, 0xEE, 4, 5, 6, 0xEE, 0xAA, 0xAA, 0xAA, 0xAA, 7, 8, 9, 0xEE, 10, 11, 12, 0xEE, 0xBB, 0xBB, 0xBB, 0xBB, ];
1118 let px =
1119 zenpixels::InPlacePixels::new(&mut bytes, 2, 2, 12, PixelDescriptor::RGBX8_SRGB, None);
1120 let out = drop_lane_impl(px, PixelDescriptor::RGB8_SRGB, &[0, 1, 2], 4, 3, 1);
1121 assert_eq!(out.stride(), 12, "divisible stride preserved verbatim");
1122 assert_eq!(out.row(0), &[1u8, 2, 3, 4, 5, 6]);
1123 assert_eq!(out.row(1), &[7u8, 8, 9, 10, 11, 12]);
1124 drop(out);
1125 assert_eq!(&bytes[8..12], &[0xAA; 4], "row-0 tail padding untouched");
1126 assert_eq!(&bytes[20..24], &[0xBB; 4], "row-1 tail padding untouched");
1127 }
1128
1129 #[test]
1130 fn in_place_opaque_bgra_to_rgb_reorders_while_dropping() {
1131 let mut buf = buf_from(
1133 &[10u8, 20, 30, 255, 40, 50, 60, 255],
1134 2,
1135 1,
1136 PixelDescriptor::BGRA8_SRGB.with_alpha_mode(Some(AlphaMode::Opaque)),
1137 );
1138 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB).expect("opaque drop allowed");
1139 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 60, 50, 40]);
1140 }
1141
1142 #[test]
1143 fn in_place_opaque_rgba16_to_rgb16_drops_lane() {
1144 let px16 = |r: u16, g: u16, b: u16| {
1146 [r, g, b, 0xFFFF]
1147 .iter()
1148 .flat_map(|v| v.to_ne_bytes())
1149 .collect::<Vec<u8>>()
1150 };
1151 let bytes: Vec<u8> = [px16(0x1234, 0x5678, 0x9ABC), px16(0x1111, 0x2222, 0x3333)].concat();
1152 let mut buf = buf_from(
1153 &bytes,
1154 2,
1155 1,
1156 PixelDescriptor::RGBA16_SRGB.with_alpha_mode(Some(AlphaMode::Opaque)),
1157 );
1158 try_adapt_in_place(&mut buf, PixelDescriptor::RGB16_SRGB).expect("u16 lane drop");
1159 let expected: Vec<u8> = [0x1234u16, 0x5678, 0x9ABC, 0x1111, 0x2222, 0x3333]
1160 .iter()
1161 .flat_map(|v| v.to_ne_bytes())
1162 .collect();
1163 assert_eq!(buf.as_slice().row(0), &expected[..]);
1164 assert_eq!(buf.stride(), 12, "16-px input stride rounds to 12");
1165 }
1166
1167 #[test]
1168 fn in_place_opaque_graya_to_gray_matches_allocating_path() {
1169 let original = [10u8, 255, 20, 255, 30, 255, 40, 255];
1171 let src = PixelDescriptor::new(
1172 ChannelType::U8,
1173 ChannelLayout::GrayAlpha,
1174 Some(AlphaMode::Opaque),
1175 zenpixels::TransferFunction::Srgb,
1176 );
1177 let target = PixelDescriptor::GRAY8_SRGB;
1178
1179 let mut buf = buf_from(&original, 4, 1, src);
1180 try_adapt_in_place(&mut buf, target).expect("graya drop");
1181 let in_place_row = buf.as_slice().row(0).to_vec();
1182
1183 let allocated = convert_buffer(&original, 4, 1, src, target).expect("allocating path");
1184 assert_eq!(in_place_row, allocated, "in-place must match allocating");
1185 }
1186
1187 #[test]
1188 fn transfer_agnostic_match_requires_same_primaries() {
1189 let data = test_rgb8_data();
1190 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
1191 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
1194
1195 assert!(
1199 matches!(result.data, Cow::Owned(_)),
1200 "different primaries must trigger conversion, not zero-copy relabel"
1201 );
1202 }
1203
1204 #[test]
1210 fn signal_range_mismatch_refuses_not_relabels() {
1211 let data = test_rgb8_data();
1212 let source = PixelDescriptor::RGB8.with_signal_range(SignalRange::Narrow);
1213 let target = PixelDescriptor::RGB8_SRGB; let err = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap_err();
1216 assert!(
1217 matches!(*err.error(), ConvertError::NoPath { .. }),
1218 "range crossing must refuse (no kernels), got: {}",
1219 err.error()
1220 );
1221 }
1222
1223 #[test]
1227 fn signal_range_match_zero_copies_narrow_verbatim() {
1228 let data = test_rgb8_data();
1229 let source = PixelDescriptor::RGB8
1230 .with_primaries(ColorPrimaries::Bt709)
1231 .with_signal_range(SignalRange::Narrow);
1232 let full_target = PixelDescriptor::RGB8_SRGB;
1233 let narrow_target = PixelDescriptor::RGB8_SRGB.with_signal_range(SignalRange::Narrow);
1234
1235 let result =
1236 adapt_for_encode(&data, source, 2, 1, 6, &[full_target, narrow_target]).unwrap();
1237 assert!(
1238 matches!(result.data, Cow::Borrowed(_)),
1239 "same-range target must zero-copy"
1240 );
1241 assert_eq!(result.descriptor.signal_range, SignalRange::Narrow);
1242 }
1243
1244 #[test]
1245 fn transfer_agnostic_match_allows_zero_copy_when_all_match() {
1246 let data = test_rgb8_data();
1247 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt709);
1249 let target = PixelDescriptor::RGB8_SRGB;
1251
1252 let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
1253
1254 assert!(
1256 matches!(result.data, Cow::Borrowed(_)),
1257 "should be zero-copy when only transfer differs"
1258 );
1259 assert_eq!(result.descriptor, target);
1260 }
1261
1262 #[test]
1263 fn exact_match_is_zero_copy() {
1264 let data = test_rgb8_data();
1265 let desc = PixelDescriptor::RGB8_SRGB;
1266
1267 let result = adapt_for_encode(&data, desc, 2, 1, 6, &[desc]).unwrap();
1268
1269 assert!(matches!(result.data, Cow::Borrowed(_)));
1270 assert_eq!(result.descriptor, desc);
1271 }
1272
1273 #[test]
1274 #[should_panic(expected = "CMYK pixel data cannot be processed")]
1275 fn cmyk_rejected_by_adapt_for_encode() {
1276 let cmyk_data = vec![0u8; 4 * 4]; let _ = adapt_for_encode(
1278 &cmyk_data,
1279 PixelDescriptor::CMYK8,
1280 2,
1281 2,
1282 8,
1283 &[PixelDescriptor::RGB8_SRGB],
1284 );
1285 }
1286
1287 #[test]
1288 #[should_panic(expected = "CMYK pixel data cannot be processed")]
1289 fn cmyk_rejected_by_convert_buffer() {
1290 let cmyk_data = vec![0u8; 4 * 4];
1291 let _ = convert_buffer(
1292 &cmyk_data,
1293 2,
1294 2,
1295 PixelDescriptor::CMYK8,
1296 PixelDescriptor::RGB8_SRGB,
1297 );
1298 }
1299
1300 #[test]
1301 #[should_panic(expected = "CMYK pixel data cannot be processed")]
1302 fn cmyk_rejected_by_convert_buffer_as_target() {
1303 let rgb_data = vec![0u8; 3 * 4];
1304 let _ = convert_buffer(
1305 &rgb_data,
1306 2,
1307 2,
1308 PixelDescriptor::RGB8_SRGB,
1309 PixelDescriptor::CMYK8,
1310 );
1311 }
1312
1313 #[test]
1314 fn explicit_variant_also_checks_primaries() {
1315 let data = test_rgb8_data();
1316 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
1317 let target = PixelDescriptor::RGB8_SRGB;
1318 let options = ConvertOptions::forbid_lossy()
1319 .with_alpha_policy(AlphaPolicy::DiscardUnchecked)
1320 .with_depth_policy(DepthPolicy::Round);
1321
1322 let result =
1323 adapt_for_encode_explicit(&data, source, 2, 1, 6, &[target], &options).unwrap();
1324
1325 assert!(
1326 matches!(result.data, Cow::Owned(_)),
1327 "explicit variant: different primaries must trigger conversion"
1328 );
1329 }
1330}