1#[cfg(test)]
33use alloc::vec::Vec;
34
35use alloc::sync::Arc;
36
37use zenpixels::{
38 AlphaMode, ChannelLayout, ChannelType, ColorContext, InPlacePixels, PixelBuffer,
39 PixelDescriptor, PixelFormat, PixelSlice, PixelSliceMut,
40};
41
42use crate::scan::{self, FusedRequest};
43
44#[derive(Clone, Copy, Debug, Default)]
60#[non_exhaustive]
61pub struct LoadBearingReport {
62 pub uses_alpha: Option<bool>,
68
69 pub uses_chroma: Option<bool>,
74
75 pub uses_low_bits: Option<bool>,
81}
82
83impl LoadBearingReport {
84 #[inline]
91 pub const fn any_analyzed(&self) -> bool {
92 self.uses_alpha.is_some() || self.uses_chroma.is_some() || self.uses_low_bits.is_some()
93 }
94
95 #[must_use]
118 pub fn apply_to(&self, src: &PixelDescriptor) -> PixelDescriptor {
119 let mut channel_type = src.channel_type();
120 let mut layout = src.layout();
121 let mut alpha = src.alpha;
122
123 if matches!(self.uses_low_bits, Some(false)) && channel_type == ChannelType::U16 {
129 channel_type = ChannelType::U8;
130 }
131
132 if matches!(self.uses_alpha, Some(false)) {
135 layout = match layout {
136 ChannelLayout::Rgba | ChannelLayout::Bgra => ChannelLayout::Rgb,
137 ChannelLayout::GrayAlpha => ChannelLayout::Gray,
138 other => other,
139 };
140 if layout != src.layout() {
141 alpha = None;
142 }
143 }
144
145 if matches!(self.uses_chroma, Some(false)) {
147 layout = match layout {
148 ChannelLayout::Rgb => ChannelLayout::Gray,
149 ChannelLayout::Rgba | ChannelLayout::Bgra => ChannelLayout::GrayAlpha,
150 other => other,
151 };
152 }
153
154 let format = PixelFormat::from_parts(channel_type, layout, alpha).unwrap_or(src.format);
157
158 PixelDescriptor::from_pixel_format(format)
159 .with_transfer(src.transfer)
160 .with_primaries(src.primaries)
161 .with_alpha(alpha)
162 .with_signal_range(src.signal_range)
163 }
164}
165
166mod sealed {
169 pub trait Sealed {}
173 impl<P> Sealed for zenpixels::PixelSlice<'_, P> {}
174 impl<P> Sealed for zenpixels::PixelSliceMut<'_, P> {}
175}
176
177pub trait PixelSliceLoadBearingExt: sealed::Sealed {
182 fn determine_load_bearing(&self) -> LoadBearingReport;
189
190 fn try_reduce_to_load_bearing_format(&self) -> Option<PixelBuffer>;
199}
200
201impl<P> PixelSliceLoadBearingExt for PixelSlice<'_, P> {
202 fn determine_load_bearing(&self) -> LoadBearingReport {
203 let descriptor = self.descriptor();
204 let layout = descriptor.layout();
205 let channel_type = descriptor.channel_type();
206
207 let alpha_structural: Option<Option<bool>> = if layout.has_alpha() {
222 match descriptor.alpha {
223 Some(AlphaMode::Undefined) | Some(AlphaMode::Opaque) => Some(Some(false)),
224 _ => None,
225 }
226 } else {
227 None
228 };
229 let scan_alpha = alpha_structural.is_none();
230
231 let (mut uses_alpha, uses_chroma) = match (layout, channel_type) {
239 (ChannelLayout::Rgba | ChannelLayout::Bgra, ChannelType::U8) => {
240 let fused = fused_rgba8_over_rows(
241 self,
242 FusedRequest {
243 check_opaque: scan_alpha,
244 check_grayscale: true,
245 },
246 );
247 (Some(!fused.is_opaque), Some(!fused.is_grayscale))
248 }
249 (ChannelLayout::Rgba, ChannelType::U16) => (
250 Some(scan_alpha && !rows_all(self, cast_u16, scan::is_opaque_rgba16)),
251 Some(!rows_all(self, cast_u16, scan::is_grayscale_rgba16)),
252 ),
253 (ChannelLayout::Rgb, ChannelType::U8) => (
254 Some(false), Some(!rows_all(self, cast_u8, scan::is_grayscale_rgb8)),
256 ),
257 (ChannelLayout::Rgb, ChannelType::U16) => (
258 Some(false),
259 Some(!rows_all(self, cast_u16, scan::is_grayscale_rgb16)),
260 ),
261 (ChannelLayout::GrayAlpha, ChannelType::U8) => (
262 Some(scan_alpha && !rows_all(self, cast_u8, scan::is_opaque_ga8)),
263 Some(false), ),
265 (ChannelLayout::GrayAlpha, ChannelType::U16) => (
266 Some(scan_alpha && !rows_all(self, cast_u16, scan::is_opaque_ga16)),
267 Some(false),
268 ),
269
270 (ChannelLayout::Gray, _) => (Some(false), Some(false)),
274
275 (ChannelLayout::Rgba, ChannelType::F32) => (
277 Some(scan_alpha && !rows_all(self, cast_f32, scan::is_opaque_rgba_f32)),
278 Some(!rows_all(self, cast_f32, scan::is_grayscale_rgba_f32)),
279 ),
280 (ChannelLayout::Rgb, ChannelType::F32) => (
281 Some(false),
282 Some(!rows_all(self, cast_f32, scan::is_grayscale_rgb_f32)),
283 ),
284 (ChannelLayout::GrayAlpha, ChannelType::F32) => (
285 Some(scan_alpha && !rows_all(self, cast_f32, scan::is_opaque_ga_f32)),
286 Some(false),
287 ),
288
289 _ => (None, None),
292 };
293
294 if let Some(structural_uses) = alpha_structural
299 && uses_alpha.is_some()
300 {
301 uses_alpha = structural_uses;
302 }
303
304 let uses_low_bits = match channel_type {
306 ChannelType::U16 => Some(!rows_all(
307 self,
308 cast_u16,
309 scan::bit_replication_lossless_u16,
310 )),
311 ChannelType::U8 => Some(false),
314 _ => None,
317 };
318
319 LoadBearingReport {
320 uses_alpha,
321 uses_chroma,
322 uses_low_bits,
323 }
324 }
325
326 fn try_reduce_to_load_bearing_format(&self) -> Option<PixelBuffer> {
327 let src = self.descriptor();
328 let mut report = self.determine_load_bearing();
329 let plan = plan_chroma_collapse_signaling(self.color_context());
336 if matches!(plan, GraySignalPlan::Suppress) {
337 report.uses_chroma = None;
338 }
339 let target = report.apply_to(&src);
340 if target == src {
341 return None;
342 }
343 let mut out = PixelBuffer::try_new(self.width(), self.rows(), target).ok()?;
347 transform_into(self, &src, &target, &mut out)?;
348 let ctx = match (chroma_collapsed(src.layout(), target.layout()), plan) {
353 (true, GraySignalPlan::Swap(swapped)) => swapped,
354 _ => self.color_context().cloned(),
355 };
356 Some(match ctx {
357 Some(ctx) => out.with_color_context(ctx),
358 None => out,
359 })
360 }
361}
362
363enum GraySignalPlan {
367 Carry,
370 Swap(Option<Arc<ColorContext>>),
376 Suppress,
379}
380
381fn plan_chroma_collapse_signaling(ctx: Option<&Arc<ColorContext>>) -> GraySignalPlan {
410 use crate::icc_profiles::{SynthesizedIcc, synthesize_gray_icc_for_cicp};
411
412 let Some(ctx) = ctx else {
413 return GraySignalPlan::Carry;
414 };
415 let Some(icc) = ctx.icc.as_deref() else {
416 return GraySignalPlan::Carry;
417 };
418
419 let cicp = ctx
420 .cicp
421 .or_else(|| zenpixels::icc::extract_cicp(icc))
422 .or_else(|| zenpixels::icc::identify_common(icc).and_then(|id| id.to_cicp()));
423 let Some(cicp) = cicp else {
424 return GraySignalPlan::Suppress;
425 };
426
427 match synthesize_gray_icc_for_cicp(cicp) {
428 SynthesizedIcc::Profile(bytes) => {
429 let mut swapped = ColorContext::from_icc(bytes.into_owned());
430 swapped.cicp = ctx.cicp;
431 GraySignalPlan::Swap(Some(Arc::new(swapped)))
432 }
433 SynthesizedIcc::NotNeeded => {
437 GraySignalPlan::Swap(ctx.cicp.map(|c| Arc::new(ColorContext::from_cicp(c))))
438 }
439 _ => GraySignalPlan::Suppress,
441 }
442}
443
444fn chroma_collapsed(src: ChannelLayout, dst: ChannelLayout) -> bool {
447 matches!(
448 src,
449 ChannelLayout::Rgb | ChannelLayout::Rgba | ChannelLayout::Bgra
450 ) && matches!(dst, ChannelLayout::Gray | ChannelLayout::GrayAlpha)
451}
452
453pub trait PixelBufferLoadBearingExt: sealed::Sealed {
463 fn reduce_to_load_bearing_format_in_place(&mut self, force_alpha_restructuring: bool);
500}
501
502impl sealed::Sealed for PixelBuffer {}
503
504impl PixelBufferLoadBearingExt for PixelBuffer {
505 fn reduce_to_load_bearing_format_in_place(&mut self, force_alpha_restructuring: bool) {
506 self.transform_in_place(|px| reduce_in_place_impl(px, force_alpha_restructuring));
507 }
508}
509
510fn reduce_in_place_impl(
516 px: InPlacePixels<'_>,
517 force_alpha_restructuring: bool,
518) -> PixelSliceMut<'_> {
519 let InPlacePixels {
520 bytes,
521 width,
522 rows,
523 stride: in_stride,
524 descriptor: src,
525 color: original_ctx,
526 ..
527 } = px;
528 fn rewrap<'b>(
529 bytes: &'b mut [u8],
530 width: u32,
531 rows: u32,
532 stride: usize,
533 desc: PixelDescriptor,
534 ctx: Option<Arc<ColorContext>>,
535 ) -> PixelSliceMut<'b> {
536 let out = PixelSliceMut::new(bytes, width, rows, stride, desc)
537 .expect("in-place reduction geometry is always valid");
538 match ctx {
539 Some(c) => out.with_color_context(c),
540 None => out,
541 }
542 }
543 if width == 0 || rows == 0 {
544 return rewrap(bytes, width, rows, in_stride, src, original_ctx);
545 }
546
547 let mut report = {
548 let view = PixelSlice::new(&bytes[..], width, rows, in_stride, src)
549 .expect("buffer-backed view is always valid");
550 view.determine_load_bearing()
551 };
552
553 let plan = plan_chroma_collapse_signaling(original_ctx.as_ref());
555 if matches!(plan, GraySignalPlan::Suppress) {
556 report.uses_chroma = None;
557 }
558
559 let alpha_droppable = matches!(report.uses_alpha, Some(false)) && src.layout().has_alpha();
563 if !force_alpha_restructuring {
564 report.uses_alpha = None;
565 }
566
567 let mut target = report.apply_to(&src);
568 if !force_alpha_restructuring
569 && alpha_droppable
570 && target.layout().has_alpha()
571 && !matches!(
572 src.alpha,
573 Some(AlphaMode::Undefined) | Some(AlphaMode::Opaque)
574 )
575 {
576 target = target.with_alpha(Some(AlphaMode::Opaque));
577 }
578
579 if target == src {
580 return rewrap(bytes, width, rows, in_stride, src, original_ctx);
581 }
582
583 if target.bytes_per_pixel() == src.bytes_per_pixel() {
590 let retagged = src.with_alpha(target.alpha);
591 return rewrap(bytes, width, rows, in_stride, retagged, original_ctx);
592 }
593
594 let narrow16 =
598 src.channel_type() == ChannelType::U16 && target.channel_type() == ChannelType::U8;
599 let Some(map) = selection_map(src.layout(), target.layout()) else {
600 return rewrap(bytes, width, rows, in_stride, src, original_ctx);
601 };
602
603 let ctx = match (chroma_collapsed(src.layout(), target.layout()), plan) {
607 (true, GraySignalPlan::Swap(swapped)) => swapped,
608 _ => original_ctx,
609 };
610
611 let in_bpp = src.bytes_per_pixel();
612 let out_bpp = target.bytes_per_pixel();
613 debug_assert!(out_bpp < in_bpp, "reduction always shrinks bpp");
614 let out_stride = width as usize * out_bpp;
615
616 compact_rows_in_place(
617 bytes,
618 width as usize,
619 rows as usize,
620 in_stride,
621 in_bpp,
622 out_bpp,
623 src.layout(),
624 target.layout(),
625 map,
626 narrow16,
627 );
628
629 rewrap(bytes, width, rows, out_stride, target, ctx)
630}
631
632#[allow(clippy::too_many_arguments)]
647fn compact_rows_in_place(
648 data: &mut [u8],
649 width: usize,
650 rows: usize,
651 in_stride: usize,
652 in_bpp: usize,
653 out_bpp: usize,
654 src_layout: ChannelLayout,
655 dst_layout: ChannelLayout,
656 map: &[usize],
657 narrow16: bool,
658) {
659 let out_stride = width * out_bpp;
660 let in_ch = src_layout.channels();
661 let elem = if narrow16 { 2 } else { in_bpp / in_ch };
662 let row_in_len = width * in_bpp;
663 for y in 0..rows {
664 let src_start = y * in_stride;
665 let dst_start = y * out_stride;
666 let dst_end = dst_start + out_stride;
667 if dst_end <= src_start {
668 let (head, tail) = data.split_at_mut(src_start);
670 let row_in = &tail[..row_in_len];
671 let row_out = &mut head[dst_start..dst_end];
672 if narrow16 {
673 select_row_u16_to_u8(row_in, row_out, in_ch, map);
674 } else {
675 match (elem, src_layout, dst_layout) {
676 (1, ChannelLayout::Rgba, ChannelLayout::Rgb) => {
677 if garb::bytes::rgba_to_rgb(row_in, row_out).is_err() {
681 select_row::<1>(row_in, row_out, in_ch, map);
682 }
683 }
684 (1, ChannelLayout::Bgra, ChannelLayout::Rgb) => {
685 if garb::bytes::bgra_to_rgb(row_in, row_out).is_err() {
686 select_row::<1>(row_in, row_out, in_ch, map);
687 }
688 }
689 (1, ..) => select_row::<1>(row_in, row_out, in_ch, map),
690 (2, ..) => select_row::<2>(row_in, row_out, in_ch, map),
691 _ => select_row::<4>(row_in, row_out, in_ch, map),
692 }
693 }
694 } else {
695 for x in 0..width {
698 let s = src_start + x * in_bpp;
699 let mut tmp = [0u8; 16];
700 tmp[..in_bpp].copy_from_slice(&data[s..s + in_bpp]);
701 let d = dst_start + x * out_bpp;
702 if narrow16 {
703 for (k, &c) in map.iter().enumerate() {
704 data[d + k] = tmp[c * 2];
705 }
706 } else {
707 for (k, &c) in map.iter().enumerate() {
708 data[d + k * elem..d + (k + 1) * elem]
709 .copy_from_slice(&tmp[c * elem..(c + 1) * elem]);
710 }
711 }
712 }
713 }
714 }
715}
716
717#[inline]
731fn rows_all<P, T, F>(slice: &PixelSlice<'_, P>, cast: fn(&[u8]) -> &[T], predicate: F) -> bool
732where
733 T: 'static,
734 F: Fn(&[T]) -> bool,
735{
736 if let Some(bytes) = slice.as_contiguous_bytes() {
737 predicate(cast(bytes))
738 } else {
739 for y in 0..slice.rows() {
740 if !predicate(cast(slice.row(y))) {
741 return false;
742 }
743 }
744 true
745 }
746}
747
748fn fused_rgba8_over_rows<P>(slice: &PixelSlice<'_, P>, request: FusedRequest) -> scan::FusedResult {
753 if let Some(bytes) = slice.as_contiguous_bytes() {
754 return scan::fused_predicates_rgba8_cg(bytes, request);
755 }
756 let mut req = request;
757 let mut total = scan::FusedResult {
758 is_opaque: req.check_opaque,
759 is_grayscale: req.check_grayscale,
760 };
761 for y in 0..slice.rows() {
762 if !req.check_opaque && !req.check_grayscale {
763 break;
764 }
765 let row = slice.row(y);
766 let r = scan::fused_predicates_rgba8_cg(row, req);
767 if req.check_opaque && !r.is_opaque {
768 total.is_opaque = false;
769 req.check_opaque = false;
770 }
771 if req.check_grayscale && !r.is_grayscale {
772 total.is_grayscale = false;
773 req.check_grayscale = false;
774 }
775 }
776 total
777}
778
779fn cast_u8(bytes: &[u8]) -> &[u8] {
782 bytes
783}
784
785fn cast_u16(bytes: &[u8]) -> &[u16] {
786 bytemuck::cast_slice(bytes)
787}
788
789fn cast_f32(bytes: &[u8]) -> &[f32] {
790 bytemuck::cast_slice(bytes)
791}
792
793fn transform_into<P>(
804 slice: &PixelSlice<'_, P>,
805 src: &PixelDescriptor,
806 dst: &PixelDescriptor,
807 out: &mut PixelBuffer,
808) -> Option<()> {
809 let src_ct = src.channel_type();
810 let dst_ct = dst.channel_type();
811 let src_layout = src.layout();
812 let dst_layout = dst.layout();
813
814 let narrow16 = src_ct == ChannelType::U16 && dst_ct == ChannelType::U8;
819 if !narrow16 && src_ct != dst_ct {
820 return None;
821 }
822
823 let in_ch = src_layout.channels();
825 let map: &[usize] = selection_map(src_layout, dst_layout)?;
826
827 let mut out_rows = out.as_slice_mut();
828 for y in 0..slice.rows() {
829 let row_in = slice.row(y);
830 let row_out = out_rows.row_mut(y);
831 if narrow16 {
832 select_row_u16_to_u8(row_in, row_out, in_ch, map);
833 } else {
834 match (dst_ct.byte_size(), src_layout, dst_layout) {
835 (1, ChannelLayout::Rgba, ChannelLayout::Rgb) => {
836 garb::bytes::rgba_to_rgb(row_in, row_out).ok()?;
837 }
838 (1, ChannelLayout::Bgra, ChannelLayout::Rgb) => {
839 garb::bytes::bgra_to_rgb(row_in, row_out).ok()?;
840 }
841 (1, ..) => select_row::<1>(row_in, row_out, in_ch, map),
842 (2, ..) => select_row::<2>(row_in, row_out, in_ch, map),
843 (4, ..) => select_row::<4>(row_in, row_out, in_ch, map),
844 _ => return None,
845 }
846 }
847 }
848 Some(())
849}
850
851fn selection_map(src_layout: ChannelLayout, dst_layout: ChannelLayout) -> Option<&'static [usize]> {
855 static IDENTITY: [usize; 4] = [0, 1, 2, 3];
856 Some(match (src_layout, dst_layout) {
857 _ if src_layout == dst_layout => &IDENTITY[..src_layout.channels()],
858 (ChannelLayout::Rgba, ChannelLayout::Rgb) => &[0, 1, 2],
859 (ChannelLayout::Bgra, ChannelLayout::Rgb) => &[2, 1, 0],
862 (ChannelLayout::Rgba | ChannelLayout::Bgra, ChannelLayout::GrayAlpha) => &[0, 3],
866 (ChannelLayout::Rgba | ChannelLayout::Bgra, ChannelLayout::Gray) => &[0],
867 (ChannelLayout::Rgb, ChannelLayout::Gray) => &[0],
868 (ChannelLayout::GrayAlpha, ChannelLayout::Gray) => &[0],
869 _ => return None,
870 })
871}
872
873#[inline]
877fn select_row<const E: usize>(row_in: &[u8], row_out: &mut [u8], in_ch: usize, map: &[usize]) {
878 let out_px = map.len() * E;
879 let in_px = in_ch * E;
880 for (dst, src) in row_out
881 .chunks_exact_mut(out_px)
882 .zip(row_in.chunks_exact(in_px))
883 {
884 for (k, &c) in map.iter().enumerate() {
885 dst[k * E..(k + 1) * E].copy_from_slice(&src[c * E..c * E + E]);
886 }
887 }
888}
889
890#[inline]
893fn select_row_u16_to_u8(row_in: &[u8], row_out: &mut [u8], in_ch: usize, map: &[usize]) {
894 let in_px = in_ch * 2;
895 for (dst, src) in row_out
896 .chunks_exact_mut(map.len())
897 .zip(row_in.chunks_exact(in_px))
898 {
899 for (k, &c) in map.iter().enumerate() {
900 dst[k] = src[c * 2];
901 }
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908 use zenpixels::{Cicp, ColorPrimaries, PixelSlice, TransferFunction};
909
910 fn make_slice<'a>(
911 bytes: &'a [u8],
912 width: u32,
913 height: u32,
914 format: PixelFormat,
915 ) -> PixelSlice<'a> {
916 let descriptor =
917 PixelDescriptor::from_pixel_format(format).with_transfer(TransferFunction::Srgb);
918 let stride = width as usize * format.bytes_per_pixel();
919 PixelSlice::new(bytes, width, height, stride, descriptor).unwrap()
920 }
921
922 fn make_slice_with_primaries<'a>(
923 bytes: &'a [u8],
924 width: u32,
925 height: u32,
926 format: PixelFormat,
927 primaries: ColorPrimaries,
928 ) -> PixelSlice<'a> {
929 let descriptor = PixelDescriptor::from_pixel_format(format)
930 .with_transfer(TransferFunction::Srgb)
931 .with_primaries(primaries);
932 let stride = width as usize * format.bytes_per_pixel();
933 PixelSlice::new(bytes, width, height, stride, descriptor).unwrap()
934 }
935
936 fn reduced(slice: &PixelSlice<'_>) -> PixelDescriptor {
938 slice.determine_load_bearing().apply_to(&slice.descriptor())
939 }
940
941 #[test]
944 fn rgba8_all_opaque_gray_reduces_to_gray8() {
945 let bytes: Vec<u8> = (0..4)
946 .flat_map(|i| {
947 let g = (i * 30) as u8;
948 [g, g, g, 255]
949 })
950 .collect();
951 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8);
952 let r = slice.determine_load_bearing();
953 assert_eq!(r.uses_alpha, Some(false));
955 assert_eq!(r.uses_chroma, Some(false));
956
957 let target = r.apply_to(&slice.descriptor());
958 assert_eq!(target.format, PixelFormat::Gray8);
959 }
960
961 #[test]
962 fn rgba8_with_real_color_keeps_rgba_drops_alpha() {
963 let bytes: Vec<u8> = (0..4)
964 .flat_map(|i| {
965 [
966 (i * 60 + 10) as u8,
967 (i * 30 + 50) as u8,
968 (i * 90 + 20) as u8,
969 255,
970 ]
971 })
972 .collect();
973 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8);
974 let r = slice.determine_load_bearing();
975 assert_eq!(r.uses_alpha, Some(false));
977 assert_eq!(r.uses_chroma, Some(true));
978
979 let target = r.apply_to(&slice.descriptor());
980 assert_eq!(target.format, PixelFormat::Rgb8);
981 }
982
983 #[test]
984 fn rgba8_alpha_mix_0_and_255_reports_binary() {
985 let bytes: Vec<u8> = (0..4)
986 .flat_map(|i| {
987 let a = if i & 1 == 0 { 0 } else { 255 };
988 [50, 50, 50, a]
989 })
990 .collect();
991 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8);
992 let r = slice.determine_load_bearing();
993 assert_eq!(r.uses_alpha, Some(true), "alpha varies → load-bearing");
994 assert_eq!(r.uses_chroma, Some(false));
995 }
996
997 #[test]
998 fn rgba16_bit_replicated_reduces_to_rgba8() {
999 let bytes: Vec<u8> = (0..4)
1000 .flat_map(|i| {
1001 let r = (i * 60) as u8;
1002 let g = (i * 30 + 10) as u8;
1003 let b = (i * 80 + 5) as u8;
1004 let a = 0xFF;
1005 [r, r, g, g, b, b, a, a]
1006 })
1007 .collect();
1008 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba16);
1009 let r = slice.determine_load_bearing();
1010 assert_eq!(r.uses_low_bits, Some(false));
1012 assert_eq!(r.uses_alpha, Some(false));
1013 let target = r.apply_to(&slice.descriptor());
1014 assert_eq!(target.format, PixelFormat::Rgb8);
1015 }
1016
1017 #[test]
1018 fn rgba16_actual_high_precision_keeps_u16() {
1019 let bytes: Vec<u8> = (0..4)
1020 .flat_map(|i| {
1021 let r_lo = (i * 17 + 1) as u8;
1022 let r_hi = (i * 60) as u8;
1023 [r_hi, r_lo, 0, 0, 0, 0, 0xFF, 0xFF]
1024 })
1025 .collect();
1026 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba16);
1027 let r = slice.determine_load_bearing();
1028 assert_eq!(r.uses_low_bits, Some(true));
1029 }
1030
1031 #[test]
1036 fn try_reduce_returns_some_when_reduction_available() {
1037 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
1038 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8);
1039 let result = slice.try_reduce_to_load_bearing_format();
1040 let out = result.expect("should reduce");
1041 assert_eq!(out.descriptor().format, PixelFormat::Gray8);
1042 assert_eq!(out.as_slice().row(0), &[0u8, 30, 60, 90]);
1043 }
1044
1045 #[test]
1046 fn try_reduce_returns_none_when_already_minimal() {
1047 let bytes: Vec<u8> = (0..4)
1048 .flat_map(|i| [i * 60, 100, 200, i * 40 + 1])
1049 .collect();
1050 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8);
1051 assert!(slice.try_reduce_to_load_bearing_format().is_none());
1052 }
1053
1054 #[test]
1057 fn default_report_is_fully_unanalyzed() {
1058 let r = LoadBearingReport::default();
1062 assert_eq!(r.uses_alpha, None);
1063 assert_eq!(r.uses_chroma, None);
1064 assert_eq!(r.uses_low_bits, None);
1065 assert!(!r.any_analyzed());
1066 }
1067
1068 #[test]
1069 fn any_analyzed_fires_when_at_least_one_field_set() {
1070 let mut r = LoadBearingReport::default();
1071 assert!(!r.any_analyzed());
1072 r.uses_alpha = Some(true);
1073 assert!(r.any_analyzed(), "any_analyzed fires for any Some");
1074 r.uses_alpha = None;
1075 r.uses_low_bits = Some(false);
1076 assert!(r.any_analyzed(), "any_analyzed fires on low-bits too");
1077 }
1078
1079 #[test]
1082 fn wide_primaries_tag_is_preserved_and_ignored_by_analysis() {
1083 let bytes: Vec<u8> = (0..4)
1089 .flat_map(|i| {
1090 let g = (i * 30) as u8;
1091 [g, g, g, 255]
1092 })
1093 .collect();
1094 let p3 =
1095 make_slice_with_primaries(&bytes, 4, 1, PixelFormat::Rgba8, ColorPrimaries::DisplayP3);
1096 let srgb =
1097 make_slice_with_primaries(&bytes, 4, 1, PixelFormat::Rgba8, ColorPrimaries::Bt709);
1098
1099 let r_p3 = p3.determine_load_bearing();
1100 let r_srgb = srgb.determine_load_bearing();
1101 assert_eq!(r_p3.uses_alpha, r_srgb.uses_alpha);
1102 assert_eq!(r_p3.uses_chroma, r_srgb.uses_chroma);
1103
1104 let out = p3
1105 .try_reduce_to_load_bearing_format()
1106 .expect("gray+opaque should reduce");
1107 assert_eq!(out.descriptor().format, PixelFormat::Gray8);
1108 assert_eq!(
1109 out.descriptor().primaries,
1110 ColorPrimaries::DisplayP3,
1111 "primaries tag must carry over untouched"
1112 );
1113 assert_eq!(out.as_slice().row(0), &[0u8, 30, 60, 90]);
1115 }
1116
1117 #[test]
1120 fn apply_to_no_op_on_fully_load_bearing() {
1121 let src = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8);
1122 let r = LoadBearingReport::default();
1123 assert_eq!(r.apply_to(&src), src);
1124 }
1125
1126 #[test]
1127 fn ga8_opaque_reduces_to_gray8() {
1128 let bytes = [10u8, 255, 50, 255, 100, 255];
1129 let slice = make_slice(&bytes, 3, 1, PixelFormat::GrayA8);
1130 assert_eq!(reduced(&slice).format, PixelFormat::Gray8);
1131 }
1132
1133 #[test]
1134 fn rgba16_grayscale_alpha_replicated_reduces_to_gray8() {
1135 let bytes: Vec<u8> = (0..4)
1136 .flat_map(|i| {
1137 let g = (i * 60) as u8;
1138 [g, g, g, g, g, g, 0xFF, 0xFF]
1139 })
1140 .collect();
1141 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba16);
1142 assert_eq!(reduced(&slice).format, PixelFormat::Gray8);
1143 }
1144
1145 #[test]
1148 fn undefined_alpha_padding_is_structurally_droppable() {
1149 let bytes = [10u8, 20, 30, 0x7B, 40, 50, 60, 0x01];
1154 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8)
1155 .with_transfer(TransferFunction::Srgb)
1156 .with_alpha(Some(AlphaMode::Undefined));
1157 let slice = PixelSlice::new(&bytes, 2, 1, 8, descriptor).unwrap();
1158 let r = slice.determine_load_bearing();
1159 assert_eq!(
1160 r.uses_alpha,
1161 Some(false),
1162 "padding lane is never load-bearing"
1163 );
1164 assert_eq!(r.uses_chroma, Some(true), "chroma still measured");
1165 let out = slice
1167 .try_reduce_to_load_bearing_format()
1168 .expect("padding drop is a reduction");
1169 assert_eq!(out.descriptor().format, PixelFormat::Rgb8);
1170 assert_eq!(out.as_slice().row(0), &[10u8, 20, 30, 40, 50, 60]);
1171 }
1172
1173 #[test]
1174 fn declared_opaque_alpha_is_trusted_without_scanning() {
1175 let bytes = [10u8, 10, 10, 255, 20, 20, 20, 255];
1180 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8)
1181 .with_transfer(TransferFunction::Srgb)
1182 .with_alpha(Some(AlphaMode::Opaque));
1183 let slice = PixelSlice::new(&bytes, 2, 1, 8, descriptor).unwrap();
1184 let r = slice.determine_load_bearing();
1185 assert_eq!(r.uses_alpha, Some(false));
1186 assert_eq!(r.uses_chroma, Some(false), "chroma still measured");
1187 }
1188
1189 #[test]
1190 fn premultiplied_alpha_scans_like_straight() {
1191 let bytes = [10u8, 10, 10, 128, 20, 20, 20, 64];
1195 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8)
1196 .with_transfer(TransferFunction::Srgb)
1197 .with_alpha(Some(AlphaMode::Premultiplied));
1198 let slice = PixelSlice::new(&bytes, 2, 1, 8, descriptor).unwrap();
1199 let r = slice.determine_load_bearing();
1200 assert_eq!(
1201 r.uses_alpha,
1202 Some(true),
1203 "varying premul alpha is load-bearing"
1204 );
1205 }
1206
1207 fn build_strided_rgba8(
1222 width: u32,
1223 height: u32,
1224 padding_bytes: usize,
1225 mut pixel_at: impl FnMut(u32, u32) -> [u8; 4],
1226 ) -> (Vec<u8>, usize) {
1227 let row_pixels = width as usize * 4;
1228 let stride = row_pixels + padding_bytes;
1229 let mut buf = vec![0xAAu8; stride * height as usize]; for y in 0..height {
1231 for x in 0..width {
1232 let p = pixel_at(x, y);
1233 let off = y as usize * stride + x as usize * 4;
1234 buf[off..off + 4].copy_from_slice(&p);
1235 }
1236 for k in row_pixels..stride {
1238 buf[y as usize * stride + k] = 0xCD;
1239 }
1240 }
1241 (buf, stride)
1242 }
1243
1244 fn slice_from_strided<'a>(
1245 bytes: &'a [u8],
1246 width: u32,
1247 height: u32,
1248 stride: usize,
1249 format: PixelFormat,
1250 ) -> PixelSlice<'a> {
1251 let descriptor =
1252 PixelDescriptor::from_pixel_format(format).with_transfer(TransferFunction::Srgb);
1253 PixelSlice::new(bytes, width, height, stride, descriptor).unwrap()
1254 }
1255
1256 #[test]
1257 fn strided_rgba8_all_opaque_gray_reduces_correctly() {
1258 let (buf, stride) = build_strided_rgba8(4, 4, 32, |x, y| {
1260 let g = ((x + y) * 30) as u8;
1261 [g, g, g, 255]
1262 });
1263 let slice = slice_from_strided(&buf, 4, 4, stride, PixelFormat::Rgba8);
1264 assert!(!slice.is_contiguous(), "test fixture must be strided");
1265 let r = slice.determine_load_bearing();
1266 assert_eq!(r.uses_alpha, Some(false));
1268 assert_eq!(r.uses_chroma, Some(false));
1269 let target = r.apply_to(&slice.descriptor());
1270 assert_eq!(target.format, PixelFormat::Gray8);
1271 }
1272
1273 #[test]
1274 fn strided_rgba8_garbage_padding_doesnt_poison_predicates() {
1275 let (buf, stride) = build_strided_rgba8(8, 3, 16, |_x, _y| [50, 50, 50, 255]);
1280 let slice = slice_from_strided(&buf, 8, 3, stride, PixelFormat::Rgba8);
1281 let r = slice.determine_load_bearing();
1282 assert_eq!(
1283 r.uses_alpha,
1284 Some(false),
1285 "alpha is uniformly 255 -- must not be confused by 0xCD padding"
1286 );
1287 let (buf, stride) = build_strided_rgba8(8, 3, 16, |x, y| {
1289 if x == 2 && y == 1 {
1290 [10, 10, 10, 0]
1291 } else {
1292 [50, 50, 50, 255]
1293 }
1294 });
1295 let slice = slice_from_strided(&buf, 8, 3, stride, PixelFormat::Rgba8);
1296 let r = slice.determine_load_bearing();
1297 assert_eq!(
1298 r.uses_alpha,
1299 Some(true),
1300 "real transparent pixel must be detected"
1301 );
1302 }
1303
1304 #[test]
1305 fn strided_rgba8_try_reduce_produces_tight_output() {
1306 let (buf, stride) = build_strided_rgba8(4, 4, 16, |x, y| {
1308 let g = ((x + y) * 20) as u8;
1309 [g, g, g, 255]
1310 });
1311 let slice = slice_from_strided(&buf, 4, 4, stride, PixelFormat::Rgba8);
1312 let out = slice
1313 .try_reduce_to_load_bearing_format()
1314 .expect("strided buffer should reduce");
1315 assert_eq!(out.descriptor().format, PixelFormat::Gray8);
1316 let view = out.as_slice();
1318 for y in 0..4u32 {
1319 let row = view.row(y);
1320 for (x, &g) in row.iter().enumerate() {
1321 let expected = ((x as u32 + y) * 20) as u8;
1322 assert_eq!(g, expected, "gray byte at ({x},{y}) wrong");
1323 }
1324 }
1325 }
1326
1327 #[test]
1328 fn strided_rgba8_matches_contiguous_result() {
1329 fn fill(x: u32, y: u32) -> [u8; 4] {
1332 [(x * 30) as u8, (y * 50) as u8, ((x + y) * 11) as u8, 255]
1333 }
1334 let width = 6;
1335 let height = 5;
1336
1337 let mut contig = Vec::with_capacity(width as usize * height as usize * 4);
1339 for y in 0..height {
1340 for x in 0..width {
1341 contig.extend_from_slice(&fill(x, y));
1342 }
1343 }
1344 let contig_slice = make_slice(&contig, width, height, PixelFormat::Rgba8);
1345
1346 let (strided, stride) = build_strided_rgba8(width, height, 24, fill);
1348 let strided_slice = slice_from_strided(&strided, width, height, stride, PixelFormat::Rgba8);
1349
1350 let r_contig = contig_slice.determine_load_bearing();
1351 let r_strided = strided_slice.determine_load_bearing();
1352
1353 assert_eq!(r_contig.any_analyzed(), r_strided.any_analyzed());
1355 assert_eq!(r_contig.uses_alpha, r_strided.uses_alpha);
1356 assert_eq!(r_contig.uses_chroma, r_strided.uses_chroma);
1357 assert_eq!(r_contig.uses_low_bits, r_strided.uses_low_bits);
1358 }
1359
1360 fn make_f32_slice<'a>(
1363 bytes: &'a [u8],
1364 width: u32,
1365 height: u32,
1366 format: PixelFormat,
1367 transfer: TransferFunction,
1368 ) -> PixelSlice<'a> {
1369 let descriptor = PixelDescriptor::from_pixel_format(format).with_transfer(transfer);
1370 let stride = width as usize * format.bytes_per_pixel();
1371 PixelSlice::new(bytes, width, height, stride, descriptor).unwrap()
1372 }
1373
1374 #[test]
1375 fn rgba_f32_all_opaque_gray_reduces_to_gray_f32() {
1376 let pixels: [f32; 16] = [
1378 0.1, 0.1, 0.1, 1.0, 0.5, 0.5, 0.5, 1.0, 0.9, 0.9, 0.9, 1.0, 0.0, 0.0, 0.0, 1.0,
1382 ];
1383 let bytes: &[u8] = bytemuck::cast_slice(&pixels);
1384 let slice = make_f32_slice(bytes, 4, 1, PixelFormat::RgbaF32, TransferFunction::Linear);
1385 let r = slice.determine_load_bearing();
1386 assert_eq!(r.uses_alpha, Some(false));
1388 assert_eq!(r.uses_chroma, Some(false));
1389
1390 let target = r.apply_to(&slice.descriptor());
1391 assert_eq!(target.format, PixelFormat::GrayF32);
1392 }
1393
1394 #[test]
1395 fn rgba_f32_with_real_color_reduces_to_rgb_f32() {
1396 let pixels: [f32; 16] = [
1397 0.1, 0.2, 0.3, 1.0, 0.4, 0.5, 0.6, 1.0, 0.7, 0.8, 0.9, 1.0, 0.0, 0.5, 1.0, 1.0,
1398 ];
1399 let bytes: &[u8] = bytemuck::cast_slice(&pixels);
1400 let slice = make_f32_slice(bytes, 4, 1, PixelFormat::RgbaF32, TransferFunction::Linear);
1401 let r = slice.determine_load_bearing();
1402 assert_eq!(r.uses_alpha, Some(false));
1403 assert_eq!(r.uses_chroma, Some(true));
1404
1405 let target = r.apply_to(&slice.descriptor());
1406 assert_eq!(target.format, PixelFormat::RgbF32);
1407 }
1408
1409 #[test]
1410 fn rgba_f32_with_intermediate_alpha_keeps_alpha() {
1411 let pixels: [f32; 12] = [0.5, 0.5, 0.5, 0.25, 0.7, 0.7, 0.7, 0.5, 0.3, 0.3, 0.3, 0.75];
1412 let bytes: &[u8] = bytemuck::cast_slice(&pixels);
1413 let slice = make_f32_slice(bytes, 3, 1, PixelFormat::RgbaF32, TransferFunction::Linear);
1414 let r = slice.determine_load_bearing();
1415 assert_eq!(r.uses_alpha, Some(true));
1416 assert_eq!(r.uses_chroma, Some(false));
1417
1418 let target = r.apply_to(&slice.descriptor());
1419 assert_eq!(target.format, PixelFormat::GrayAF32);
1420 }
1421
1422 #[test]
1423 fn try_reduce_rgba_f32_to_gray_f32() {
1424 let pixels: [f32; 16] = [
1425 0.1, 0.1, 0.1, 1.0, 0.5, 0.5, 0.5, 1.0, 0.9, 0.9, 0.9, 1.0, 0.4, 0.4, 0.4, 1.0,
1429 ];
1430 let bytes: &[u8] = bytemuck::cast_slice(&pixels);
1431 let slice = make_f32_slice(bytes, 4, 1, PixelFormat::RgbaF32, TransferFunction::Linear);
1432 let out = slice
1433 .try_reduce_to_load_bearing_format()
1434 .expect("should reduce");
1435 assert_eq!(out.descriptor().format, PixelFormat::GrayF32);
1436 let view = out.as_slice();
1437 let gray: &[f32] = bytemuck::cast_slice(view.row(0));
1438 assert_eq!(gray, &[0.1, 0.5, 0.9, 0.4]);
1439 }
1440
1441 #[test]
1442 fn linear_f32_wide_primaries_reduce_keeps_tag_and_values() {
1443 let pixels: [f32; 16] = [
1447 0.5, 0.5, 0.5, 1.0, 0.25, 0.25, 0.25, 1.0, 0.75, 0.75, 0.75, 1.0, 0.1, 0.1, 0.1, 1.0,
1448 ];
1449 let bytes: &[u8] = bytemuck::cast_slice(&pixels);
1450 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::RgbaF32)
1451 .with_transfer(TransferFunction::Linear)
1452 .with_primaries(ColorPrimaries::DisplayP3);
1453 let slice = PixelSlice::new(bytes, 4, 1, 4 * 16, descriptor).unwrap();
1454 let out = slice
1455 .try_reduce_to_load_bearing_format()
1456 .expect("should reduce");
1457 assert_eq!(out.descriptor().format, PixelFormat::GrayF32);
1458 assert_eq!(out.descriptor().primaries, ColorPrimaries::DisplayP3);
1459 let view = out.as_slice();
1460 let gray: &[f32] = bytemuck::cast_slice(view.row(0));
1461 assert_eq!(gray, &[0.5_f32, 0.25, 0.75, 0.1], "values bit-exact");
1462 }
1463
1464 #[test]
1465 fn ga_f32_opaque_reduces_to_gray_f32() {
1466 let pixels: [f32; 6] = [0.1, 1.0, 0.5, 1.0, 0.9, 1.0];
1467 let bytes: &[u8] = bytemuck::cast_slice(&pixels);
1468 let slice = make_f32_slice(bytes, 3, 1, PixelFormat::GrayAF32, TransferFunction::Linear);
1469 assert_eq!(reduced(&slice).format, PixelFormat::GrayF32);
1470 }
1471
1472 #[test]
1473 fn rgb_f32_grayscale_reduces_to_gray_f32() {
1474 let pixels: [f32; 9] = [0.1, 0.1, 0.1, 0.5, 0.5, 0.5, 0.9, 0.9, 0.9];
1475 let bytes: &[u8] = bytemuck::cast_slice(&pixels);
1476 let slice = make_f32_slice(bytes, 3, 1, PixelFormat::RgbF32, TransferFunction::Linear);
1477 assert_eq!(reduced(&slice).format, PixelFormat::GrayF32);
1478 }
1479
1480 #[test]
1488 fn apply_to_is_idempotent() {
1489 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
1490 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8);
1491 let r = slice.determine_load_bearing();
1492 let target_a = r.apply_to(&slice.descriptor());
1493 let target_b = r.apply_to(&target_a);
1494 assert_eq!(
1495 target_a, target_b,
1496 "apply_to twice must equal apply_to once"
1497 );
1498 }
1499
1500 #[test]
1501 fn apply_to_no_op_on_already_minimal_gray8() {
1502 let bytes = [50u8, 100, 150, 200];
1505 let slice = make_slice(&bytes, 4, 1, PixelFormat::Gray8);
1506 let r = slice.determine_load_bearing();
1507 assert_eq!(r.uses_alpha, Some(false));
1508 assert_eq!(r.uses_chroma, Some(false));
1509 assert_eq!(r.uses_low_bits, Some(false));
1510 let target = r.apply_to(&slice.descriptor());
1511 assert_eq!(target, slice.descriptor());
1512 }
1513
1514 #[test]
1521 fn try_reduce_descriptor_matches_determine_reduced() {
1522 let bytes: Vec<u8> = (0..8).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
1523 let slice = make_slice(&bytes, 8, 1, PixelFormat::Rgba8);
1524 let determined = reduced(&slice);
1525 let out = slice.try_reduce_to_load_bearing_format().unwrap();
1526 assert_eq!(determined, out.descriptor());
1527 }
1528
1529 #[test]
1530 fn try_reduce_returns_none_when_descriptor_unchanged() {
1531 let bytes = [50u8, 100, 150, 200];
1533 let slice = make_slice(&bytes, 4, 1, PixelFormat::Gray8);
1534 assert!(slice.try_reduce_to_load_bearing_format().is_none());
1535 assert_eq!(reduced(&slice), slice.descriptor());
1537 }
1538
1539 #[test]
1542 fn single_pixel_inputs_for_each_layout() {
1543 let s = make_slice(&[100u8, 100, 100, 255], 1, 1, PixelFormat::Rgba8);
1545 assert_eq!(reduced(&s).format, PixelFormat::Gray8);
1546
1547 let s = make_slice(&[42u8, 42, 42], 1, 1, PixelFormat::Rgb8);
1549 assert_eq!(reduced(&s).format, PixelFormat::Gray8);
1550
1551 let s = make_slice(&[42u8, 255], 1, 1, PixelFormat::GrayA8);
1553 assert_eq!(reduced(&s).format, PixelFormat::Gray8);
1554
1555 let s = make_slice(&[42u8], 1, 1, PixelFormat::Gray8);
1557 assert_eq!(reduced(&s), s.descriptor());
1558 }
1559
1560 #[test]
1561 fn single_row_tall_buffer() {
1562 let bytes: Vec<u8> = (0..32).flat_map(|i| [i * 7, i * 7, i * 7, 255]).collect();
1564 let s = make_slice(&bytes, 32, 1, PixelFormat::Rgba8);
1565 assert_eq!(reduced(&s).format, PixelFormat::Gray8);
1566 }
1567
1568 #[test]
1569 fn single_col_tall_buffer() {
1570 let height = 16u32;
1572 let width = 1u32;
1573 let stride = 32; let mut buf = vec![0xAAu8; stride * height as usize];
1575 for y in 0..height {
1576 buf[y as usize * stride] = (y * 7) as u8;
1577 }
1578 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Gray8)
1579 .with_transfer(TransferFunction::Srgb);
1580 let s = PixelSlice::new(&buf, width, height, stride, descriptor).unwrap();
1581 assert!(!s.is_contiguous());
1582 assert!(s.determine_load_bearing().any_analyzed());
1585 }
1586
1587 fn dummy_bytes_for(format: PixelFormat) -> Vec<u8> {
1593 vec![0u8; format.bytes_per_pixel()]
1595 }
1596
1597 #[test]
1598 fn analyzed_status_for_every_pixel_format() {
1599 for fmt in [
1601 PixelFormat::Rgb8,
1602 PixelFormat::Rgba8,
1603 PixelFormat::Bgra8,
1604 PixelFormat::Gray8,
1605 PixelFormat::GrayA8,
1606 ] {
1607 let bytes = dummy_bytes_for(fmt);
1608 let s = make_slice(&bytes, 1, 1, fmt);
1609 assert!(
1610 s.determine_load_bearing().any_analyzed(),
1611 "{fmt:?} should produce at least one Some field"
1612 );
1613 }
1614 for fmt in [
1616 PixelFormat::Rgb16,
1617 PixelFormat::Rgba16,
1618 PixelFormat::Gray16,
1619 PixelFormat::GrayA16,
1620 ] {
1621 let bytes = dummy_bytes_for(fmt);
1622 let s = make_slice(&bytes, 1, 1, fmt);
1623 assert!(
1624 s.determine_load_bearing().any_analyzed(),
1625 "{fmt:?} should produce at least one Some field"
1626 );
1627 }
1628 for fmt in [
1630 PixelFormat::RgbF32,
1631 PixelFormat::RgbaF32,
1632 PixelFormat::GrayAF32,
1633 ] {
1634 let bytes = dummy_bytes_for(fmt);
1635 let s = make_slice(&bytes, 1, 1, fmt);
1636 assert!(
1637 s.determine_load_bearing().any_analyzed(),
1638 "{fmt:?} should produce at least one Some field"
1639 );
1640 }
1641 for fmt in [PixelFormat::GrayF32, PixelFormat::GrayF16] {
1646 let bytes = dummy_bytes_for(fmt);
1647 let s = make_slice(&bytes, 1, 1, fmt);
1648 let r = s.determine_load_bearing();
1653 assert_eq!(r.uses_alpha, Some(false), "{fmt:?} alpha");
1654 assert_eq!(r.uses_chroma, Some(false), "{fmt:?} chroma");
1655 }
1656
1657 for fmt in [
1660 PixelFormat::RgbF16,
1661 PixelFormat::RgbaF16,
1662 PixelFormat::GrayAF16,
1663 PixelFormat::OklabF32,
1664 PixelFormat::OklabaF32,
1665 PixelFormat::Cmyk8,
1666 ] {
1667 let bytes = dummy_bytes_for(fmt);
1668 let s = make_slice(&bytes, 1, 1, fmt);
1669 let r = s.determine_load_bearing();
1670 assert_eq!(r.uses_alpha, None, "{fmt:?} alpha should be None");
1673 assert_eq!(r.uses_chroma, None, "{fmt:?} chroma should be None");
1674 }
1675 }
1676
1677 #[test]
1680 fn bgra8_opaque_color_reduces_to_rgb8_with_reorder() {
1681 let bytes = [50u8, 100, 150, 255, 60, 110, 160, 255];
1685 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Bgra8)
1686 .with_transfer(TransferFunction::Srgb);
1687 let s = PixelSlice::new(&bytes, 2, 1, 8, descriptor).unwrap();
1688 let r = s.determine_load_bearing();
1689 assert_eq!(r.uses_alpha, Some(false));
1690 assert_eq!(r.uses_chroma, Some(true));
1691 let target = r.apply_to(&s.descriptor());
1692 assert_eq!(target.format, PixelFormat::Rgb8);
1693
1694 let out = s
1695 .try_reduce_to_load_bearing_format()
1696 .expect("opaque Bgra8 should reduce");
1697 assert_eq!(out.descriptor().format, PixelFormat::Rgb8);
1698 assert_eq!(
1699 out.as_slice().row(0),
1700 &[150u8, 100, 50, 160, 110, 60],
1701 "B,G,R,A → R,G,B requires the B↔R swap"
1702 );
1703 }
1704
1705 #[test]
1706 fn bgra8_grayscale_collapses_to_gray_alpha8() {
1707 let bytes = [42u8, 42, 42, 100, 99, 99, 99, 200];
1710 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Bgra8)
1711 .with_transfer(TransferFunction::Srgb);
1712 let s = PixelSlice::new(&bytes, 2, 1, 8, descriptor).unwrap();
1713 let r = s.determine_load_bearing();
1714 assert_eq!(reduced(&s).format, PixelFormat::GrayA8);
1715 assert_eq!(r.uses_chroma, Some(false));
1716 let out = s.try_reduce_to_load_bearing_format().unwrap();
1718 assert_eq!(out.descriptor().format, PixelFormat::GrayA8);
1719 assert_eq!(out.as_slice().row(0), &[42u8, 100, 99, 200]);
1720 }
1721
1722 #[test]
1725 fn fully_load_bearing_apply_to_is_identity() {
1726 let r = LoadBearingReport::default();
1728 for fmt in [
1729 PixelFormat::Rgb8,
1730 PixelFormat::Rgba8,
1731 PixelFormat::Rgba16,
1732 PixelFormat::GrayAF32,
1733 ] {
1734 let src = PixelDescriptor::from_pixel_format(fmt);
1735 assert_eq!(r.apply_to(&src), src, "{fmt:?} identity broke");
1736 }
1737 }
1738
1739 #[test]
1745 fn zero_pixel_buffer_analyzes_with_vacuous_truth() {
1746 let bytes: [u8; 0] = [];
1748 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8)
1752 .with_transfer(TransferFunction::Srgb);
1753 if let Ok(s) = PixelSlice::new(&bytes, 0, 0, 0, descriptor) {
1754 let r = s.determine_load_bearing();
1755 assert_eq!(r.uses_alpha, Some(false));
1759 assert_eq!(r.uses_chroma, Some(false));
1760 }
1761 }
1765
1766 fn lb_buf(bytes: &[u8], width: u32, height: u32, format: PixelFormat) -> PixelBuffer {
1772 let descriptor =
1773 PixelDescriptor::from_pixel_format(format).with_transfer(TransferFunction::Srgb);
1774 PixelBuffer::from_vec(bytes.to_vec(), width, height, descriptor).unwrap()
1775 }
1776
1777 #[test]
1778 fn in_place_rgba8_gray_opaque_force_true_compacts_to_gray8() {
1779 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
1780 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba8);
1781 buf.reduce_to_load_bearing_format_in_place(true);
1782 assert_eq!(buf.descriptor().format, PixelFormat::Gray8);
1783 assert_eq!(buf.stride(), 4, "tight stride");
1784 assert_eq!(buf.as_slice().row(0), &[0u8, 30, 60, 90]);
1785 }
1786
1787 #[test]
1788 fn in_place_rgba8_gray_opaque_force_false_keeps_alpha_lane() {
1789 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
1792 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba8);
1793 buf.reduce_to_load_bearing_format_in_place(false);
1794 assert_eq!(buf.descriptor().format, PixelFormat::GrayA8);
1795 assert_eq!(buf.descriptor().alpha, Some(AlphaMode::Opaque));
1796 assert_eq!(
1797 buf.as_slice().row(0),
1798 &[0u8, 255, 30, 255, 60, 255, 90, 255]
1799 );
1800 }
1801
1802 #[test]
1803 fn in_place_colorful_opaque_force_false_is_retag_only() {
1804 let original: Vec<u8> = (0..4i32)
1805 .flat_map(|i| {
1806 [
1807 (i * 60 + 10) as u8,
1808 (i * 30 + 50) as u8,
1809 (i * 90 + 20) as u8,
1810 255,
1811 ]
1812 })
1813 .collect();
1814 let mut buf = lb_buf(&original, 4, 1, PixelFormat::Rgba8);
1815 let in_stride = buf.stride();
1816 buf.reduce_to_load_bearing_format_in_place(false);
1817 assert_eq!(buf.descriptor().format, PixelFormat::Rgba8, "layout kept");
1818 assert_eq!(
1819 buf.descriptor().alpha,
1820 Some(AlphaMode::Opaque),
1821 "scanned-opaque straight alpha upgrades to the Opaque contract"
1822 );
1823 assert_eq!(buf.stride(), in_stride, "no bytes moved");
1824 assert_eq!(buf.as_slice().row(0), &original[..], "no bytes changed");
1825 }
1826
1827 #[test]
1828 fn in_place_colorful_opaque_force_true_drops_alpha() {
1829 let bytes: Vec<u8> = (0..4i32)
1830 .flat_map(|i| {
1831 [
1832 (i * 60 + 10) as u8,
1833 (i * 30 + 50) as u8,
1834 (i * 90 + 20) as u8,
1835 255,
1836 ]
1837 })
1838 .collect();
1839 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba8);
1840 buf.reduce_to_load_bearing_format_in_place(true);
1841 assert_eq!(buf.descriptor().format, PixelFormat::Rgb8);
1842 assert_eq!(
1843 buf.as_slice().row(0),
1844 &[10u8, 50, 20, 70, 80, 110, 130, 110, 200, 190, 140, 34]
1845 );
1846 }
1847
1848 #[test]
1849 fn in_place_bgra8_force_true_reorders_to_rgb8() {
1850 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Bgra8)
1851 .with_transfer(TransferFunction::Srgb);
1852 let mut buf = PixelBuffer::from_vec(
1853 vec![50u8, 100, 150, 255, 60, 110, 160, 255],
1854 2,
1855 1,
1856 descriptor,
1857 )
1858 .unwrap();
1859 buf.reduce_to_load_bearing_format_in_place(true);
1860 assert_eq!(buf.descriptor().format, PixelFormat::Rgb8);
1861 assert_eq!(buf.as_slice().row(0), &[150u8, 100, 50, 160, 110, 60]);
1862 }
1863
1864 #[test]
1865 fn in_place_rgba16_replicated_gray_opaque_both_force_modes() {
1866 let build = |i: u8| {
1867 let g = i * 60;
1868 [g, g, g, g, g, g, 0xFF, 0xFF]
1869 };
1870 let bytes: Vec<u8> = (0..4).flat_map(build).collect();
1871 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba16);
1872 buf.reduce_to_load_bearing_format_in_place(true);
1873 assert_eq!(buf.descriptor().format, PixelFormat::Gray8);
1874 assert_eq!(buf.as_slice().row(0), &[0u8, 60, 120, 180]);
1875
1876 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba16);
1877 buf.reduce_to_load_bearing_format_in_place(false);
1878 assert_eq!(buf.descriptor().format, PixelFormat::GrayA8);
1879 assert_eq!(buf.descriptor().alpha, Some(AlphaMode::Opaque));
1880 assert_eq!(
1881 buf.as_slice().row(0),
1882 &[0u8, 255, 60, 255, 120, 255, 180, 255]
1883 );
1884 }
1885
1886 #[test]
1887 fn in_place_gray16_replicated_single_row_overlap_path() {
1888 let bytes: Vec<u8> = (0..64u16).flat_map(|i| [(i * 4) as u8; 2]).collect();
1891 let mut buf = lb_buf(&bytes, 64, 1, PixelFormat::Gray16);
1892 buf.reduce_to_load_bearing_format_in_place(true);
1893 assert_eq!(buf.descriptor().format, PixelFormat::Gray8);
1894 let expected: Vec<u8> = (0..64u16).map(|i| (i * 4) as u8).collect();
1895 assert_eq!(buf.as_slice().row(0), &expected[..]);
1896 }
1897
1898 #[test]
1899 fn in_place_undefined_padding_retag_vs_restructure() {
1900 let original = [10u8, 20, 30, 0x7B, 40, 50, 60, 0x01];
1904 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8)
1905 .with_transfer(TransferFunction::Srgb)
1906 .with_alpha(Some(AlphaMode::Undefined));
1907 let mut buf = PixelBuffer::from_vec(original.to_vec(), 2, 1, descriptor).unwrap();
1908 buf.reduce_to_load_bearing_format_in_place(false);
1909 assert_eq!(buf.descriptor(), descriptor, "fully unchanged");
1910 assert_eq!(buf.as_slice().row(0), &original[..]);
1911
1912 let mut buf = PixelBuffer::from_vec(original.to_vec(), 2, 1, descriptor).unwrap();
1913 buf.reduce_to_load_bearing_format_in_place(true);
1914 assert_eq!(buf.descriptor().format, PixelFormat::Rgb8);
1915 assert_eq!(buf.as_slice().row(0), &[10u8, 20, 30, 40, 50, 60]);
1916 }
1917
1918 #[test]
1919 fn in_place_load_bearing_alpha_is_untouched() {
1920 let original = [10u8, 20, 30, 128, 40, 50, 60, 64];
1923 for force in [false, true] {
1924 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8)
1925 .with_transfer(TransferFunction::Srgb)
1926 .with_alpha(Some(AlphaMode::Premultiplied));
1927 let mut buf = PixelBuffer::from_vec(original.to_vec(), 2, 1, descriptor).unwrap();
1928 buf.reduce_to_load_bearing_format_in_place(force);
1929 assert_eq!(buf.descriptor(), descriptor, "force={force}");
1930 assert_eq!(buf.as_slice().row(0), &original[..]);
1931 }
1932 }
1933
1934 #[test]
1935 fn reduce_impl_strided_input_compacts_like_allocating() {
1936 let (buf, stride) = build_strided_rgba8(5, 4, 24, |x, y| {
1940 let g = ((x + y) * 19) as u8;
1941 [g, g, g, 255]
1942 });
1943 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8)
1944 .with_transfer(TransferFunction::Srgb);
1945 let reference = PixelSlice::new(&buf, 5, 4, stride, descriptor)
1946 .unwrap()
1947 .try_reduce_to_load_bearing_format()
1948 .expect("reduces");
1949
1950 let mut mut_buf = buf.clone();
1951 let out = reduce_in_place_impl(
1952 InPlacePixels::new(&mut mut_buf, 5, 4, stride, descriptor, None),
1953 true,
1954 );
1955 assert_eq!(out.descriptor(), reference.descriptor());
1956 for y in 0..4 {
1957 assert_eq!(out.row(y), reference.as_slice().row(y), "row {y}");
1958 }
1959 }
1960
1961 #[test]
1962 fn in_place_matches_allocating_across_geometries() {
1963 #[derive(Clone, Copy)]
1968 enum Content {
1969 GrayOpaque, ColorOpaque, GrayVaryAlpha, Replicated16, }
1974 let mut lcg: u32 = 0x2F6E_2B1D;
1975 let mut next = move || {
1976 lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1977 (lcg >> 24) as u8
1978 };
1979 for content in [
1980 Content::GrayOpaque,
1981 Content::ColorOpaque,
1982 Content::GrayVaryAlpha,
1983 Content::Replicated16,
1984 ] {
1985 for (width, rows) in [
1986 (1u32, 1u32),
1987 (1, 7),
1988 (2, 3),
1989 (3, 2),
1990 (5, 5),
1991 (17, 3),
1992 (64, 4),
1993 (65, 2),
1994 ] {
1995 let (format, bytes): (PixelFormat, Vec<u8>) = match content {
1996 Content::GrayOpaque => (
1997 PixelFormat::Rgba8,
1998 (0..width * rows)
1999 .flat_map(|_| {
2000 let g = next();
2001 [g, g, g, 255]
2002 })
2003 .collect(),
2004 ),
2005 Content::ColorOpaque => (
2006 PixelFormat::Rgba8,
2007 (0..width * rows)
2008 .flat_map(|_| [next(), next(), next(), 255])
2009 .collect(),
2010 ),
2011 Content::GrayVaryAlpha => (
2012 PixelFormat::Rgba8,
2013 (0..width * rows)
2014 .flat_map(|_| {
2015 let g = next();
2016 [g, g, g, next()]
2017 })
2018 .collect(),
2019 ),
2020 Content::Replicated16 => (
2021 PixelFormat::Rgba16,
2022 (0..width * rows)
2023 .flat_map(|_| {
2024 let g = next();
2025 [g, g, g, g, g, g, 0xFF, 0xFF]
2026 })
2027 .collect(),
2028 ),
2029 };
2030 let reference =
2031 make_slice(&bytes, width, rows, format).try_reduce_to_load_bearing_format();
2032 let mut buf = lb_buf(&bytes, width, rows, format);
2033 buf.reduce_to_load_bearing_format_in_place(true);
2034 match reference {
2035 Some(reference) => {
2036 assert_eq!(
2037 buf.descriptor(),
2038 reference.descriptor(),
2039 "{width}x{rows} descriptor"
2040 );
2041 for y in 0..rows {
2042 assert_eq!(
2043 buf.as_slice().row(y),
2044 reference.as_slice().row(y),
2045 "{width}x{rows} row {y}"
2046 );
2047 }
2048 }
2049 None => {
2050 panic!("{width}x{rows} expected a reduction");
2053 }
2054 }
2055 }
2056 }
2057 }
2058
2059 #[test]
2062 fn color_context_carries_through_class_preserving_reductions() {
2063 let ctx = Arc::new(ColorContext::from_icc(alloc::vec![0u8; 8]));
2065 let bytes: Vec<u8> = (0..4i32)
2066 .flat_map(|i| {
2067 [
2068 (i * 60 + 10) as u8,
2069 (i * 30 + 50) as u8,
2070 (i * 90 + 20) as u8,
2071 255,
2072 ]
2073 })
2074 .collect();
2075 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2076 let out = slice
2077 .try_reduce_to_load_bearing_format()
2078 .expect("alpha drop available");
2079 assert_eq!(out.descriptor().format, PixelFormat::Rgb8);
2080 assert!(
2081 out.as_slice()
2082 .color_context()
2083 .is_some_and(|c| Arc::ptr_eq(c, &ctx)),
2084 "ICC context must carry over for class-preserving reductions"
2085 );
2086
2087 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2088 buf.reduce_to_load_bearing_format_in_place(true);
2089 assert_eq!(buf.descriptor().format, PixelFormat::Rgb8);
2090 assert!(
2091 buf.as_slice()
2092 .color_context()
2093 .is_some_and(|c| Arc::ptr_eq(c, &ctx))
2094 );
2095 }
2096
2097 #[test]
2098 fn icc_context_suppresses_gray_collapse_but_not_other_reductions() {
2099 let ctx = Arc::new(ColorContext::from_icc(alloc::vec![0u8; 8]));
2104 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
2105 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2106 assert_eq!(
2107 slice.determine_load_bearing().uses_chroma,
2108 Some(false),
2109 "analysis stays truthful"
2110 );
2111 let out = slice
2112 .try_reduce_to_load_bearing_format()
2113 .expect("alpha drop still available");
2114 assert_eq!(
2115 out.descriptor().format,
2116 PixelFormat::Rgb8,
2117 "gray collapse suppressed, alpha drop kept"
2118 );
2119 assert!(
2120 out.as_slice()
2121 .color_context()
2122 .is_some_and(|c| Arc::ptr_eq(c, &ctx))
2123 );
2124
2125 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2126 buf.reduce_to_load_bearing_format_in_place(true);
2127 assert_eq!(buf.descriptor().format, PixelFormat::Rgb8);
2128 assert!(
2129 buf.as_slice()
2130 .color_context()
2131 .is_some_and(|c| Arc::ptr_eq(c, &ctx))
2132 );
2133 }
2134
2135 #[test]
2136 fn cicp_only_context_carries_through_gray_collapse() {
2137 let ctx = Arc::new(ColorContext::from_cicp(Cicp::DISPLAY_P3));
2142 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
2143 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2144 let out = slice
2145 .try_reduce_to_load_bearing_format()
2146 .expect("gray collapse available");
2147 assert_eq!(out.descriptor().format, PixelFormat::Gray8);
2148 assert!(
2149 out.as_slice()
2150 .color_context()
2151 .is_some_and(|c| Arc::ptr_eq(c, &ctx))
2152 );
2153
2154 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2155 buf.reduce_to_load_bearing_format_in_place(true);
2156 assert_eq!(buf.descriptor().format, PixelFormat::Gray8);
2157 assert!(
2158 buf.as_slice()
2159 .color_context()
2160 .is_some_and(|c| Arc::ptr_eq(c, &ctx))
2161 );
2162 }
2163
2164 fn assert_gray_class_icc(ctx: Option<&Arc<ColorContext>>) -> Arc<[u8]> {
2168 let icc = ctx
2169 .expect("reduced buffer must carry a context")
2170 .icc
2171 .clone()
2172 .expect("swapped context must hold ICC bytes");
2173 assert_eq!(&icc[16..20], b"GRAY", "swapped profile must be GRAY-class");
2174 assert_eq!(&icc[36..40], b"acsp", "swapped profile must be a valid ICC");
2175 icc
2176 }
2177
2178 #[cfg(feature = "icc-db")]
2179 #[test]
2180 fn icc_with_cicp_swaps_to_gray_class_profile_on_collapse() {
2181 let mut both = ColorContext::from_icc(alloc::vec![0u8; 8]);
2185 both.cicp = Some(Cicp::DISPLAY_P3);
2186 let ctx = Arc::new(both);
2187 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
2188
2189 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2190 let out = slice
2191 .try_reduce_to_load_bearing_format()
2192 .expect("gray collapse available via gray-ICC swap");
2193 assert_eq!(out.descriptor().format, PixelFormat::Gray8);
2194 let swapped = assert_gray_class_icc(out.as_slice().color_context());
2195 assert_eq!(
2196 out.as_slice().color_context().unwrap().cicp,
2197 Some(Cicp::DISPLAY_P3),
2198 "source cicp must ride along"
2199 );
2200
2201 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2203 buf.reduce_to_load_bearing_format_in_place(true);
2204 assert_eq!(buf.descriptor().format, PixelFormat::Gray8);
2205 let view = buf.as_slice();
2206 let swapped_in_place = assert_gray_class_icc(view.color_context());
2207 assert_eq!(swapped_in_place.as_ref(), swapped.as_ref());
2208 }
2209
2210 #[cfg(feature = "icc-db")]
2211 #[test]
2212 fn recognized_rgb_profile_swaps_to_gray_class_on_collapse() {
2213 let ctx = Arc::new(ColorContext::from_icc(
2217 crate::icc_profiles::DISPLAY_P3_V4.to_vec(),
2218 ));
2219 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
2220 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2221 let out = slice
2222 .try_reduce_to_load_bearing_format()
2223 .expect("gray collapse available via identification");
2224 assert_eq!(out.descriptor().format, PixelFormat::Gray8);
2225 assert_gray_class_icc(out.as_slice().color_context());
2226 assert_eq!(
2227 out.as_slice().color_context().unwrap().cicp,
2228 None,
2229 "no cicp on the source context, none invented"
2230 );
2231 }
2232
2233 #[test]
2234 fn srgb_described_icc_drops_to_cicp_only_on_collapse() {
2235 let mut both = ColorContext::from_icc(alloc::vec![0u8; 8]);
2239 both.cicp = Some(Cicp::SRGB);
2240 let ctx = Arc::new(both);
2241 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
2242 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2243 let out = slice
2244 .try_reduce_to_load_bearing_format()
2245 .expect("gray collapse available");
2246 assert_eq!(out.descriptor().format, PixelFormat::Gray8);
2247 let new_ctx = out
2248 .as_slice()
2249 .color_context()
2250 .cloned()
2251 .expect("cicp-only context expected");
2252 assert!(new_ctx.icc.is_none(), "sRGB-default gray needs no ICC");
2253 assert_eq!(new_ctx.cicp, Some(Cicp::SRGB));
2254 }
2255
2256 #[test]
2257 fn non_cicp_recognized_profile_still_suppresses_collapse() {
2258 let ctx = Arc::new(ColorContext::from_icc(
2262 crate::icc_profiles::ADOBE_RGB.to_vec(),
2263 ));
2264 let bytes: Vec<u8> = (0..4).flat_map(|i| [i * 30, i * 30, i * 30, 255]).collect();
2265 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2266 let out = slice
2267 .try_reduce_to_load_bearing_format()
2268 .expect("alpha drop still available");
2269 assert_eq!(
2270 out.descriptor().format,
2271 PixelFormat::Rgb8,
2272 "collapse suppressed without a CICP-expressible color"
2273 );
2274 assert!(
2275 out.as_slice()
2276 .color_context()
2277 .is_some_and(|c| Arc::ptr_eq(c, &ctx))
2278 );
2279 }
2280
2281 #[test]
2282 fn colorful_content_keeps_original_context_despite_swap_plan() {
2283 let mut both = ColorContext::from_icc(alloc::vec![0u8; 8]);
2287 both.cicp = Some(Cicp::DISPLAY_P3);
2288 let ctx = Arc::new(both);
2289 let bytes: Vec<u8> = (0..4i32)
2290 .flat_map(|i| {
2291 [
2292 (i * 60 + 10) as u8,
2293 (i * 30 + 50) as u8,
2294 (i * 90 + 20) as u8,
2295 255,
2296 ]
2297 })
2298 .collect();
2299 let slice = make_slice(&bytes, 4, 1, PixelFormat::Rgba8).with_color_context(ctx.clone());
2300 let out = slice
2301 .try_reduce_to_load_bearing_format()
2302 .expect("alpha drop available");
2303 assert_eq!(out.descriptor().format, PixelFormat::Rgb8);
2304 assert!(
2305 out.as_slice()
2306 .color_context()
2307 .is_some_and(|c| Arc::ptr_eq(c, &ctx)),
2308 "no collapse -> original context, not the swap"
2309 );
2310 }
2311
2312 #[test]
2313 fn in_place_rgba_f32_gray_opaque_force_true() {
2314 let pixels: [f32; 16] = [
2315 0.1, 0.1, 0.1, 1.0, 0.5, 0.5, 0.5, 1.0, 0.9, 0.9, 0.9, 1.0, 0.4, 0.4, 0.4, 1.0,
2319 ];
2320 let descriptor = PixelDescriptor::from_pixel_format(PixelFormat::RgbaF32)
2321 .with_transfer(TransferFunction::Linear);
2322 let mut buf =
2323 PixelBuffer::from_vec(bytemuck::cast_slice(&pixels).to_vec(), 4, 1, descriptor)
2324 .unwrap();
2325 buf.reduce_to_load_bearing_format_in_place(true);
2326 assert_eq!(buf.descriptor().format, PixelFormat::GrayF32);
2327 let view = buf.as_slice();
2328 let gray: &[f32] = bytemuck::cast_slice(view.row(0));
2329 assert_eq!(gray, &[0.1_f32, 0.5, 0.9, 0.4], "values bit-exact");
2330 }
2331
2332 #[test]
2333 fn in_place_true_u16_keeps_channel_type() {
2334 let bytes: Vec<u8> = (0..4u16)
2338 .flat_map(|i| {
2339 let r = 0x1234 + i * 0x0101;
2340 let g = 0x4567;
2341 let b = 0x89AB;
2342 [r, g, b, 0xFFFF]
2343 })
2344 .flat_map(u16::to_ne_bytes)
2345 .collect();
2346 let mut buf = lb_buf(&bytes, 4, 1, PixelFormat::Rgba16);
2347 buf.reduce_to_load_bearing_format_in_place(false);
2348 assert_eq!(buf.descriptor().format, PixelFormat::Rgba16);
2349 assert_eq!(buf.descriptor().alpha, Some(AlphaMode::Opaque));
2350 }
2351
2352 #[test]
2353 fn every_reduction_target_is_constructable() {
2354 struct Case {
2357 src: PixelFormat,
2358 bytes: Vec<u8>,
2359 width: u32,
2360 height: u32,
2361 expect_format: PixelFormat,
2362 expect_size: usize,
2363 }
2364 let cases = vec![
2365 Case {
2366 src: PixelFormat::Rgba8,
2367 bytes: vec![10, 10, 10, 255, 20, 20, 20, 255],
2368 width: 2,
2369 height: 1,
2370 expect_format: PixelFormat::Gray8,
2371 expect_size: 2,
2372 },
2373 Case {
2374 src: PixelFormat::Rgba8,
2375 bytes: vec![10, 20, 30, 255, 40, 50, 60, 255],
2376 width: 2,
2377 height: 1,
2378 expect_format: PixelFormat::Rgb8,
2379 expect_size: 6,
2380 },
2381 Case {
2382 src: PixelFormat::GrayA8,
2383 bytes: vec![10, 255, 50, 255],
2384 width: 2,
2385 height: 1,
2386 expect_format: PixelFormat::Gray8,
2387 expect_size: 2,
2388 },
2389 Case {
2390 src: PixelFormat::Rgba16,
2391 bytes: vec![
2392 10, 10, 10, 10, 10, 10, 0xFF, 0xFF, 20, 20, 20, 20, 20, 20, 0xFF, 0xFF,
2394 ],
2395 width: 2,
2396 height: 1,
2397 expect_format: PixelFormat::Gray8,
2398 expect_size: 2,
2399 },
2400 ];
2401 for c in cases {
2402 let s = make_slice(&c.bytes, c.width, c.height, c.src);
2403 let out = s
2404 .try_reduce_to_load_bearing_format()
2405 .unwrap_or_else(|| panic!("{:?} should reduce", c.src));
2406 assert_eq!(
2407 out.descriptor().format,
2408 c.expect_format,
2409 "format from {:?}",
2410 c.src
2411 );
2412 assert_eq!(
2413 out.as_slice().row(0).len(),
2414 c.expect_size,
2415 "row size from {:?}",
2416 c.src
2417 );
2418 }
2419 }
2420}