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]
116pub fn adapt_for_encode<'a>(
117 data: &'a [u8],
118 descriptor: PixelDescriptor,
119 width: u32,
120 rows: u32,
121 stride: usize,
122 supported: &[PixelDescriptor],
123) -> Result<Adapted<'a>, At<ConvertError>> {
124 adapt_for_encode_with_intent(
125 data,
126 descriptor,
127 width,
128 rows,
129 stride,
130 supported,
131 ConvertIntent::Fastest,
132 )
133}
134
135#[track_caller]
139pub fn adapt_for_encode_with_intent<'a>(
140 data: &'a [u8],
141 descriptor: PixelDescriptor,
142 width: u32,
143 rows: u32,
144 stride: usize,
145 supported: &[PixelDescriptor],
146 intent: ConvertIntent,
147) -> Result<Adapted<'a>, At<ConvertError>> {
148 assert_not_cmyk(&descriptor);
149 if supported.is_empty() {
150 return Err(whereat::at!(ConvertError::EmptyFormatList));
151 }
152
153 if supported.contains(&descriptor) {
155 return Ok(Adapted {
156 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
157 descriptor,
158 width,
159 rows,
160 });
161 }
162
163 for &target in supported {
168 if descriptor.channel_type() == target.channel_type()
169 && descriptor.layout() == target.layout()
170 && descriptor.alpha() == target.alpha()
171 && descriptor.primaries == target.primaries
172 && descriptor.signal_range == target.signal_range
173 {
174 return Ok(Adapted {
175 data: contiguous_from_strided(
176 data,
177 width,
178 rows,
179 stride,
180 descriptor.bytes_per_pixel(),
181 ),
182 descriptor: target,
183 width,
184 rows,
185 });
186 }
187 }
188
189 let target = best_match(descriptor, supported, intent)
191 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
192
193 let mut converter = RowConverter::new(descriptor, target).at()?;
194
195 let src_bpp = descriptor.bytes_per_pixel();
196 let dst_bpp = target.bytes_per_pixel();
197 let dst_stride = (width as usize) * dst_bpp;
198 let mut output = vec![0u8; dst_stride * rows as usize];
199
200 for y in 0..rows {
201 let src_start = y as usize * stride;
202 let src_end = src_start + (width as usize * src_bpp);
203 let dst_start = y as usize * dst_stride;
204 let dst_end = dst_start + dst_stride;
205 converter.convert_row(
206 &data[src_start..src_end],
207 &mut output[dst_start..dst_end],
208 width,
209 );
210 }
211
212 Ok(Adapted {
213 data: Cow::Owned(output),
214 descriptor: target,
215 width,
216 rows,
217 })
218}
219
220#[track_caller]
224pub fn convert_buffer(
225 src: &[u8],
226 width: u32,
227 rows: u32,
228 from: PixelDescriptor,
229 to: PixelDescriptor,
230) -> Result<Vec<u8>, At<ConvertError>> {
231 assert_not_cmyk(&from);
232 assert_not_cmyk(&to);
233 if from == to {
234 return Ok(src.to_vec());
235 }
236
237 let mut converter = RowConverter::new(from, to).at()?;
238 let src_bpp = from.bytes_per_pixel();
239 let dst_bpp = to.bytes_per_pixel();
240 let src_stride = (width as usize) * src_bpp;
241 let dst_stride = (width as usize) * dst_bpp;
242 let mut output = vec![0u8; dst_stride * rows as usize];
243
244 for y in 0..rows {
245 let src_start = y as usize * src_stride;
246 let src_end = src_start + src_stride;
247 let dst_start = y as usize * dst_stride;
248 let dst_end = dst_start + dst_stride;
249 converter.convert_row(
250 &src[src_start..src_end],
251 &mut output[dst_start..dst_end],
252 width,
253 );
254 }
255
256 Ok(output)
257}
258
259pub fn try_adapt_in_place(
299 buf: &mut PixelBuffer,
300 target: PixelDescriptor,
301) -> Result<(), At<ConvertError>> {
302 let src = buf.descriptor();
303 let no_path = || {
304 Err(whereat::at!(ConvertError::NoPath {
305 from: src,
306 to: target
307 }))
308 };
309
310 if src.format == target.format {
312 buf.transform_in_place(|px| {
313 rewrap(px.bytes, px.width, px.rows, px.stride, target, px.color)
314 });
315 return Ok(());
316 }
317
318 if src.bytes_per_pixel() == target.bytes_per_pixel() {
321 let swappable = src.channel_type() == ChannelType::U8
322 && target.channel_type() == ChannelType::U8
323 && matches!(
324 (src.layout(), target.layout()),
325 (ChannelLayout::Rgba, ChannelLayout::Bgra)
326 | (ChannelLayout::Bgra, ChannelLayout::Rgba)
327 );
328 if !swappable {
329 return no_path();
330 }
331 buf.transform_in_place(|px| {
332 let width = px.width as usize;
333 let rows = px.rows as usize;
334 let _ = garb::bytes::rgba_to_bgra_inplace_strided(px.bytes, width, rows, px.stride);
337 rewrap(px.bytes, px.width, px.rows, px.stride, target, px.color)
338 });
339 return Ok(());
340 }
341
342 if !matches!(
347 src.alpha,
348 Some(AlphaMode::Undefined) | Some(AlphaMode::Opaque)
349 ) {
350 return no_path();
351 }
352 if src.channel_type() != target.channel_type() {
353 return no_path();
354 }
355 let map: &'static [usize] = match (src.layout(), target.layout()) {
358 (ChannelLayout::Rgba, ChannelLayout::Rgb) => &[0, 1, 2],
359 (ChannelLayout::Bgra, ChannelLayout::Rgb) => &[2, 1, 0],
362 (ChannelLayout::GrayAlpha, ChannelLayout::Gray) => &[0],
363 _ => return no_path(),
364 };
365
366 let in_bpp = src.bytes_per_pixel();
367 let out_bpp = target.bytes_per_pixel();
368 let elem = src.bytes_per_channel();
369
370 buf.transform_in_place(|px| drop_lane_impl(px, target, map, in_bpp, out_bpp, elem));
371 Ok(())
372}
373
374fn drop_lane_impl<'a>(
378 px: zenpixels::InPlacePixels<'a>,
379 target: PixelDescriptor,
380 map: &'static [usize],
381 in_bpp: usize,
382 out_bpp: usize,
383 elem: usize,
384) -> PixelSliceMut<'a> {
385 let width = px.width as usize;
386 let out_stride = px.stride - (px.stride % out_bpp);
395 for y in 0..px.rows as usize {
396 let sbase = y * px.stride;
397 let dbase = y * out_stride;
398 for x in 0..width {
399 let s = sbase + x * in_bpp;
400 let mut tmp = [0u8; 16];
401 tmp[..in_bpp].copy_from_slice(&px.bytes[s..s + in_bpp]);
402 let d = dbase + x * out_bpp;
403 for (k, &c) in map.iter().enumerate() {
404 px.bytes[d + k * elem..d + (k + 1) * elem]
405 .copy_from_slice(&tmp[c * elem..(c + 1) * elem]);
406 }
407 }
408 }
409 rewrap(px.bytes, px.width, px.rows, out_stride, target, px.color)
410}
411
412fn rewrap<'a>(
415 bytes: &'a mut [u8],
416 width: u32,
417 rows: u32,
418 stride: usize,
419 descriptor: PixelDescriptor,
420 color: Option<alloc::sync::Arc<zenpixels::ColorContext>>,
421) -> PixelSliceMut<'a> {
422 let out = PixelSliceMut::new(bytes, width, rows, stride, descriptor)
423 .expect("in-place adaptation geometry is always valid");
424 match color {
425 Some(c) => out.with_color_context(c),
426 None => out,
427 }
428}
429
430#[track_caller]
436pub fn adapt_for_encode_explicit<'a>(
437 data: &'a [u8],
438 descriptor: PixelDescriptor,
439 width: u32,
440 rows: u32,
441 stride: usize,
442 supported: &[PixelDescriptor],
443 options: &ConvertOptions,
444) -> Result<Adapted<'a>, At<ConvertError>> {
445 assert_not_cmyk(&descriptor);
446 if supported.is_empty() {
447 return Err(whereat::at!(ConvertError::EmptyFormatList));
448 }
449
450 if supported.contains(&descriptor) {
452 return Ok(Adapted {
453 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
454 descriptor,
455 width,
456 rows,
457 });
458 }
459
460 for &target in supported {
462 if descriptor.channel_type() == target.channel_type()
463 && descriptor.layout() == target.layout()
464 && descriptor.alpha() == target.alpha()
465 && descriptor.primaries == target.primaries
466 && descriptor.signal_range == target.signal_range
467 {
468 return Ok(Adapted {
469 data: contiguous_from_strided(
470 data,
471 width,
472 rows,
473 stride,
474 descriptor.bytes_per_pixel(),
475 ),
476 descriptor: target,
477 width,
478 rows,
479 });
480 }
481 }
482
483 let target = best_match(descriptor, supported, ConvertIntent::Fastest)
485 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
486
487 let plan = ConvertPlan::new_explicit(descriptor, target, options).at()?;
489
490 let drops_alpha = descriptor.alpha().is_some() && target.alpha().is_none();
492 if drops_alpha && options.alpha_policy == AlphaPolicy::DiscardIfOpaque {
493 let src_bpp = descriptor.bytes_per_pixel();
494 if !is_fully_opaque(data, width, rows, stride, src_bpp, &descriptor) {
495 return Err(whereat::at!(ConvertError::AlphaNotOpaque));
496 }
497 }
498
499 let mut converter = RowConverter::from_plan(plan);
500 let src_bpp = descriptor.bytes_per_pixel();
501 let dst_bpp = target.bytes_per_pixel();
502 let dst_stride = (width as usize) * dst_bpp;
503 let mut output = vec![0u8; dst_stride * rows as usize];
504
505 for y in 0..rows {
506 let src_start = y as usize * stride;
507 let src_end = src_start + (width as usize * src_bpp);
508 let dst_start = y as usize * dst_stride;
509 let dst_end = dst_start + dst_stride;
510 converter.convert_row(
511 &data[src_start..src_end],
512 &mut output[dst_start..dst_end],
513 width,
514 );
515 }
516
517 Ok(Adapted {
518 data: Cow::Owned(output),
519 descriptor: target,
520 width,
521 rows,
522 })
523}
524
525fn is_fully_opaque(
527 data: &[u8],
528 width: u32,
529 rows: u32,
530 stride: usize,
531 bpp: usize,
532 desc: &PixelDescriptor,
533) -> bool {
534 if desc.alpha().is_none() {
535 return true;
536 }
537 let cs = desc.channel_type().byte_size();
538 let alpha_offset = (desc.layout().channels() - 1) * cs;
539 for y in 0..rows {
540 let row_start = y as usize * stride;
541 for x in 0..width as usize {
542 let off = row_start + x * bpp + alpha_offset;
543 match desc.channel_type() {
544 crate::ChannelType::U8 => {
545 if data[off] != 255 {
546 return false;
547 }
548 }
549 crate::ChannelType::U16 => {
550 let v = u16::from_ne_bytes([data[off], data[off + 1]]);
551 if v != 65535 {
552 return false;
553 }
554 }
555 crate::ChannelType::F32 => {
556 let v = f32::from_ne_bytes([
557 data[off],
558 data[off + 1],
559 data[off + 2],
560 data[off + 3],
561 ]);
562 if v < 1.0 {
563 return false;
564 }
565 }
566 _ => return false,
567 }
568 }
569 }
570 true
571}
572
573fn contiguous_from_strided<'a>(
575 data: &'a [u8],
576 width: u32,
577 rows: u32,
578 stride: usize,
579 bpp: usize,
580) -> Cow<'a, [u8]> {
581 let row_bytes = width as usize * bpp;
582 if stride == row_bytes {
583 let total = row_bytes * rows as usize;
585 Cow::Borrowed(&data[..total])
586 } else {
587 let mut packed = Vec::with_capacity(row_bytes * rows as usize);
589 for y in 0..rows as usize {
590 let start = y * stride;
591 packed.extend_from_slice(&data[start..start + row_bytes]);
592 }
593 Cow::Owned(packed)
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use zenpixels::descriptor::{ColorPrimaries, SignalRange};
601 use zenpixels::policy::{AlphaPolicy, DepthPolicy};
602
603 fn test_rgb8_data() -> Vec<u8> {
605 vec![255, 0, 0, 0, 255, 0]
606 }
607
608 fn buf_from(bytes: &[u8], w: u32, h: u32, desc: PixelDescriptor) -> zenpixels::PixelBuffer {
611 zenpixels::PixelBuffer::from_vec(bytes.to_vec(), w, h, desc).unwrap()
612 }
613
614 #[test]
615 fn in_place_bgra_to_rgba_swaps_bytes_and_updates_buffer() {
616 let mut buf = buf_from(
618 &[10u8, 20, 30, 255, 40, 50, 60, 128],
619 2,
620 1,
621 PixelDescriptor::BGRA8_SRGB,
622 );
623 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB)
624 .expect("4bpp B<->R swap is in-place");
625 assert_eq!(buf.descriptor(), PixelDescriptor::RGBA8_SRGB);
626 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 255, 60, 50, 40, 128]);
627 }
628
629 #[test]
630 fn in_place_rgba_to_bgra_roundtrips() {
631 let original = [1u8, 2, 3, 4, 5, 6, 7, 8];
632 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGBA8_SRGB);
633 try_adapt_in_place(&mut buf, PixelDescriptor::BGRA8_SRGB).expect("to bgra");
634 assert_eq!(buf.as_slice().row(0), &[3u8, 2, 1, 4, 7, 6, 5, 8]);
635 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB).expect("back to rgba");
636 assert_eq!(buf.as_slice().row(0), &original[..]);
637 }
638
639 #[test]
640 fn in_place_swap_respects_stride_padding() {
641 let mut buf =
644 zenpixels::PixelBuffer::new_simd_aligned(1, 2, PixelDescriptor::BGRA8_SRGB, 16);
645 assert_eq!(buf.stride(), 16, "fixture must be strided");
646 {
647 let mut view = buf.as_slice_mut();
648 view.row_mut(0).copy_from_slice(&[10, 20, 30, 255]);
649 view.row_mut(1).copy_from_slice(&[40, 50, 60, 128]);
650 let backing = view.as_strided_bytes_mut();
651 backing[4..16].fill(0xAA);
652 backing[20..32].fill(0xBB);
653 }
654 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA8_SRGB).expect("strided swap");
655 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 255]);
656 assert_eq!(buf.as_slice().row(1), &[60u8, 50, 40, 128]);
657 let view = buf.as_slice();
658 let backing = view.as_strided_bytes();
659 assert!(
660 backing[4..16].iter().all(|&b| b == 0xAA),
661 "row-0 padding must be untouched"
662 );
663 assert!(
664 backing[20..28].iter().all(|&b| b == 0xBB),
665 "row-1 padding must be untouched"
666 );
667 }
668
669 #[test]
670 fn in_place_metadata_retag_moves_no_bytes() {
671 let original = [1u8, 2, 3, 4, 5, 6];
672 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGB8);
673 let target = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
674 try_adapt_in_place(&mut buf, target).expect("same-format retag");
675 assert_eq!(buf.descriptor(), target);
676 assert_eq!(buf.as_slice().row(0), &original[..]);
677 }
678
679 #[test]
680 fn in_place_rejects_live_alpha_drop_and_depth_changes_unchanged() {
681 let original = [1u8, 2, 3, 4, 5, 6, 7, 8];
684 let mut buf = buf_from(&original, 2, 1, PixelDescriptor::RGBA8_SRGB);
685 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB)
686 .expect_err("straight-alpha drop is not contract-exact");
687 assert_eq!(buf.descriptor(), PixelDescriptor::RGBA8_SRGB);
688 assert_eq!(buf.as_slice().row(0), &original[..]);
689
690 try_adapt_in_place(&mut buf, PixelDescriptor::RGBA16_SRGB)
692 .expect_err("depth change cannot be in-place");
693 assert_eq!(buf.as_slice().row(0), &original[..]);
694 }
695
696 #[test]
697 fn in_place_rgbx_to_rgb_compacts_and_buffer_adopts_geometry() {
698 let mut buf = buf_from(
703 &[
704 1u8, 2, 3, 0xEE, 4, 5, 6, 0xEE, 7, 8, 9, 0xEE, 10, 11, 12, 0xEE, ],
707 2,
708 2,
709 PixelDescriptor::RGBX8_SRGB,
710 );
711 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB)
712 .expect("padding drop is contract-exact");
713 assert_eq!(buf.descriptor(), PixelDescriptor::RGB8_SRGB);
714 assert_eq!(buf.stride(), 6);
715 assert_eq!(buf.as_slice().row(0), &[1u8, 2, 3, 4, 5, 6]);
716 assert_eq!(buf.as_slice().row(1), &[7u8, 8, 9, 10, 11, 12]);
717 }
718
719 #[test]
720 fn drop_lane_impl_keeps_divisible_stride_rows_in_place() {
721 let mut bytes = [
726 1u8, 2, 3, 0xEE, 4, 5, 6, 0xEE, 0xAA, 0xAA, 0xAA, 0xAA, 7, 8, 9, 0xEE, 10, 11, 12, 0xEE, 0xBB, 0xBB, 0xBB, 0xBB, ];
729 let px = zenpixels::InPlacePixels {
730 bytes: &mut bytes,
731 width: 2,
732 rows: 2,
733 stride: 12,
734 descriptor: PixelDescriptor::RGBX8_SRGB,
735 color: None,
736 };
737 let out = drop_lane_impl(px, PixelDescriptor::RGB8_SRGB, &[0, 1, 2], 4, 3, 1);
738 assert_eq!(out.stride(), 12, "divisible stride preserved verbatim");
739 assert_eq!(out.row(0), &[1u8, 2, 3, 4, 5, 6]);
740 assert_eq!(out.row(1), &[7u8, 8, 9, 10, 11, 12]);
741 drop(out);
742 assert_eq!(&bytes[8..12], &[0xAA; 4], "row-0 tail padding untouched");
743 assert_eq!(&bytes[20..24], &[0xBB; 4], "row-1 tail padding untouched");
744 }
745
746 #[test]
747 fn in_place_opaque_bgra_to_rgb_reorders_while_dropping() {
748 let mut buf = buf_from(
750 &[10u8, 20, 30, 255, 40, 50, 60, 255],
751 2,
752 1,
753 PixelDescriptor::BGRA8_SRGB.with_alpha_mode(Some(AlphaMode::Opaque)),
754 );
755 try_adapt_in_place(&mut buf, PixelDescriptor::RGB8_SRGB).expect("opaque drop allowed");
756 assert_eq!(buf.as_slice().row(0), &[30u8, 20, 10, 60, 50, 40]);
757 }
758
759 #[test]
760 fn in_place_opaque_rgba16_to_rgb16_drops_lane() {
761 let px16 = |r: u16, g: u16, b: u16| {
763 [r, g, b, 0xFFFF]
764 .iter()
765 .flat_map(|v| v.to_ne_bytes())
766 .collect::<Vec<u8>>()
767 };
768 let bytes: Vec<u8> = [px16(0x1234, 0x5678, 0x9ABC), px16(0x1111, 0x2222, 0x3333)].concat();
769 let mut buf = buf_from(
770 &bytes,
771 2,
772 1,
773 PixelDescriptor::RGBA16_SRGB.with_alpha_mode(Some(AlphaMode::Opaque)),
774 );
775 try_adapt_in_place(&mut buf, PixelDescriptor::RGB16_SRGB).expect("u16 lane drop");
776 let expected: Vec<u8> = [0x1234u16, 0x5678, 0x9ABC, 0x1111, 0x2222, 0x3333]
777 .iter()
778 .flat_map(|v| v.to_ne_bytes())
779 .collect();
780 assert_eq!(buf.as_slice().row(0), &expected[..]);
781 assert_eq!(buf.stride(), 12, "16-px input stride rounds to 12");
782 }
783
784 #[test]
785 fn in_place_opaque_graya_to_gray_matches_allocating_path() {
786 let original = [10u8, 255, 20, 255, 30, 255, 40, 255];
788 let src = PixelDescriptor::new(
789 ChannelType::U8,
790 ChannelLayout::GrayAlpha,
791 Some(AlphaMode::Opaque),
792 zenpixels::TransferFunction::Srgb,
793 );
794 let target = PixelDescriptor::GRAY8_SRGB;
795
796 let mut buf = buf_from(&original, 4, 1, src);
797 try_adapt_in_place(&mut buf, target).expect("graya drop");
798 let in_place_row = buf.as_slice().row(0).to_vec();
799
800 let allocated = convert_buffer(&original, 4, 1, src, target).expect("allocating path");
801 assert_eq!(in_place_row, allocated, "in-place must match allocating");
802 }
803
804 #[test]
805 fn transfer_agnostic_match_requires_same_primaries() {
806 let data = test_rgb8_data();
807 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
808 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
811
812 assert!(
816 matches!(result.data, Cow::Owned(_)),
817 "different primaries must trigger conversion, not zero-copy relabel"
818 );
819 }
820
821 #[test]
822 fn transfer_agnostic_match_requires_same_signal_range() {
823 let data = test_rgb8_data();
824 let source = PixelDescriptor::RGB8.with_signal_range(SignalRange::Narrow);
825 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
828
829 assert!(
831 matches!(result.data, Cow::Owned(_)),
832 "different signal range must trigger conversion, not zero-copy relabel"
833 );
834 }
835
836 #[test]
837 fn transfer_agnostic_match_allows_zero_copy_when_all_match() {
838 let data = test_rgb8_data();
839 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt709);
841 let target = PixelDescriptor::RGB8_SRGB;
843
844 let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
845
846 assert!(
848 matches!(result.data, Cow::Borrowed(_)),
849 "should be zero-copy when only transfer differs"
850 );
851 assert_eq!(result.descriptor, target);
852 }
853
854 #[test]
855 fn exact_match_is_zero_copy() {
856 let data = test_rgb8_data();
857 let desc = PixelDescriptor::RGB8_SRGB;
858
859 let result = adapt_for_encode(&data, desc, 2, 1, 6, &[desc]).unwrap();
860
861 assert!(matches!(result.data, Cow::Borrowed(_)));
862 assert_eq!(result.descriptor, desc);
863 }
864
865 #[test]
866 #[should_panic(expected = "CMYK pixel data cannot be processed")]
867 fn cmyk_rejected_by_adapt_for_encode() {
868 let cmyk_data = vec![0u8; 4 * 4]; let _ = adapt_for_encode(
870 &cmyk_data,
871 PixelDescriptor::CMYK8,
872 2,
873 2,
874 8,
875 &[PixelDescriptor::RGB8_SRGB],
876 );
877 }
878
879 #[test]
880 #[should_panic(expected = "CMYK pixel data cannot be processed")]
881 fn cmyk_rejected_by_convert_buffer() {
882 let cmyk_data = vec![0u8; 4 * 4];
883 let _ = convert_buffer(
884 &cmyk_data,
885 2,
886 2,
887 PixelDescriptor::CMYK8,
888 PixelDescriptor::RGB8_SRGB,
889 );
890 }
891
892 #[test]
893 #[should_panic(expected = "CMYK pixel data cannot be processed")]
894 fn cmyk_rejected_by_convert_buffer_as_target() {
895 let rgb_data = vec![0u8; 3 * 4];
896 let _ = convert_buffer(
897 &rgb_data,
898 2,
899 2,
900 PixelDescriptor::RGB8_SRGB,
901 PixelDescriptor::CMYK8,
902 );
903 }
904
905 #[test]
906 fn explicit_variant_also_checks_primaries() {
907 let data = test_rgb8_data();
908 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
909 let target = PixelDescriptor::RGB8_SRGB;
910 let options = ConvertOptions::forbid_lossy()
911 .with_alpha_policy(AlphaPolicy::DiscardUnchecked)
912 .with_depth_policy(DepthPolicy::Round);
913
914 let result =
915 adapt_for_encode_explicit(&data, source, 2, 1, 6, &[target], &options).unwrap();
916
917 assert!(
918 matches!(result.data, Cow::Owned(_)),
919 "explicit variant: different primaries must trigger conversion"
920 );
921 }
922}