1use alloc::boxed::Box;
7
8use crate::convert::{ConvertPlan, ConvertScratch, convert_row_buffered};
9use crate::{ChannelLayout, ConvertError, PixelDescriptor};
10use whereat::{At, ResultAtExt};
11
12pub struct RowConverter {
33 plan: ConvertPlan,
34 scratch: ConvertScratch,
35 external: Option<ExternalTransform>,
38}
39
40enum ExternalTransform {
42 Shared(alloc::sync::Arc<dyn crate::cms::RowTransform>),
44 Owned(Box<dyn crate::cms::RowTransformMut>),
46}
47
48impl RowConverter {
49 #[track_caller]
56 pub fn new(from: PixelDescriptor, to: PixelDescriptor) -> Result<Self, At<ConvertError>> {
57 Self::new_explicit_with_cms(from, to, &crate::policy::ConvertOptions::permissive(), None)
58 }
59
60 #[track_caller]
68 pub fn new_explicit(
69 from: PixelDescriptor,
70 to: PixelDescriptor,
71 options: &crate::policy::ConvertOptions,
72 ) -> Result<Self, At<ConvertError>> {
73 Self::new_explicit_with_cms(from, to, options, None)
74 }
75
76 #[track_caller]
89 pub fn new_explicit_with_cms(
90 from: PixelDescriptor,
91 to: PixelDescriptor,
92 options: &crate::policy::ConvertOptions,
93 cms: Option<&dyn crate::cms::PluggableCms>,
94 ) -> Result<Self, At<ConvertError>> {
95 use crate::policy::{AlphaPolicy, DepthPolicy};
96
97 let primaries_differ = from.primaries != to.primaries;
110 if primaries_differ {
111 let src_src = from.color_profile_source();
112 let dst_src = to.color_profile_source();
113 let src_fmt = from.pixel_format();
114 let dst_fmt = to.pixel_format();
115
116 let try_cms = |plugin: &dyn crate::cms::PluggableCms| -> Result<
125 Option<ExternalTransform>,
126 whereat::At<crate::cms::CmsPluginError>,
127 > {
128 if let Some(result) = plugin.build_shared_source_transform(
129 src_src.clone(),
130 dst_src.clone(),
131 src_fmt,
132 dst_fmt,
133 options,
134 ) {
135 return whereat::at_crate!(result).map(|t| Some(ExternalTransform::Shared(t)));
136 }
137 if let Some(result) = plugin.build_source_transform(
138 src_src.clone(),
139 dst_src.clone(),
140 src_fmt,
141 dst_fmt,
142 options,
143 ) {
144 return whereat::at_crate!(result).map(|t| Some(ExternalTransform::Owned(t)));
145 }
146 Ok(None)
147 };
148
149 let plugin_err = |e: whereat::At<crate::cms::CmsPluginError>| {
153 whereat::at!(ConvertError::CmsError(alloc::format!("{e}")))
154 };
155
156 let mut external: Option<ExternalTransform> = None;
157 if let Some(plugin) = cms {
158 match try_cms(plugin) {
159 Ok(Some(t)) => external = Some(t),
160 Ok(None) => {}
161 Err(e) => return Err(plugin_err(e)),
162 }
163 }
164 if external.is_none() {
165 match try_cms(&crate::cms_lite::ZenCmsLite) {
166 Ok(Some(t)) => external = Some(t),
167 Ok(None) => {}
168 Err(e) => return Err(plugin_err(e)),
169 }
170 }
171
172 if let Some(external) = external {
173 let drops_alpha = from.alpha().is_some() && to.alpha().is_none();
175 if drops_alpha && options.alpha_policy == AlphaPolicy::Forbid {
176 return Err(whereat::at!(ConvertError::AlphaRemovalForbidden));
177 }
178 let reduces_depth = crate::negotiate::channel_bits(from.channel_type())
180 > crate::negotiate::channel_bits(to.channel_type());
181 if reduces_depth && options.depth_policy == DepthPolicy::Forbid {
182 return Err(whereat::at!(ConvertError::DepthReductionForbidden));
183 }
184 let src_is_rgb = matches!(
185 from.layout(),
186 ChannelLayout::Rgb | ChannelLayout::Rgba | ChannelLayout::Bgra
187 );
188 let dst_is_gray =
189 matches!(to.layout(), ChannelLayout::Gray | ChannelLayout::GrayAlpha);
190 if src_is_rgb && dst_is_gray && options.luma.is_none() {
191 return Err(whereat::at!(ConvertError::RgbToGray));
192 }
193
194 return Ok(Self {
195 plan: ConvertPlan::identity(from, to),
196 scratch: ConvertScratch::new(),
197 external: Some(external),
198 });
199 }
200 }
201
202 let plan = ConvertPlan::new_explicit(from, to, options).at()?;
204 Ok(Self {
205 plan,
206 scratch: ConvertScratch::new(),
207 external: None,
208 })
209 }
210
211 pub fn from_plan(plan: ConvertPlan) -> Self {
213 Self {
214 plan,
215 scratch: ConvertScratch::new(),
216 external: None,
217 }
218 }
219
220 #[inline]
228 pub fn convert_row(&mut self, src: &[u8], dst: &mut [u8], width: u32) {
229 match &mut self.external {
230 Some(ExternalTransform::Shared(arc)) => arc.transform_row(src, dst, width),
231 Some(ExternalTransform::Owned(b)) => b.transform_row(src, dst, width),
232 None => convert_row_buffered(&self.plan, src, dst, width, &mut self.scratch),
233 }
234 }
235
236 #[track_caller]
240 pub fn convert_rows(
241 &mut self,
242 src: &[u8],
243 src_stride: usize,
244 dst: &mut [u8],
245 dst_stride: usize,
246 width: u32,
247 rows: u32,
248 ) -> Result<(), At<ConvertError>> {
249 for y in 0..rows {
250 let src_start = y as usize * src_stride;
251 let src_end = src_start + (width as usize * self.plan.from().bytes_per_pixel());
252 let dst_start = y as usize * dst_stride;
253 let dst_end = dst_start + (width as usize * self.plan.to().bytes_per_pixel());
254
255 if src_end > src.len() || dst_end > dst.len() {
256 return Err(whereat::at!(ConvertError::BufferSize {
257 expected: dst_end,
258 actual: dst.len(),
259 }));
260 }
261
262 self.convert_row(
263 &src[src_start..src_end],
264 &mut dst[dst_start..dst_end],
265 width,
266 );
267 }
268 Ok(())
269 }
270
271 #[inline]
273 #[must_use]
274 pub fn is_identity(&self) -> bool {
275 self.external.is_none() && self.plan.is_identity()
279 }
280
281 #[inline]
283 pub fn from_descriptor(&self) -> PixelDescriptor {
284 self.plan.from()
285 }
286
287 #[inline]
289 pub fn to_descriptor(&self) -> PixelDescriptor {
290 self.plan.to()
291 }
292
293 pub fn compose(&self, other: &Self) -> Option<Self> {
301 self.plan.compose(&other.plan).map(Self::from_plan)
302 }
303
304 pub fn plan(&self) -> &ConvertPlan {
306 &self.plan
307 }
308}
309
310impl Clone for RowConverter {
311 fn clone(&self) -> Self {
312 let external = match &self.external {
316 Some(ExternalTransform::Shared(arc)) => {
317 Some(ExternalTransform::Shared(alloc::sync::Arc::clone(arc)))
318 }
319 Some(ExternalTransform::Owned(_)) | None => None,
320 };
321 Self {
322 plan: self.plan.clone(),
323 scratch: ConvertScratch::new(),
324 external,
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use alloc::vec;
332
333 use super::*;
334 use crate::convert::ConvertPlan;
335 use crate::policy::{AlphaPolicy, ConvertOptions, DepthPolicy};
336 use crate::{AlphaMode, ChannelLayout, ChannelType, ConvertError, TransferFunction};
337
338 fn convert_pixel(
340 from: PixelDescriptor,
341 to: PixelDescriptor,
342 src: &[u8],
343 ) -> alloc::vec::Vec<u8> {
344 let mut conv = RowConverter::new(from, to).unwrap();
345 let dst_bpp = to.bytes_per_pixel();
346 let mut dst = vec![0u8; dst_bpp];
347 conv.convert_row(src, &mut dst, 1);
348 dst
349 }
350
351 #[test]
356 fn identity_conversion() {
357 let desc = PixelDescriptor::RGB8_SRGB;
358 let mut conv = RowConverter::new(desc, desc).unwrap();
359 assert!(conv.is_identity());
360 assert_eq!(conv.from_descriptor(), desc);
361 assert_eq!(conv.to_descriptor(), desc);
362
363 let src = [10u8, 20, 30, 40, 50, 60];
364 let mut dst = [0u8; 6];
365 conv.convert_row(&src, &mut dst, 2);
366 assert_eq!(dst, src);
367 }
368
369 #[test]
374 fn rgb8_to_rgba8() {
375 let dst = convert_pixel(
376 PixelDescriptor::RGB8_SRGB,
377 PixelDescriptor::RGBA8_SRGB,
378 &[100, 150, 200],
379 );
380 assert_eq!(dst, [100, 150, 200, 255]);
381 }
382
383 #[test]
384 fn rgba8_to_rgb8() {
385 let dst = convert_pixel(
386 PixelDescriptor::RGBA8_SRGB,
387 PixelDescriptor::RGB8_SRGB,
388 &[100, 150, 200, 128],
389 );
390 assert_eq!(dst, [100, 150, 200]);
391 }
392
393 #[test]
394 fn bgra8_to_rgba8() {
395 let dst = convert_pixel(
396 PixelDescriptor::BGRA8_SRGB,
397 PixelDescriptor::RGBA8_SRGB,
398 &[200, 150, 100, 255], );
400 assert_eq!(dst, [100, 150, 200, 255]); }
402
403 #[test]
404 fn rgba8_to_bgra8() {
405 let dst = convert_pixel(
406 PixelDescriptor::RGBA8_SRGB,
407 PixelDescriptor::BGRA8_SRGB,
408 &[100, 150, 200, 255],
409 );
410 assert_eq!(dst, [200, 150, 100, 255]);
411 }
412
413 #[test]
414 fn rgb8_to_bgra8() {
415 let dst = convert_pixel(
417 PixelDescriptor::RGB8_SRGB,
418 PixelDescriptor::BGRA8_SRGB,
419 &[100, 150, 200],
420 );
421 assert_eq!(dst, [200, 150, 100, 255]);
422 }
423
424 #[test]
425 fn bgra8_to_rgb8() {
426 let dst = convert_pixel(
428 PixelDescriptor::BGRA8_SRGB,
429 PixelDescriptor::RGB8_SRGB,
430 &[200, 150, 100, 255],
431 );
432 assert_eq!(dst, [100, 150, 200]);
433 }
434
435 #[test]
436 fn gray8_to_rgb8() {
437 let dst = convert_pixel(
438 PixelDescriptor::GRAY8_SRGB,
439 PixelDescriptor::RGB8_SRGB,
440 &[128],
441 );
442 assert_eq!(dst, [128, 128, 128]);
443 }
444
445 #[test]
446 fn gray8_to_rgba8() {
447 let dst = convert_pixel(
448 PixelDescriptor::GRAY8_SRGB,
449 PixelDescriptor::RGBA8_SRGB,
450 &[200],
451 );
452 assert_eq!(dst, [200, 200, 200, 255]);
453 }
454
455 #[test]
456 fn gray8_to_bgra8() {
457 let dst = convert_pixel(
459 PixelDescriptor::GRAY8_SRGB,
460 PixelDescriptor::BGRA8_SRGB,
461 &[128],
462 );
463 assert_eq!(dst, [128, 128, 128, 255]);
465 }
466
467 #[test]
468 fn rgb8_to_gray8() {
469 let dst = convert_pixel(
471 PixelDescriptor::RGB8_SRGB,
472 PixelDescriptor::GRAY8_SRGB,
473 &[255, 0, 0],
474 );
475 assert_eq!(dst, [54]);
477 }
478
479 #[test]
480 fn rgba8_to_gray8() {
481 let dst = convert_pixel(
482 PixelDescriptor::RGBA8_SRGB,
483 PixelDescriptor::GRAY8_SRGB,
484 &[0, 255, 0, 255],
485 );
486 assert_eq!(dst, [182]);
488 }
489
490 #[test]
491 fn bgra8_to_gray8() {
492 let dst = convert_pixel(
494 PixelDescriptor::BGRA8_SRGB,
495 PixelDescriptor::GRAY8_SRGB,
496 &[0, 255, 0, 255], );
498 assert_eq!(dst, [182]);
500 }
501
502 #[test]
507 fn gray8_to_grayalpha8() {
508 let from = PixelDescriptor::new(
509 ChannelType::U8,
510 ChannelLayout::Gray,
511 None,
512 TransferFunction::Srgb,
513 );
514 let to = PixelDescriptor::new(
515 ChannelType::U8,
516 ChannelLayout::GrayAlpha,
517 Some(AlphaMode::Straight),
518 TransferFunction::Srgb,
519 );
520 let dst = convert_pixel(from, to, &[100]);
521 assert_eq!(dst, [100, 255]);
522 }
523
524 #[test]
525 fn grayalpha8_to_gray8() {
526 let from = PixelDescriptor::new(
527 ChannelType::U8,
528 ChannelLayout::GrayAlpha,
529 Some(AlphaMode::Straight),
530 TransferFunction::Srgb,
531 );
532 let to = PixelDescriptor::new(
533 ChannelType::U8,
534 ChannelLayout::Gray,
535 None,
536 TransferFunction::Srgb,
537 );
538 let dst = convert_pixel(from, to, &[100, 200]);
539 assert_eq!(dst, [100]); }
541
542 #[test]
543 fn grayalpha8_to_rgba8() {
544 let from = PixelDescriptor::new(
545 ChannelType::U8,
546 ChannelLayout::GrayAlpha,
547 Some(AlphaMode::Straight),
548 TransferFunction::Srgb,
549 );
550 let dst = convert_pixel(from, PixelDescriptor::RGBA8_SRGB, &[128, 200]);
551 assert_eq!(dst, [128, 128, 128, 200]);
552 }
553
554 #[test]
555 fn grayalpha8_to_rgb8() {
556 let from = PixelDescriptor::new(
557 ChannelType::U8,
558 ChannelLayout::GrayAlpha,
559 Some(AlphaMode::Straight),
560 TransferFunction::Srgb,
561 );
562 let dst = convert_pixel(from, PixelDescriptor::RGB8_SRGB, &[128, 200]);
563 assert_eq!(dst, [128, 128, 128]);
564 }
565
566 #[test]
567 fn grayalpha8_to_bgra8() {
568 let from = PixelDescriptor::new(
570 ChannelType::U8,
571 ChannelLayout::GrayAlpha,
572 Some(AlphaMode::Straight),
573 TransferFunction::Srgb,
574 );
575 let dst = convert_pixel(from, PixelDescriptor::BGRA8_SRGB, &[128, 200]);
576 assert_eq!(dst, [128, 128, 128, 200]);
578 }
579
580 #[test]
585 fn u8_to_u16_roundtrip() {
586 let u8_desc = PixelDescriptor::RGB8_SRGB;
587 let u16_desc = PixelDescriptor::new(
588 ChannelType::U16,
589 ChannelLayout::Rgb,
590 None,
591 TransferFunction::Srgb,
592 );
593 let src = [0u8, 128, 255];
594 let wide = convert_pixel(u8_desc, u16_desc, &src);
595 let wide16: &[u16] = bytemuck::cast_slice(&wide);
596 assert_eq!(wide16[0], 0); assert_eq!(wide16[1], 128 * 257); assert_eq!(wide16[2], 255 * 257); let narrow = convert_pixel(u16_desc, u8_desc, &wide);
602 assert_eq!(narrow, [0, 128, 255]);
603 }
604
605 #[test]
606 fn naive_u8_to_f32() {
607 let u8_desc = PixelDescriptor::new(
609 ChannelType::U8,
610 ChannelLayout::Rgb,
611 None,
612 TransferFunction::Linear,
613 );
614 let f32_desc = PixelDescriptor::new(
615 ChannelType::F32,
616 ChannelLayout::Rgb,
617 None,
618 TransferFunction::Linear,
619 );
620 let dst = convert_pixel(u8_desc, f32_desc, &[0, 128, 255]);
621 let f: &[f32] = bytemuck::cast_slice(&dst);
622 assert!((f[0] - 0.0).abs() < 1e-6);
623 assert!((f[1] - 128.0 / 255.0).abs() < 1e-5);
624 assert!((f[2] - 1.0).abs() < 1e-6);
625 }
626
627 #[test]
628 fn naive_f32_to_u8() {
629 let f32_desc = PixelDescriptor::new(
630 ChannelType::F32,
631 ChannelLayout::Rgb,
632 None,
633 TransferFunction::Linear,
634 );
635 let u8_desc = PixelDescriptor::new(
636 ChannelType::U8,
637 ChannelLayout::Rgb,
638 None,
639 TransferFunction::Linear,
640 );
641 let src_f: [f32; 3] = [0.0, 0.5, 1.0];
642 let src: &[u8] = bytemuck::cast_slice(&src_f);
643 let dst = convert_pixel(f32_desc, u8_desc, src);
644 assert_eq!(dst[0], 0);
645 assert_eq!(dst[1], 128); assert_eq!(dst[2], 255);
647 }
648
649 #[test]
650 fn srgb_u8_to_linear_f32() {
651 let dst = convert_pixel(
652 PixelDescriptor::RGB8_SRGB,
653 PixelDescriptor::new(
654 ChannelType::F32,
655 ChannelLayout::Rgb,
656 None,
657 TransferFunction::Linear,
658 ),
659 &[0, 128, 255],
660 );
661 let f: &[f32] = bytemuck::cast_slice(&dst);
662 assert!((f[0] - 0.0).abs() < 1e-6);
663 assert!((f[1] - 0.2158).abs() < 0.01);
665 assert!((f[2] - 1.0).abs() < 1e-5);
666 }
667
668 #[test]
669 fn linear_f32_to_srgb_u8() {
670 let f32_lin = PixelDescriptor::new(
671 ChannelType::F32,
672 ChannelLayout::Rgb,
673 None,
674 TransferFunction::Linear,
675 );
676 let src_f: [f32; 3] = [0.0, 0.5, 1.0];
677 let src: &[u8] = bytemuck::cast_slice(&src_f);
678 let dst = convert_pixel(f32_lin, PixelDescriptor::RGB8_SRGB, src);
679 assert_eq!(dst[0], 0);
680 assert!((dst[1] as i32 - 188).abs() <= 1);
682 assert_eq!(dst[2], 255);
683 }
684
685 #[test]
686 fn u16_to_f32_and_back() {
687 let u16_desc = PixelDescriptor::new(
688 ChannelType::U16,
689 ChannelLayout::Rgb,
690 None,
691 TransferFunction::Srgb,
692 );
693 let f32_desc = PixelDescriptor::new(
694 ChannelType::F32,
695 ChannelLayout::Rgb,
696 None,
697 TransferFunction::Srgb,
698 );
699 let src16: [u16; 3] = [0, 32768, 65535];
700 let src: &[u8] = bytemuck::cast_slice(&src16);
701 let mid = convert_pixel(u16_desc, f32_desc, src);
702 let f: &[f32] = bytemuck::cast_slice(&mid);
703 assert!((f[0] - 0.0).abs() < 1e-6);
704 assert!((f[1] - 0.5000076).abs() < 1e-4);
705 assert!((f[2] - 1.0).abs() < 1e-6);
706
707 let back = convert_pixel(f32_desc, u16_desc, &mid);
709 let back16: &[u16] = bytemuck::cast_slice(&back);
710 assert_eq!(back16[0], 0);
711 assert!((back16[1] as i32 - 32768).abs() <= 1);
712 assert_eq!(back16[2], 65535);
713 }
714
715 #[test]
720 fn pq_u16_to_linear_f32_roundtrip() {
721 let pq_u16 = PixelDescriptor::new(
722 ChannelType::U16,
723 ChannelLayout::Rgb,
724 None,
725 TransferFunction::Pq,
726 );
727 let lin_f32 = PixelDescriptor::new(
728 ChannelType::F32,
729 ChannelLayout::Rgb,
730 None,
731 TransferFunction::Linear,
732 );
733 let src16: [u16; 3] = [0, 32768, 65535];
734 let src: &[u8] = bytemuck::cast_slice(&src16);
735 let mid = convert_pixel(pq_u16, lin_f32, src);
736 let f: &[f32] = bytemuck::cast_slice(&mid);
737 assert_eq!(f[0], 0.0);
738 assert!(f[1] > 0.0 && f[1] < 1.0);
739
740 let back = convert_pixel(lin_f32, pq_u16, &mid);
742 let back16: &[u16] = bytemuck::cast_slice(&back);
743 assert_eq!(back16[0], 0);
744 assert!((back16[1] as i32 - 32768).abs() <= 2);
745 }
746
747 #[test]
748 fn pq_f32_to_linear_f32_roundtrip() {
749 let pq_f32 = PixelDescriptor::new(
750 ChannelType::F32,
751 ChannelLayout::Rgb,
752 None,
753 TransferFunction::Pq,
754 );
755 let lin_f32 = PixelDescriptor::new(
756 ChannelType::F32,
757 ChannelLayout::Rgb,
758 None,
759 TransferFunction::Linear,
760 );
761 let src_f: [f32; 3] = [0.0, 0.5, 1.0];
762 let src: &[u8] = bytemuck::cast_slice(&src_f);
763 let mid = convert_pixel(pq_f32, lin_f32, src);
764 let back = convert_pixel(lin_f32, pq_f32, &mid);
765 let back_f: &[f32] = bytemuck::cast_slice(&back);
766 for i in 0..3 {
767 assert!(
768 (back_f[i] - src_f[i]).abs() < 1e-4,
769 "PQ F32 roundtrip ch{i}: {:.6} vs {:.6}",
770 back_f[i],
771 src_f[i]
772 );
773 }
774 }
775
776 #[test]
777 fn hlg_u16_to_linear_f32_roundtrip() {
778 let hlg_u16 = PixelDescriptor::new(
779 ChannelType::U16,
780 ChannelLayout::Rgb,
781 None,
782 TransferFunction::Hlg,
783 );
784 let lin_f32 = PixelDescriptor::new(
785 ChannelType::F32,
786 ChannelLayout::Rgb,
787 None,
788 TransferFunction::Linear,
789 );
790 let src16: [u16; 3] = [0, 32768, 65535];
791 let src: &[u8] = bytemuck::cast_slice(&src16);
792 let mid = convert_pixel(hlg_u16, lin_f32, src);
793 let f: &[f32] = bytemuck::cast_slice(&mid);
794 assert_eq!(f[0], 0.0);
795 assert!(f[1] > 0.0);
796
797 let back = convert_pixel(lin_f32, hlg_u16, &mid);
798 let back16: &[u16] = bytemuck::cast_slice(&back);
799 assert_eq!(back16[0], 0);
800 assert!((back16[1] as i32 - 32768).abs() <= 2);
801 }
802
803 #[test]
804 fn hlg_f32_to_linear_f32_roundtrip() {
805 let hlg_f32 = PixelDescriptor::new(
806 ChannelType::F32,
807 ChannelLayout::Rgb,
808 None,
809 TransferFunction::Hlg,
810 );
811 let lin_f32 = PixelDescriptor::new(
812 ChannelType::F32,
813 ChannelLayout::Rgb,
814 None,
815 TransferFunction::Linear,
816 );
817 let src_f: [f32; 3] = [0.0, 0.5, 1.0];
818 let src: &[u8] = bytemuck::cast_slice(&src_f);
819 let mid = convert_pixel(hlg_f32, lin_f32, src);
820 let back = convert_pixel(lin_f32, hlg_f32, &mid);
821 let back_f: &[f32] = bytemuck::cast_slice(&back);
822 for i in 0..3 {
823 assert!(
824 (back_f[i] - src_f[i]).abs() < 1e-4,
825 "HLG F32 roundtrip ch{i}: {:.6} vs {:.6}",
826 back_f[i],
827 src_f[i]
828 );
829 }
830 }
831
832 #[test]
833 fn pq_to_hlg_via_linear_f32() {
834 let pq = PixelDescriptor::new(
835 ChannelType::F32,
836 ChannelLayout::Rgb,
837 None,
838 TransferFunction::Pq,
839 );
840 let hlg = PixelDescriptor::new(
841 ChannelType::F32,
842 ChannelLayout::Rgb,
843 None,
844 TransferFunction::Hlg,
845 );
846 let src_f: [f32; 3] = [0.0, 0.5, 1.0];
847 let src: &[u8] = bytemuck::cast_slice(&src_f);
848 let dst = convert_pixel(pq, hlg, src);
849 let f: &[f32] = bytemuck::cast_slice(&dst);
850 assert_eq!(f[0], 0.0);
852 assert!(f[1] > 0.0 && f[1] <= 1.0);
854 }
855
856 #[test]
857 fn hdr_u16_to_sdr_u8_pq() {
858 let pq_u16 = PixelDescriptor::new(
860 ChannelType::U16,
861 ChannelLayout::Rgb,
862 None,
863 TransferFunction::Pq,
864 );
865 let src16: [u16; 3] = [32768, 32768, 32768];
866 let src: &[u8] = bytemuck::cast_slice(&src16);
867 let dst = convert_pixel(pq_u16, PixelDescriptor::RGB8_SRGB, src);
868 assert!(dst[0] > 0 && dst[0] < 255);
870 }
871
872 #[test]
873 fn hdr_u16_to_sdr_u8_hlg() {
874 let hlg_u16 = PixelDescriptor::new(
875 ChannelType::U16,
876 ChannelLayout::Rgb,
877 None,
878 TransferFunction::Hlg,
879 );
880 let src16: [u16; 3] = [32768, 32768, 32768];
881 let src: &[u8] = bytemuck::cast_slice(&src16);
882 let dst = convert_pixel(hlg_u16, PixelDescriptor::RGB8_SRGB, src);
883 assert!(dst[0] > 0 && dst[0] < 255);
884 }
885
886 #[test]
891 fn straight_to_premul_u8() {
892 let straight = PixelDescriptor::RGBA8_SRGB;
893 let premul = PixelDescriptor::new(
894 ChannelType::U8,
895 ChannelLayout::Rgba,
896 Some(AlphaMode::Premultiplied),
897 TransferFunction::Srgb,
898 );
899 let dst = convert_pixel(straight, premul, &[200, 100, 50, 128]);
901 assert!((dst[0] as i32 - 100).abs() <= 1);
903 assert!((dst[1] as i32 - 50).abs() <= 1);
904 assert!((dst[2] as i32 - 25).abs() <= 1);
905 assert_eq!(dst[3], 128);
906 }
907
908 #[test]
909 fn premul_to_straight_u8() {
910 let premul = PixelDescriptor::new(
911 ChannelType::U8,
912 ChannelLayout::Rgba,
913 Some(AlphaMode::Premultiplied),
914 TransferFunction::Srgb,
915 );
916 let straight = PixelDescriptor::RGBA8_SRGB;
917 let dst = convert_pixel(premul, straight, &[100, 50, 25, 128]);
919 assert!((dst[0] as i32 - 200).abs() <= 1);
921 assert!((dst[1] as i32 - 100).abs() <= 1);
922 assert!((dst[2] as i32 - 50).abs() <= 1);
923 assert_eq!(dst[3], 128);
924 }
925
926 #[test]
927 fn premul_to_straight_zero_alpha() {
928 let premul = PixelDescriptor::new(
929 ChannelType::U8,
930 ChannelLayout::Rgba,
931 Some(AlphaMode::Premultiplied),
932 TransferFunction::Srgb,
933 );
934 let straight = PixelDescriptor::RGBA8_SRGB;
935 let dst = convert_pixel(premul, straight, &[0, 0, 0, 0]);
936 assert_eq!(dst, [0, 0, 0, 0]);
937 }
938
939 #[test]
940 fn straight_to_premul_f32() {
941 let straight = PixelDescriptor::new(
942 ChannelType::F32,
943 ChannelLayout::Rgba,
944 Some(AlphaMode::Straight),
945 TransferFunction::Linear,
946 );
947 let premul = PixelDescriptor::new(
948 ChannelType::F32,
949 ChannelLayout::Rgba,
950 Some(AlphaMode::Premultiplied),
951 TransferFunction::Linear,
952 );
953 let src_f: [f32; 4] = [1.0, 0.5, 0.25, 0.5];
954 let src: &[u8] = bytemuck::cast_slice(&src_f);
955 let dst = convert_pixel(straight, premul, src);
956 let f: &[f32] = bytemuck::cast_slice(&dst);
957 assert!((f[0] - 0.5).abs() < 1e-6);
958 assert!((f[1] - 0.25).abs() < 1e-6);
959 assert!((f[2] - 0.125).abs() < 1e-6);
960 assert!((f[3] - 0.5).abs() < 1e-6);
961 }
962
963 #[test]
968 fn rgb8_to_oklabf32_does_not_panic() {
969 let mut conv =
970 RowConverter::new(PixelDescriptor::RGB8_SRGB, PixelDescriptor::OKLABF32).unwrap();
971 assert!(!conv.is_identity());
972
973 let src = [128u8, 64, 200];
974 let mut dst = [0u8; 12];
975 conv.convert_row(&src, &mut dst, 1);
976
977 let oklab: [f32; 3] = bytemuck::cast(dst);
978 assert!(
979 oklab[0] >= 0.0 && oklab[0] <= 1.0,
980 "L out of range: {}",
981 oklab[0]
982 );
983 }
984
985 #[test]
986 fn oklabf32_roundtrip() {
987 let lin = PixelDescriptor::new(
989 ChannelType::F32,
990 ChannelLayout::Rgb,
991 None,
992 TransferFunction::Linear,
993 );
994 let oklab = PixelDescriptor::OKLABF32;
995
996 let src_f: [f32; 3] = [0.5, 0.3, 0.8];
997 let src: &[u8] = bytemuck::cast_slice(&src_f);
998 let mid = convert_pixel(lin, oklab, src);
999 let back = convert_pixel(oklab, lin, &mid);
1000 let back_f: &[f32] = bytemuck::cast_slice(&back);
1001 for i in 0..3 {
1002 assert!(
1003 (back_f[i] - src_f[i]).abs() < 1e-4,
1004 "Oklab roundtrip ch{i}: {:.6} vs {:.6}",
1005 back_f[i],
1006 src_f[i]
1007 );
1008 }
1009 }
1010
1011 #[test]
1012 fn oklabaf32_preserves_alpha() {
1013 let lin_rgba = PixelDescriptor::new(
1014 ChannelType::F32,
1015 ChannelLayout::Rgba,
1016 Some(AlphaMode::Straight),
1017 TransferFunction::Linear,
1018 );
1019 let oklaba = PixelDescriptor::OKLABAF32;
1020 let src_f: [f32; 4] = [0.5, 0.3, 0.8, 0.7];
1021 let src: &[u8] = bytemuck::cast_slice(&src_f);
1022 let mid = convert_pixel(lin_rgba, oklaba, src);
1023 let mid_f: &[f32] = bytemuck::cast_slice(&mid);
1024 assert!(
1025 (mid_f[3] - 0.7).abs() < 1e-6,
1026 "Alpha not preserved in Oklaba"
1027 );
1028
1029 let back = convert_pixel(oklaba, lin_rgba, &mid);
1030 let back_f: &[f32] = bytemuck::cast_slice(&back);
1031 assert!(
1032 (back_f[3] - 0.7).abs() < 1e-6,
1033 "Alpha not preserved on round-trip"
1034 );
1035 for i in 0..3 {
1036 assert!(
1037 (back_f[i] - src_f[i]).abs() < 1e-4,
1038 "Oklaba roundtrip ch{i}: {:.6} vs {:.6}",
1039 back_f[i],
1040 src_f[i]
1041 );
1042 }
1043 }
1044
1045 #[test]
1050 fn convert_rows_basic() {
1051 let mut conv =
1052 RowConverter::new(PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGBA8_SRGB).unwrap();
1053
1054 let src = [10u8, 20, 30, 40, 50, 60];
1055 let src_stride = 3; let mut dst = [0u8; 8]; let dst_stride = 4;
1058
1059 conv.convert_rows(&src, src_stride, &mut dst, dst_stride, 1, 2)
1060 .unwrap();
1061 assert_eq!(&dst[0..4], &[10, 20, 30, 255]);
1062 assert_eq!(&dst[4..8], &[40, 50, 60, 255]);
1063 }
1064
1065 #[test]
1066 fn convert_rows_buffer_too_small() {
1067 let mut conv =
1068 RowConverter::new(PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGBA8_SRGB).unwrap();
1069
1070 let src = [10u8, 20, 30];
1071 let mut dst = [0u8; 4];
1072 let err = conv.convert_rows(&src, 3, &mut dst, 4, 1, 2).unwrap_err();
1073 assert!(matches!(*err.error(), ConvertError::BufferSize { .. }));
1074 }
1075
1076 #[test]
1081 fn new_explicit_alpha_forbid() {
1082 let from = PixelDescriptor::RGBA8_SRGB;
1083 let to = PixelDescriptor::RGB8_SRGB;
1084 let opts = ConvertOptions::forbid_lossy().with_depth_policy(DepthPolicy::Round);
1085 let err = ConvertPlan::new_explicit(from, to, &opts).unwrap_err();
1086 assert_eq!(*err.error(), ConvertError::AlphaRemovalForbidden);
1087 }
1088
1089 #[test]
1090 fn new_explicit_depth_forbid() {
1091 let from = PixelDescriptor::new(
1092 ChannelType::U16,
1093 ChannelLayout::Rgb,
1094 None,
1095 TransferFunction::Srgb,
1096 );
1097 let to = PixelDescriptor::RGB8_SRGB;
1098 let opts = ConvertOptions::forbid_lossy().with_alpha_policy(AlphaPolicy::DiscardUnchecked);
1099 let err = ConvertPlan::new_explicit(from, to, &opts).unwrap_err();
1100 assert_eq!(*err.error(), ConvertError::DepthReductionForbidden);
1101 }
1102
1103 #[test]
1104 fn new_explicit_rgb_to_gray_requires_luma() {
1105 let opts = ConvertOptions::forbid_lossy()
1106 .with_alpha_policy(AlphaPolicy::DiscardUnchecked)
1107 .with_depth_policy(DepthPolicy::Round);
1108 let err = ConvertPlan::new_explicit(
1109 PixelDescriptor::RGB8_SRGB,
1110 PixelDescriptor::GRAY8_SRGB,
1111 &opts,
1112 )
1113 .unwrap_err();
1114 assert_eq!(*err.error(), ConvertError::RgbToGray);
1115 }
1116
1117 #[test]
1118 fn new_explicit_allows_when_policies_permit() {
1119 let opts = ConvertOptions::permissive().with_alpha_policy(AlphaPolicy::DiscardUnchecked);
1120 let plan = ConvertPlan::new_explicit(
1121 PixelDescriptor::RGBA8_SRGB,
1122 PixelDescriptor::GRAY8_SRGB,
1123 &opts,
1124 )
1125 .unwrap();
1126 assert!(!plan.is_identity());
1127 }
1128
1129 #[test]
1130 fn clip_out_of_gamut_false_preserves_negatives() {
1131 let p3 = PixelDescriptor::new(
1135 ChannelType::F32,
1136 ChannelLayout::Rgb,
1137 None,
1138 TransferFunction::Srgb,
1139 )
1140 .with_primaries(zenpixels::ColorPrimaries::DisplayP3);
1141 let srgb = PixelDescriptor::new(
1142 ChannelType::F32,
1143 ChannelLayout::Rgb,
1144 None,
1145 TransferFunction::Srgb,
1146 );
1147
1148 let opts = ConvertOptions::permissive().with_clip_out_of_gamut(false);
1149 let mut conv = crate::RowConverter::new_explicit(p3, srgb, &opts).unwrap();
1150
1151 let src: [f32; 3] = [0.0, 1.0, 0.0];
1152 let mut dst = [0.0f32; 3];
1153 conv.convert_row(
1154 bytemuck::cast_slice(&src),
1155 bytemuck::cast_slice_mut(&mut dst),
1156 1,
1157 );
1158 assert!(
1159 dst[0] < 0.0,
1160 "extended range should preserve negative red, got {}",
1161 dst[0]
1162 );
1163 }
1164
1165 #[test]
1166 fn clip_out_of_gamut_true_clamps_negatives() {
1167 let p3 = PixelDescriptor::new(
1169 ChannelType::F32,
1170 ChannelLayout::Rgb,
1171 None,
1172 TransferFunction::Srgb,
1173 )
1174 .with_primaries(zenpixels::ColorPrimaries::DisplayP3);
1175 let srgb = PixelDescriptor::new(
1176 ChannelType::F32,
1177 ChannelLayout::Rgb,
1178 None,
1179 TransferFunction::Srgb,
1180 );
1181
1182 let opts = ConvertOptions::permissive();
1183 assert!(opts.clip_out_of_gamut);
1184 let mut conv = crate::RowConverter::new_explicit(p3, srgb, &opts).unwrap();
1185
1186 let src: [f32; 3] = [0.0, 1.0, 0.0];
1187 let mut dst = [0.0f32; 3];
1188 conv.convert_row(
1189 bytemuck::cast_slice(&src),
1190 bytemuck::cast_slice_mut(&mut dst),
1191 1,
1192 );
1193 assert!(
1194 dst[0] >= 0.0,
1195 "clamped path should not produce negatives, got {}",
1196 dst[0]
1197 );
1198 }
1199
1200 struct PaintRedCms {
1207 accepted: core::sync::atomic::AtomicUsize,
1208 }
1209
1210 struct PaintRedTransform;
1211
1212 impl crate::cms::RowTransformMut for PaintRedTransform {
1213 fn transform_row(&mut self, _src: &[u8], dst: &mut [u8], width: u32) {
1214 for px in dst.chunks_exact_mut(3).take(width as usize) {
1215 px[0] = 255;
1216 px[1] = 0;
1217 px[2] = 0;
1218 }
1219 }
1220 }
1221
1222 impl crate::cms::PluggableCms for PaintRedCms {
1223 fn build_source_transform(
1224 &self,
1225 _src: zenpixels::ColorProfileSource<'_>,
1226 _dst: zenpixels::ColorProfileSource<'_>,
1227 _src_format: zenpixels::PixelFormat,
1228 _dst_format: zenpixels::PixelFormat,
1229 _options: &crate::policy::ConvertOptions,
1230 ) -> Option<
1231 Result<Box<dyn crate::cms::RowTransformMut>, whereat::At<crate::cms::CmsPluginError>>,
1232 > {
1233 self.accepted
1234 .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1235 Some(Ok(Box::new(PaintRedTransform)))
1236 }
1237 }
1238
1239 #[test]
1240 fn pluggable_cms_drives_row_when_profiles_differ() {
1241 let p3 = PixelDescriptor::RGB8_SRGB.with_primaries(zenpixels::ColorPrimaries::DisplayP3);
1244 let srgb = PixelDescriptor::RGB8_SRGB;
1245
1246 let cms = PaintRedCms {
1247 accepted: core::sync::atomic::AtomicUsize::new(0),
1248 };
1249 let opts = ConvertOptions::permissive();
1250 let mut conv =
1251 crate::RowConverter::new_explicit_with_cms(p3, srgb, &opts, Some(&cms)).unwrap();
1252
1253 assert_eq!(cms.accepted.load(core::sync::atomic::Ordering::Relaxed), 1);
1254
1255 let src = [10u8, 20, 30, 40, 50, 60];
1256 let mut dst = [0u8; 6];
1257 conv.convert_row(&src, &mut dst, 2);
1258 assert_eq!(dst, [255, 0, 0, 255, 0, 0]);
1259 }
1260
1261 #[test]
1262 fn pluggable_cms_skipped_when_profiles_match() {
1263 let cms = PaintRedCms {
1266 accepted: core::sync::atomic::AtomicUsize::new(0),
1267 };
1268 let opts = ConvertOptions::permissive();
1269 let conv = crate::RowConverter::new_explicit_with_cms(
1270 PixelDescriptor::RGB8_SRGB,
1271 PixelDescriptor::RGB8_SRGB,
1272 &opts,
1273 Some(&cms),
1274 )
1275 .unwrap();
1276 assert!(conv.is_identity());
1277 assert_eq!(cms.accepted.load(core::sync::atomic::Ordering::Relaxed), 0);
1278 }
1279
1280 #[test]
1281 fn pluggable_cms_shared_path_used_when_offered() {
1282 struct SharedPaintBlueCms;
1285 struct PaintBlueShared;
1286
1287 impl crate::cms::RowTransform for PaintBlueShared {
1288 fn transform_row(&self, _src: &[u8], dst: &mut [u8], width: u32) {
1289 for px in dst.chunks_exact_mut(3).take(width as usize) {
1290 px[0] = 0;
1291 px[1] = 0;
1292 px[2] = 255;
1293 }
1294 }
1295 }
1296
1297 impl crate::cms::PluggableCms for SharedPaintBlueCms {
1298 fn build_source_transform(
1299 &self,
1300 _src: zenpixels::ColorProfileSource<'_>,
1301 _dst: zenpixels::ColorProfileSource<'_>,
1302 _src_format: zenpixels::PixelFormat,
1303 _dst_format: zenpixels::PixelFormat,
1304 _options: &crate::policy::ConvertOptions,
1305 ) -> Option<
1306 Result<
1307 Box<dyn crate::cms::RowTransformMut>,
1308 whereat::At<crate::cms::CmsPluginError>,
1309 >,
1310 > {
1311 panic!("owned path must not be used when shared is offered");
1312 }
1313
1314 fn build_shared_source_transform(
1315 &self,
1316 _src: zenpixels::ColorProfileSource<'_>,
1317 _dst: zenpixels::ColorProfileSource<'_>,
1318 _src_format: zenpixels::PixelFormat,
1319 _dst_format: zenpixels::PixelFormat,
1320 _options: &crate::policy::ConvertOptions,
1321 ) -> Option<
1322 Result<
1323 alloc::sync::Arc<dyn crate::cms::RowTransform>,
1324 whereat::At<crate::cms::CmsPluginError>,
1325 >,
1326 > {
1327 Some(Ok(alloc::sync::Arc::new(PaintBlueShared)))
1328 }
1329 }
1330
1331 let p3 = PixelDescriptor::RGB8_SRGB.with_primaries(zenpixels::ColorPrimaries::DisplayP3);
1332 let srgb = PixelDescriptor::RGB8_SRGB;
1333 let opts = ConvertOptions::permissive();
1334 let conv =
1335 crate::RowConverter::new_explicit_with_cms(p3, srgb, &opts, Some(&SharedPaintBlueCms))
1336 .unwrap();
1337
1338 let mut conv2 = conv.clone();
1340 let mut dst = [0u8; 3];
1341 conv2.convert_row(&[1, 2, 3], &mut dst, 1);
1342 assert_eq!(
1343 dst,
1344 [0, 0, 255],
1345 "cloned converter should inherit shared transform"
1346 );
1347 }
1348
1349 #[test]
1350 fn pluggable_cms_declines_falls_back_to_builtin() {
1351 struct DeclineCms;
1353 impl crate::cms::PluggableCms for DeclineCms {
1354 fn build_source_transform(
1355 &self,
1356 _src: zenpixels::ColorProfileSource<'_>,
1357 _dst: zenpixels::ColorProfileSource<'_>,
1358 _src_format: zenpixels::PixelFormat,
1359 _dst_format: zenpixels::PixelFormat,
1360 _options: &crate::policy::ConvertOptions,
1361 ) -> Option<
1362 Result<
1363 Box<dyn crate::cms::RowTransformMut>,
1364 whereat::At<crate::cms::CmsPluginError>,
1365 >,
1366 > {
1367 None
1368 }
1369 }
1370
1371 let p3 = PixelDescriptor::RGB8_SRGB.with_primaries(zenpixels::ColorPrimaries::DisplayP3);
1372 let srgb = PixelDescriptor::RGB8_SRGB;
1373 let opts = ConvertOptions::permissive();
1374 let mut conv =
1375 crate::RowConverter::new_explicit_with_cms(p3, srgb, &opts, Some(&DeclineCms)).unwrap();
1376
1377 let src = [128u8, 128, 128];
1379 let mut dst = [0u8; 3];
1380 conv.convert_row(&src, &mut dst, 1);
1381 assert_ne!(dst, [255, 0, 0]);
1384 }
1385
1386 #[test]
1391 fn plan_accessors() {
1392 let from = PixelDescriptor::RGB8_SRGB;
1393 let to = PixelDescriptor::RGBA8_SRGB;
1394 let conv = RowConverter::new(from, to).unwrap();
1395 let plan = conv.plan();
1396 assert_eq!(plan.from(), from);
1397 assert_eq!(plan.to(), to);
1398 assert!(!plan.is_identity());
1399 }
1400
1401 #[test]
1402 fn from_plan() {
1403 let plan =
1404 ConvertPlan::new(PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGBA8_SRGB).unwrap();
1405 let conv = RowConverter::from_plan(plan);
1406 assert!(!conv.is_identity());
1407 }
1408
1409 #[test]
1414 fn convert_multiple_pixels() {
1415 let mut conv =
1416 RowConverter::new(PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGBA8_SRGB).unwrap();
1417 let src = [10, 20, 30, 40, 50, 60, 70, 80, 90];
1418 let mut dst = [0u8; 12];
1419 conv.convert_row(&src, &mut dst, 3);
1420 assert_eq!(dst, [10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255]);
1421 }
1422
1423 #[test]
1428 fn gray8_to_rgbaf32_linear() {
1429 let from = PixelDescriptor::GRAY8_SRGB;
1431 let to = PixelDescriptor::new(
1432 ChannelType::F32,
1433 ChannelLayout::Rgba,
1434 Some(AlphaMode::Straight),
1435 TransferFunction::Linear,
1436 );
1437 let dst = convert_pixel(from, to, &[128]);
1438 let f: &[f32] = bytemuck::cast_slice(&dst);
1439 assert!((f[0] - 0.2158).abs() < 0.01);
1441 assert!((f[0] - f[1]).abs() < 1e-6);
1442 assert!((f[0] - f[2]).abs() < 1e-6);
1443 assert!((f[3] - 1.0).abs() < 1e-6);
1444 }
1445
1446 #[test]
1447 fn rgba8_to_rgb16() {
1448 let from = PixelDescriptor::RGBA8_SRGB;
1450 let to = PixelDescriptor::new(
1451 ChannelType::U16,
1452 ChannelLayout::Rgb,
1453 None,
1454 TransferFunction::Srgb,
1455 );
1456 let dst = convert_pixel(from, to, &[100, 150, 200, 255]);
1457 let u16s: &[u16] = bytemuck::cast_slice(&dst);
1458 assert_eq!(u16s[0], 100 * 257);
1459 assert_eq!(u16s[1], 150 * 257);
1460 assert_eq!(u16s[2], 200 * 257);
1461 }
1462
1463 #[test]
1468 fn gray_u16_to_rgb_u16() {
1469 let from = PixelDescriptor::new(
1470 ChannelType::U16,
1471 ChannelLayout::Gray,
1472 None,
1473 TransferFunction::Srgb,
1474 );
1475 let to = PixelDescriptor::new(
1476 ChannelType::U16,
1477 ChannelLayout::Rgb,
1478 None,
1479 TransferFunction::Srgb,
1480 );
1481 let src16: [u16; 1] = [40000];
1482 let src: &[u8] = bytemuck::cast_slice(&src16);
1483 let dst = convert_pixel(from, to, src);
1484 let out: &[u16] = bytemuck::cast_slice(&dst);
1485 assert_eq!(out, [40000, 40000, 40000]);
1486 }
1487
1488 #[test]
1489 fn gray_f32_to_rgba_f32() {
1490 let from = PixelDescriptor::new(
1491 ChannelType::F32,
1492 ChannelLayout::Gray,
1493 None,
1494 TransferFunction::Linear,
1495 );
1496 let to = PixelDescriptor::new(
1497 ChannelType::F32,
1498 ChannelLayout::Rgba,
1499 Some(AlphaMode::Straight),
1500 TransferFunction::Linear,
1501 );
1502 let src_f: [f32; 1] = [0.6];
1503 let src: &[u8] = bytemuck::cast_slice(&src_f);
1504 let dst = convert_pixel(from, to, src);
1505 let out: &[f32] = bytemuck::cast_slice(&dst);
1506 assert!((out[0] - 0.6).abs() < 1e-6);
1507 assert!((out[1] - 0.6).abs() < 1e-6);
1508 assert!((out[2] - 0.6).abs() < 1e-6);
1509 assert!((out[3] - 1.0).abs() < 1e-6);
1510 }
1511
1512 #[test]
1513 fn rgb_f32_to_rgba_f32() {
1514 let from = PixelDescriptor::new(
1515 ChannelType::F32,
1516 ChannelLayout::Rgb,
1517 None,
1518 TransferFunction::Linear,
1519 );
1520 let to = PixelDescriptor::new(
1521 ChannelType::F32,
1522 ChannelLayout::Rgba,
1523 Some(AlphaMode::Straight),
1524 TransferFunction::Linear,
1525 );
1526 let src_f: [f32; 3] = [0.2, 0.4, 0.8];
1527 let src: &[u8] = bytemuck::cast_slice(&src_f);
1528 let dst = convert_pixel(from, to, src);
1529 let out: &[f32] = bytemuck::cast_slice(&dst);
1530 assert!((out[0] - 0.2).abs() < 1e-6);
1531 assert!((out[1] - 0.4).abs() < 1e-6);
1532 assert!((out[2] - 0.8).abs() < 1e-6);
1533 assert!((out[3] - 1.0).abs() < 1e-6);
1534 }
1535
1536 #[test]
1537 fn rgba_u16_to_rgb_u16() {
1538 let from = PixelDescriptor::new(
1539 ChannelType::U16,
1540 ChannelLayout::Rgba,
1541 Some(AlphaMode::Straight),
1542 TransferFunction::Srgb,
1543 );
1544 let to = PixelDescriptor::new(
1545 ChannelType::U16,
1546 ChannelLayout::Rgb,
1547 None,
1548 TransferFunction::Srgb,
1549 );
1550 let src16: [u16; 4] = [10000, 20000, 30000, 65535];
1551 let src: &[u8] = bytemuck::cast_slice(&src16);
1552 let dst = convert_pixel(from, to, src);
1553 let out: &[u16] = bytemuck::cast_slice(&dst);
1554 assert_eq!(out, [10000, 20000, 30000]);
1555 }
1556
1557 #[test]
1558 fn gray_alpha_u16_to_rgba_u16() {
1559 let from = PixelDescriptor::new(
1560 ChannelType::U16,
1561 ChannelLayout::GrayAlpha,
1562 Some(AlphaMode::Straight),
1563 TransferFunction::Srgb,
1564 );
1565 let to = PixelDescriptor::new(
1566 ChannelType::U16,
1567 ChannelLayout::Rgba,
1568 Some(AlphaMode::Straight),
1569 TransferFunction::Srgb,
1570 );
1571 let src16: [u16; 2] = [50000, 32768];
1572 let src: &[u8] = bytemuck::cast_slice(&src16);
1573 let dst = convert_pixel(from, to, src);
1574 let out: &[u16] = bytemuck::cast_slice(&dst);
1575 assert_eq!(out, [50000, 50000, 50000, 32768]);
1576 }
1577
1578 #[test]
1579 fn gray_alpha_f32_to_rgb_f32() {
1580 let from = PixelDescriptor::new(
1581 ChannelType::F32,
1582 ChannelLayout::GrayAlpha,
1583 Some(AlphaMode::Straight),
1584 TransferFunction::Linear,
1585 );
1586 let to = PixelDescriptor::new(
1587 ChannelType::F32,
1588 ChannelLayout::Rgb,
1589 None,
1590 TransferFunction::Linear,
1591 );
1592 let src_f: [f32; 2] = [0.75, 0.5];
1593 let src: &[u8] = bytemuck::cast_slice(&src_f);
1594 let dst = convert_pixel(from, to, src);
1595 let out: &[f32] = bytemuck::cast_slice(&dst);
1596 assert!((out[0] - 0.75).abs() < 1e-6);
1597 assert!((out[1] - 0.75).abs() < 1e-6);
1598 assert!((out[2] - 0.75).abs() < 1e-6);
1599 }
1600
1601 #[test]
1602 fn gray_u16_to_gray_alpha_u16() {
1603 let from = PixelDescriptor::new(
1604 ChannelType::U16,
1605 ChannelLayout::Gray,
1606 None,
1607 TransferFunction::Srgb,
1608 );
1609 let to = PixelDescriptor::new(
1610 ChannelType::U16,
1611 ChannelLayout::GrayAlpha,
1612 Some(AlphaMode::Straight),
1613 TransferFunction::Srgb,
1614 );
1615 let src16: [u16; 1] = [12345];
1616 let src: &[u8] = bytemuck::cast_slice(&src16);
1617 let dst = convert_pixel(from, to, src);
1618 let out: &[u16] = bytemuck::cast_slice(&dst);
1619 assert_eq!(out, [12345, 65535]);
1620 }
1621
1622 #[test]
1623 fn gray_alpha_f32_to_gray_f32() {
1624 let from = PixelDescriptor::new(
1625 ChannelType::F32,
1626 ChannelLayout::GrayAlpha,
1627 Some(AlphaMode::Straight),
1628 TransferFunction::Linear,
1629 );
1630 let to = PixelDescriptor::new(
1631 ChannelType::F32,
1632 ChannelLayout::Gray,
1633 None,
1634 TransferFunction::Linear,
1635 );
1636 let src_f: [f32; 2] = [0.33, 0.9];
1637 let src: &[u8] = bytemuck::cast_slice(&src_f);
1638 let dst = convert_pixel(from, to, src);
1639 let out: &[f32] = bytemuck::cast_slice(&dst);
1640 assert!((out[0] - 0.33).abs() < 1e-6);
1641 }
1642
1643 #[test]
1648 fn bt709_linear_f32_roundtrip() {
1649 use crate::TransferFunctionExt;
1650 let tf = TransferFunction::Bt709;
1651 let values = [0.0f32, 0.1, 0.25, 0.5, 0.75, 1.0];
1652 for &v in &values {
1653 let linear = tf.linearize(v);
1654 let back = tf.delinearize(linear);
1655 assert!(
1656 (back - v).abs() < 1e-5,
1657 "Bt709 roundtrip failed for {v}: linearize={linear}, delinearize={back}"
1658 );
1659 }
1660 }
1661
1662 #[test]
1663 fn unknown_transfer_roundtrip() {
1664 use crate::TransferFunctionExt;
1665 let tf = TransferFunction::Unknown;
1666 let values = [0.0f32, 0.1, 0.5, 0.99, 1.0];
1667 for &v in &values {
1668 let linear = tf.linearize(v);
1669 assert!(
1670 (linear - v).abs() < 1e-7,
1671 "Unknown linearize should be identity: {v} -> {linear}"
1672 );
1673 let back = tf.delinearize(linear);
1674 assert!(
1675 (back - v).abs() < 1e-7,
1676 "Unknown delinearize should be identity: {linear} -> {back}"
1677 );
1678 }
1679 }
1680
1681 #[test]
1682 fn oklab_unknown_primaries_returns_error() {
1683 use crate::ColorPrimaries;
1684 let from = PixelDescriptor::new(
1685 ChannelType::F32,
1686 ChannelLayout::Rgb,
1687 None,
1688 TransferFunction::Linear,
1689 )
1690 .with_primaries(ColorPrimaries::Unknown);
1691 let to = PixelDescriptor::new(
1692 ChannelType::F32,
1693 ChannelLayout::Oklab,
1694 None,
1695 TransferFunction::Linear,
1696 )
1697 .with_primaries(ColorPrimaries::Unknown);
1698 let result = RowConverter::new(from, to);
1699 assert!(result.is_err(), "Oklab with Unknown primaries should fail");
1700 }
1701
1702 #[test]
1707 fn compose_roundtrip_cancels_to_identity() {
1708 let a = RowConverter::new(PixelDescriptor::RGBA8_SRGB, PixelDescriptor::RGBAF32_LINEAR)
1709 .unwrap();
1710 let b = RowConverter::new(PixelDescriptor::RGBAF32_LINEAR, PixelDescriptor::RGBA8_SRGB)
1711 .unwrap();
1712 let composed = a.compose(&b).unwrap();
1713 assert!(composed.is_identity(), "sRGB→linear→sRGB should cancel");
1714 }
1715
1716 #[test]
1717 fn compose_chain_reduces_steps() {
1718 let a = RowConverter::new(PixelDescriptor::RGBA8_SRGB, PixelDescriptor::RGBAF32_LINEAR)
1724 .unwrap();
1725 let srgb_f32 = PixelDescriptor::new(
1726 ChannelType::F32,
1727 ChannelLayout::Rgba,
1728 Some(AlphaMode::Straight),
1729 TransferFunction::Srgb,
1730 );
1731 let b = RowConverter::new(PixelDescriptor::RGBAF32_LINEAR, srgb_f32).unwrap();
1732 let composed = a.compose(&b).unwrap();
1733 assert!(!composed.is_identity());
1734 assert_eq!(composed.from_descriptor(), PixelDescriptor::RGBA8_SRGB);
1735 assert_eq!(composed.to_descriptor(), srgb_f32);
1736 }
1737
1738 #[test]
1739 fn compose_incompatible_returns_none() {
1740 let a = RowConverter::new(PixelDescriptor::RGBA8_SRGB, PixelDescriptor::RGBAF32_LINEAR)
1741 .unwrap();
1742 let b = RowConverter::new(PixelDescriptor::RGB8_SRGB, PixelDescriptor::RGBA8_SRGB).unwrap();
1743 assert!(a.compose(&b).is_none(), "RGBAF32_LINEAR != RGB8_SRGB");
1744 }
1745
1746 #[test]
1747 fn compose_premul_roundtrip_cancels() {
1748 let premul = PixelDescriptor::new(
1749 ChannelType::F32,
1750 ChannelLayout::Rgba,
1751 Some(AlphaMode::Premultiplied),
1752 TransferFunction::Linear,
1753 );
1754 let a = RowConverter::new(PixelDescriptor::RGBAF32_LINEAR, premul).unwrap();
1755 let b = RowConverter::new(premul, PixelDescriptor::RGBAF32_LINEAR).unwrap();
1756 let composed = a.compose(&b).unwrap();
1757 assert!(
1758 composed.is_identity(),
1759 "straight→premul→straight should cancel"
1760 );
1761 }
1762
1763 #[test]
1768 #[should_panic(expected = "CMYK pixel data cannot be processed")]
1769 fn cmyk_rejected_by_row_converter() {
1770 let _ = RowConverter::new(PixelDescriptor::CMYK8, PixelDescriptor::RGB8_SRGB);
1771 }
1772}