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