yuvutils_rs/
yuv_to_rgba.rs

1/*
2 * Copyright (c) Radzivon Bartoshyk, 10/2024. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification,
5 * are permitted provided that the following conditions are met:
6 *
7 * 1.  Redistributions of source code must retain the above copyright notice, this
8 * list of conditions and the following disclaimer.
9 *
10 * 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 *
14 * 3.  Neither the name of the copyright holder nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29use crate::internals::{ProcessedOffset, WideRow420InversionHandler, WideRowInversionHandler};
30use crate::numerics::qrshr;
31use crate::yuv_error::check_rgba_destination;
32use crate::yuv_support::*;
33use crate::{YuvError, YuvPlanarImage};
34#[cfg(feature = "rayon")]
35use rayon::iter::{IndexedParallelIterator, ParallelIterator};
36#[cfg(feature = "rayon")]
37use rayon::prelude::{ParallelSlice, ParallelSliceMut};
38
39type RowHandle = Option<
40    unsafe fn(
41        range: &YuvChromaRange,
42        transform: &CbCrInverseTransform<i32>,
43        y_plane: &[u8],
44        u_plane: &[u8],
45        v_plane: &[u8],
46        rgba: &mut [u8],
47        start_cx: usize,
48        start_ux: usize,
49        width: usize,
50    ) -> ProcessedOffset,
51>;
52
53type RowHandle420 = Option<
54    unsafe fn(
55        range: &YuvChromaRange,
56        transform: &CbCrInverseTransform<i32>,
57        y_plane0: &[u8],
58        y_plane1: &[u8],
59        u_plane: &[u8],
60        v_plane: &[u8],
61        rgba0: &mut [u8],
62        rgba1: &mut [u8],
63        start_cx: usize,
64        start_ux: usize,
65        width: usize,
66    ) -> ProcessedOffset,
67>;
68
69struct RowHandler<const DESTINATION_CHANNELS: u8, const SAMPLING: u8, const PRECISION: i32> {
70    handler: RowHandle,
71}
72
73macro_rules! impl_row_inversion_handler {
74    ($struct_name:ident) => {
75        impl<const DESTINATION_CHANNELS: u8, const SAMPLING: u8, const PRECISION: i32>
76            WideRowInversionHandler<u8, i32>
77            for $struct_name<DESTINATION_CHANNELS, SAMPLING, PRECISION>
78        {
79            fn handle_row(
80                &self,
81                y_plane: &[u8],
82                u_plane: &[u8],
83                v_plane: &[u8],
84                rgba: &mut [u8],
85                width: u32,
86                chroma: YuvChromaRange,
87                transform: &CbCrInverseTransform<i32>,
88            ) -> ProcessedOffset {
89                if let Some(handler) = self.handler {
90                    unsafe {
91                        return handler(
92                            &chroma,
93                            transform,
94                            y_plane,
95                            u_plane,
96                            v_plane,
97                            rgba,
98                            0,
99                            0,
100                            width as usize,
101                        );
102                    }
103                }
104                ProcessedOffset { cx: 0, ux: 0 }
105            }
106        }
107    };
108}
109
110impl_row_inversion_handler!(RowHandler);
111
112impl<const DESTINATION_CHANNELS: u8, const SAMPLING: u8, const PRECISION: i32> Default
113    for RowHandler<DESTINATION_CHANNELS, SAMPLING, PRECISION>
114{
115    fn default() -> Self {
116        if PRECISION != 13 {
117            return RowHandler { handler: None };
118        }
119        assert_eq!(PRECISION, 13);
120        #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
121        {
122            #[cfg(feature = "rdm")]
123            {
124                let is_rdm_available = std::arch::is_aarch64_feature_detected!("rdm");
125                if is_rdm_available {
126                    use crate::neon::neon_yuv_to_rgba_row_rdm;
127
128                    return RowHandler {
129                        handler: Some(neon_yuv_to_rgba_row_rdm::<DESTINATION_CHANNELS, SAMPLING>),
130                    };
131                }
132            }
133            use crate::neon::neon_yuv_to_rgba_row;
134            RowHandler {
135                handler: Some(neon_yuv_to_rgba_row::<DESTINATION_CHANNELS, SAMPLING>),
136            }
137        }
138        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
139        {
140            #[cfg(feature = "nightly_avx512")]
141            {
142                let use_avx512 = std::arch::is_x86_feature_detected!("avx512bw");
143                let use_vbmi = std::arch::is_x86_feature_detected!("avx512vbmi");
144                let sampling: YuvChromaSubsampling = SAMPLING.into();
145                if use_avx512 {
146                    return if sampling == YuvChromaSubsampling::Yuv420
147                        || sampling == YuvChromaSubsampling::Yuv422
148                    {
149                        assert!(
150                            sampling == YuvChromaSubsampling::Yuv420
151                                || sampling == YuvChromaSubsampling::Yuv422
152                        );
153                        use crate::avx512bw::avx512_yuv_to_rgba422;
154                        RowHandler {
155                            handler: Some(if use_vbmi {
156                                avx512_yuv_to_rgba422::<DESTINATION_CHANNELS, true>
157                            } else {
158                                avx512_yuv_to_rgba422::<DESTINATION_CHANNELS, false>
159                            }),
160                        }
161                    } else {
162                        use crate::avx512bw::avx512_yuv_to_rgba;
163                        RowHandler {
164                            handler: Some(if use_vbmi {
165                                avx512_yuv_to_rgba::<DESTINATION_CHANNELS, SAMPLING, true>
166                            } else {
167                                avx512_yuv_to_rgba::<DESTINATION_CHANNELS, SAMPLING, false>
168                            }),
169                        }
170                    };
171                }
172            }
173
174            #[cfg(feature = "avx")]
175            {
176                let use_avx2 = std::arch::is_x86_feature_detected!("avx2");
177                if use_avx2 {
178                    let sampling: YuvChromaSubsampling = SAMPLING.into();
179                    return if sampling == YuvChromaSubsampling::Yuv420
180                        || sampling == YuvChromaSubsampling::Yuv422
181                    {
182                        assert!(
183                            sampling == YuvChromaSubsampling::Yuv420
184                                || sampling == YuvChromaSubsampling::Yuv422
185                        );
186                        use crate::avx2::avx2_yuv_to_rgba_row422;
187                        RowHandler {
188                            handler: Some(avx2_yuv_to_rgba_row422::<DESTINATION_CHANNELS>),
189                        }
190                    } else {
191                        use crate::avx2::avx2_yuv_to_rgba_row;
192                        RowHandler {
193                            handler: Some(avx2_yuv_to_rgba_row::<DESTINATION_CHANNELS, SAMPLING>),
194                        }
195                    };
196                }
197            }
198
199            #[cfg(feature = "sse")]
200            {
201                let use_sse = std::arch::is_x86_feature_detected!("sse4.1");
202                if use_sse {
203                    let sampling: YuvChromaSubsampling = SAMPLING.into();
204                    if sampling == YuvChromaSubsampling::Yuv420
205                        || sampling == YuvChromaSubsampling::Yuv422
206                    {
207                        assert!(
208                            sampling == YuvChromaSubsampling::Yuv420
209                                || sampling == YuvChromaSubsampling::Yuv422
210                        );
211                        use crate::sse::sse_yuv_to_rgba_row422;
212                        return RowHandler {
213                            handler: Some(sse_yuv_to_rgba_row422::<DESTINATION_CHANNELS>),
214                        };
215                    } else {
216                        use crate::sse::sse_yuv_to_rgba_row;
217                        return RowHandler {
218                            handler: Some(sse_yuv_to_rgba_row::<DESTINATION_CHANNELS, SAMPLING>),
219                        };
220                    }
221                }
222            }
223        }
224        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
225        {
226            use crate::wasm32::wasm_yuv_to_rgba_row;
227            return RowHandler {
228                handler: Some(wasm_yuv_to_rgba_row::<DESTINATION_CHANNELS, SAMPLING>),
229            };
230        }
231        #[cfg(not(any(
232            all(target_arch = "aarch64", target_feature = "neon"),
233            all(target_arch = "wasm32", target_feature = "simd128")
234        )))]
235        {
236            RowHandler { handler: None }
237        }
238    }
239}
240
241struct RowHandler420<const DESTINATION_CHANNELS: u8, const SAMPLING: u8, const PRECISION: i32> {
242    handler: RowHandle420,
243}
244
245impl<const DESTINATION_CHANNELS: u8, const SAMPLING: u8, const PRECISION: i32> Default
246    for RowHandler420<DESTINATION_CHANNELS, SAMPLING, PRECISION>
247{
248    fn default() -> Self {
249        if PRECISION != 13 {
250            return RowHandler420 { handler: None };
251        }
252        assert_eq!(PRECISION, 13);
253        let sampling: YuvChromaSubsampling = SAMPLING.into();
254        if sampling != YuvChromaSubsampling::Yuv420 {
255            return RowHandler420 { handler: None };
256        }
257        assert_eq!(sampling, YuvChromaSubsampling::Yuv420);
258        #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
259        {
260            #[cfg(feature = "rdm")]
261            {
262                let is_rdm_available = std::arch::is_aarch64_feature_detected!("rdm");
263                if is_rdm_available {
264                    use crate::neon::neon_yuv_to_rgba_row_rdm420;
265                    return RowHandler420 {
266                        handler: Some(neon_yuv_to_rgba_row_rdm420::<DESTINATION_CHANNELS>),
267                    };
268                }
269            }
270            use crate::neon::neon_yuv_to_rgba_row420;
271            RowHandler420 {
272                handler: Some(neon_yuv_to_rgba_row420::<DESTINATION_CHANNELS>),
273            }
274        }
275        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
276        {
277            #[cfg(feature = "nightly_avx512")]
278            {
279                let use_avx512 = std::arch::is_x86_feature_detected!("avx512bw");
280                let use_vbmi = std::arch::is_x86_feature_detected!("avx512vbmi");
281                if use_avx512 {
282                    use crate::avx512bw::avx512_yuv_to_rgba420;
283                    return RowHandler420 {
284                        handler: Some(if use_vbmi {
285                            avx512_yuv_to_rgba420::<DESTINATION_CHANNELS, true>
286                        } else {
287                            avx512_yuv_to_rgba420::<DESTINATION_CHANNELS, false>
288                        }),
289                    };
290                }
291            }
292            #[cfg(feature = "avx")]
293            {
294                let use_avx2 = std::arch::is_x86_feature_detected!("avx2");
295                if use_avx2 {
296                    use crate::avx2::avx2_yuv_to_rgba_row420;
297                    return RowHandler420 {
298                        handler: Some(avx2_yuv_to_rgba_row420::<DESTINATION_CHANNELS>),
299                    };
300                }
301            }
302            #[cfg(feature = "sse")]
303            {
304                let use_sse = std::arch::is_x86_feature_detected!("sse4.1");
305                if use_sse {
306                    use crate::sse::sse_yuv_to_rgba_row420;
307                    return RowHandler420 {
308                        handler: Some(sse_yuv_to_rgba_row420::<DESTINATION_CHANNELS>),
309                    };
310                }
311            }
312        }
313        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
314        {
315            use crate::wasm32::wasm_yuv_to_rgba_row420;
316            return RowHandler420 {
317                handler: Some(wasm_yuv_to_rgba_row420::<DESTINATION_CHANNELS, SAMPLING>),
318            };
319        }
320        #[cfg(not(any(
321            all(target_arch = "aarch64", target_feature = "neon"),
322            all(target_arch = "wasm32", target_feature = "simd128")
323        )))]
324        {
325            RowHandler420 { handler: None }
326        }
327    }
328}
329
330macro_rules! impl_row_420_inversion_handler {
331    ($struct_name:ident) => {
332        impl<const DESTINATION_CHANNELS: u8, const SAMPLING: u8, const PRECISION: i32>
333            WideRow420InversionHandler<u8, i32>
334            for $struct_name<DESTINATION_CHANNELS, SAMPLING, PRECISION>
335        {
336            fn handle_row(
337                &self,
338                y0_plane: &[u8],
339                y1_plane: &[u8],
340                u_plane: &[u8],
341                v_plane: &[u8],
342                rgba0: &mut [u8],
343                rgba1: &mut [u8],
344                width: u32,
345                chroma: YuvChromaRange,
346                transform: &CbCrInverseTransform<i32>,
347            ) -> ProcessedOffset {
348                if let Some(handler) = self.handler {
349                    unsafe {
350                        return handler(
351                            &chroma,
352                            transform,
353                            y0_plane,
354                            y1_plane,
355                            u_plane,
356                            v_plane,
357                            rgba0,
358                            rgba1,
359                            0,
360                            0,
361                            width as usize,
362                        );
363                    }
364                }
365                ProcessedOffset { cx: 0, ux: 0 }
366            }
367        }
368    };
369}
370
371impl_row_420_inversion_handler!(RowHandler420);
372
373fn yuv_to_rgbx_impl<const DESTINATION_CHANNELS: u8, const SAMPLING: u8, const PRECISION: i32>(
374    image: &YuvPlanarImage<u8>,
375    rgba: &mut [u8],
376    rgba_stride: u32,
377    range: YuvRange,
378    matrix: YuvStandardMatrix,
379    row_handler: impl WideRowInversionHandler<u8, i32> + Send + Sync,
380    row_handler420: impl WideRow420InversionHandler<u8, i32> + Send + Sync,
381) -> Result<(), YuvError> {
382    let chroma_subsampling: YuvChromaSubsampling = SAMPLING.into();
383    let dst_chans: YuvSourceChannels = DESTINATION_CHANNELS.into();
384    let channels = dst_chans.get_channels_count();
385
386    check_rgba_destination(rgba, rgba_stride, image.width, image.height, channels)?;
387    image.check_constraints(chroma_subsampling)?;
388
389    let chroma_range = get_yuv_range(8, range);
390    let kr_kb = matrix.get_kr_kb();
391
392    let inverse_transform =
393        search_inverse_transform(PRECISION, 8, range, matrix, chroma_range, kr_kb);
394    let cr_coef = inverse_transform.cr_coef;
395    let cb_coef = inverse_transform.cb_coef;
396    let y_coef = inverse_transform.y_coef;
397    let g_coef_1 = inverse_transform.g_coeff_1;
398    let g_coef_2 = inverse_transform.g_coeff_2;
399
400    let bias_y = chroma_range.bias_y as i32;
401    let bias_uv = chroma_range.bias_uv as i32;
402
403    const BIT_DEPTH: usize = 8;
404
405    let process_halved_chroma_row =
406        |y_plane: &[u8], u_plane: &[u8], v_plane: &[u8], rgba: &mut [u8]| {
407            let cx = row_handler
408                .handle_row(
409                    y_plane,
410                    u_plane,
411                    v_plane,
412                    rgba,
413                    image.width,
414                    chroma_range,
415                    &inverse_transform,
416                )
417                .cx;
418
419            if cx != image.width as usize {
420                for (((rgba, y_src), &u_src), &v_src) in rgba
421                    .chunks_exact_mut(channels * 2)
422                    .zip(y_plane.chunks_exact(2))
423                    .zip(u_plane.iter())
424                    .zip(v_plane.iter())
425                    .skip(cx / 2)
426                {
427                    let y_value0 = (y_src[0] as i32 - bias_y) * y_coef;
428                    let cb_value = u_src as i32 - bias_uv;
429                    let cr_value = v_src as i32 - bias_uv;
430
431                    let r0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cr_coef * cr_value);
432                    let b0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cb_coef * cb_value);
433                    let g0 = qrshr::<PRECISION, BIT_DEPTH>(
434                        y_value0 - g_coef_1 * cr_value - g_coef_2 * cb_value,
435                    );
436
437                    let rgba0 = &mut rgba[0..channels];
438
439                    rgba0[dst_chans.get_r_channel_offset()] = r0 as u8;
440                    rgba0[dst_chans.get_g_channel_offset()] = g0 as u8;
441                    rgba0[dst_chans.get_b_channel_offset()] = b0 as u8;
442                    if dst_chans.has_alpha() {
443                        rgba0[dst_chans.get_a_channel_offset()] = 255u8;
444                    }
445
446                    let y_value1 = (y_src[1] as i32 - bias_y) * y_coef;
447
448                    let r1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cr_coef * cr_value);
449                    let b1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cb_coef * cb_value);
450                    let g1 = qrshr::<PRECISION, BIT_DEPTH>(
451                        y_value1 - g_coef_1 * cr_value - g_coef_2 * cb_value,
452                    );
453
454                    let rgba1 = &mut rgba[channels..channels * 2];
455
456                    rgba1[dst_chans.get_r_channel_offset()] = r1 as u8;
457                    rgba1[dst_chans.get_g_channel_offset()] = g1 as u8;
458                    rgba1[dst_chans.get_b_channel_offset()] = b1 as u8;
459                    if dst_chans.has_alpha() {
460                        rgba1[dst_chans.get_a_channel_offset()] = 255u8;
461                    }
462                }
463
464                if image.width & 1 != 0 {
465                    let y_value0 = (*y_plane.last().unwrap() as i32 - bias_y) * y_coef;
466                    let cb_value = *u_plane.last().unwrap() as i32 - bias_uv;
467                    let cr_value = *v_plane.last().unwrap() as i32 - bias_uv;
468                    let rgba = rgba.chunks_exact_mut(channels).last().unwrap();
469                    let rgba0 = &mut rgba[0..channels];
470
471                    let r0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cr_coef * cr_value);
472                    let b0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cb_coef * cb_value);
473                    let g0 = qrshr::<PRECISION, BIT_DEPTH>(
474                        y_value0 - g_coef_1 * cr_value - g_coef_2 * cb_value,
475                    );
476                    rgba0[dst_chans.get_r_channel_offset()] = r0 as u8;
477                    rgba0[dst_chans.get_g_channel_offset()] = g0 as u8;
478                    rgba0[dst_chans.get_b_channel_offset()] = b0 as u8;
479                    if dst_chans.has_alpha() {
480                        rgba0[dst_chans.get_a_channel_offset()] = 255;
481                    }
482                }
483            }
484        };
485
486    let process_doubled_chroma_row = |y_plane0: &[u8],
487                                      y_plane1: &[u8],
488                                      u_plane: &[u8],
489                                      v_plane: &[u8],
490                                      rgba0: &mut [u8],
491                                      rgba1: &mut [u8]| {
492        let cx = row_handler420
493            .handle_row(
494                y_plane0,
495                y_plane1,
496                u_plane,
497                v_plane,
498                rgba0,
499                rgba1,
500                image.width,
501                chroma_range,
502                &inverse_transform,
503            )
504            .cx;
505
506        if cx != image.width as usize {
507            for (((((rgba0, rgba1), y_src0), y_src1), &u_src), &v_src) in rgba0
508                .chunks_exact_mut(channels * 2)
509                .zip(rgba1.chunks_exact_mut(channels * 2))
510                .zip(y_plane0.chunks_exact(2))
511                .zip(y_plane1.chunks_exact(2))
512                .zip(u_plane.iter())
513                .zip(v_plane.iter())
514                .skip(cx / 2)
515            {
516                let y_value0 = (y_src0[0] as i32 - bias_y) * y_coef;
517                let cb_value = u_src as i32 - bias_uv;
518                let cr_value = v_src as i32 - bias_uv;
519
520                let g_built_coeff = -g_coef_1 * cr_value - g_coef_2 * cb_value;
521
522                let r0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cr_coef * cr_value);
523                let b0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cb_coef * cb_value);
524                let g0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + g_built_coeff);
525
526                let rgba00 = &mut rgba0[0..channels];
527
528                rgba00[dst_chans.get_r_channel_offset()] = r0 as u8;
529                rgba00[dst_chans.get_g_channel_offset()] = g0 as u8;
530                rgba00[dst_chans.get_b_channel_offset()] = b0 as u8;
531                if dst_chans.has_alpha() {
532                    rgba00[dst_chans.get_a_channel_offset()] = 255u8;
533                }
534
535                let y_value1 = (y_src0[1] as i32 - bias_y) * y_coef;
536
537                let r1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cr_coef * cr_value);
538                let b1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cb_coef * cb_value);
539                let g1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + g_built_coeff);
540
541                let rgba01 = &mut rgba0[channels..channels * 2];
542
543                rgba01[dst_chans.get_r_channel_offset()] = r1 as u8;
544                rgba01[dst_chans.get_g_channel_offset()] = g1 as u8;
545                rgba01[dst_chans.get_b_channel_offset()] = b1 as u8;
546                if dst_chans.has_alpha() {
547                    rgba01[dst_chans.get_a_channel_offset()] = 255u8;
548                }
549
550                let y_value10 = (y_src1[0] as i32 - bias_y) * y_coef;
551
552                let r10 = qrshr::<PRECISION, BIT_DEPTH>(y_value10 + cr_coef * cr_value);
553                let b10 = qrshr::<PRECISION, BIT_DEPTH>(y_value10 + cb_coef * cb_value);
554                let g10 = qrshr::<PRECISION, BIT_DEPTH>(y_value10 + g_built_coeff);
555
556                let rgba10 = &mut rgba1[0..channels];
557
558                rgba10[dst_chans.get_r_channel_offset()] = r10 as u8;
559                rgba10[dst_chans.get_g_channel_offset()] = g10 as u8;
560                rgba10[dst_chans.get_b_channel_offset()] = b10 as u8;
561                if dst_chans.has_alpha() {
562                    rgba10[dst_chans.get_a_channel_offset()] = 255u8;
563                }
564
565                let y_value11 = (y_src1[1] as i32 - bias_y) * y_coef;
566
567                let r11 = qrshr::<PRECISION, BIT_DEPTH>(y_value11 + cr_coef * cr_value);
568                let b11 = qrshr::<PRECISION, BIT_DEPTH>(y_value11 + cb_coef * cb_value);
569                let g11 = qrshr::<PRECISION, BIT_DEPTH>(y_value11 + g_built_coeff);
570
571                let rgba11 = &mut rgba1[channels..channels * 2];
572
573                rgba11[dst_chans.get_r_channel_offset()] = r11 as u8;
574                rgba11[dst_chans.get_g_channel_offset()] = g11 as u8;
575                rgba11[dst_chans.get_b_channel_offset()] = b11 as u8;
576                if dst_chans.has_alpha() {
577                    rgba11[dst_chans.get_a_channel_offset()] = 255u8;
578                }
579            }
580
581            if image.width & 1 != 0 {
582                let y_value0 = (*y_plane0.last().unwrap() as i32 - bias_y) * y_coef;
583                let y_value1 = (*y_plane1.last().unwrap() as i32 - bias_y) * y_coef;
584                let cb_value = *u_plane.last().unwrap() as i32 - bias_uv;
585                let cr_value = *v_plane.last().unwrap() as i32 - bias_uv;
586                let rgba = rgba0.chunks_exact_mut(channels).last().unwrap();
587                let rgba0 = &mut rgba[0..channels];
588
589                let g_built_coeff = -g_coef_1 * cr_value - g_coef_2 * cb_value;
590
591                let r0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cr_coef * cr_value);
592                let b0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cb_coef * cb_value);
593                let g0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + g_built_coeff);
594
595                rgba0[dst_chans.get_r_channel_offset()] = r0 as u8;
596                rgba0[dst_chans.get_g_channel_offset()] = g0 as u8;
597                rgba0[dst_chans.get_b_channel_offset()] = b0 as u8;
598                if dst_chans.has_alpha() {
599                    rgba0[dst_chans.get_a_channel_offset()] = 255;
600                }
601
602                let r1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cr_coef * cr_value);
603                let b1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cb_coef * cb_value);
604                let g1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + g_built_coeff);
605
606                let rgba = rgba1.chunks_exact_mut(channels).last().unwrap();
607                let rgba1 = &mut rgba[0..channels];
608                rgba1[dst_chans.get_r_channel_offset()] = r1 as u8;
609                rgba1[dst_chans.get_g_channel_offset()] = g1 as u8;
610                rgba1[dst_chans.get_b_channel_offset()] = b1 as u8;
611                if dst_chans.has_alpha() {
612                    rgba1[dst_chans.get_a_channel_offset()] = 255;
613                }
614            }
615        }
616    };
617
618    if chroma_subsampling == YuvChromaSubsampling::Yuv444 {
619        let iter;
620        #[cfg(feature = "rayon")]
621        {
622            iter = rgba
623                .par_chunks_exact_mut(rgba_stride as usize)
624                .zip(image.y_plane.par_chunks_exact(image.y_stride as usize))
625                .zip(image.u_plane.par_chunks_exact(image.u_stride as usize))
626                .zip(image.v_plane.par_chunks_exact(image.v_stride as usize));
627        }
628        #[cfg(not(feature = "rayon"))]
629        {
630            iter = rgba
631                .chunks_exact_mut(rgba_stride as usize)
632                .zip(image.y_plane.chunks_exact(image.y_stride as usize))
633                .zip(image.u_plane.chunks_exact(image.u_stride as usize))
634                .zip(image.v_plane.chunks_exact(image.v_stride as usize));
635        }
636        iter.for_each(|(((rgba, y_plane), u_plane), v_plane)| {
637            let y_plane = &y_plane[0..image.width as usize];
638            let cx = row_handler
639                .handle_row(
640                    y_plane,
641                    u_plane,
642                    v_plane,
643                    rgba,
644                    image.width,
645                    chroma_range,
646                    &inverse_transform,
647                )
648                .cx;
649
650            if cx != image.width as usize {
651                for (((rgba, &y_src), &u_src), &v_src) in rgba
652                    .chunks_exact_mut(channels)
653                    .zip(y_plane.iter())
654                    .zip(u_plane.iter())
655                    .zip(v_plane.iter())
656                    .skip(cx)
657                {
658                    let y_value = (y_src as i32 - bias_y) * y_coef;
659                    let cb_value = u_src as i32 - bias_uv;
660                    let cr_value = v_src as i32 - bias_uv;
661
662                    let r = qrshr::<PRECISION, BIT_DEPTH>(y_value + cr_coef * cr_value);
663                    let b = qrshr::<PRECISION, BIT_DEPTH>(y_value + cb_coef * cb_value);
664                    let g = qrshr::<PRECISION, BIT_DEPTH>(
665                        y_value - g_coef_1 * cr_value - g_coef_2 * cb_value,
666                    );
667
668                    rgba[dst_chans.get_r_channel_offset()] = r as u8;
669                    rgba[dst_chans.get_g_channel_offset()] = g as u8;
670                    rgba[dst_chans.get_b_channel_offset()] = b as u8;
671                    if dst_chans.has_alpha() {
672                        rgba[dst_chans.get_a_channel_offset()] = 255;
673                    }
674                }
675            }
676        });
677    } else if chroma_subsampling == YuvChromaSubsampling::Yuv422 {
678        let iter;
679        #[cfg(feature = "rayon")]
680        {
681            iter = rgba
682                .par_chunks_exact_mut(rgba_stride as usize)
683                .zip(image.y_plane.par_chunks_exact(image.y_stride as usize))
684                .zip(image.u_plane.par_chunks_exact(image.u_stride as usize))
685                .zip(image.v_plane.par_chunks_exact(image.v_stride as usize));
686        }
687        #[cfg(not(feature = "rayon"))]
688        {
689            iter = rgba
690                .chunks_exact_mut(rgba_stride as usize)
691                .zip(image.y_plane.chunks_exact(image.y_stride as usize))
692                .zip(image.u_plane.chunks_exact(image.u_stride as usize))
693                .zip(image.v_plane.chunks_exact(image.v_stride as usize));
694        }
695        iter.for_each(|(((rgba, y_plane), u_plane), v_plane)| {
696            process_halved_chroma_row(
697                &y_plane[0..image.width as usize],
698                &u_plane[0..(image.width as usize).div_ceil(2)],
699                &v_plane[0..(image.width as usize).div_ceil(2)],
700                &mut rgba[0..image.width as usize * channels],
701            );
702        });
703    } else if chroma_subsampling == YuvChromaSubsampling::Yuv420 {
704        let iter;
705        #[cfg(feature = "rayon")]
706        {
707            iter = rgba
708                .par_chunks_exact_mut(rgba_stride as usize * 2)
709                .zip(image.y_plane.par_chunks_exact(image.y_stride as usize * 2))
710                .zip(image.u_plane.par_chunks_exact(image.u_stride as usize))
711                .zip(image.v_plane.par_chunks_exact(image.v_stride as usize));
712        }
713        #[cfg(not(feature = "rayon"))]
714        {
715            iter = rgba
716                .chunks_exact_mut(rgba_stride as usize * 2)
717                .zip(image.y_plane.chunks_exact(image.y_stride as usize * 2))
718                .zip(image.u_plane.chunks_exact(image.u_stride as usize))
719                .zip(image.v_plane.chunks_exact(image.v_stride as usize));
720        }
721        iter.for_each(|(((rgba, y_plane), u_plane), v_plane)| {
722            let (rgba0, rgba1) = rgba.split_at_mut(rgba_stride as usize);
723            let (y_plane0, y_plane1) = y_plane.split_at(image.y_stride as usize);
724            process_doubled_chroma_row(
725                &y_plane0[0..image.width as usize],
726                &y_plane1[0..image.width as usize],
727                &u_plane[0..(image.width as usize).div_ceil(2)],
728                &v_plane[0..(image.width as usize).div_ceil(2)],
729                &mut rgba0[0..image.width as usize * channels],
730                &mut rgba1[0..image.width as usize * channels],
731            );
732        });
733
734        if image.height & 1 != 0 {
735            let rgba = rgba.chunks_exact_mut(rgba_stride as usize).last().unwrap();
736            let u_plane = image
737                .u_plane
738                .chunks_exact(image.u_stride as usize)
739                .last()
740                .unwrap();
741            let v_plane = image
742                .v_plane
743                .chunks_exact(image.v_stride as usize)
744                .last()
745                .unwrap();
746            let y_plane = image
747                .y_plane
748                .chunks_exact(image.y_stride as usize)
749                .last()
750                .unwrap();
751            process_halved_chroma_row(
752                &y_plane[0..image.width as usize],
753                &u_plane[0..(image.width as usize).div_ceil(2)],
754                &v_plane[0..(image.width as usize).div_ceil(2)],
755                &mut rgba[0..image.width as usize * channels],
756            );
757        }
758    } else {
759        unreachable!();
760    }
761
762    Ok(())
763}
764
765fn yuv_to_rgbx<const DESTINATION_CHANNELS: u8, const SAMPLING: u8>(
766    image: &YuvPlanarImage<u8>,
767    rgba: &mut [u8],
768    rgba_stride: u32,
769    range: YuvRange,
770    matrix: YuvStandardMatrix,
771) -> Result<(), YuvError> {
772    yuv_to_rgbx_impl::<DESTINATION_CHANNELS, SAMPLING, 13>(
773        image,
774        rgba,
775        rgba_stride,
776        range,
777        matrix,
778        RowHandler::<DESTINATION_CHANNELS, SAMPLING, 13>::default(),
779        RowHandler420::<DESTINATION_CHANNELS, SAMPLING, 13>::default(),
780    )
781}
782
783/// Convert YUV 420 planar format to RGB format.
784///
785/// This function takes YUV 420 planar format data with 8-bit precision,
786/// and converts it to RGB format with 8-bit per channel precision.
787///
788/// # Arguments
789///
790/// * `planar_image` - Source planar image.
791/// * `rgb` - A mutable slice to store the converted RGB data.
792/// * `rgb_stride` - The stride (components per row) for the RGB image data.
793/// * `range` - The YUV range (limited or full).
794/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
795///
796/// # Panics
797///
798/// This function panics if the lengths of the planes or the input RGB data are not valid based
799/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
800///
801pub fn yuv420_to_rgb(
802    planar_image: &YuvPlanarImage<u8>,
803    rgb: &mut [u8],
804    rgb_stride: u32,
805    range: YuvRange,
806    matrix: YuvStandardMatrix,
807) -> Result<(), YuvError> {
808    yuv_to_rgbx::<{ YuvSourceChannels::Rgb as u8 }, { YuvChromaSubsampling::Yuv420 as u8 }>(
809        planar_image,
810        rgb,
811        rgb_stride,
812        range,
813        matrix,
814    )
815}
816
817/// Convert YUV 420 planar format to BGR format.
818///
819/// This function takes YUV 420 planar format data with 8-bit precision,
820/// and converts it to BGR format with 8-bit per channel precision.
821///
822/// # Arguments
823///
824/// * `planar_image` - Source planar image.
825/// * `rgb` - A mutable slice to store the converted BGR data.
826/// * `rgb_stride` - The stride (components per row) for the BGR image data.
827/// * `range` - The YUV range (limited or full).
828/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
829///
830/// # Panics
831///
832/// This function panics if the lengths of the planes or the input BGR data are not valid based
833/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
834///
835pub fn yuv420_to_bgr(
836    planar_image: &YuvPlanarImage<u8>,
837    bgr: &mut [u8],
838    bgr_stride: u32,
839    range: YuvRange,
840    matrix: YuvStandardMatrix,
841) -> Result<(), YuvError> {
842    yuv_to_rgbx::<{ YuvSourceChannels::Bgr as u8 }, { YuvChromaSubsampling::Yuv420 as u8 }>(
843        planar_image,
844        bgr,
845        bgr_stride,
846        range,
847        matrix,
848    )
849}
850
851/// Convert YUV 420 planar format to RGBA format.
852///
853/// This function takes YUV 420 planar format data with 8-bit precision,
854/// and converts it to RGBA format with 8-bit per channel precision.
855///
856/// # Arguments
857///
858/// * `planar_image` - Source planar image.
859/// * `rgba` - A mutable slice to store the converted RGBA data.
860/// * `rgba_stride` - Elements per row.
861/// * `range` - The YUV range (limited or full).
862/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
863///
864/// # Panics
865///
866/// This function panics if the lengths of the planes or the input RGBA data are not valid based
867/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
868///
869pub fn yuv420_to_rgba(
870    planar_image: &YuvPlanarImage<u8>,
871    rgba: &mut [u8],
872    rgba_stride: u32,
873    range: YuvRange,
874    matrix: YuvStandardMatrix,
875) -> Result<(), YuvError> {
876    yuv_to_rgbx::<{ YuvSourceChannels::Rgba as u8 }, { YuvChromaSubsampling::Yuv420 as u8 }>(
877        planar_image,
878        rgba,
879        rgba_stride,
880        range,
881        matrix,
882    )
883}
884
885/// Convert YUV 420 planar format to BGRA format.
886///
887/// This function takes YUV 420 planar format data with 8-bit precision,
888/// and converts it to BGRA format with 8-bit per channel precision.
889///
890/// # Arguments
891///
892/// * `planar_image` - Source planar image.
893/// * `bgra` - A mutable slice to store the converted BGRA data.
894/// * `bgra_stride` - Elements per BGRA row.
895/// * `range` - The YUV range (limited or full).
896/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
897///
898/// # Panics
899///
900/// This function panics if the lengths of the planes or the input BGRA data are not valid based
901/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
902///
903pub fn yuv420_to_bgra(
904    planar_image: &YuvPlanarImage<u8>,
905    bgra: &mut [u8],
906    bgra_stride: u32,
907    range: YuvRange,
908    matrix: YuvStandardMatrix,
909) -> Result<(), YuvError> {
910    yuv_to_rgbx::<{ YuvSourceChannels::Bgra as u8 }, { YuvChromaSubsampling::Yuv420 as u8 }>(
911        planar_image,
912        bgra,
913        bgra_stride,
914        range,
915        matrix,
916    )
917}
918
919/// Convert YUV 422 planar format to RGB format.
920///
921/// This function takes YUV 422 data with 8-bit precision,
922/// and converts it to RGB format with 8-bit per channel precision.
923///
924/// # Arguments
925///
926/// * `planar_image` - Source planar image.
927/// * `rgb` - A mutable slice to store the converted RGB data.
928/// * `rgb_stride` - The stride (components per row) for the RGB image data.
929/// * `range` - The YUV range (limited or full).
930/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
931///
932/// # Panics
933///
934/// This function panics if the lengths of the planes or the input RGB data are not valid based
935/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
936///
937pub fn yuv422_to_rgb(
938    planar_image: &YuvPlanarImage<u8>,
939    rgb: &mut [u8],
940    rgb_stride: u32,
941    range: YuvRange,
942    matrix: YuvStandardMatrix,
943) -> Result<(), YuvError> {
944    yuv_to_rgbx::<{ YuvSourceChannels::Rgb as u8 }, { YuvChromaSubsampling::Yuv422 as u8 }>(
945        planar_image,
946        rgb,
947        rgb_stride,
948        range,
949        matrix,
950    )
951}
952
953/// Convert YUV 422 planar format to BGR format.
954///
955/// This function takes YUV 422 data with 8-bit precision,
956/// and converts it to BGR format with 8-bit per channel precision.
957///
958/// # Arguments
959///
960/// * `planar_image` - Source planar image.
961/// * `bgr` - A mutable slice to store the converted BGR data.
962/// * `bgr_stride` - The stride (components per row) for the BGR image data.
963/// * `range` - The YUV range (limited or full).
964/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
965///
966/// # Panics
967///
968/// This function panics if the lengths of the planes or the input BGR data are not valid based
969/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
970///
971pub fn yuv422_to_bgr(
972    planar_image: &YuvPlanarImage<u8>,
973    bgr: &mut [u8],
974    bgr_stride: u32,
975    range: YuvRange,
976    matrix: YuvStandardMatrix,
977) -> Result<(), YuvError> {
978    yuv_to_rgbx::<{ YuvSourceChannels::Bgr as u8 }, { YuvChromaSubsampling::Yuv422 as u8 }>(
979        planar_image,
980        bgr,
981        bgr_stride,
982        range,
983        matrix,
984    )
985}
986
987/// Convert YUV 422 planar format to RGBA format.
988///
989/// This function takes YUV 422 data with 8-bit precision,
990/// and converts it to RGBA format with 8-bit per channel precision.
991///
992/// # Arguments
993///
994/// * `planar_image` - Source planar image.
995/// * `rgba` - A mutable slice to store the converted RGBA data.
996/// * `rgba_stride` - Elements per RGBA data row.
997/// * `range` - The YUV range (limited or full).
998/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
999///
1000/// # Panics
1001///
1002/// This function panics if the lengths of the planes or the input BGRA data are not valid based
1003/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
1004///
1005pub fn yuv422_to_rgba(
1006    planar_image: &YuvPlanarImage<u8>,
1007    rgba: &mut [u8],
1008    rgba_stride: u32,
1009    range: YuvRange,
1010    matrix: YuvStandardMatrix,
1011) -> Result<(), YuvError> {
1012    yuv_to_rgbx::<{ YuvSourceChannels::Rgba as u8 }, { YuvChromaSubsampling::Yuv422 as u8 }>(
1013        planar_image,
1014        rgba,
1015        rgba_stride,
1016        range,
1017        matrix,
1018    )
1019}
1020
1021/// Convert YUV 422 planar format to BGRA format.
1022///
1023/// This function takes YUV 422 data with 8-bit precision,
1024/// and converts it to BGRA format with 8-bit per channel precision.
1025///
1026/// # Arguments
1027///
1028/// * `planar_image` - Source planar image.
1029/// * `bgra` - A mutable slice to store the converted BGRA data.
1030/// * `bgra_stride` - Elements per RGBA data row.
1031/// * `range` - The YUV range (limited or full).
1032/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
1033///
1034/// # Panics
1035///
1036/// This function panics if the lengths of the planes or the input BGRA data are not valid based
1037/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
1038///
1039pub fn yuv422_to_bgra(
1040    planar_image: &YuvPlanarImage<u8>,
1041    bgra: &mut [u8],
1042    bgra_stride: u32,
1043    range: YuvRange,
1044    matrix: YuvStandardMatrix,
1045) -> Result<(), YuvError> {
1046    yuv_to_rgbx::<{ YuvSourceChannels::Bgra as u8 }, { YuvChromaSubsampling::Yuv422 as u8 }>(
1047        planar_image,
1048        bgra,
1049        bgra_stride,
1050        range,
1051        matrix,
1052    )
1053}
1054
1055/// Convert YUV 444 planar format to RGBA format.
1056///
1057/// This function takes YUV 444 data with 8-bit precision,
1058/// and converts it to RGBA format with 8-bit per channel precision.
1059///
1060/// # Arguments
1061///
1062/// * `planar_image` - Source planar image.
1063/// * `rgba` - A mutable slice to store the converted RGBA data.
1064/// * `rgba_stride` - Elements per RGBA data row.
1065/// * `range` - The YUV range (limited or full).
1066/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
1067///
1068/// # Panics
1069///
1070/// This function panics if the lengths of the planes or the input BGRA data are not valid based
1071/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
1072///
1073pub fn yuv444_to_rgba(
1074    planar_image: &YuvPlanarImage<u8>,
1075    rgba: &mut [u8],
1076    rgba_stride: u32,
1077    range: YuvRange,
1078    matrix: YuvStandardMatrix,
1079) -> Result<(), YuvError> {
1080    yuv_to_rgbx::<{ YuvSourceChannels::Rgba as u8 }, { YuvChromaSubsampling::Yuv444 as u8 }>(
1081        planar_image,
1082        rgba,
1083        rgba_stride,
1084        range,
1085        matrix,
1086    )
1087}
1088
1089/// Convert YUV 444 planar format to BGRA format.
1090///
1091/// This function takes YUV 444 data with 8-bit precision,
1092/// and converts it to BGRA format with 8-bit per channel precision.
1093///
1094/// # Arguments
1095///
1096/// * `planar_image` - Source planar image.
1097/// * `height` - The height of the YUV image.
1098/// * `bgra` - A mutable slice to store the converted BGRA data.
1099/// * `bgra_stride` - Elements per BGRA data row.
1100/// * `range` - The YUV range (limited or full).
1101/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
1102///
1103/// # Panics
1104///
1105/// This function panics if the lengths of the planes or the input BGRA data are not valid based
1106/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
1107///
1108pub fn yuv444_to_bgra(
1109    planar_image: &YuvPlanarImage<u8>,
1110    bgra: &mut [u8],
1111    bgra_stride: u32,
1112    range: YuvRange,
1113    matrix: YuvStandardMatrix,
1114) -> Result<(), YuvError> {
1115    yuv_to_rgbx::<{ YuvSourceChannels::Bgra as u8 }, { YuvChromaSubsampling::Yuv444 as u8 }>(
1116        planar_image,
1117        bgra,
1118        bgra_stride,
1119        range,
1120        matrix,
1121    )
1122}
1123
1124/// Convert YUV 444 planar format to RGB format.
1125///
1126/// This function takes YUV 444 data with 8-bit precision,
1127/// and converts it to RGB format with 8-bit per channel precision.
1128///
1129/// # Arguments
1130///
1131/// * `planar_image` - Source planar image.
1132/// * `rgb` - A mutable slice to store the converted RGB data.
1133/// * `rgb_stride` - The stride (components per row) for the RGB image data.
1134/// * `range` - The YUV range (limited or full).
1135/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
1136///
1137/// # Panics
1138///
1139/// This function panics if the lengths of the planes or the input RGB data are not valid based
1140/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
1141///
1142pub fn yuv444_to_rgb(
1143    planar_image: &YuvPlanarImage<u8>,
1144    rgb: &mut [u8],
1145    rgb_stride: u32,
1146    range: YuvRange,
1147    matrix: YuvStandardMatrix,
1148) -> Result<(), YuvError> {
1149    yuv_to_rgbx::<{ YuvSourceChannels::Rgb as u8 }, { YuvChromaSubsampling::Yuv444 as u8 }>(
1150        planar_image,
1151        rgb,
1152        rgb_stride,
1153        range,
1154        matrix,
1155    )
1156}
1157
1158/// Convert YUV 444 planar format to BGR format.
1159///
1160/// This function takes YUV 444 data with 8-bit precision,
1161/// and converts it to BGR format with 8-bit per channel precision.
1162///
1163/// # Arguments
1164///
1165/// * `planar_image` - Source planar image.
1166/// * `bgr` - A mutable slice to store the converted BGR data.
1167/// * `bgr_stride` - The stride (components per row) for the BGR image data.
1168/// * `range` - The YUV range (limited or full).
1169/// * `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
1170///
1171/// # Panics
1172///
1173/// This function panics if the lengths of the planes or the input BGR data are not valid based
1174/// on the specified width, height, and strides, or if invalid YUV range or matrix is provided.
1175///
1176pub fn yuv444_to_bgr(
1177    planar_image: &YuvPlanarImage<u8>,
1178    bgr: &mut [u8],
1179    bgr_stride: u32,
1180    range: YuvRange,
1181    matrix: YuvStandardMatrix,
1182) -> Result<(), YuvError> {
1183    yuv_to_rgbx::<{ YuvSourceChannels::Bgr as u8 }, { YuvChromaSubsampling::Yuv444 as u8 }>(
1184        planar_image,
1185        bgr,
1186        bgr_stride,
1187        range,
1188        matrix,
1189    )
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194    use super::*;
1195    use crate::{rgb_to_yuv420, rgb_to_yuv422, rgb_to_yuv444, yuv444_to_rgb, YuvPlanarImageMut};
1196    use rand::Rng;
1197
1198    #[test]
1199    fn test_yuv444_round_trip_full_range() {
1200        fn matrix(yuv_accuracy: YuvConversionMode, max_diff: i32) {
1201            let image_width = 256usize;
1202            let image_height = 256usize;
1203
1204            let random_point_x = rand::rng().random_range(0..image_width);
1205            let random_point_y = rand::rng().random_range(0..image_height);
1206
1207            let pixel_points = [
1208                [0, 0],
1209                [image_width - 1, image_height - 1],
1210                [image_width - 1, 0],
1211                [0, image_height - 1],
1212                [(image_width - 1) / 2, (image_height - 1) / 2],
1213                [image_width / 5, image_height / 5],
1214                [0, image_height / 5],
1215                [image_width / 5, 0],
1216                [image_width / 5 * 3, image_height / 5],
1217                [image_width / 5 * 3, image_height / 5 * 3],
1218                [image_width / 5, image_height / 5 * 3],
1219                [random_point_x, random_point_y],
1220            ];
1221            let mut image_rgb = vec![0u8; image_width * image_height * 3];
1222
1223            let or = rand::rng().random_range(0..256) as u8;
1224            let og = rand::rng().random_range(0..256) as u8;
1225            let ob = rand::rng().random_range(0..256) as u8;
1226
1227            for point in &pixel_points {
1228                image_rgb[point[0] * 3 + point[1] * image_width * 3] = or;
1229                image_rgb[point[0] * 3 + point[1] * image_width * 3 + 1] = og;
1230                image_rgb[point[0] * 3 + point[1] * image_width * 3 + 2] = ob;
1231            }
1232
1233            let mut planar_image = YuvPlanarImageMut::<u8>::alloc(
1234                image_width as u32,
1235                image_height as u32,
1236                YuvChromaSubsampling::Yuv444,
1237            );
1238
1239            rgb_to_yuv444(
1240                &mut planar_image,
1241                &image_rgb,
1242                image_width as u32 * 3,
1243                YuvRange::Full,
1244                YuvStandardMatrix::Bt709,
1245                yuv_accuracy,
1246            )
1247            .unwrap();
1248
1249            image_rgb.fill(0);
1250
1251            let fixed_planar = planar_image.to_fixed();
1252
1253            yuv444_to_rgb(
1254                &fixed_planar,
1255                &mut image_rgb,
1256                image_width as u32 * 3,
1257                YuvRange::Full,
1258                YuvStandardMatrix::Bt709,
1259            )
1260            .unwrap();
1261
1262            for point in &pixel_points {
1263                let x = point[0];
1264                let y = point[1];
1265                let r = image_rgb[x * 3 + y * image_width * 3];
1266                let g = image_rgb[x * 3 + y * image_width * 3 + 1];
1267                let b = image_rgb[x * 3 + y * image_width * 3 + 2];
1268
1269                let diff_r = (r as i32 - or as i32).abs();
1270                let diff_g = (g as i32 - og as i32).abs();
1271                let diff_b = (b as i32 - ob as i32).abs();
1272
1273                assert!(
1274                    diff_r <= max_diff,
1275                    "Matrix {}, diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1276                    diff_r,
1277                    yuv_accuracy,
1278                    [or, og, ob],
1279                    [r, g, b]
1280                );
1281                assert!(
1282                    diff_g <= max_diff,
1283                    "Matrix {}, diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1284                    diff_g,
1285                    yuv_accuracy,
1286                    [or, og, ob],
1287                    [r, g, b]
1288                );
1289                assert!(
1290                    diff_b <= max_diff,
1291                    "Matrix {}, diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1292                    diff_b,
1293                    yuv_accuracy,
1294                    [or, og, ob],
1295                    [r, g, b]
1296                );
1297            }
1298        }
1299        matrix(YuvConversionMode::Balanced, 3);
1300        #[cfg(feature = "fast_mode")]
1301        matrix(YuvConversionMode::Fast, 6);
1302        #[cfg(feature = "professional_mode")]
1303        matrix(YuvConversionMode::Professional, 3);
1304    }
1305
1306    #[test]
1307    fn test_yuv444_round_trip_limited_range() {
1308        fn matrix(yuv_accuracy: YuvConversionMode, max_diff: i32) {
1309            let image_width = 256usize;
1310            let image_height = 256usize;
1311
1312            let random_point_x = rand::rng().random_range(0..image_width);
1313            let random_point_y = rand::rng().random_range(0..image_height);
1314
1315            let pixel_points = [
1316                [0, 0],
1317                [image_width - 1, image_height - 1],
1318                [image_width - 1, 0],
1319                [0, image_height - 1],
1320                [(image_width - 1) / 2, (image_height - 1) / 2],
1321                [image_width / 5, image_height / 5],
1322                [0, image_height / 5],
1323                [image_width / 5, 0],
1324                [image_width / 5 * 3, image_height / 5],
1325                [image_width / 5 * 3, image_height / 5 * 3],
1326                [image_width / 5, image_height / 5 * 3],
1327                [random_point_x, random_point_y],
1328            ];
1329            let mut image_rgb = vec![0u8; image_width * image_height * 3];
1330
1331            let or = rand::rng().random_range(0..256) as u8;
1332            let og = rand::rng().random_range(0..256) as u8;
1333            let ob = rand::rng().random_range(0..256) as u8;
1334
1335            for point in &pixel_points {
1336                image_rgb[point[0] * 3 + point[1] * image_width * 3] = or;
1337                image_rgb[point[0] * 3 + point[1] * image_width * 3 + 1] = og;
1338                image_rgb[point[0] * 3 + point[1] * image_width * 3 + 2] = ob;
1339            }
1340
1341            let mut planar_image = YuvPlanarImageMut::<u8>::alloc(
1342                image_width as u32,
1343                image_height as u32,
1344                YuvChromaSubsampling::Yuv444,
1345            );
1346
1347            rgb_to_yuv444(
1348                &mut planar_image,
1349                &image_rgb,
1350                image_width as u32 * 3,
1351                YuvRange::Limited,
1352                YuvStandardMatrix::Bt709,
1353                yuv_accuracy,
1354            )
1355            .unwrap();
1356
1357            image_rgb.fill(0);
1358
1359            let fixed_planar = planar_image.to_fixed();
1360
1361            yuv444_to_rgb(
1362                &fixed_planar,
1363                &mut image_rgb,
1364                image_width as u32 * 3,
1365                YuvRange::Limited,
1366                YuvStandardMatrix::Bt709,
1367            )
1368            .unwrap();
1369
1370            for point in &pixel_points {
1371                let x = point[0];
1372                let y = point[1];
1373                let r = image_rgb[x * 3 + y * image_width * 3];
1374                let g = image_rgb[x * 3 + y * image_width * 3 + 1];
1375                let b = image_rgb[x * 3 + y * image_width * 3 + 2];
1376
1377                let diff_r = (r as i32 - or as i32).abs();
1378                let diff_g = (g as i32 - og as i32).abs();
1379                let diff_b = (b as i32 - ob as i32).abs();
1380
1381                assert!(
1382                    diff_r <= max_diff,
1383                    "Matrix {} Original RGB {:?}, Round-tripped RGB {:?}, diff {}",
1384                    yuv_accuracy,
1385                    [or, og, ob],
1386                    [r, g, b],
1387                    diff_r
1388                );
1389                assert!(
1390                    diff_g <= max_diff,
1391                    "Matrix {} Original RGB {:?}, Round-tripped RGB {:?}, diff {}",
1392                    yuv_accuracy,
1393                    [or, og, ob],
1394                    [r, g, b],
1395                    diff_g,
1396                );
1397                assert!(
1398                    diff_b <= max_diff,
1399                    "Matrix {} Original RGB {:?}, Round-tripped RGB {:?}, diff {}",
1400                    yuv_accuracy,
1401                    [or, og, ob],
1402                    [r, g, b],
1403                    diff_b,
1404                );
1405            }
1406        }
1407        matrix(YuvConversionMode::Balanced, 20);
1408        #[cfg(feature = "fast_mode")]
1409        matrix(YuvConversionMode::Fast, 30);
1410    }
1411
1412    #[test]
1413    fn test_yuv422_round_trip_full_range() {
1414        fn matrix(yuv_accuracy: YuvConversionMode, max_diff: i32) {
1415            let image_width = 256usize;
1416            let image_height = 256usize;
1417
1418            let random_point_x = rand::rng().random_range(0..image_width);
1419            let random_point_y = rand::rng().random_range(0..image_height);
1420
1421            const CHANNELS: usize = 3;
1422
1423            let pixel_points = [
1424                [0, 0],
1425                [image_width - 1, image_height - 1],
1426                [image_width - 1, 0],
1427                [0, image_height - 1],
1428                [(image_width - 1) / 2, (image_height - 1) / 2],
1429                [image_width / 5, image_height / 5],
1430                [0, image_height / 5],
1431                [image_width / 5, 0],
1432                [image_width / 5 * 3, image_height / 5],
1433                [image_width / 5 * 3, image_height / 5 * 3],
1434                [image_width / 5, image_height / 5 * 3],
1435                [random_point_x, random_point_y],
1436            ];
1437
1438            let mut source_rgb = vec![0u8; image_width * image_height * CHANNELS];
1439
1440            let or = rand::rng().random_range(0..256) as u8;
1441            let og = rand::rng().random_range(0..256) as u8;
1442            let ob = rand::rng().random_range(0..256) as u8;
1443
1444            for point in &pixel_points {
1445                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1446                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1447                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1448
1449                let nx = (point[0] + 1).min(image_width - 1);
1450                let ny = point[1].min(image_height - 1);
1451
1452                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1453                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1454                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1455
1456                let nx = point[0].saturating_sub(1).min(image_width - 1);
1457                let ny = point[1].min(image_height - 1);
1458
1459                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1460                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1461                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1462            }
1463
1464            let mut planar_image = YuvPlanarImageMut::<u8>::alloc(
1465                image_width as u32,
1466                image_height as u32,
1467                YuvChromaSubsampling::Yuv422,
1468            );
1469
1470            rgb_to_yuv422(
1471                &mut planar_image,
1472                &source_rgb,
1473                image_width as u32 * 3,
1474                YuvRange::Full,
1475                YuvStandardMatrix::Bt709,
1476                yuv_accuracy,
1477            )
1478            .unwrap();
1479
1480            let mut dest_rgb = vec![0u8; image_width * image_height * CHANNELS];
1481
1482            let fixed_planar = planar_image.to_fixed();
1483
1484            yuv422_to_rgb(
1485                &fixed_planar,
1486                &mut dest_rgb,
1487                image_width as u32 * 3,
1488                YuvRange::Full,
1489                YuvStandardMatrix::Bt709,
1490            )
1491            .unwrap();
1492
1493            for point in &pixel_points {
1494                let x = point[0];
1495                let y = point[1];
1496                let px = x * CHANNELS + y * image_width * CHANNELS;
1497
1498                let r = dest_rgb[px];
1499                let g = dest_rgb[px + 1];
1500                let b = dest_rgb[px + 2];
1501
1502                let diff_r = r as i32 - or as i32;
1503                let diff_g = g as i32 - og as i32;
1504                let diff_b = b as i32 - ob as i32;
1505
1506                assert!(
1507                    diff_r <= max_diff,
1508                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1509                    yuv_accuracy,
1510                    diff_r,
1511                    [or, og, ob],
1512                    [r, g, b]
1513                );
1514                assert!(
1515                    diff_g <= max_diff,
1516                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1517                    yuv_accuracy,
1518                    diff_g,
1519                    [or, og, ob],
1520                    [r, g, b]
1521                );
1522                assert!(
1523                    diff_b <= max_diff,
1524                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1525                    yuv_accuracy,
1526                    diff_b,
1527                    [or, og, ob],
1528                    [r, g, b]
1529                );
1530            }
1531        }
1532        matrix(YuvConversionMode::Balanced, 3);
1533        #[cfg(feature = "fast_mode")]
1534        matrix(YuvConversionMode::Fast, 7);
1535        #[cfg(feature = "professional_mode")]
1536        matrix(YuvConversionMode::Professional, 3);
1537    }
1538
1539    #[test]
1540    fn test_yuv422_round_trip_limited_range() {
1541        fn matrix(yuv_accuracy: YuvConversionMode, max_diff: i32) {
1542            let image_width = 256usize;
1543            let image_height = 256usize;
1544
1545            let random_point_x = rand::rng().random_range(0..image_width);
1546            let random_point_y = rand::rng().random_range(0..image_height);
1547
1548            const CHANNELS: usize = 3;
1549
1550            let pixel_points = [
1551                [0, 0],
1552                [image_width - 1, image_height - 1],
1553                [image_width - 1, 0],
1554                [0, image_height - 1],
1555                [(image_width - 1) / 2, (image_height - 1) / 2],
1556                [image_width / 5, image_height / 5],
1557                [0, image_height / 5],
1558                [image_width / 5, 0],
1559                [image_width / 5 * 3, image_height / 5],
1560                [image_width / 5 * 3, image_height / 5 * 3],
1561                [image_width / 5, image_height / 5 * 3],
1562                [random_point_x, random_point_y],
1563            ];
1564
1565            let mut source_rgb = vec![0u8; image_width * image_height * CHANNELS];
1566
1567            let or = rand::rng().random_range(0..256) as u8;
1568            let og = rand::rng().random_range(0..256) as u8;
1569            let ob = rand::rng().random_range(0..256) as u8;
1570
1571            for point in &pixel_points {
1572                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1573                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1574                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1575
1576                let nx = (point[0] + 1).min(image_width - 1);
1577                let ny = point[1].min(image_height - 1);
1578
1579                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1580                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1581                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1582
1583                let nx = point[0].saturating_sub(1).min(image_width - 1);
1584                let ny = point[1].min(image_height - 1);
1585
1586                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1587                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1588                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1589            }
1590
1591            let mut planar_image = YuvPlanarImageMut::<u8>::alloc(
1592                image_width as u32,
1593                image_height as u32,
1594                YuvChromaSubsampling::Yuv422,
1595            );
1596
1597            rgb_to_yuv422(
1598                &mut planar_image,
1599                &source_rgb,
1600                image_width as u32 * 3,
1601                YuvRange::Limited,
1602                YuvStandardMatrix::Bt709,
1603                yuv_accuracy,
1604            )
1605            .unwrap();
1606
1607            let mut dest_rgb = vec![0u8; image_width * image_height * CHANNELS];
1608
1609            let fixed_planar = planar_image.to_fixed();
1610
1611            yuv422_to_rgb(
1612                &fixed_planar,
1613                &mut dest_rgb,
1614                image_width as u32 * 3,
1615                YuvRange::Limited,
1616                YuvStandardMatrix::Bt709,
1617            )
1618            .unwrap();
1619
1620            for point in pixel_points.iter() {
1621                let x = point[0];
1622                let y = point[1];
1623                let px = x * CHANNELS + y * image_width * CHANNELS;
1624
1625                let r = dest_rgb[px];
1626                let g = dest_rgb[px + 1];
1627                let b = dest_rgb[px + 2];
1628
1629                let diff_r = r as i32 - or as i32;
1630                let diff_g = g as i32 - og as i32;
1631                let diff_b = b as i32 - ob as i32;
1632
1633                assert!(
1634                    diff_r <= max_diff,
1635                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1636                    yuv_accuracy,
1637                    diff_r,
1638                    [or, og, ob],
1639                    [r, g, b]
1640                );
1641                assert!(
1642                    diff_g <= max_diff,
1643                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1644                    yuv_accuracy,
1645                    diff_g,
1646                    [or, og, ob],
1647                    [r, g, b]
1648                );
1649                assert!(
1650                    diff_b <= max_diff,
1651                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1652                    yuv_accuracy,
1653                    diff_b,
1654                    [or, og, ob],
1655                    [r, g, b]
1656                );
1657            }
1658        }
1659        #[cfg(feature = "fast_mode")]
1660        matrix(YuvConversionMode::Fast, 15);
1661        matrix(YuvConversionMode::Balanced, 10);
1662        #[cfg(feature = "professional_mode")]
1663        matrix(YuvConversionMode::Professional, 10);
1664    }
1665
1666    #[test]
1667    fn test_yuv420_round_trip_full_range() {
1668        fn matrix(yuv_accuracy: YuvConversionMode, max_diff: i32) {
1669            let image_width = 256usize;
1670            let image_height = 256usize;
1671
1672            let random_point_x = rand::rng().random_range(0..image_width);
1673            let random_point_y = rand::rng().random_range(0..image_height);
1674
1675            const CHANNELS: usize = 3;
1676
1677            let pixel_points = [
1678                [0, 0],
1679                [image_width - 1, image_height - 1],
1680                [image_width - 1, 0],
1681                [0, image_height - 1],
1682                [(image_width - 1) / 2, (image_height - 1) / 2],
1683                [image_width / 5, image_height / 5],
1684                [0, image_height / 5],
1685                [image_width / 5, 0],
1686                [image_width / 5 * 3, image_height / 5],
1687                [image_width / 5 * 3, image_height / 5 * 3],
1688                [image_width / 5, image_height / 5 * 3],
1689                [random_point_x, random_point_y],
1690            ];
1691
1692            let mut source_rgb = vec![0u8; image_width * image_height * CHANNELS];
1693
1694            let or = rand::rng().random_range(0..256) as u8;
1695            let og = rand::rng().random_range(0..256) as u8;
1696            let ob = rand::rng().random_range(0..256) as u8;
1697
1698            for point in &pixel_points {
1699                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1700                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1701                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1702
1703                let nx = (point[0] + 1).min(image_width - 1);
1704                let ny = point[1].min(image_height - 1);
1705
1706                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1707                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1708                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1709
1710                let nx = (point[0] + 1).min(image_width - 1);
1711                let ny = (point[1] + 1).min(image_height - 1);
1712
1713                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1714                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1715                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1716
1717                let nx = point[0].min(image_width - 1);
1718                let ny = (point[1] + 1).min(image_height - 1);
1719
1720                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1721                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1722                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1723
1724                let nx = point[0].saturating_sub(1).min(image_width - 1);
1725                let ny = point[1].saturating_sub(1).min(image_height - 1);
1726
1727                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1728                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1729                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1730
1731                let nx = point[0].min(image_width - 1);
1732                let ny = point[1].saturating_sub(1).min(image_height - 1);
1733
1734                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1735                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1736                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1737
1738                let nx = point[0].saturating_sub(1).min(image_width - 1);
1739                let ny = point[1].min(image_height - 1);
1740
1741                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1742                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1743                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1744            }
1745
1746            let mut planar_image = YuvPlanarImageMut::<u8>::alloc(
1747                image_width as u32,
1748                image_height as u32,
1749                YuvChromaSubsampling::Yuv420,
1750            );
1751
1752            rgb_to_yuv420(
1753                &mut planar_image,
1754                &source_rgb,
1755                image_width as u32 * 3,
1756                YuvRange::Full,
1757                YuvStandardMatrix::Bt709,
1758                yuv_accuracy,
1759            )
1760            .unwrap();
1761
1762            let mut dest_rgb = vec![0u8; image_width * image_height * CHANNELS];
1763
1764            let fixed_planar = planar_image.to_fixed();
1765
1766            yuv420_to_rgb(
1767                &fixed_planar,
1768                &mut dest_rgb,
1769                image_width as u32 * 3,
1770                YuvRange::Full,
1771                YuvStandardMatrix::Bt709,
1772            )
1773            .unwrap();
1774
1775            for point in &pixel_points {
1776                let x = point[0];
1777                let y = point[1];
1778                let px = x * CHANNELS + y * image_width * CHANNELS;
1779
1780                let r = dest_rgb[px];
1781                let g = dest_rgb[px + 1];
1782                let b = dest_rgb[px + 2];
1783
1784                let diff_r = r as i32 - or as i32;
1785                let diff_g = g as i32 - og as i32;
1786                let diff_b = b as i32 - ob as i32;
1787
1788                assert!(
1789                    diff_r <= max_diff,
1790                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1791                    yuv_accuracy,
1792                    diff_r,
1793                    [or, og, ob],
1794                    [r, g, b]
1795                );
1796                assert!(
1797                    diff_g <= max_diff,
1798                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1799                    yuv_accuracy,
1800                    diff_g,
1801                    [or, og, ob],
1802                    [r, g, b]
1803                );
1804                assert!(
1805                    diff_b <= max_diff,
1806                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1807                    yuv_accuracy,
1808                    diff_b,
1809                    [or, og, ob],
1810                    [r, g, b]
1811                );
1812            }
1813        }
1814        matrix(YuvConversionMode::Balanced, 80);
1815        #[cfg(feature = "fast_mode")]
1816        matrix(YuvConversionMode::Fast, 80);
1817        #[cfg(feature = "professional_mode")]
1818        matrix(YuvConversionMode::Professional, 72);
1819    }
1820
1821    #[test]
1822    fn test_yuv420_round_trip_limited_range() {
1823        fn matrix(yuv_accuracy: YuvConversionMode, max_diff: i32) {
1824            let image_width = 256usize;
1825            let image_height = 256usize;
1826
1827            let random_point_x = rand::rng().random_range(0..image_width);
1828            let random_point_y = rand::rng().random_range(0..image_height);
1829
1830            const CHANNELS: usize = 3;
1831
1832            let pixel_points = [
1833                [0, 0],
1834                [image_width - 1, image_height - 1],
1835                [image_width - 1, 0],
1836                [0, image_height - 1],
1837                [(image_width - 1) / 2, (image_height - 1) / 2],
1838                [image_width / 5, image_height / 5],
1839                [0, image_height / 5],
1840                [image_width / 5, 0],
1841                [image_width / 5 * 3, image_height / 5],
1842                [image_width / 5 * 3, image_height / 5 * 3],
1843                [image_width / 5, image_height / 5 * 3],
1844                [random_point_x, random_point_y],
1845            ];
1846
1847            let mut source_rgb = vec![0u8; image_width * image_height * CHANNELS];
1848
1849            let or = rand::rng().random_range(0..256) as u8;
1850            let og = rand::rng().random_range(0..256) as u8;
1851            let ob = rand::rng().random_range(0..256) as u8;
1852
1853            for point in &pixel_points {
1854                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1855                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1856                source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1857
1858                let nx = (point[0] + 1).min(image_width - 1);
1859                let ny = point[1].min(image_height - 1);
1860
1861                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1862                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1863                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1864
1865                let nx = (point[0] + 1).min(image_width - 1);
1866                let ny = (point[1] + 1).min(image_height - 1);
1867
1868                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1869                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1870                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1871
1872                let nx = point[0].min(image_width - 1);
1873                let ny = (point[1] + 1).min(image_height - 1);
1874
1875                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1876                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1877                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1878
1879                let nx = point[0].saturating_sub(1).min(image_width - 1);
1880                let ny = point[1].saturating_sub(1).min(image_height - 1);
1881
1882                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1883                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1884                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1885
1886                let nx = point[0].min(image_width - 1);
1887                let ny = point[1].saturating_sub(1).min(image_height - 1);
1888
1889                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1890                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1891                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1892
1893                let nx = point[0].saturating_sub(1).min(image_width - 1);
1894                let ny = point[1].min(image_height - 1);
1895
1896                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1897                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1898                source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1899            }
1900
1901            let mut planar_image = YuvPlanarImageMut::<u8>::alloc(
1902                image_width as u32,
1903                image_height as u32,
1904                YuvChromaSubsampling::Yuv420,
1905            );
1906
1907            rgb_to_yuv420(
1908                &mut planar_image,
1909                &source_rgb,
1910                image_width as u32 * 3,
1911                YuvRange::Limited,
1912                YuvStandardMatrix::Bt709,
1913                yuv_accuracy,
1914            )
1915            .unwrap();
1916
1917            let mut dest_rgb = vec![0u8; image_width * image_height * CHANNELS];
1918
1919            let fixed_planar = planar_image.to_fixed();
1920
1921            yuv420_to_rgb(
1922                &fixed_planar,
1923                &mut dest_rgb,
1924                image_width as u32 * 3,
1925                YuvRange::Limited,
1926                YuvStandardMatrix::Bt709,
1927            )
1928            .unwrap();
1929
1930            for point in &pixel_points {
1931                let x = point[0];
1932                let y = point[1];
1933                let px = x * CHANNELS + y * image_width * CHANNELS;
1934
1935                let r = dest_rgb[px];
1936                let g = dest_rgb[px + 1];
1937                let b = dest_rgb[px + 2];
1938
1939                let diff_r = r as i32 - or as i32;
1940                let diff_g = g as i32 - og as i32;
1941                let diff_b = b as i32 - ob as i32;
1942
1943                assert!(
1944                    diff_r <= max_diff,
1945                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1946                    yuv_accuracy,
1947                    diff_r,
1948                    [or, og, ob],
1949                    [r, g, b]
1950                );
1951                assert!(
1952                    diff_g <= max_diff,
1953                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1954                    yuv_accuracy,
1955                    diff_g,
1956                    [or, og, ob],
1957                    [r, g, b]
1958                );
1959                assert!(
1960                    diff_b <= max_diff,
1961                    "Matrix {}, Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1962                    yuv_accuracy,
1963                    diff_b,
1964                    [or, og, ob],
1965                    [r, g, b]
1966                );
1967            }
1968        }
1969        matrix(YuvConversionMode::Balanced, 82);
1970        #[cfg(feature = "fast_mode")]
1971        matrix(YuvConversionMode::Fast, 78);
1972        #[cfg(feature = "professional_mode")]
1973        matrix(YuvConversionMode::Professional, 74);
1974    }
1975}