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
97#[cfg(feature = "buffer")]
102use alloc::sync::Arc;
103#[cfg(feature = "buffer")]
104use alloc::vec;
105#[cfg(feature = "buffer")]
106use zenpixels::PixelDescriptor;
107#[cfg(feature = "buffer")]
108use zenpixels::buffer::{Pixel, PixelBuffer};
109#[cfg(feature = "buffer")]
110use zenpixels::descriptor::{AlphaMode, ChannelLayout, ChannelType};
111
112#[cfg(feature = "buffer")]
114pub trait PixelBufferConvertExt {
115 fn convert_to(&self, target: PixelDescriptor) -> Result<PixelBuffer, crate::ConvertError>;
122
123 fn try_add_alpha(&self) -> Result<PixelBuffer, crate::ConvertError>;
129
130 fn try_widen_to_u16(&self) -> Result<PixelBuffer, crate::ConvertError>;
132
133 fn try_narrow_to_u8(&self) -> Result<PixelBuffer, crate::ConvertError>;
135
136 fn to_rgb8(&self) -> PixelBuffer<rgb::Rgb<u8>>;
138
139 fn to_rgba8(&self) -> PixelBuffer<rgb::Rgba<u8>>;
141
142 fn to_gray8(&self) -> PixelBuffer<rgb::Gray<u8>>;
144
145 fn to_bgra8(&self) -> PixelBuffer<rgb::alt::BGRA<u8>>;
147}
148
149#[cfg(feature = "buffer")]
150impl PixelBufferConvertExt for PixelBuffer {
151 fn convert_to(&self, target: PixelDescriptor) -> Result<PixelBuffer, crate::ConvertError> {
152 let src_desc = self.descriptor();
153 if src_desc == target {
154 let dst_stride = target.aligned_stride(self.width());
156 let total = dst_stride
157 .checked_mul(self.height() as usize)
158 .ok_or(crate::ConvertError::AllocationFailed)?;
159 let mut out = alloc::vec![0u8; total];
160 let src_slice = self.as_slice();
161 for y in 0..self.height() {
162 let src_row = src_slice.row(y);
163 let dst_start = y as usize * dst_stride;
164 out[dst_start..dst_start + src_row.len()].copy_from_slice(src_row);
165 }
166 let mut buf = PixelBuffer::from_vec(out, self.width(), self.height(), target)
167 .map_err(|_| crate::ConvertError::AllocationFailed)?;
168 if let Some(ctx) = self.color_context() {
169 buf = buf.with_color_context(Arc::clone(ctx));
170 }
171 return Ok(buf);
172 }
173
174 let converter = crate::RowConverter::new(src_desc, target)?;
175
176 let dst_stride = target.aligned_stride(self.width());
177 let total = dst_stride
178 .checked_mul(self.height() as usize)
179 .ok_or(crate::ConvertError::AllocationFailed)?;
180 let mut out = alloc::vec![0u8; total];
181
182 let src_slice = self.as_slice();
183 for y in 0..self.height() {
184 let src_row = src_slice.row(y);
185 let dst_start = y as usize * dst_stride;
186 let dst_end = dst_start + dst_stride;
187 converter.convert_row(src_row, &mut out[dst_start..dst_end], self.width());
188 }
189
190 let mut buf = PixelBuffer::from_vec(out, self.width(), self.height(), target)
191 .map_err(|_| crate::ConvertError::AllocationFailed)?;
192 if let Some(ctx) = self.color_context() {
193 buf = buf.with_color_context(Arc::clone(ctx));
194 }
195 Ok(buf)
196 }
197
198 fn try_add_alpha(&self) -> Result<PixelBuffer, crate::ConvertError> {
199 let desc = self.descriptor();
200 let target_layout = match desc.layout() {
201 ChannelLayout::Gray => ChannelLayout::GrayAlpha,
202 ChannelLayout::Rgb => ChannelLayout::Rgba,
203 other => other,
204 };
205 let alpha = if target_layout.has_alpha() && desc.alpha().is_none() {
206 Some(AlphaMode::Straight)
207 } else {
208 desc.alpha()
209 };
210 let target =
211 PixelDescriptor::new(desc.channel_type(), target_layout, alpha, desc.transfer());
212 self.convert_to(target)
213 }
214
215 fn try_widen_to_u16(&self) -> Result<PixelBuffer, crate::ConvertError> {
216 let desc = self.descriptor();
217 let target = PixelDescriptor::new(
218 ChannelType::U16,
219 desc.layout(),
220 desc.alpha(),
221 desc.transfer(),
222 );
223 self.convert_to(target)
224 }
225
226 fn try_narrow_to_u8(&self) -> Result<PixelBuffer, crate::ConvertError> {
227 let desc = self.descriptor();
228 let target = PixelDescriptor::new(
229 ChannelType::U8,
230 desc.layout(),
231 desc.alpha(),
232 desc.transfer(),
233 );
234 self.convert_to(target)
235 }
236
237 fn to_rgb8(&self) -> PixelBuffer<rgb::Rgb<u8>> {
238 convert_to_typed(self, PixelDescriptor::RGB8_SRGB)
239 }
240
241 fn to_rgba8(&self) -> PixelBuffer<rgb::Rgba<u8>> {
242 convert_to_typed(self, PixelDescriptor::RGBA8_SRGB)
243 }
244
245 fn to_gray8(&self) -> PixelBuffer<rgb::Gray<u8>> {
246 convert_to_typed(self, PixelDescriptor::GRAY8_SRGB)
247 }
248
249 fn to_bgra8(&self) -> PixelBuffer<rgb::alt::BGRA<u8>> {
250 convert_to_typed(self, PixelDescriptor::BGRA8_SRGB)
251 }
252}
253
254#[cfg(feature = "buffer")]
256fn convert_to_typed<Q: Pixel>(buf: &PixelBuffer, target: PixelDescriptor) -> PixelBuffer<Q> {
257 let conv = crate::RowConverter::new(buf.descriptor(), target)
258 .expect("RowConverter: no conversion path");
259 let dst_bpp = target.bytes_per_pixel();
260 let dst_stride = target.aligned_stride(buf.width());
261 let total = dst_stride * buf.height() as usize;
262 let mut out = vec![0u8; total];
263 let src_slice = buf.as_slice();
264 for y in 0..buf.height() {
265 let src_row = src_slice.row(y);
266 let dst_start = y as usize * dst_stride;
267 let dst_end = dst_start + buf.width() as usize * dst_bpp;
268 conv.convert_row(src_row, &mut out[dst_start..dst_end], buf.width());
269 }
270 let erased = PixelBuffer::from_vec(out, buf.width(), buf.height(), target)
273 .expect("convert_to_typed: buffer construction failed");
274 let erased = if let Some(ctx) = buf.color_context() {
276 erased.with_color_context(Arc::clone(ctx))
277 } else {
278 erased
279 };
280 erased
281 .try_typed::<Q>()
282 .expect("convert_to_typed: type mismatch after conversion")
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
292 fn srgb_linearize_roundtrip() {
293 let tf = TransferFunction::Srgb;
294 for &v in &[0.0, 0.04045, 0.1, 0.5, 0.73, 1.0] {
295 let lin = tf.linearize(v);
296 let back = tf.delinearize(lin);
297 assert!(
298 (v - back).abs() < 1e-5,
299 "sRGB roundtrip failed for {v}: linearize={lin}, delinearize={back}"
300 );
301 }
302 }
303
304 #[test]
305 fn pq_linearize_roundtrip() {
306 let tf = TransferFunction::Pq;
307 for &v in &[0.0, 0.1, 0.5, 0.75, 1.0] {
310 let lin = tf.linearize(v);
311 let back = tf.delinearize(lin);
312 assert!(
313 (v - back).abs() < 5e-4,
314 "PQ roundtrip failed for {v}: linearize={lin}, delinearize={back}"
315 );
316 }
317 }
318
319 #[test]
320 fn hlg_linearize_roundtrip() {
321 let tf = TransferFunction::Hlg;
322 for &v in &[0.0, 0.1, 0.3, 0.5, 0.8, 1.0] {
323 let lin = tf.linearize(v);
324 let back = tf.delinearize(lin);
325 assert!(
326 (v - back).abs() < 1e-4,
327 "HLG roundtrip failed for {v}: linearize={lin}, delinearize={back}"
328 );
329 }
330 }
331
332 #[test]
333 fn linear_identity() {
334 let tf = TransferFunction::Linear;
335 for &v in &[0.0, 0.5, 1.0] {
336 assert_eq!(tf.linearize(v), v);
337 assert_eq!(tf.delinearize(v), v);
338 }
339 }
340
341 #[test]
344 fn xyz_matrix_availability() {
345 assert!(ColorPrimaries::Bt709.to_xyz_matrix().is_some());
346 assert!(ColorPrimaries::Bt709.from_xyz_matrix().is_some());
347 assert!(ColorPrimaries::DisplayP3.to_xyz_matrix().is_some());
348 assert!(ColorPrimaries::Bt2020.to_xyz_matrix().is_some());
349 assert!(ColorPrimaries::Unknown.to_xyz_matrix().is_none());
350 assert!(ColorPrimaries::Unknown.from_xyz_matrix().is_none());
351 }
352
353 #[test]
354 fn xyz_roundtrip_bt709() {
355 let to = ColorPrimaries::Bt709.to_xyz_matrix().unwrap();
356 let from = ColorPrimaries::Bt709.from_xyz_matrix().unwrap();
357 let rgb = [0.5f32, 0.3, 0.8];
358 let mut v = rgb;
359 crate::gamut::apply_matrix_f32(&mut v, to);
360 crate::gamut::apply_matrix_f32(&mut v, from);
361 for c in 0..3 {
362 assert!(
363 (v[c] - rgb[c]).abs() < 1e-4,
364 "XYZ roundtrip BT.709 ch{c}: {:.6} vs {:.6}",
365 v[c],
366 rgb[c]
367 );
368 }
369 }
370
371 #[test]
374 fn bt709_linearize_roundtrip() {
375 let tf = TransferFunction::Bt709;
376 for &v in &[0.0, 0.04045, 0.1, 0.5, 0.73, 1.0] {
377 let lin = tf.linearize(v);
378 let back = tf.delinearize(lin);
379 assert!(
380 (v - back).abs() < 1e-5,
381 "BT.709 roundtrip failed for {v}: linearize={lin}, delinearize={back}"
382 );
383 }
384 }
385
386 #[test]
387 fn unknown_transfer_identity() {
388 let tf = TransferFunction::Unknown;
389 for &v in &[0.0, 0.25, 0.5, 0.75, 1.0] {
390 assert_eq!(
391 tf.linearize(v),
392 v,
393 "Unknown linearize should be identity for {v}"
394 );
395 assert_eq!(
396 tf.delinearize(v),
397 v,
398 "Unknown delinearize should be identity for {v}"
399 );
400 }
401 }
402
403 #[cfg(feature = "buffer")]
406 use super::PixelBufferConvertExt;
407
408 #[cfg(feature = "buffer")]
409 use zenpixels::PixelDescriptor;
410
411 #[cfg(feature = "buffer")]
412 use zenpixels::buffer::PixelBuffer;
413
414 #[test]
415 #[cfg(feature = "buffer")]
416 fn convert_to_identity() {
417 let data = vec![100u8, 150, 200, 50, 100, 150];
418 let buf = PixelBuffer::from_vec(data.clone(), 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
419 let out = buf.convert_to(PixelDescriptor::RGB8_SRGB).unwrap();
420 assert_eq!(out.descriptor(), PixelDescriptor::RGB8_SRGB);
421 assert_eq!(out.width(), 2);
422 assert_eq!(out.height(), 1);
423 assert_eq!(&out.as_slice().row(0)[..6], &data[..]);
424 }
425
426 #[test]
427 #[cfg(feature = "buffer")]
428 fn convert_to_rgba8() {
429 let data = vec![100u8, 150, 200, 50, 100, 150];
430 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
431 let out = buf.convert_to(PixelDescriptor::RGBA8_SRGB).unwrap();
432 assert_eq!(out.descriptor(), PixelDescriptor::RGBA8_SRGB);
433 let slice = out.as_slice();
434 let row = slice.row(0);
435 assert_eq!(row[0], 100);
437 assert_eq!(row[1], 150);
438 assert_eq!(row[2], 200);
439 assert_eq!(row[3], 255);
440 assert_eq!(row[4], 50);
442 assert_eq!(row[5], 100);
443 assert_eq!(row[6], 150);
444 assert_eq!(row[7], 255);
445 }
446
447 #[test]
448 #[cfg(feature = "buffer")]
449 fn try_add_alpha_rgb() {
450 let data = vec![100u8, 150, 200, 50, 100, 150];
451 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
452 let out = buf.try_add_alpha().unwrap();
453 assert_eq!(
455 out.descriptor().layout(),
456 zenpixels::descriptor::ChannelLayout::Rgba
457 );
458 let slice = out.as_slice();
459 let row = slice.row(0);
460 assert_eq!(row[3], 255);
461 assert_eq!(row[7], 255);
462 }
463
464 #[test]
465 #[cfg(feature = "buffer")]
466 fn try_widen_to_u16() {
467 let data = vec![100u8, 150, 200, 50, 100, 150];
468 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
469 let out = buf.try_widen_to_u16().unwrap();
470 assert_eq!(
471 out.descriptor().channel_type(),
472 zenpixels::descriptor::ChannelType::U16
473 );
474 let slice = out.as_slice();
475 let row = slice.row(0);
476 for (i, &expected_u8) in [100u8, 150, 200, 50, 100, 150].iter().enumerate() {
478 let lo = row[i * 2];
479 let hi = row[i * 2 + 1];
480 let val = u16::from_le_bytes([lo, hi]);
481 let expected = expected_u8 as u16 * 257;
482 assert_eq!(
483 val, expected,
484 "channel {i}: expected {expected} (u8={expected_u8}*257), got {val}"
485 );
486 }
487 }
488
489 #[test]
490 #[cfg(feature = "buffer")]
491 fn try_narrow_to_u8() {
492 let values: [u16; 6] = [
494 100 * 257,
495 150 * 257,
496 200 * 257,
497 50 * 257,
498 100 * 257,
499 150 * 257,
500 ];
501 let mut data = vec![0u8; 12];
502 for (i, &v) in values.iter().enumerate() {
503 let bytes = v.to_le_bytes();
504 data[i * 2] = bytes[0];
505 data[i * 2 + 1] = bytes[1];
506 }
507 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB16_SRGB).unwrap();
508 let out = buf.try_narrow_to_u8().unwrap();
509 assert_eq!(
510 out.descriptor().channel_type(),
511 zenpixels::descriptor::ChannelType::U8
512 );
513 let slice = out.as_slice();
514 let row = slice.row(0);
515 assert_eq!(row[0], 100);
516 assert_eq!(row[1], 150);
517 assert_eq!(row[2], 200);
518 assert_eq!(row[3], 50);
519 assert_eq!(row[4], 100);
520 assert_eq!(row[5], 150);
521 }
522
523 #[test]
524 #[cfg(feature = "buffer")]
525 fn to_rgb8() {
526 let data = vec![100u8, 150, 200, 255, 50, 100, 150, 255];
528 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGBA8_SRGB).unwrap();
529 let typed: PixelBuffer<rgb::Rgb<u8>> = buf.to_rgb8();
530 assert_eq!(typed.width(), 2);
531 assert_eq!(typed.height(), 1);
532 let slice = typed.as_slice();
533 let row = slice.row(0);
534 assert_eq!(row[0], 100);
536 assert_eq!(row[1], 150);
537 assert_eq!(row[2], 200);
538 assert_eq!(row[3], 50);
539 assert_eq!(row[4], 100);
540 assert_eq!(row[5], 150);
541 }
542
543 #[test]
544 #[cfg(feature = "buffer")]
545 fn to_rgba8() {
546 let data = vec![100u8, 150, 200, 50, 100, 150];
547 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
548 let typed: PixelBuffer<rgb::Rgba<u8>> = buf.to_rgba8();
549 assert_eq!(typed.width(), 2);
550 assert_eq!(typed.height(), 1);
551 let slice = typed.as_slice();
552 let row = slice.row(0);
553 assert_eq!(row[0], 100);
555 assert_eq!(row[1], 150);
556 assert_eq!(row[2], 200);
557 assert_eq!(row[3], 255);
558 assert_eq!(row[4], 50);
559 assert_eq!(row[5], 100);
560 assert_eq!(row[6], 150);
561 assert_eq!(row[7], 255);
562 }
563
564 #[test]
565 #[cfg(feature = "buffer")]
566 fn to_gray8() {
567 let data = vec![100u8, 150, 200, 50, 100, 150];
568 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
569 let typed: PixelBuffer<rgb::Gray<u8>> = buf.to_gray8();
570 assert_eq!(typed.width(), 2);
571 assert_eq!(typed.height(), 1);
572 let slice = typed.as_slice();
573 let row = slice.row(0);
574 assert!(row[0] > 0, "gray pixel 0 should be non-zero");
576 assert!(row[1] > 0, "gray pixel 1 should be non-zero");
577 }
578
579 #[test]
580 #[cfg(feature = "buffer")]
581 fn to_bgra8() {
582 let data = vec![100u8, 150, 200, 50, 100, 150];
583 let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
584 let typed: PixelBuffer<rgb::alt::BGRA<u8>> = buf.to_bgra8();
585 assert_eq!(typed.width(), 2);
586 assert_eq!(typed.height(), 1);
587 let slice = typed.as_slice();
588 let row = slice.row(0);
589 assert_eq!(row[0], 200);
592 assert_eq!(row[1], 150);
593 assert_eq!(row[2], 100);
594 assert_eq!(row[3], 255);
595 assert_eq!(row[4], 150);
597 assert_eq!(row[5], 100);
598 assert_eq!(row[6], 50);
599 assert_eq!(row[7], 255);
600 }
601}