Skip to main content

zenpixels_convert/
ext.rs

1//! Extension traits that add conversion methods to zenpixels interchange types.
2//!
3//! These traits bridge the type–conversion boundary: the types live in
4//! `zenpixels` (no heavy deps), while the conversion math lives here
5//! (depends on `linear-srgb`).
6
7use zenpixels::{ColorPrimaries, TransferFunction};
8
9use crate::convert::{hlg_eotf, hlg_oetf, pq_eotf, pq_oetf};
10use crate::gamut::GamutMatrix;
11
12// ---------------------------------------------------------------------------
13// TransferFunctionExt
14// ---------------------------------------------------------------------------
15
16/// Adds scalar EOTF/OETF methods to [`TransferFunction`].
17pub trait TransferFunctionExt {
18    /// Scalar EOTF: encoded signal → linear light.
19    ///
20    /// Canonical reference implementation for testing SIMD paths.
21    #[must_use]
22    fn linearize(&self, v: f32) -> f32;
23
24    /// Scalar OETF: linear light → encoded signal.
25    ///
26    /// Canonical reference implementation for testing SIMD paths.
27    #[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// ---------------------------------------------------------------------------
58// ColorPrimariesExt
59// ---------------------------------------------------------------------------
60
61/// Adds XYZ matrix lookups to [`ColorPrimaries`].
62#[allow(clippy::wrong_self_convention)]
63pub trait ColorPrimariesExt {
64    /// Linear RGB → CIE XYZ (D65 white point).
65    ///
66    /// Returns `None` for [`Unknown`](ColorPrimaries::Unknown).
67    fn to_xyz_matrix(&self) -> Option<&'static GamutMatrix>;
68
69    /// CIE XYZ (D65 white point) → linear RGB.
70    ///
71    /// Returns `None` for [`Unknown`](ColorPrimaries::Unknown).
72    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// ---------------------------------------------------------------------------
98// PixelBufferConvertExt
99// ---------------------------------------------------------------------------
100
101#[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/// Adds format conversion methods to type-erased [`PixelBuffer`].
113#[cfg(feature = "buffer")]
114pub trait PixelBufferConvertExt {
115    /// Convert pixel data to a different layout and depth.
116    ///
117    /// Uses [`RowConverter`](crate::RowConverter) for transfer-function-aware
118    /// conversion. Color metadata is preserved.
119    ///
120    /// **Allocates** a new [`PixelBuffer`].
121    fn convert_to(&self, target: PixelDescriptor) -> Result<PixelBuffer, crate::ConvertError>;
122
123    /// Add an alpha channel. **Allocates** a new `PixelBuffer`.
124    ///
125    /// - Gray → GrayAlpha (opaque alpha)
126    /// - Rgb → Rgba (opaque alpha)
127    /// - Already has alpha → identity copy
128    fn try_add_alpha(&self) -> Result<PixelBuffer, crate::ConvertError>;
129
130    /// Widen to U16 depth (lossless, ×257). **Allocates** a new `PixelBuffer`.
131    fn try_widen_to_u16(&self) -> Result<PixelBuffer, crate::ConvertError>;
132
133    /// Narrow to U8 depth (lossy, rounded). **Allocates** a new `PixelBuffer`.
134    fn try_narrow_to_u8(&self) -> Result<PixelBuffer, crate::ConvertError>;
135
136    /// Convert to RGB8, allocating a new buffer.
137    fn to_rgb8(&self) -> PixelBuffer<rgb::Rgb<u8>>;
138
139    /// Convert to RGBA8, allocating a new buffer.
140    fn to_rgba8(&self) -> PixelBuffer<rgb::Rgba<u8>>;
141
142    /// Convert to Gray8, allocating a new buffer.
143    fn to_gray8(&self) -> PixelBuffer<rgb::Gray<u8>>;
144
145    /// Convert to BGRA8, allocating a new buffer.
146    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            // Identity — just copy.
155            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/// Internal: convert to any target descriptor, returning a typed buffer.
255#[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    // We need to construct PixelBuffer<Q> from raw parts.
271    // Use from_vec to build the erased form, then reinterpret.
272    let erased = PixelBuffer::from_vec(out, buf.width(), buf.height(), target)
273        .expect("convert_to_typed: buffer construction failed");
274    // Carry over color context
275    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    // --- TransferFunction linearize/delinearize tests ---
290
291    #[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        // linear-srgb 0.6 rational poly: ~3e-4 roundtrip error at low signal.
308        // Tighten to 1e-5 after upgrading to linear-srgb with two-range EOTF.
309        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    // --- ColorPrimaries XYZ matrix tests ---
342
343    #[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    // --- Bt709 and Unknown transfer function tests ---
372
373    #[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    // --- PixelBufferConvertExt tests ---
404
405    #[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        // Pixel 0: R=100, G=150, B=200, A=255
436        assert_eq!(row[0], 100);
437        assert_eq!(row[1], 150);
438        assert_eq!(row[2], 200);
439        assert_eq!(row[3], 255);
440        // Pixel 1: R=50, G=100, B=150, A=255
441        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        // Should now be RGBA with straight alpha
454        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        // U16 little-endian: value * 257
477        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        // Create RGB16 buffer with known values
493        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        // Start with RGBA8 buffer, convert to typed RGB8
527        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        // Alpha should be dropped: 3 bytes per pixel
535        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        // RGB -> RGBA with alpha=255
554        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        // Gray values should be luminance-weighted, not zero
575        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        // BGRA layout: B, G, R, A
590        // Pixel 0: R=100, G=150, B=200 -> BGRA = 200, 150, 100, 255
591        assert_eq!(row[0], 200);
592        assert_eq!(row[1], 150);
593        assert_eq!(row[2], 100);
594        assert_eq!(row[3], 255);
595        // Pixel 1: R=50, G=100, B=150 -> BGRA = 150, 100, 50, 255
596        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}