1use zenpixels::{ColorPrimaries, TransferFunction};
8
9use crate::convert::{hlg_eotf, hlg_oetf, pq_eotf, pq_oetf};
10use crate::gamut::GamutMatrix;
11
12pub trait TransferFunctionExt {
18 #[must_use]
22 fn linearize(&self, v: f32) -> f32;
23
24 #[must_use]
28 fn delinearize(&self, v: f32) -> f32;
29}
30
31impl TransferFunctionExt for TransferFunction {
32 #[allow(unreachable_patterns)]
33 fn linearize(&self, v: f32) -> f32 {
34 match self {
35 Self::Linear | Self::Unknown => v,
36 Self::Srgb => linear_srgb::precise::srgb_to_linear(v),
37 Self::Bt709 => linear_srgb::tf::bt709_to_linear(v),
38 Self::Pq => pq_eotf(v),
39 Self::Hlg => hlg_eotf(v),
40 _ => v,
41 }
42 }
43
44 #[allow(unreachable_patterns)]
45 fn delinearize(&self, v: f32) -> f32 {
46 match self {
47 Self::Linear | Self::Unknown => v,
48 Self::Srgb => linear_srgb::precise::linear_to_srgb(v),
49 Self::Bt709 => linear_srgb::tf::linear_to_bt709(v),
50 Self::Pq => pq_oetf(v),
51 Self::Hlg => hlg_oetf(v),
52 _ => v,
53 }
54 }
55}
56
57#[allow(clippy::wrong_self_convention)]
63pub trait ColorPrimariesExt {
64 fn to_xyz_matrix(&self) -> Option<&'static GamutMatrix>;
68
69 fn from_xyz_matrix(&self) -> Option<&'static GamutMatrix>;
73}
74
75impl ColorPrimariesExt for ColorPrimaries {
76 #[allow(unreachable_patterns)]
77 fn to_xyz_matrix(&self) -> Option<&'static GamutMatrix> {
78 match self {
79 Self::Bt709 => Some(&crate::gamut::BT709_TO_XYZ),
80 Self::DisplayP3 => Some(&crate::gamut::DISPLAY_P3_TO_XYZ),
81 Self::Bt2020 => Some(&crate::gamut::BT2020_TO_XYZ),
82 _ => None,
83 }
84 }
85
86 #[allow(unreachable_patterns)]
87 fn from_xyz_matrix(&self) -> Option<&'static GamutMatrix> {
88 match self {
89 Self::Bt709 => Some(&crate::gamut::XYZ_TO_BT709),
90 Self::DisplayP3 => Some(&crate::gamut::XYZ_TO_DISPLAY_P3),
91 Self::Bt2020 => Some(&crate::gamut::XYZ_TO_BT2020),
92 _ => None,
93 }
94 }
95}
96
97use alloc::sync::Arc;
102use whereat::{At, ResultAtExt};
103use zenpixels::PixelDescriptor;
104use zenpixels::buffer::PixelBuffer;
105use zenpixels::descriptor::{AlphaMode, ChannelLayout, ChannelType};
106
107pub trait PixelBufferConvertExt {
109 fn convert_to(&self, target: PixelDescriptor) -> Result<PixelBuffer, At<crate::ConvertError>>;
116
117 fn try_add_alpha(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
123
124 fn try_widen_to_u16(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
126
127 fn try_narrow_to_u8(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
129
130 fn linearize(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
137
138 fn delinearize(
147 &self,
148 transfer: TransferFunction,
149 ) -> Result<PixelBuffer, At<crate::ConvertError>>;
150}
151
152#[cfg(feature = "rgb")]
156pub trait PixelBufferConvertTypedExt: PixelBufferConvertExt {
157 fn to_rgb8(&self) -> PixelBuffer<rgb::Rgb<u8>>;
159
160 fn to_rgba8(&self) -> PixelBuffer<rgb::Rgba<u8>>;
162
163 fn to_gray8(&self) -> PixelBuffer<rgb::Gray<u8>>;
165
166 fn to_bgra8(&self) -> PixelBuffer<rgb::alt::BGRA<u8>>;
168}
169
170#[inline]
181fn check_needs_cms(
182 from: &PixelDescriptor,
183 to: &PixelDescriptor,
184) -> Result<(), At<crate::ConvertError>> {
185 if crate::convert::requires_cms(from, to) {
186 return Err(whereat::at!(crate::ConvertError::NeedsCms {
187 from: *from,
188 to: *to,
189 }));
190 }
191 Ok(())
192}
193
194impl PixelBufferConvertExt for PixelBuffer {
195 #[track_caller]
196 fn convert_to(&self, target: PixelDescriptor) -> Result<PixelBuffer, At<crate::ConvertError>> {
197 let src_desc = self.descriptor();
198 check_needs_cms(&src_desc, &target)?;
199 if src_desc == target {
200 let dst_stride = target.aligned_stride(self.width());
202 let total = dst_stride
203 .checked_mul(self.height() as usize)
204 .ok_or_else(|| whereat::at!(crate::ConvertError::AllocationFailed))?;
205 let mut out = alloc::vec![0u8; total];
206 let src_slice = self.as_slice();
207 for y in 0..self.height() {
208 let src_row = src_slice.row(y);
209 let dst_start = y as usize * dst_stride;
210 out[dst_start..dst_start + src_row.len()].copy_from_slice(src_row);
211 }
212 let mut buf = PixelBuffer::from_vec(out, self.width(), self.height(), target)
213 .map_err_at(crate::ConvertError::from)?;
214 if let Some(ctx) = self.color_context() {
215 buf = buf.with_color_context(Arc::clone(ctx));
216 }
217 return Ok(buf);
218 }
219
220 let mut converter = crate::RowConverter::new(src_desc, target).at()?;
221
222 let dst_stride = target.aligned_stride(self.width());
223 let total = dst_stride
224 .checked_mul(self.height() as usize)
225 .ok_or_else(|| whereat::at!(crate::ConvertError::AllocationFailed))?;
226 let mut out = alloc::vec![0u8; total];
227
228 let src_slice = self.as_slice();
229 for y in 0..self.height() {
230 let src_row = src_slice.row(y);
231 let dst_start = y as usize * dst_stride;
232 let dst_end = dst_start + dst_stride;
233 converter.convert_row(src_row, &mut out[dst_start..dst_end], self.width());
234 }
235
236 let mut buf = PixelBuffer::from_vec(out, self.width(), self.height(), target)
237 .map_err_at(crate::ConvertError::from)?;
238 if let Some(ctx) = self.color_context() {
239 buf = buf.with_color_context(Arc::clone(ctx));
240 }
241 Ok(buf)
242 }
243
244 #[track_caller]
245 fn try_add_alpha(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
246 let desc = self.descriptor();
247 let target_layout = match desc.layout() {
248 ChannelLayout::Gray => ChannelLayout::GrayAlpha,
249 ChannelLayout::Rgb => ChannelLayout::Rgba,
250 other => other,
251 };
252 let alpha = if target_layout.has_alpha() && desc.alpha().is_none() {
253 Some(AlphaMode::Straight)
254 } else {
255 desc.alpha()
256 };
257 let target =
258 PixelDescriptor::new(desc.channel_type(), target_layout, alpha, desc.transfer());
259 self.convert_to(target)
260 }
261
262 #[track_caller]
263 fn try_widen_to_u16(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
264 let desc = self.descriptor();
265 let target = PixelDescriptor::new(
266 ChannelType::U16,
267 desc.layout(),
268 desc.alpha(),
269 desc.transfer(),
270 );
271 self.convert_to(target)
272 }
273
274 #[track_caller]
275 fn try_narrow_to_u8(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
276 let desc = self.descriptor();
277 let target = PixelDescriptor::new(
278 ChannelType::U8,
279 desc.layout(),
280 desc.alpha(),
281 desc.transfer(),
282 );
283 self.convert_to(target)
284 }
285
286 #[track_caller]
287 fn linearize(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
288 let desc = self.descriptor();
289 let target = PixelDescriptor::new_full(
290 ChannelType::F32,
291 desc.layout(),
292 desc.alpha(),
293 TransferFunction::Linear,
294 desc.primaries,
295 );
296 self.convert_to(target)
297 }
298
299 #[track_caller]
300 fn delinearize(
301 &self,
302 transfer: TransferFunction,
303 ) -> Result<PixelBuffer, At<crate::ConvertError>> {
304 let target = self.descriptor().with_transfer(transfer);
305 self.convert_to(target)
306 }
307}
308
309#[cfg(feature = "hdr-experimental")]
322pub trait PixelBufferHdrConvertExt {
323 fn convert_to_sdr(
337 &self,
338 target: PixelDescriptor,
339 ) -> Result<PixelBuffer, At<crate::ConvertError>>;
340
341 fn convert_to_with_hdr_config(
353 &self,
354 target: PixelDescriptor,
355 hdr: crate::HdrConfig,
356 ) -> Result<PixelBuffer, At<crate::ConvertError>>;
357}
358
359#[cfg(feature = "hdr-experimental")]
360impl PixelBufferHdrConvertExt for PixelBuffer {
361 #[track_caller]
362 fn convert_to_sdr(
363 &self,
364 target: PixelDescriptor,
365 ) -> Result<PixelBuffer, At<crate::ConvertError>> {
366 use crate::hdr::{CllMeasure, LightLevelMethod};
367 use zenpixels::hdr::{ContentLightLevel, DiffuseWhite};
368
369 let src_desc = self.descriptor();
370 check_needs_cms(&src_desc, &target)?;
371
372 if !matches!(
376 src_desc.transfer(),
377 TransferFunction::Pq | TransferFunction::Hlg
378 ) {
379 return self.convert_to(target);
380 }
381
382 let lin_desc = PixelDescriptor::new_full(
386 ChannelType::F32,
387 if src_desc.has_alpha() {
388 ChannelLayout::Rgba
389 } else {
390 ChannelLayout::Rgb
391 },
392 src_desc.alpha(),
393 TransferFunction::Linear,
394 src_desc.primaries,
395 );
396 let linear_src = self.convert_to(lin_desc)?;
397 let lin_slice = linear_src.as_slice();
398 let diffuse_white = self
399 .color_context()
400 .and_then(|c| c.diffuse_white)
401 .unwrap_or(DiffuseWhite::BT2408);
402 let cll =
403 ContentLightLevel::measure_max(lin_slice, diffuse_white, LightLevelMethod::MaxRgb)
404 .unwrap_or(ContentLightLevel::new(1000, 0));
405 let source_peak_nits = f32::from(cll.max_content_light_level).max(100.0);
406 self.convert_to_with_hdr_config(target, crate::HdrConfig::for_source_peak(source_peak_nits))
407 }
408
409 #[track_caller]
410 fn convert_to_with_hdr_config(
411 &self,
412 target: PixelDescriptor,
413 hdr: crate::HdrConfig,
414 ) -> Result<PixelBuffer, At<crate::ConvertError>> {
415 let src_desc = self.descriptor();
416 check_needs_cms(&src_desc, &target)?;
417 let plan = crate::ConvertPlan::new_with_hdr_config(src_desc, target, hdr).at()?;
425 let mut converter = crate::RowConverter::from_plan(plan);
426
427 let dst_stride = target.aligned_stride(self.width());
428 let total = dst_stride
429 .checked_mul(self.height() as usize)
430 .ok_or_else(|| whereat::at!(crate::ConvertError::AllocationFailed))?;
431 let mut out = alloc::vec![0u8; total];
432
433 let src_slice = self.as_slice();
434 for y in 0..self.height() {
435 let src_row = src_slice.row(y);
436 let dst_start = y as usize * dst_stride;
437 let dst_end = dst_start + dst_stride;
438 converter.convert_row(src_row, &mut out[dst_start..dst_end], self.width());
439 }
440
441 let mut buf = PixelBuffer::from_vec(out, self.width(), self.height(), target)
442 .map_err_at(crate::ConvertError::from)?;
443 if let Some(ctx) = self.color_context() {
444 buf = buf.with_color_context(Arc::clone(ctx));
445 }
446 Ok(buf)
447 }
448}
449
450#[cfg(feature = "rgb")]
451use zenpixels::buffer::Pixel;
452
453#[cfg(feature = "rgb")]
454impl PixelBufferConvertTypedExt for PixelBuffer {
455 fn to_rgb8(&self) -> PixelBuffer<rgb::Rgb<u8>> {
456 convert_to_typed(self, PixelDescriptor::RGB8_SRGB)
457 }
458
459 fn to_rgba8(&self) -> PixelBuffer<rgb::Rgba<u8>> {
460 convert_to_typed(self, PixelDescriptor::RGBA8_SRGB)
461 }
462
463 fn to_gray8(&self) -> PixelBuffer<rgb::Gray<u8>> {
464 convert_to_typed(self, PixelDescriptor::GRAY8_SRGB)
465 }
466
467 fn to_bgra8(&self) -> PixelBuffer<rgb::alt::BGRA<u8>> {
468 convert_to_typed(self, PixelDescriptor::BGRA8_SRGB)
469 }
470}
471
472#[cfg(feature = "rgb")]
474fn convert_to_typed<Q: Pixel>(buf: &PixelBuffer, target: PixelDescriptor) -> PixelBuffer<Q> {
475 use alloc::vec;
476 let mut conv = crate::RowConverter::new(buf.descriptor(), target)
477 .expect("RowConverter: no conversion path");
478 let dst_bpp = target.bytes_per_pixel();
479 let dst_stride = target.aligned_stride(buf.width());
480 let total = dst_stride * buf.height() as usize;
481 let mut out = vec![0u8; total];
482 let src_slice = buf.as_slice();
483 for y in 0..buf.height() {
484 let src_row = src_slice.row(y);
485 let dst_start = y as usize * dst_stride;
486 let dst_end = dst_start + buf.width() as usize * dst_bpp;
487 conv.convert_row(src_row, &mut out[dst_start..dst_end], buf.width());
488 }
489 let erased = PixelBuffer::from_vec(out, buf.width(), buf.height(), target)
492 .expect("convert_to_typed: buffer construction failed");
493 let erased = if let Some(ctx) = buf.color_context() {
495 erased.with_color_context(Arc::clone(ctx))
496 } else {
497 erased
498 };
499 erased
500 .try_typed::<Q>()
501 .expect("convert_to_typed: type mismatch after conversion")
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
517 fn cmyk_source_returns_needs_cms_from_convert_to() {
518 let cmyk_data = vec![0u8; 4 * 4]; let buf = PixelBuffer::from_vec(cmyk_data, 2, 2, PixelDescriptor::CMYK8).unwrap();
520 let err = match buf.convert_to(PixelDescriptor::RGB8_SRGB) {
521 Ok(_) => panic!("CMYK→RGB on the no-CMS extension entry must error"),
522 Err(e) => e,
523 };
524 assert!(
525 matches!(*err.error(), crate::ConvertError::NeedsCms { .. }),
526 "expected NeedsCms, got {:?}",
527 err.error(),
528 );
529 }
530
531 #[test]
532 fn cmyk_target_returns_needs_cms_from_convert_to() {
533 let rgb_data = vec![0u8; 3 * 4]; let buf = PixelBuffer::from_vec(rgb_data, 2, 2, PixelDescriptor::RGB8_SRGB).unwrap();
535 let err = match buf.convert_to(PixelDescriptor::CMYK8) {
536 Ok(_) => panic!("RGB→CMYK on the no-CMS extension entry must error"),
537 Err(e) => e,
538 };
539 assert!(
540 matches!(*err.error(), crate::ConvertError::NeedsCms { .. }),
541 "expected NeedsCms, got {:?}",
542 err.error(),
543 );
544 }
545
546 #[test]
549 fn srgb_linearize_roundtrip() {
550 let tf = TransferFunction::Srgb;
551 for &v in &[0.0, 0.04045, 0.1, 0.5, 0.73, 1.0] {
552 let lin = tf.linearize(v);
553 let back = tf.delinearize(lin);
554 assert!(
555 (v - back).abs() < 1e-5,
556 "sRGB roundtrip failed for {v}: linearize={lin}, delinearize={back}"
557 );
558 }
559 }
560
561 #[test]
562 fn pq_linearize_roundtrip() {
563 let tf = TransferFunction::Pq;
564 for &v in &[0.0, 0.1, 0.5, 0.75, 1.0] {
567 let lin = tf.linearize(v);
568 let back = tf.delinearize(lin);
569 assert!(
570 (v - back).abs() < 5e-4,
571 "PQ roundtrip failed for {v}: linearize={lin}, delinearize={back}"
572 );
573 }
574 }
575
576 #[test]
577 fn hlg_linearize_roundtrip() {
578 let tf = TransferFunction::Hlg;
579 for &v in &[0.0, 0.1, 0.3, 0.5, 0.8, 1.0] {
580 let lin = tf.linearize(v);
581 let back = tf.delinearize(lin);
582 assert!(
583 (v - back).abs() < 1e-4,
584 "HLG roundtrip failed for {v}: linearize={lin}, delinearize={back}"
585 );
586 }
587 }
588
589 #[test]
590 fn linear_identity() {
591 let tf = TransferFunction::Linear;
592 for &v in &[0.0, 0.5, 1.0] {
593 assert_eq!(tf.linearize(v), v);
594 assert_eq!(tf.delinearize(v), v);
595 }
596 }
597
598 #[test]
601 fn xyz_matrix_availability() {
602 assert!(ColorPrimaries::Bt709.to_xyz_matrix().is_some());
603 assert!(ColorPrimaries::Bt709.from_xyz_matrix().is_some());
604 assert!(ColorPrimaries::DisplayP3.to_xyz_matrix().is_some());
605 assert!(ColorPrimaries::Bt2020.to_xyz_matrix().is_some());
606 assert!(ColorPrimaries::Unknown.to_xyz_matrix().is_none());
607 assert!(ColorPrimaries::Unknown.from_xyz_matrix().is_none());
608 }
609
610 #[test]
611 fn xyz_roundtrip_bt709() {
612 let to = ColorPrimaries::Bt709.to_xyz_matrix().unwrap();
613 let from = ColorPrimaries::Bt709.from_xyz_matrix().unwrap();
614 let rgb = [0.5f32, 0.3, 0.8];
615 let mut v = rgb;
616 crate::gamut::apply_matrix_f32(&mut v, to);
617 crate::gamut::apply_matrix_f32(&mut v, from);
618 for c in 0..3 {
619 assert!(
620 (v[c] - rgb[c]).abs() < 1e-4,
621 "XYZ roundtrip BT.709 ch{c}: {:.6} vs {:.6}",
622 v[c],
623 rgb[c]
624 );
625 }
626 }
627
628 #[test]
631 fn bt709_linearize_roundtrip() {
632 let tf = TransferFunction::Bt709;
633 for &v in &[0.0, 0.04045, 0.1, 0.5, 0.73, 1.0] {
634 let lin = tf.linearize(v);
635 let back = tf.delinearize(lin);
636 assert!(
637 (v - back).abs() < 1e-5,
638 "BT.709 roundtrip failed for {v}: linearize={lin}, delinearize={back}"
639 );
640 }
641 }
642
643 #[test]
644 fn unknown_transfer_identity() {
645 let tf = TransferFunction::Unknown;
646 for &v in &[0.0, 0.25, 0.5, 0.75, 1.0] {
647 assert_eq!(
648 tf.linearize(v),
649 v,
650 "Unknown linearize should be identity for {v}"
651 );
652 assert_eq!(
653 tf.delinearize(v),
654 v,
655 "Unknown delinearize should be identity for {v}"
656 );
657 }
658 }
659
660 use super::PixelBufferConvertExt;
663
664 #[test]
665 fn convert_to_identity() {
666 let data = vec![100u8, 150, 200, 50, 100, 150];
667 let buf = PixelBuffer::from_vec(data.clone(), 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
668 let out = buf.convert_to(PixelDescriptor::RGB8_SRGB).unwrap();
669 assert_eq!(out.descriptor(), PixelDescriptor::RGB8_SRGB);
670 assert_eq!(out.width(), 2);
671 assert_eq!(out.height(), 1);
672 assert_eq!(&out.as_slice().row(0)[..6], &data[..]);
673 }
674
675 #[test]
676 fn convert_to_rgba8() {
677 let data = vec![100u8, 150, 200, 50, 100, 150];
678 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
679 let out = buf.convert_to(PixelDescriptor::RGBA8_SRGB).unwrap();
680 assert_eq!(out.descriptor(), PixelDescriptor::RGBA8_SRGB);
681 let slice = out.as_slice();
682 let row = slice.row(0);
683 assert_eq!(row[0], 100);
685 assert_eq!(row[1], 150);
686 assert_eq!(row[2], 200);
687 assert_eq!(row[3], 255);
688 assert_eq!(row[4], 50);
690 assert_eq!(row[5], 100);
691 assert_eq!(row[6], 150);
692 assert_eq!(row[7], 255);
693 }
694
695 #[test]
696 fn try_add_alpha_rgb() {
697 let data = vec![100u8, 150, 200, 50, 100, 150];
698 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
699 let out = buf.try_add_alpha().unwrap();
700 assert_eq!(
702 out.descriptor().layout(),
703 zenpixels::descriptor::ChannelLayout::Rgba
704 );
705 let slice = out.as_slice();
706 let row = slice.row(0);
707 assert_eq!(row[3], 255);
708 assert_eq!(row[7], 255);
709 }
710
711 #[test]
712 fn try_widen_to_u16() {
713 let data = vec![100u8, 150, 200, 50, 100, 150];
714 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
715 let out = buf.try_widen_to_u16().unwrap();
716 assert_eq!(
717 out.descriptor().channel_type(),
718 zenpixels::descriptor::ChannelType::U16
719 );
720 let slice = out.as_slice();
721 let row = slice.row(0);
722 for (i, &expected_u8) in [100u8, 150, 200, 50, 100, 150].iter().enumerate() {
724 let lo = row[i * 2];
725 let hi = row[i * 2 + 1];
726 let val = u16::from_le_bytes([lo, hi]);
727 let expected = expected_u8 as u16 * 257;
728 assert_eq!(
729 val, expected,
730 "channel {i}: expected {expected} (u8={expected_u8}*257), got {val}"
731 );
732 }
733 }
734
735 #[test]
736 fn linearize_srgb_to_linear_f32() {
737 let data = vec![128u8, 128, 128, 64, 64, 64];
738 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
739 let lin = buf.linearize().unwrap();
740 assert_eq!(lin.descriptor().transfer(), TransferFunction::Linear);
741 assert_eq!(
742 lin.descriptor().channel_type(),
743 zenpixels::descriptor::ChannelType::F32
744 );
745 assert_eq!(lin.descriptor().primaries, ColorPrimaries::Bt709);
746 let slice = lin.as_slice();
748 let row = slice.row(0);
749 let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
750 assert!(
751 (r - 0.216).abs() < 0.01,
752 "sRGB 128 should linearize to ~0.216, got {r}"
753 );
754 }
755
756 #[test]
757 fn delinearize_linear_to_srgb() {
758 let linear_val: f32 = 0.216;
760 let mut data = vec![0u8; 24]; for i in 0..6 {
762 let bytes = linear_val.to_le_bytes();
763 data[i * 4..i * 4 + 4].copy_from_slice(&bytes);
764 }
765 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGBF32_LINEAR).unwrap();
766 let srgb = buf.delinearize(TransferFunction::Srgb).unwrap();
767 assert_eq!(srgb.descriptor().transfer(), TransferFunction::Srgb);
768 let slice = srgb.as_slice();
770 let row = slice.row(0);
771 let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
772 assert!(
773 (r - 0.502).abs() < 0.01,
774 "linear 0.216 should delinearize to ~0.502, got {r}"
775 );
776 }
777
778 #[test]
779 fn linearize_delinearize_roundtrip() {
780 let data = vec![100u8, 150, 200, 50, 100, 150];
781 let buf = PixelBuffer::from_vec(data.clone(), 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
782 let lin = buf.linearize().unwrap();
783 let back = lin.delinearize(TransferFunction::Srgb).unwrap();
785 let slice = back.as_slice();
787 let row = slice.row(0);
788 let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
789 let expected = 100.0 / 255.0;
790 assert!(
791 (r - expected).abs() < 0.005,
792 "roundtrip pixel 0 R: expected ~{expected}, got {r}"
793 );
794 }
795
796 #[test]
797 fn linearize_preserves_alpha() {
798 let data = vec![100u8, 150, 200, 128, 50, 100, 150, 64];
799 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGBA8_SRGB).unwrap();
800 let lin = buf.linearize().unwrap();
801 assert_eq!(
802 lin.descriptor().layout(),
803 zenpixels::descriptor::ChannelLayout::Rgba
804 );
805 assert!(lin.descriptor().alpha().is_some());
806 }
807
808 #[test]
809 fn linearize_preserves_primaries() {
810 let data = vec![100u8, 150, 200, 50, 100, 150];
811 let desc = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
812 let buf = PixelBuffer::from_vec(data, 2, 1, desc).unwrap();
813 let lin = buf.linearize().unwrap();
814 assert_eq!(lin.descriptor().primaries, ColorPrimaries::DisplayP3);
815 }
816
817 #[test]
818 fn linearize_already_linear_is_identity() {
819 let val: f32 = 0.5;
820 let mut data = vec![0u8; 12]; for i in 0..3 {
822 data[i * 4..i * 4 + 4].copy_from_slice(&val.to_le_bytes());
823 }
824 let buf = PixelBuffer::from_vec(data, 1, 1, PixelDescriptor::RGBF32_LINEAR).unwrap();
825 let lin = buf.linearize().unwrap();
826 let slice = lin.as_slice();
827 let row = slice.row(0);
828 let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
829 assert!(
830 (r - val).abs() < 1e-6,
831 "already-linear should be identity, got {r}"
832 );
833 }
834
835 #[test]
836 fn try_narrow_to_u8() {
837 let values: [u16; 6] = [
839 100 * 257,
840 150 * 257,
841 200 * 257,
842 50 * 257,
843 100 * 257,
844 150 * 257,
845 ];
846 let mut data = vec![0u8; 12];
847 for (i, &v) in values.iter().enumerate() {
848 let bytes = v.to_le_bytes();
849 data[i * 2] = bytes[0];
850 data[i * 2 + 1] = bytes[1];
851 }
852 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB16_SRGB).unwrap();
853 let out = buf.try_narrow_to_u8().unwrap();
854 assert_eq!(
855 out.descriptor().channel_type(),
856 zenpixels::descriptor::ChannelType::U8
857 );
858 let slice = out.as_slice();
859 let row = slice.row(0);
860 assert_eq!(row[0], 100);
861 assert_eq!(row[1], 150);
862 assert_eq!(row[2], 200);
863 assert_eq!(row[3], 50);
864 assert_eq!(row[4], 100);
865 assert_eq!(row[5], 150);
866 }
867
868 #[test]
869 #[cfg(feature = "rgb")]
870 fn to_rgb8() {
871 let data = vec![100u8, 150, 200, 255, 50, 100, 150, 255];
873 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGBA8_SRGB).unwrap();
874 let typed: PixelBuffer<rgb::Rgb<u8>> = buf.to_rgb8();
875 assert_eq!(typed.width(), 2);
876 assert_eq!(typed.height(), 1);
877 let slice = typed.as_slice();
878 let row = slice.row(0);
879 assert_eq!(row[0], 100);
881 assert_eq!(row[1], 150);
882 assert_eq!(row[2], 200);
883 assert_eq!(row[3], 50);
884 assert_eq!(row[4], 100);
885 assert_eq!(row[5], 150);
886 }
887
888 #[test]
889 #[cfg(feature = "rgb")]
890 fn to_rgba8() {
891 let data = vec![100u8, 150, 200, 50, 100, 150];
892 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
893 let typed: PixelBuffer<rgb::Rgba<u8>> = buf.to_rgba8();
894 assert_eq!(typed.width(), 2);
895 assert_eq!(typed.height(), 1);
896 let slice = typed.as_slice();
897 let row = slice.row(0);
898 assert_eq!(row[0], 100);
900 assert_eq!(row[1], 150);
901 assert_eq!(row[2], 200);
902 assert_eq!(row[3], 255);
903 assert_eq!(row[4], 50);
904 assert_eq!(row[5], 100);
905 assert_eq!(row[6], 150);
906 assert_eq!(row[7], 255);
907 }
908
909 #[test]
910 #[cfg(feature = "rgb")]
911 fn to_gray8() {
912 let data = vec![100u8, 150, 200, 50, 100, 150];
913 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
914 let typed: PixelBuffer<rgb::Gray<u8>> = buf.to_gray8();
915 assert_eq!(typed.width(), 2);
916 assert_eq!(typed.height(), 1);
917 let slice = typed.as_slice();
918 let row = slice.row(0);
919 assert!(row[0] > 0, "gray pixel 0 should be non-zero");
921 assert!(row[1] > 0, "gray pixel 1 should be non-zero");
922 }
923
924 #[test]
925 #[cfg(feature = "rgb")]
926 fn to_bgra8() {
927 let data = vec![100u8, 150, 200, 50, 100, 150];
928 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
929 let typed: PixelBuffer<rgb::alt::BGRA<u8>> = buf.to_bgra8();
930 assert_eq!(typed.width(), 2);
931 assert_eq!(typed.height(), 1);
932 let slice = typed.as_slice();
933 let row = slice.row(0);
934 assert_eq!(row[0], 200);
937 assert_eq!(row[1], 150);
938 assert_eq!(row[2], 100);
939 assert_eq!(row[3], 255);
940 assert_eq!(row[4], 150);
942 assert_eq!(row[5], 100);
943 assert_eq!(row[6], 50);
944 assert_eq!(row[7], 255);
945 }
946}