1use alloc::borrow::Cow;
65use alloc::vec;
66use alloc::vec::Vec;
67
68use crate::convert::ConvertPlan;
69use crate::converter::RowConverter;
70use crate::negotiate::{ConvertIntent, best_match};
71use crate::policy::{AlphaPolicy, ConvertOptions};
72use crate::{
73 AlphaMode, ChannelLayout, ChannelType, ColorModel, ConvertError, PixelBuffer, PixelCow,
74 PixelDescriptor, PixelSlice, PixelSliceMut,
75};
76use whereat::{At, ResultAtExt};
77
78fn reject_cmyk(from: PixelDescriptor, to: PixelDescriptor) -> Result<(), At<ConvertError>> {
98 if from.color_model() == ColorModel::Cmyk || to.color_model() == ColorModel::Cmyk {
99 return Err(whereat::at!(ConvertError::NoPath { from, to }));
100 }
101 Ok(())
102}
103
104fn ensure_src_buffer_fits(
115 data_len: usize,
116 rows: u32,
117 stride: usize,
118) -> Result<(), At<ConvertError>> {
119 if rows == 0 {
120 return Ok(());
121 }
122 let needed = (rows as usize)
123 .checked_mul(stride)
124 .ok_or_else(|| whereat::at!(ConvertError::AllocationFailed))?;
125 if data_len < needed {
126 return Err(whereat::at!(ConvertError::BufferSize {
127 expected: needed,
128 actual: data_len,
129 }));
130 }
131 Ok(())
132}
133
134fn checked_byte_alloc(rows: u32, stride: usize) -> Result<usize, At<ConvertError>> {
139 (rows as usize)
140 .checked_mul(stride)
141 .ok_or_else(|| whereat::at!(ConvertError::AllocationFailed))
142}
143
144#[deprecated(
146 since = "0.2.15",
147 note = "use PixelCow via adapt_for_encode_cow; this packed compatibility type is retained for 0.2.x callers"
148)]
149#[derive(Clone, Debug)]
150pub struct Adapted<'a> {
151 pub data: Cow<'a, [u8]>,
153 pub descriptor: PixelDescriptor,
155 pub width: u32,
157 pub rows: u32,
159}
160
161#[allow(deprecated)]
162impl Adapted<'_> {
163 pub fn as_pixel_slice(&self) -> Result<PixelSlice<'_>, At<ConvertError>> {
165 let stride = (self.width as usize)
166 .checked_mul(self.descriptor.bytes_per_pixel())
167 .ok_or_else(|| whereat::at!(ConvertError::AllocationFailed))?;
168 PixelSlice::new(&self.data, self.width, self.rows, stride, self.descriptor)
169 .map_err(|error| error.map_error(ConvertError::from))
170 }
171}
172
173#[allow(deprecated)]
174fn into_adapted<'a>(pixels: PixelCow<'a>) -> Adapted<'a> {
175 match pixels {
176 PixelCow::Borrowed(slice) => Adapted {
177 data: slice.contiguous_bytes(),
178 descriptor: slice.descriptor(),
179 width: slice.width(),
180 rows: slice.rows(),
181 },
182 PixelCow::Owned(buffer) => Adapted {
183 data: Cow::Owned(buffer.copy_to_contiguous_bytes()),
184 descriptor: buffer.descriptor(),
185 width: buffer.width(),
186 rows: buffer.height(),
187 },
188 }
189}
190
191fn borrow_or_copy<'a>(
192 data: &'a [u8],
193 width: u32,
194 rows: u32,
195 stride: usize,
196 descriptor: PixelDescriptor,
197) -> Result<PixelCow<'a>, At<ConvertError>> {
198 if let Ok(slice) = PixelSlice::new(data, width, rows, stride, descriptor) {
199 return Ok(PixelCow::Borrowed(slice));
200 }
201
202 let row_bytes = (width as usize)
206 .checked_mul(descriptor.bytes_per_pixel())
207 .ok_or_else(|| whereat::at!(ConvertError::AllocationFailed))?;
208 if stride < row_bytes {
209 return Err(whereat::at!(ConvertError::Buffer(
210 zenpixels::BufferError::StrideTooSmall,
211 )));
212 }
213 let mut output = PixelBuffer::try_new(width, rows, descriptor)
214 .map_err(|_| whereat::at!(ConvertError::AllocationFailed))?;
215 {
216 let mut destination = output.as_slice_mut();
217 for y in 0..rows {
218 let start = y as usize * stride;
219 destination
220 .row_mut(y)
221 .copy_from_slice(&data[start..start + row_bytes]);
222 }
223 }
224 Ok(PixelCow::Owned(output))
225}
226
227#[track_caller]
249#[deprecated(since = "0.2.15", note = "use adapt_for_encode_cow")]
250#[allow(deprecated)]
251pub fn adapt_for_encode<'a>(
252 data: &'a [u8],
253 descriptor: PixelDescriptor,
254 width: u32,
255 rows: u32,
256 stride: usize,
257 supported: &[PixelDescriptor],
258) -> Result<Adapted<'a>, At<ConvertError>> {
259 adapt_for_encode_cow(data, descriptor, width, rows, stride, supported).map(into_adapted)
260}
261
262#[track_caller]
264pub fn adapt_for_encode_cow<'a>(
265 data: &'a [u8],
266 descriptor: PixelDescriptor,
267 width: u32,
268 rows: u32,
269 stride: usize,
270 supported: &[PixelDescriptor],
271) -> Result<PixelCow<'a>, At<ConvertError>> {
272 adapt_for_encode_with_intent_cow(
273 data,
274 descriptor,
275 width,
276 rows,
277 stride,
278 supported,
279 ConvertIntent::Fastest,
280 )
281}
282
283#[track_caller]
287#[deprecated(since = "0.2.15", note = "use adapt_for_encode_with_intent_cow")]
288#[allow(deprecated)]
289pub fn adapt_for_encode_with_intent<'a>(
290 data: &'a [u8],
291 descriptor: PixelDescriptor,
292 width: u32,
293 rows: u32,
294 stride: usize,
295 supported: &[PixelDescriptor],
296 intent: ConvertIntent,
297) -> Result<Adapted<'a>, At<ConvertError>> {
298 adapt_for_encode_with_intent_cow(data, descriptor, width, rows, stride, supported, intent)
299 .map(into_adapted)
300}
301
302#[track_caller]
304pub fn adapt_for_encode_with_intent_cow<'a>(
305 data: &'a [u8],
306 descriptor: PixelDescriptor,
307 width: u32,
308 rows: u32,
309 stride: usize,
310 supported: &[PixelDescriptor],
311 intent: ConvertIntent,
312) -> Result<PixelCow<'a>, At<ConvertError>> {
313 ensure_src_buffer_fits(data.len(), rows, stride)?;
314 if supported.is_empty() {
315 return Err(whereat::at!(ConvertError::EmptyFormatList));
316 }
317 reject_cmyk(descriptor, supported[0])?;
320
321 if supported.contains(&descriptor) {
323 return borrow_or_copy(data, width, rows, stride, descriptor);
324 }
325
326 for &target in supported {
331 if descriptor.channel_type() == target.channel_type()
332 && descriptor.layout() == target.layout()
333 && descriptor.alpha() == target.alpha()
334 && descriptor.primaries == target.primaries
335 && descriptor.signal_range == target.signal_range
336 {
337 return borrow_or_copy(data, width, rows, stride, target);
338 }
339 }
340
341 let target = best_match(descriptor, supported, intent)
343 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
344
345 let mut converter = RowConverter::new(descriptor, target).at()?;
346
347 let src_bpp = descriptor.bytes_per_pixel();
348 let mut output = PixelBuffer::try_new(width, rows, target)
349 .map_err(|_| whereat::at!(ConvertError::AllocationFailed))?;
350 {
351 let mut destination = output.as_slice_mut();
352 for y in 0..rows {
353 let src_start = y as usize * stride;
354 let src_end = src_start + (width as usize * src_bpp);
355 converter.convert_row(&data[src_start..src_end], destination.row_mut(y), width);
356 }
357 }
358 Ok(PixelCow::Owned(output))
359}
360
361#[track_caller]
365pub fn convert_buffer(
366 src: &[u8],
367 width: u32,
368 rows: u32,
369 from: PixelDescriptor,
370 to: PixelDescriptor,
371) -> Result<Vec<u8>, At<ConvertError>> {
372 reject_cmyk(from, to)?;
373 let src_bpp = from.bytes_per_pixel();
374 let src_stride = (width as usize) * src_bpp;
375 ensure_src_buffer_fits(src.len(), rows, src_stride)?;
376 if from == to {
377 return Ok(src.to_vec());
378 }
379
380 let mut converter = RowConverter::new(from, to).at()?;
381 let dst_bpp = to.bytes_per_pixel();
382 let dst_stride = (width as usize) * dst_bpp;
383 let mut output = vec![0u8; checked_byte_alloc(rows, dst_stride)?];
384
385 for y in 0..rows {
386 let src_start = y as usize * src_stride;
387 let src_end = src_start + src_stride;
388 let dst_start = y as usize * dst_stride;
389 let dst_end = dst_start + dst_stride;
390 converter.convert_row(
391 &src[src_start..src_end],
392 &mut output[dst_start..dst_end],
393 width,
394 );
395 }
396
397 Ok(output)
398}
399
400#[track_caller]
417pub(crate) fn convert_buffer_with_anchor(
418 src: &[u8],
419 width: u32,
420 rows: u32,
421 src_stride: usize,
422 from: PixelDescriptor,
423 to: PixelDescriptor,
424 anchor: zenpixels::hdr::DiffuseWhite,
425) -> Result<PixelBuffer, At<ConvertError>> {
426 reject_cmyk(from, to)?;
430 ensure_src_buffer_fits(src.len(), rows, src_stride)?;
431 let mut buf = PixelBuffer::try_new(width, rows, to)
435 .map_err(|_| whereat::at!(ConvertError::AllocationFailed))?;
436 let dst_stride = buf.stride();
437 {
438 let mut slice = buf.as_slice_mut();
439 convert_into_with_anchor(
440 src,
441 width,
442 rows,
443 src_stride,
444 from,
445 to,
446 anchor,
447 slice.as_strided_bytes_mut(),
448 dst_stride,
449 )?;
450 }
451 Ok(buf)
452}
453
454#[track_caller]
461#[allow(clippy::too_many_arguments)] pub(crate) fn convert_into_with_anchor(
463 src: &[u8],
464 width: u32,
465 rows: u32,
466 src_stride: usize,
467 from: PixelDescriptor,
468 to: PixelDescriptor,
469 anchor: zenpixels::hdr::DiffuseWhite,
470 dst: &mut [u8],
471 dst_stride: usize,
472) -> Result<(), At<ConvertError>> {
473 reject_cmyk(from, to)?;
474 ensure_src_buffer_fits(src.len(), rows, src_stride)?;
475
476 let dst_row = (width as usize) * to.bytes_per_pixel();
477 if rows > 0 {
478 let needed = (rows as usize - 1) * dst_stride + dst_row;
482 if dst_stride < dst_row || dst.len() < needed {
483 return Err(whereat::at!(ConvertError::BufferSize {
484 expected: needed.max(dst_row),
485 actual: dst.len().min(dst_stride),
486 }));
487 }
488 }
489
490 let plan = ConvertPlan::new(from, to).at()?.with_pq_anchor(anchor);
493 let mut converter = RowConverter::from_plan(plan);
494 let src_row = (width as usize) * from.bytes_per_pixel();
495
496 for y in 0..rows as usize {
497 let src_start = y * src_stride;
498 let dst_start = y * dst_stride;
499 converter.convert_row(
500 &src[src_start..src_start + src_row],
501 &mut dst[dst_start..dst_start + dst_row],
502 width,
503 );
504 }
505
506 Ok(())
507}
508
509pub fn try_adapt_in_place(
549 buf: &mut PixelBuffer,
550 target: PixelDescriptor,
551) -> Result<(), At<ConvertError>> {
552 let src = buf.descriptor();
553 let no_path = || {
554 Err(whereat::at!(ConvertError::NoPath {
555 from: src,
556 to: target
557 }))
558 };
559
560 if src.format == target.format {
562 buf.transform_in_place(|px| {
563 rewrap(px.bytes, px.width, px.rows, px.stride, target, px.color)
564 });
565 return Ok(());
566 }
567
568 if src.bytes_per_pixel() == target.bytes_per_pixel() {
571 let swappable = src.channel_type() == ChannelType::U8
572 && target.channel_type() == ChannelType::U8
573 && matches!(
574 (src.layout(), target.layout()),
575 (ChannelLayout::Rgba, ChannelLayout::Bgra)
576 | (ChannelLayout::Bgra, ChannelLayout::Rgba)
577 );
578 if !swappable {
579 return no_path();
580 }
581 buf.transform_in_place(|px| {
582 let width = px.width as usize;
583 let rows = px.rows as usize;
584 let _ = garb::bytes::rgba_to_bgra_inplace_strided(px.bytes, width, rows, px.stride);
587 rewrap(px.bytes, px.width, px.rows, px.stride, target, px.color)
588 });
589 return Ok(());
590 }
591
592 if !matches!(
597 src.alpha,
598 Some(AlphaMode::Undefined) | Some(AlphaMode::Opaque)
599 ) {
600 return no_path();
601 }
602 if src.channel_type() != target.channel_type() {
603 return no_path();
604 }
605 let map: &'static [usize] = match (src.layout(), target.layout()) {
608 (ChannelLayout::Rgba, ChannelLayout::Rgb) => &[0, 1, 2],
609 (ChannelLayout::Bgra, ChannelLayout::Rgb) => &[2, 1, 0],
612 (ChannelLayout::GrayAlpha, ChannelLayout::Gray) => &[0],
613 _ => return no_path(),
614 };
615
616 let in_bpp = src.bytes_per_pixel();
617 let out_bpp = target.bytes_per_pixel();
618 let elem = src.bytes_per_channel();
619
620 buf.transform_in_place(|px| drop_lane_impl(px, target, map, in_bpp, out_bpp, elem));
621 Ok(())
622}
623
624fn drop_lane_impl<'a>(
628 px: zenpixels::InPlacePixels<'a>,
629 target: PixelDescriptor,
630 map: &'static [usize],
631 in_bpp: usize,
632 out_bpp: usize,
633 elem: usize,
634) -> PixelSliceMut<'a> {
635 let width = px.width as usize;
636 let out_stride = px.stride - (px.stride % out_bpp);
645 for y in 0..px.rows as usize {
646 let sbase = y * px.stride;
647 let dbase = y * out_stride;
648 for x in 0..width {
649 let s = sbase + x * in_bpp;
650 let mut tmp = [0u8; 16];
651 tmp[..in_bpp].copy_from_slice(&px.bytes[s..s + in_bpp]);
652 let d = dbase + x * out_bpp;
653 for (k, &c) in map.iter().enumerate() {
654 px.bytes[d + k * elem..d + (k + 1) * elem]
655 .copy_from_slice(&tmp[c * elem..(c + 1) * elem]);
656 }
657 }
658 }
659 rewrap(px.bytes, px.width, px.rows, out_stride, target, px.color)
660}
661
662fn rewrap<'a>(
665 bytes: &'a mut [u8],
666 width: u32,
667 rows: u32,
668 stride: usize,
669 descriptor: PixelDescriptor,
670 color: Option<alloc::sync::Arc<zenpixels::ColorContext>>,
671) -> PixelSliceMut<'a> {
672 let out = PixelSliceMut::new(bytes, width, rows, stride, descriptor)
673 .expect("in-place adaptation geometry is always valid");
674 match color {
675 Some(c) => out.with_color_context(c),
676 None => out,
677 }
678}
679
680#[track_caller]
686#[deprecated(since = "0.2.15", note = "use adapt_for_encode_explicit_cow")]
687#[allow(deprecated)]
688pub fn adapt_for_encode_explicit<'a>(
689 data: &'a [u8],
690 descriptor: PixelDescriptor,
691 width: u32,
692 rows: u32,
693 stride: usize,
694 supported: &[PixelDescriptor],
695 options: &ConvertOptions,
696) -> Result<Adapted<'a>, At<ConvertError>> {
697 adapt_for_encode_explicit_cow(data, descriptor, width, rows, stride, supported, options)
698 .map(into_adapted)
699}
700
701#[track_caller]
703pub fn adapt_for_encode_explicit_cow<'a>(
704 data: &'a [u8],
705 descriptor: PixelDescriptor,
706 width: u32,
707 rows: u32,
708 stride: usize,
709 supported: &[PixelDescriptor],
710 options: &ConvertOptions,
711) -> Result<PixelCow<'a>, At<ConvertError>> {
712 ensure_src_buffer_fits(data.len(), rows, stride)?;
713 if supported.is_empty() {
714 return Err(whereat::at!(ConvertError::EmptyFormatList));
715 }
716 reject_cmyk(descriptor, supported[0])?;
717
718 if supported.contains(&descriptor) {
720 return borrow_or_copy(data, width, rows, stride, descriptor);
721 }
722
723 for &target in supported {
725 if descriptor.channel_type() == target.channel_type()
726 && descriptor.layout() == target.layout()
727 && descriptor.alpha() == target.alpha()
728 && descriptor.primaries == target.primaries
729 && descriptor.signal_range == target.signal_range
730 {
731 return borrow_or_copy(data, width, rows, stride, target);
732 }
733 }
734
735 let target = best_match(descriptor, supported, ConvertIntent::Fastest)
737 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
738
739 let plan = ConvertPlan::new_explicit(descriptor, target, options).at()?;
741
742 let drops_alpha = descriptor.alpha().is_some() && target.alpha().is_none();
744 if drops_alpha && options.alpha_policy == AlphaPolicy::DiscardIfOpaque {
745 let src_bpp = descriptor.bytes_per_pixel();
746 if !is_fully_opaque(data, width, rows, stride, src_bpp, &descriptor) {
747 return Err(whereat::at!(ConvertError::AlphaNotOpaque));
748 }
749 }
750
751 let mut converter = RowConverter::from_plan(plan);
752 let src_bpp = descriptor.bytes_per_pixel();
753 let mut output = PixelBuffer::try_new(width, rows, target)
754 .map_err(|_| whereat::at!(ConvertError::AllocationFailed))?;
755 {
756 let mut destination = output.as_slice_mut();
757 for y in 0..rows {
758 let src_start = y as usize * stride;
759 let src_end = src_start + (width as usize * src_bpp);
760 converter.convert_row(&data[src_start..src_end], destination.row_mut(y), width);
761 }
762 }
763 Ok(PixelCow::Owned(output))
764}
765
766fn is_fully_opaque(
768 data: &[u8],
769 width: u32,
770 rows: u32,
771 stride: usize,
772 bpp: usize,
773 desc: &PixelDescriptor,
774) -> bool {
775 if desc.alpha().is_none() {
776 return true;
777 }
778 let cs = desc.channel_type().byte_size();
779 let alpha_offset = (desc.layout().channels() - 1) * cs;
780 for y in 0..rows {
781 let row_start = y as usize * stride;
782 for x in 0..width as usize {
783 let off = row_start + x * bpp + alpha_offset;
784 match desc.channel_type() {
785 crate::ChannelType::U8 => {
786 if data[off] != 255 {
787 return false;
788 }
789 }
790 crate::ChannelType::U16 => {
791 let v = u16::from_ne_bytes([data[off], data[off + 1]]);
792 if v != 65535 {
793 return false;
794 }
795 }
796 crate::ChannelType::F32 => {
797 let v = f32::from_ne_bytes([
798 data[off],
799 data[off + 1],
800 data[off + 2],
801 data[off + 3],
802 ]);
803 if v < 1.0 {
804 return false;
805 }
806 }
807 _ => return false,
808 }
809 }
810 }
811 true
812}
813
814#[cfg(test)]
815mod anchor_tests {
816 use super::convert_buffer_with_anchor;
820 use crate::{PixelDescriptor, TransferFunction};
821 use alloc::vec;
822 use alloc::vec::Vec;
823 use zenpixels::hdr::DiffuseWhite;
824
825 fn pq_oetf(x: f64) -> f64 {
827 if x <= 0.0 {
828 return 0.0;
829 }
830 let m1 = 2610.0 / 16384.0;
831 let m2 = 2523.0 / 4096.0 * 128.0;
832 let c1 = 3424.0 / 4096.0;
833 let c2 = 2413.0 / 4096.0 * 32.0;
834 let c3 = 2392.0 / 4096.0 * 32.0;
835 let xp = x.powf(m1);
836 ((c1 + c2 * xp) / (1.0 + c3 * xp)).powf(m2)
837 }
838
839 fn gray_rgb_f32(values: &[f32]) -> Vec<u8> {
841 let mut v = Vec::with_capacity(values.len() * 12);
842 for &g in values {
843 for _ in 0..3 {
844 v.extend_from_slice(&g.to_ne_bytes());
845 }
846 }
847 v
848 }
849
850 fn gray_rgba_f32(pixels: &[(f32, f32)]) -> Vec<u8> {
852 let mut v = Vec::with_capacity(pixels.len() * 16);
853 for &(g, a) in pixels {
854 for _ in 0..3 {
855 v.extend_from_slice(&g.to_ne_bytes());
856 }
857 v.extend_from_slice(&a.to_ne_bytes());
858 }
859 v
860 }
861
862 fn packed_stride(width: usize, desc: PixelDescriptor) -> usize {
864 width * desc.bytes_per_pixel()
865 }
866
867 fn pq16_target() -> (PixelDescriptor, PixelDescriptor) {
868 let target = PixelDescriptor::RGB16_BT2100_PQ;
869 let lin = PixelDescriptor::RGBF32_LINEAR.with_primaries(target.primaries);
871 (lin, target)
872 }
873
874 #[test]
875 fn anchor_pq16_encode_matches_st2084_oracle() {
876 let values = [0.001f32, 0.1, 1.0, 2.0, 49.0];
877 let (lin, target) = pq16_target();
878 let out = convert_buffer_with_anchor(
879 &gray_rgb_f32(&values),
880 values.len() as u32,
881 1,
882 packed_stride(values.len(), lin),
883 lin,
884 target,
885 DiffuseWhite::BT2408,
886 )
887 .unwrap();
888 let codes: &[u16] = bytemuck::cast_slice(out.as_slice().as_strided_bytes());
889 for (i, &v) in values.iter().enumerate() {
890 let got = i64::from(codes[i * 3]);
891 let want = (pq_oetf(f64::from(v) * 203.0 / 10_000.0) * 65535.0).round() as i64;
893 assert!(
894 (got - want).abs() <= 1,
895 "@203 at {v}: got {got} want {want}"
896 );
897 }
898 }
899
900 #[test]
901 fn anchor_changes_pq_output_in_kernel() {
902 let (lin, target) = pq16_target();
903 let src = gray_rgb_f32(&[1.0]);
904 let enc = |w: DiffuseWhite| {
905 let o = convert_buffer_with_anchor(&src, 1, 1, packed_stride(1, lin), lin, target, w)
906 .unwrap();
907 let ob = o.as_slice().as_strided_bytes();
908 i64::from(u16::from_ne_bytes([ob[0], ob[1]]))
909 };
910 let c100 = enc(DiffuseWhite::new(100.0));
911 let c203 = enc(DiffuseWhite::BT2408);
912 assert_ne!(c100, c203);
915 let want100 = (pq_oetf(100.0 / 10_000.0) * 65535.0).round() as i64;
916 assert!(
917 (c100 - want100).abs() <= 1,
918 "@100: got {c100} want {want100}"
919 );
920 }
921
922 #[test]
923 fn anchor_pq16_decode_divides_and_roundtrips() {
924 let values = [0.05f32, 0.2, 1.0, 5.0];
927 let (lin, target) = pq16_target();
928 let pq = convert_buffer_with_anchor(
929 &gray_rgb_f32(&values),
930 values.len() as u32,
931 1,
932 packed_stride(values.len(), lin),
933 lin,
934 target,
935 DiffuseWhite::BT2408,
936 )
937 .unwrap();
938 let back = convert_buffer_with_anchor(
939 pq.as_slice().as_strided_bytes(),
940 values.len() as u32,
941 1,
942 pq.stride(),
943 target,
944 lin,
945 DiffuseWhite::BT2408,
946 )
947 .unwrap();
948 let backf: &[f32] = bytemuck::cast_slice(back.as_slice().as_strided_bytes());
949 for (i, &v) in values.iter().enumerate() {
950 let got = backf[i * 3];
951 let rel = ((f64::from(got) - f64::from(v)) / f64::from(v)).abs();
952 assert!(rel < 0.02, "roundtrip @203 at {v}: got {got} (rel {rel})");
953 }
954 }
955
956 #[test]
957 fn anchor_threads_through_f32_pq_slice_kernel() {
958 let values = [0.1f32, 1.0, 4.0];
961 let target = PixelDescriptor::RGB16_BT2100_PQ;
962 let lin = PixelDescriptor::RGBF32_LINEAR.with_primaries(target.primaries);
963 let pqf32 = lin.with_transfer(TransferFunction::Pq);
964 let out = convert_buffer_with_anchor(
965 &gray_rgb_f32(&values),
966 values.len() as u32,
967 1,
968 packed_stride(values.len(), lin),
969 lin,
970 pqf32,
971 DiffuseWhite::BT2408,
972 )
973 .unwrap();
974 let encoded: &[f32] = bytemuck::cast_slice(out.as_slice().as_strided_bytes());
975 for (i, &v) in values.iter().enumerate() {
976 let got = f64::from(encoded[i * 3]);
977 let want = pq_oetf(f64::from(v) * 203.0 / 10_000.0);
978 assert!(
979 (got - want).abs() < 1e-3,
980 "f32 PQ @203 at {v}: got {got} want {want}"
981 );
982 }
983 }
984
985 #[test]
986 fn no_anchor_default_is_unscaled() {
987 let (lin, target) = pq16_target();
990 let out = convert_buffer_with_anchor(
991 &gray_rgb_f32(&[1.0]),
992 1,
993 1,
994 packed_stride(1, lin),
995 lin,
996 target,
997 DiffuseWhite::new(10_000.0),
998 )
999 .unwrap();
1000 let ob = out.as_slice().as_strided_bytes();
1001 assert_eq!(u16::from_ne_bytes([ob[0], ob[1]]), 65535);
1002 }
1003
1004 #[test]
1005 fn anchor_preserves_alpha_through_rgba_pq16() {
1006 let rgb_pq = PixelDescriptor::RGB16_BT2100_PQ;
1010 let src = PixelDescriptor::RGBAF32_LINEAR.with_primaries(rgb_pq.primaries);
1011 let target = PixelDescriptor::RGBA16
1012 .with_transfer(TransferFunction::Pq)
1013 .with_primaries(rgb_pq.primaries);
1014 let pixels = [(1.0f32, 0.5f32), (2.0, 0.25)];
1015 let out = convert_buffer_with_anchor(
1016 &gray_rgba_f32(&pixels),
1017 pixels.len() as u32,
1018 1,
1019 packed_stride(pixels.len(), src),
1020 src,
1021 target,
1022 DiffuseWhite::BT2408,
1023 )
1024 .unwrap();
1025 let codes: &[u16] = bytemuck::cast_slice(out.as_slice().as_strided_bytes());
1026 for (i, &(g, a)) in pixels.iter().enumerate() {
1027 let r = i64::from(codes[i * 4]);
1028 let want_rgb = (pq_oetf(f64::from(g) * 203.0 / 10_000.0) * 65535.0).round() as i64;
1029 assert!(
1030 (r - want_rgb).abs() <= 1,
1031 "rgb @203 at {g}: got {r} want {want_rgb}"
1032 );
1033 let alpha = codes[i * 4 + 3];
1035 let want_a = (f64::from(a) * 65535.0).round() as u16;
1036 assert_eq!(
1037 alpha, want_a,
1038 "alpha must pass through linearly: got {alpha} want {want_a}"
1039 );
1040 }
1041 }
1042
1043 #[test]
1044 fn anchor_honors_source_stride() {
1045 let (lin, target) = pq16_target();
1048 let row_vals = [0.1f32, 1.0, 3.0];
1049 let width = row_vals.len() as u32;
1050 let rows = 2u32;
1051 let row = packed_stride(row_vals.len(), lin);
1052 let stride = row + 2 * 12; let mut packed = gray_rgb_f32(&row_vals);
1055 packed.extend_from_slice(&gray_rgb_f32(&row_vals));
1056 let want = convert_buffer_with_anchor(
1057 &packed,
1058 width,
1059 rows,
1060 row,
1061 lin,
1062 target,
1063 DiffuseWhite::BT2408,
1064 )
1065 .unwrap();
1066
1067 let mut strided = vec![0u8; stride * rows as usize];
1068 for y in 0..rows as usize {
1069 let s = y * stride;
1070 strided[s..s + row].copy_from_slice(&gray_rgb_f32(&row_vals));
1071 for b in strided[s + row..s + stride].chunks_exact_mut(4) {
1072 b.copy_from_slice(&999.0f32.to_ne_bytes());
1073 }
1074 }
1075 let got = convert_buffer_with_anchor(
1076 &strided,
1077 width,
1078 rows,
1079 stride,
1080 lin,
1081 target,
1082 DiffuseWhite::BT2408,
1083 )
1084 .unwrap();
1085 assert_eq!(
1086 got.as_slice().as_strided_bytes(),
1087 want.as_slice().as_strided_bytes(),
1088 "strided source must convert identically to packed"
1089 );
1090 }
1091}
1092
1093#[cfg(test)]
1094#[allow(deprecated)]
1095mod tests {
1096 use super::*;
1097 use zenpixels::descriptor::{ColorPrimaries, SignalRange};
1098 use zenpixels::policy::{AlphaPolicy, DepthPolicy};
1099
1100 fn test_rgb8_data() -> Vec<u8> {
1102 vec![255, 0, 0, 0, 255, 0]
1103 }
1104
1105 fn buf_from(bytes: &[u8], w: u32, h: u32, desc: PixelDescriptor) -> zenpixels::PixelBuffer {
1108 zenpixels::PixelBuffer::from_vec(bytes.to_vec(), w, h, desc).unwrap()
1109 }
1110
1111 #[test]
1112 fn in_place_bgra_to_rgba_swaps_bytes_and_updates_buffer() {
1113 let mut buf = buf_from(
1115 &[10u8, 20, 30, 255, 40, 50, 60, 128],
1116 2,
1117 1,
1118 PixelDescriptor::BGRA8_SRGB,
1119 );
1120 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB)
1121 .expect("4bpp B<->R swap is in-place");
1122 assert_eq!(buf.descriptor(), PixelDescriptor::RGBA8_SRGB);
1123 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 255, 60, 50, 40, 128]);
1124 }
1125
1126 #[test]
1127 fn in_place_rgba_to_bgra_roundtrips() {
1128 let original = [1u8, 2, 3, 4, 5, 6, 7, 8];
1129 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGBA8_SRGB);
1130 try_adapt_in_place(&mut buf, PixelDescriptor::BGRA8_SRGB).expect("to bgra");
1131 assert_eq!(buf.as_slice().row(0), &[3u8, 2, 1, 4, 7, 6, 5, 8]);
1132 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB).expect("back to rgba");
1133 assert_eq!(buf.as_slice().row(0), &original[..]);
1134 }
1135
1136 #[test]
1137 fn in_place_swap_respects_stride_padding() {
1138 let mut buf =
1141 zenpixels::PixelBuffer::new_simd_aligned(1, 2, PixelDescriptor::BGRA8_SRGB, 16);
1142 assert_eq!(buf.stride(), 16, "fixture must be strided");
1143 {
1144 let mut view = buf.as_slice_mut();
1145 view.row_mut(0).copy_from_slice(&[10, 20, 30, 255]);
1146 view.row_mut(1).copy_from_slice(&[40, 50, 60, 128]);
1147 let backing = view.as_strided_bytes_mut();
1148 backing[4..16].fill(0xAA);
1149 backing[20..32].fill(0xBB);
1150 }
1151 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB).expect("strided swap");
1152 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 255]);
1153 assert_eq!(buf.as_slice().row(1), &[60u8, 50, 40, 128]);
1154 let view = buf.as_slice();
1155 let backing = view.as_strided_bytes();
1156 assert!(
1157 backing[4..16].iter().all(|&b| b == 0xAA),
1158 "row-0 padding must be untouched"
1159 );
1160 assert!(
1161 backing[20..28].iter().all(|&b| b == 0xBB),
1162 "row-1 padding must be untouched"
1163 );
1164 }
1165
1166 #[test]
1167 fn in_place_metadata_retag_moves_no_bytes() {
1168 let original = [1u8, 2, 3, 4, 5, 6];
1169 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGB8);
1170 let target = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
1171 try_adapt_in_place(&mut buf, target).expect("same-format retag");
1172 assert_eq!(buf.descriptor(), target);
1173 assert_eq!(buf.as_slice().row(0), &original[..]);
1174 }
1175
1176 #[test]
1177 fn in_place_rejects_live_alpha_drop_and_depth_changes_unchanged() {
1178 let original = [1u8, 2, 3, 4, 5, 6, 7, 8];
1181 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGBA8_SRGB);
1182 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB)
1183 .expect_err("straight-alpha drop is not contract-exact");
1184 assert_eq!(buf.descriptor(), PixelDescriptor::RGBA8_SRGB);
1185 assert_eq!(buf.as_slice().row(0), &original[..]);
1186
1187 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA16_SRGB)
1189 .expect_err("depth change cannot be in-place");
1190 assert_eq!(buf.as_slice().row(0), &original[..]);
1191 }
1192
1193 #[test]
1194 fn in_place_rgbx_to_rgb_compacts_and_buffer_adopts_geometry() {
1195 let mut buf = buf_from(
1200 &[
1201 1u8, 2, 3, 0xEE, 4, 5, 6, 0xEE, 7, 8, 9, 0xEE, 10, 11, 12, 0xEE, ],
1204 2,
1205 2,
1206 PixelDescriptor::RGBX8_SRGB,
1207 );
1208 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB)
1209 .expect("padding drop is contract-exact");
1210 assert_eq!(buf.descriptor(), PixelDescriptor::RGB8_SRGB);
1211 assert_eq!(buf.stride(), 6);
1212 assert_eq!(buf.as_slice().row(0), &[1u8, 2, 3, 4, 5, 6]);
1213 assert_eq!(buf.as_slice().row(1), &[7u8, 8, 9, 10, 11, 12]);
1214 }
1215
1216 #[test]
1217 fn drop_lane_impl_keeps_divisible_stride_rows_in_place() {
1218 let mut bytes = [
1223 1u8, 2, 3, 0xEE, 4, 5, 6, 0xEE, 0xAA, 0xAA, 0xAA, 0xAA, 7, 8, 9, 0xEE, 10, 11, 12, 0xEE, 0xBB, 0xBB, 0xBB, 0xBB, ];
1226 let px =
1227 zenpixels::InPlacePixels::new(&mut bytes, 2, 2, 12, PixelDescriptor::RGBX8_SRGB, None);
1228 let out = drop_lane_impl(px, PixelDescriptor::RGB8_SRGB, &[0, 1, 2], 4, 3, 1);
1229 assert_eq!(out.stride(), 12, "divisible stride preserved verbatim");
1230 assert_eq!(out.row(0), &[1u8, 2, 3, 4, 5, 6]);
1231 assert_eq!(out.row(1), &[7u8, 8, 9, 10, 11, 12]);
1232 drop(out);
1233 assert_eq!(&bytes[8..12], &[0xAA; 4], "row-0 tail padding untouched");
1234 assert_eq!(&bytes[20..24], &[0xBB; 4], "row-1 tail padding untouched");
1235 }
1236
1237 #[test]
1238 fn in_place_opaque_bgra_to_rgb_reorders_while_dropping() {
1239 let mut buf = buf_from(
1241 &[10u8, 20, 30, 255, 40, 50, 60, 255],
1242 2,
1243 1,
1244 PixelDescriptor::BGRA8_SRGB.with_alpha_mode(Some(AlphaMode::Opaque)),
1245 );
1246 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB).expect("opaque drop allowed");
1247 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 60, 50, 40]);
1248 }
1249
1250 #[test]
1251 fn in_place_opaque_rgba16_to_rgb16_drops_lane() {
1252 let px16 = |r: u16, g: u16, b: u16| {
1254 [r, g, b, 0xFFFF]
1255 .iter()
1256 .flat_map(|v| v.to_ne_bytes())
1257 .collect::<Vec<u8>>()
1258 };
1259 let bytes: Vec<u8> = [px16(0x1234, 0x5678, 0x9ABC), px16(0x1111, 0x2222, 0x3333)].concat();
1260 let mut buf = buf_from(
1261 &bytes,
1262 2,
1263 1,
1264 PixelDescriptor::RGBA16_SRGB.with_alpha_mode(Some(AlphaMode::Opaque)),
1265 );
1266 try_adapt_in_place(&mut buf, PixelDescriptor::RGB16_SRGB).expect("u16 lane drop");
1267 let expected: Vec<u8> = [0x1234u16, 0x5678, 0x9ABC, 0x1111, 0x2222, 0x3333]
1268 .iter()
1269 .flat_map(|v| v.to_ne_bytes())
1270 .collect();
1271 assert_eq!(buf.as_slice().row(0), &expected[..]);
1272 assert_eq!(buf.stride(), 12, "16-px input stride rounds to 12");
1273 }
1274
1275 #[test]
1276 fn in_place_opaque_graya_to_gray_matches_allocating_path() {
1277 let original = [10u8, 255, 20, 255, 30, 255, 40, 255];
1279 let src = PixelDescriptor::new(
1280 ChannelType::U8,
1281 ChannelLayout::GrayAlpha,
1282 Some(AlphaMode::Opaque),
1283 zenpixels::TransferFunction::Srgb,
1284 );
1285 let target = PixelDescriptor::GRAY8_SRGB;
1286
1287 let mut buf = buf_from(&original, 4, 1, src);
1288 try_adapt_in_place(&mut buf, target).expect("graya drop");
1289 let in_place_row = buf.as_slice().row(0).to_vec();
1290
1291 let allocated = convert_buffer(&original, 4, 1, src, target).expect("allocating path");
1292 assert_eq!(in_place_row, allocated, "in-place must match allocating");
1293 }
1294
1295 #[test]
1296 fn transfer_agnostic_match_requires_same_primaries() {
1297 let data = test_rgb8_data();
1298 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
1299 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
1302
1303 assert!(
1307 matches!(result.data, Cow::Owned(_)),
1308 "different primaries must trigger conversion, not zero-copy relabel"
1309 );
1310 }
1311
1312 #[test]
1318 fn signal_range_mismatch_refuses_not_relabels() {
1319 let data = test_rgb8_data();
1320 let source = PixelDescriptor::RGB8.with_signal_range(SignalRange::Narrow);
1321 let target = PixelDescriptor::RGB8_SRGB; let err = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap_err();
1324 assert!(
1325 matches!(*err.error(), ConvertError::NoPath { .. }),
1326 "range crossing must refuse (no kernels), got: {}",
1327 err.error()
1328 );
1329 }
1330
1331 #[test]
1335 fn signal_range_match_zero_copies_narrow_verbatim() {
1336 let data = test_rgb8_data();
1337 let source = PixelDescriptor::RGB8
1338 .with_primaries(ColorPrimaries::Bt709)
1339 .with_signal_range(SignalRange::Narrow);
1340 let full_target = PixelDescriptor::RGB8_SRGB;
1341 let narrow_target = PixelDescriptor::RGB8_SRGB.with_signal_range(SignalRange::Narrow);
1342
1343 let result =
1344 adapt_for_encode(&data, source, 2, 1, 6, &[full_target, narrow_target]).unwrap();
1345 assert!(
1346 matches!(result.data, Cow::Borrowed(_)),
1347 "same-range target must zero-copy"
1348 );
1349 assert_eq!(result.descriptor.signal_range, SignalRange::Narrow);
1350 }
1351
1352 #[test]
1353 fn transfer_agnostic_match_allows_zero_copy_when_all_match() {
1354 let data = test_rgb8_data();
1355 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt709);
1357 let target = PixelDescriptor::RGB8_SRGB;
1359
1360 let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
1361
1362 assert!(
1364 matches!(result.data, Cow::Borrowed(_)),
1365 "should be zero-copy when only transfer differs"
1366 );
1367 assert_eq!(result.descriptor, target);
1368 }
1369
1370 #[test]
1371 fn exact_match_is_zero_copy() {
1372 let data = test_rgb8_data();
1373 let desc = PixelDescriptor::RGB8_SRGB;
1374
1375 let result = adapt_for_encode(&data, desc, 2, 1, 6, &[desc]).unwrap();
1376
1377 assert!(matches!(result.data, Cow::Borrowed(_)));
1378 assert_eq!(result.descriptor, desc);
1379 }
1380
1381 #[test]
1388 fn cmyk_rejected_by_adapt_for_encode() {
1389 let cmyk_data = vec![0u8; 4 * 4]; let err = adapt_for_encode(
1391 &cmyk_data,
1392 PixelDescriptor::CMYK8,
1393 2,
1394 2,
1395 8,
1396 &[PixelDescriptor::RGB8_SRGB],
1397 )
1398 .unwrap_err();
1399 assert!(matches!(
1400 *err.error(),
1401 ConvertError::NoPath { from, .. } if from.color_model() == ColorModel::Cmyk
1402 ));
1403 }
1404
1405 #[test]
1406 fn cmyk_rejected_by_convert_buffer() {
1407 let cmyk_data = vec![0u8; 4 * 4];
1408 let err = convert_buffer(
1409 &cmyk_data,
1410 2,
1411 2,
1412 PixelDescriptor::CMYK8,
1413 PixelDescriptor::RGB8_SRGB,
1414 )
1415 .unwrap_err();
1416 assert!(matches!(
1417 *err.error(),
1418 ConvertError::NoPath { from, .. } if from.color_model() == ColorModel::Cmyk
1419 ));
1420 }
1421
1422 #[test]
1423 fn cmyk_rejected_by_convert_buffer_as_target() {
1424 let rgb_data = vec![0u8; 3 * 4];
1425 let err = convert_buffer(
1426 &rgb_data,
1427 2,
1428 2,
1429 PixelDescriptor::RGB8_SRGB,
1430 PixelDescriptor::CMYK8,
1431 )
1432 .unwrap_err();
1433 assert!(matches!(
1434 *err.error(),
1435 ConvertError::NoPath { to, .. } if to.color_model() == ColorModel::Cmyk
1436 ));
1437 }
1438
1439 #[test]
1440 fn explicit_variant_also_checks_primaries() {
1441 let data = test_rgb8_data();
1442 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
1443 let target = PixelDescriptor::RGB8_SRGB;
1444 let options = ConvertOptions::forbid_lossy()
1445 .with_alpha_policy(AlphaPolicy::DiscardUnchecked)
1446 .with_depth_policy(DepthPolicy::Round);
1447
1448 let result =
1449 adapt_for_encode_explicit(&data, source, 2, 1, 6, &[target], &options).unwrap();
1450
1451 assert!(
1452 matches!(result.data, Cow::Owned(_)),
1453 "explicit variant: different primaries must trigger conversion"
1454 );
1455 }
1456
1457 #[test]
1460 fn cmyk_input_returns_typed_error_from_adapt_for_encode() {
1461 let data = [0u8; 8];
1464 let cmyk = PixelDescriptor::CMYK8;
1465 let target = PixelDescriptor::RGB8_SRGB;
1466 let err = adapt_for_encode(&data, cmyk, 2, 1, 8, &[target]).unwrap_err();
1467 assert!(
1468 matches!(
1469 *err.error(),
1470 ConvertError::NoPath { from, .. } if from.color_model() == ColorModel::Cmyk
1471 ),
1472 "got: {:?}",
1473 err.error()
1474 );
1475 let msg = format!("{}", err.error());
1478 assert!(
1479 msg.contains("CMYK") && msg.contains("moxcms"),
1480 "Display message lost CMYK hint: {msg}"
1481 );
1482 }
1483
1484 #[test]
1485 fn cmyk_input_returns_typed_error_from_convert_buffer() {
1486 let data = [0u8; 8];
1487 let err = convert_buffer(
1488 &data,
1489 2,
1490 1,
1491 PixelDescriptor::CMYK8,
1492 PixelDescriptor::RGB8_SRGB,
1493 )
1494 .unwrap_err();
1495 assert!(matches!(
1496 *err.error(),
1497 ConvertError::NoPath { from, .. } if from.color_model() == ColorModel::Cmyk
1498 ));
1499 }
1500
1501 #[test]
1502 fn cmyk_target_returns_typed_error_from_convert_buffer() {
1503 let data = [0u8; 6];
1505 let err = convert_buffer(
1506 &data,
1507 2,
1508 1,
1509 PixelDescriptor::RGB8_SRGB,
1510 PixelDescriptor::CMYK8,
1511 )
1512 .unwrap_err();
1513 assert!(matches!(
1514 *err.error(),
1515 ConvertError::NoPath { to, .. } if to.color_model() == ColorModel::Cmyk
1516 ));
1517 }
1518
1519 #[test]
1522 fn truncated_src_returns_buffer_size_error_from_adapt_for_encode() {
1523 let data = [255u8, 0, 0, 0, 255, 0];
1525 let err = adapt_for_encode(
1526 &data,
1527 PixelDescriptor::RGB8_SRGB,
1528 2,
1529 2,
1530 6, &[PixelDescriptor::RGB8_SRGB],
1532 )
1533 .unwrap_err();
1534 assert!(
1535 matches!(
1536 *err.error(),
1537 ConvertError::BufferSize {
1538 expected: 12,
1539 actual: 6
1540 }
1541 ),
1542 "got: {:?}",
1543 err.error()
1544 );
1545 }
1546
1547 #[test]
1548 fn truncated_src_returns_buffer_size_error_from_convert_buffer() {
1549 let data = [0u8; 8];
1551 let err = convert_buffer(
1552 &data,
1553 4,
1554 1,
1555 PixelDescriptor::RGBA8_SRGB,
1556 PixelDescriptor::RGB8_SRGB,
1557 )
1558 .unwrap_err();
1559 assert!(matches!(*err.error(), ConvertError::BufferSize { .. }));
1560 }
1561
1562 #[test]
1565 fn zero_rows_does_not_trigger_size_check() {
1566 let data: &[u8] = &[];
1568 let result = adapt_for_encode(
1569 data,
1570 PixelDescriptor::RGB8_SRGB,
1571 0,
1572 0,
1573 0,
1574 &[PixelDescriptor::RGB8_SRGB],
1575 );
1576 assert!(result.is_ok());
1577 }
1578
1579 #[test]
1580 fn extreme_rows_stride_returns_allocation_failed_not_panic() {
1581 let data = [0u8; 1];
1587 let err = convert_buffer(
1588 &data,
1589 1,
1590 u32::MAX,
1591 PixelDescriptor::RGB8_SRGB,
1592 PixelDescriptor::RGB8_SRGB, )
1594 .unwrap_err();
1595 assert!(
1596 matches!(
1597 *err.error(),
1598 ConvertError::AllocationFailed | ConvertError::BufferSize { .. }
1599 ),
1600 "got: {:?}",
1601 err.error()
1602 );
1603 }
1604}