yuvutils_rs/
yuv_p16_rgba_p16.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 */
29#[cfg(all(
30    any(target_arch = "x86", target_arch = "x86_64"),
31    feature = "nightly_avx512"
32))]
33use crate::avx512bw::avx512_yuv_p16_to_rgba16_row;
34#[allow(unused_imports)]
35use crate::internals::ProcessedOffset;
36use crate::internals::WideRowInversionHandler;
37#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
38use crate::neon::neon_yuv_p16_to_rgba16_row;
39use crate::numerics::{qrshr, to_ne};
40use crate::yuv_error::check_rgba_destination;
41use crate::yuv_support::{
42    get_yuv_range, search_inverse_transform, CbCrInverseTransform, YuvBytesPacking, YuvChromaRange,
43    YuvChromaSubsampling, YuvEndianness, YuvRange, YuvSourceChannels, YuvStandardMatrix,
44};
45use crate::{YuvError, YuvPlanarImage};
46#[cfg(feature = "rayon")]
47use rayon::iter::{IndexedParallelIterator, ParallelIterator};
48#[cfg(feature = "rayon")]
49use rayon::prelude::{ParallelSlice, ParallelSliceMut};
50
51struct WideRowAnyHandler<
52    const DESTINATION_CHANNELS: u8,
53    const SAMPLING: u8,
54    const ENDIANNESS: u8,
55    const BYTES_POSITION: u8,
56    const PRECISION: i32,
57    const BIT_DEPTH: usize,
58> {
59    handler: Option<
60        unsafe fn(
61            y_ld_ptr: &[u16],
62            u_ld_ptr: &[u16],
63            v_ld_ptr: &[u16],
64            rgba: &mut [u16],
65            width: u32,
66            range: &YuvChromaRange,
67            transform: &CbCrInverseTransform<i32>,
68            start_cx: usize,
69            start_ux: usize,
70        ) -> ProcessedOffset,
71    >,
72}
73
74impl<
75        const DESTINATION_CHANNELS: u8,
76        const SAMPLING: u8,
77        const ENDIANNESS: u8,
78        const BYTES_POSITION: u8,
79        const PRECISION: i32,
80        const BIT_DEPTH: usize,
81    > Default
82    for WideRowAnyHandler<
83        DESTINATION_CHANNELS,
84        SAMPLING,
85        ENDIANNESS,
86        BYTES_POSITION,
87        PRECISION,
88        BIT_DEPTH,
89    >
90{
91    fn default() -> WideRowAnyHandler<
92        DESTINATION_CHANNELS,
93        SAMPLING,
94        ENDIANNESS,
95        BYTES_POSITION,
96        PRECISION,
97        BIT_DEPTH,
98    > {
99        #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
100        {
101            #[cfg(feature = "rdm")]
102            {
103                let is_rdm_available = std::arch::is_aarch64_feature_detected!("rdm");
104                if is_rdm_available && BIT_DEPTH == 10 {
105                    use crate::neon::neon_yuv_p16_to_rgba16_row_rdm;
106                    return WideRowAnyHandler {
107                        handler: Some(
108                            neon_yuv_p16_to_rgba16_row_rdm::<
109                                DESTINATION_CHANNELS,
110                                SAMPLING,
111                                ENDIANNESS,
112                                BYTES_POSITION,
113                                PRECISION,
114                                BIT_DEPTH,
115                            >,
116                        ),
117                    };
118                }
119            }
120            if BIT_DEPTH <= 16 {
121                return WideRowAnyHandler {
122                    handler: Some(
123                        neon_yuv_p16_to_rgba16_row::<
124                            DESTINATION_CHANNELS,
125                            SAMPLING,
126                            ENDIANNESS,
127                            BYTES_POSITION,
128                            PRECISION,
129                            BIT_DEPTH,
130                        >,
131                    ),
132                };
133            }
134        }
135        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
136        {
137            #[cfg(feature = "sse")]
138            let use_sse = std::arch::is_x86_feature_detected!("sse4.1");
139            #[cfg(feature = "avx")]
140            let use_avx = std::arch::is_x86_feature_detected!("avx2");
141            #[cfg(feature = "nightly_avx512")]
142            let use_avx512 = std::arch::is_x86_feature_detected!("avx512bw");
143            #[cfg(feature = "nightly_avx512")]
144            if use_avx512 && BIT_DEPTH <= 12 {
145                return WideRowAnyHandler {
146                    handler: Some(
147                        avx512_yuv_p16_to_rgba16_row::<
148                            DESTINATION_CHANNELS,
149                            SAMPLING,
150                            ENDIANNESS,
151                            BYTES_POSITION,
152                            BIT_DEPTH,
153                            PRECISION,
154                        >,
155                    ),
156                };
157            }
158            #[cfg(feature = "avx")]
159            {
160                if use_avx {
161                    if BIT_DEPTH == 10 {
162                        assert_eq!(BIT_DEPTH, 10);
163                        use crate::avx2::avx_yuv_p16_to_rgba_row;
164                        return WideRowAnyHandler {
165                            handler: Some(
166                                avx_yuv_p16_to_rgba_row::<
167                                    DESTINATION_CHANNELS,
168                                    SAMPLING,
169                                    ENDIANNESS,
170                                    BYTES_POSITION,
171                                    BIT_DEPTH,
172                                    PRECISION,
173                                >,
174                            ),
175                        };
176                    } else {
177                        use crate::avx2::avx_yuv_p16_to_rgba_d16_row;
178                        return WideRowAnyHandler {
179                            handler: Some(
180                                avx_yuv_p16_to_rgba_d16_row::<
181                                    DESTINATION_CHANNELS,
182                                    SAMPLING,
183                                    ENDIANNESS,
184                                    BYTES_POSITION,
185                                    BIT_DEPTH,
186                                    PRECISION,
187                                >,
188                            ),
189                        };
190                    }
191                }
192            }
193            #[cfg(feature = "sse")]
194            if use_sse && BIT_DEPTH <= 12 {
195                use crate::sse::sse_yuv_p16_to_rgba_row;
196                return WideRowAnyHandler {
197                    handler: Some(
198                        sse_yuv_p16_to_rgba_row::<
199                            DESTINATION_CHANNELS,
200                            SAMPLING,
201                            ENDIANNESS,
202                            BYTES_POSITION,
203                            BIT_DEPTH,
204                            PRECISION,
205                        >,
206                    ),
207                };
208            }
209        }
210        WideRowAnyHandler { handler: None }
211    }
212}
213
214impl<
215        const DESTINATION_CHANNELS: u8,
216        const SAMPLING: u8,
217        const ENDIANNESS: u8,
218        const BYTES_POSITION: u8,
219        const PRECISION: i32,
220        const BIT_DEPTH: usize,
221    > WideRowInversionHandler<u16, i32>
222    for WideRowAnyHandler<
223        DESTINATION_CHANNELS,
224        SAMPLING,
225        ENDIANNESS,
226        BYTES_POSITION,
227        PRECISION,
228        BIT_DEPTH,
229    >
230{
231    #[inline]
232    fn handle_row(
233        &self,
234        y_plane: &[u16],
235        u_plane: &[u16],
236        v_plane: &[u16],
237        rgba: &mut [u16],
238        width: u32,
239        yuv_chroma_range: YuvChromaRange,
240        transform: &CbCrInverseTransform<i32>,
241    ) -> ProcessedOffset {
242        if let Some(handler) = self.handler {
243            unsafe {
244                return handler(
245                    y_plane,
246                    u_plane,
247                    v_plane,
248                    rgba,
249                    width,
250                    &yuv_chroma_range,
251                    transform,
252                    0,
253                    0,
254                );
255            }
256        }
257        ProcessedOffset { cx: 0, ux: 0 }
258    }
259}
260
261fn yuv_p16_to_image_p16_ant<
262    const DESTINATION_CHANNELS: u8,
263    const SAMPLING: u8,
264    const ENDIANNESS: u8,
265    const BYTES_POSITION: u8,
266    const BIT_DEPTH: usize,
267>(
268    image: &YuvPlanarImage<u16>,
269    rgba16: &mut [u16],
270    rgba_stride: u32,
271    range: YuvRange,
272    matrix: YuvStandardMatrix,
273) -> Result<(), YuvError> {
274    let dst_chans: YuvSourceChannels = DESTINATION_CHANNELS.into();
275    let channels = dst_chans.get_channels_count();
276
277    let chroma_subsampling: YuvChromaSubsampling = SAMPLING.into();
278    let chroma_range = get_yuv_range(BIT_DEPTH as u32, range);
279
280    image.check_constraints(chroma_subsampling)?;
281    check_rgba_destination(rgba16, rgba_stride, image.width, image.height, channels)?;
282
283    let kr_kb = matrix.get_kr_kb();
284    let max_range_p16 = ((1u32 << BIT_DEPTH as u32) - 1) as i32;
285    const PRECISION: i32 = 13;
286
287    let i_transform = search_inverse_transform(
288        PRECISION,
289        BIT_DEPTH as u32,
290        range,
291        matrix,
292        chroma_range,
293        kr_kb,
294    );
295
296    let cr_coef = i_transform.cr_coef;
297    let cb_coef = i_transform.cb_coef;
298    let y_coef = i_transform.y_coef;
299    let g_coef_1 = i_transform.g_coeff_1;
300    let g_coef_2 = i_transform.g_coeff_2;
301
302    let bias_y = chroma_range.bias_y as i32;
303    let bias_uv = chroma_range.bias_uv as i32;
304
305    let msb_shift = (16 - BIT_DEPTH) as i32;
306    let wide_row_handler = WideRowAnyHandler::<
307        DESTINATION_CHANNELS,
308        SAMPLING,
309        ENDIANNESS,
310        BYTES_POSITION,
311        PRECISION,
312        BIT_DEPTH,
313    >::default();
314
315    let process_halved_chroma_row = |y_plane: &[u16],
316                                     u_plane: &[u16],
317                                     v_plane: &[u16],
318                                     rgba: &mut [u16]| {
319        let cx = wide_row_handler
320            .handle_row(
321                y_plane,
322                u_plane,
323                v_plane,
324                rgba,
325                image.width,
326                chroma_range,
327                &i_transform,
328            )
329            .cx;
330        if cx != image.width as usize {
331            for (((rgba, y_src), &u_src), &v_src) in rgba
332                .chunks_exact_mut(channels * 2)
333                .zip(y_plane.chunks_exact(2))
334                .zip(u_plane.iter())
335                .zip(v_plane.iter())
336                .skip(cx / 2)
337            {
338                let y_value0 = (to_ne::<ENDIANNESS, BYTES_POSITION>(y_src[0], msb_shift) as i32
339                    - bias_y)
340                    * y_coef;
341                let cb_value =
342                    to_ne::<ENDIANNESS, BYTES_POSITION>(u_src, msb_shift) as i32 - bias_uv;
343                let cr_value =
344                    to_ne::<ENDIANNESS, BYTES_POSITION>(v_src, msb_shift) as i32 - bias_uv;
345
346                let r0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cr_coef * cr_value);
347                let b0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cb_coef * cb_value);
348                let g0 = qrshr::<PRECISION, BIT_DEPTH>(
349                    y_value0 - g_coef_1 * cr_value - g_coef_2 * cb_value,
350                );
351
352                let rgba0 = &mut rgba[0..channels];
353
354                rgba0[dst_chans.get_r_channel_offset()] = r0 as u16;
355                rgba0[dst_chans.get_g_channel_offset()] = g0 as u16;
356                rgba0[dst_chans.get_b_channel_offset()] = b0 as u16;
357                if dst_chans.has_alpha() {
358                    rgba0[dst_chans.get_a_channel_offset()] = max_range_p16 as u16;
359                }
360
361                let y_value1 = (to_ne::<ENDIANNESS, BYTES_POSITION>(y_src[1], msb_shift) as i32
362                    - bias_y)
363                    * y_coef;
364
365                let r1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cr_coef * cr_value);
366                let b1 = qrshr::<PRECISION, BIT_DEPTH>(y_value1 + cb_coef * cb_value);
367                let g1 = qrshr::<PRECISION, BIT_DEPTH>(
368                    y_value1 - g_coef_1 * cr_value - g_coef_2 * cb_value,
369                );
370
371                let rgba1 = &mut rgba[channels..channels * 2];
372
373                rgba1[dst_chans.get_r_channel_offset()] = r1 as u16;
374                rgba1[dst_chans.get_g_channel_offset()] = g1 as u16;
375                rgba1[dst_chans.get_b_channel_offset()] = b1 as u16;
376                if dst_chans.has_alpha() {
377                    rgba1[dst_chans.get_a_channel_offset()] = max_range_p16 as u16;
378                }
379            }
380
381            if image.width & 1 != 0 {
382                let y_value0 =
383                    (to_ne::<ENDIANNESS, BYTES_POSITION>(*y_plane.last().unwrap(), msb_shift)
384                        as i32
385                        - bias_y)
386                        * y_coef;
387                let cb_value =
388                    to_ne::<ENDIANNESS, BYTES_POSITION>(*u_plane.last().unwrap(), msb_shift) as i32
389                        - bias_uv;
390                let cr_value =
391                    to_ne::<ENDIANNESS, BYTES_POSITION>(*v_plane.last().unwrap(), msb_shift) as i32
392                        - bias_uv;
393                let rgba = rgba.chunks_exact_mut(channels).last().unwrap();
394                let rgba0 = &mut rgba[0..channels];
395
396                let r0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cr_coef * cr_value);
397                let b0 = qrshr::<PRECISION, BIT_DEPTH>(y_value0 + cb_coef * cb_value);
398                let g0 = qrshr::<PRECISION, BIT_DEPTH>(
399                    y_value0 - g_coef_1 * cr_value - g_coef_2 * cb_value,
400                );
401                rgba0[dst_chans.get_r_channel_offset()] = r0 as u16;
402                rgba0[dst_chans.get_g_channel_offset()] = g0 as u16;
403                rgba0[dst_chans.get_b_channel_offset()] = b0 as u16;
404                if dst_chans.has_alpha() {
405                    rgba0[dst_chans.get_a_channel_offset()] = max_range_p16 as u16;
406                }
407            }
408        }
409    };
410
411    if chroma_subsampling == YuvChromaSubsampling::Yuv444 {
412        let iter;
413        #[cfg(feature = "rayon")]
414        {
415            iter = rgba16
416                .par_chunks_exact_mut(rgba_stride as usize)
417                .zip(image.y_plane.par_chunks_exact(image.y_stride as usize))
418                .zip(image.u_plane.par_chunks_exact(image.u_stride as usize))
419                .zip(image.v_plane.par_chunks_exact(image.v_stride as usize));
420        }
421        #[cfg(not(feature = "rayon"))]
422        {
423            iter = rgba16
424                .chunks_exact_mut(rgba_stride as usize)
425                .zip(image.y_plane.chunks_exact(image.y_stride as usize))
426                .zip(image.u_plane.chunks_exact(image.u_stride as usize))
427                .zip(image.v_plane.chunks_exact(image.v_stride as usize));
428        }
429        iter.for_each(|(((rgba, y_plane), u_plane), v_plane)| {
430            let y_plane = &y_plane[0..image.width as usize];
431            let cx = wide_row_handler
432                .handle_row(
433                    y_plane,
434                    u_plane,
435                    v_plane,
436                    rgba,
437                    image.width,
438                    chroma_range,
439                    &i_transform,
440                )
441                .cx;
442            if cx != image.width as usize {
443                for (((rgba, &y_src), &u_src), &v_src) in rgba
444                    .chunks_exact_mut(channels)
445                    .zip(y_plane.iter())
446                    .zip(u_plane.iter())
447                    .zip(v_plane.iter())
448                    .skip(cx)
449                {
450                    let y_value = (to_ne::<ENDIANNESS, BYTES_POSITION>(y_src, msb_shift) as i32
451                        - bias_y)
452                        * y_coef;
453                    let cb_value =
454                        to_ne::<ENDIANNESS, BYTES_POSITION>(u_src, msb_shift) as i32 - bias_uv;
455                    let cr_value =
456                        to_ne::<ENDIANNESS, BYTES_POSITION>(v_src, msb_shift) as i32 - bias_uv;
457
458                    let r = qrshr::<PRECISION, BIT_DEPTH>(y_value + cr_coef * cr_value);
459                    let b = qrshr::<PRECISION, BIT_DEPTH>(y_value + cb_coef * cb_value);
460                    let g = qrshr::<PRECISION, BIT_DEPTH>(
461                        y_value - g_coef_1 * cr_value - g_coef_2 * cb_value,
462                    );
463
464                    rgba[dst_chans.get_r_channel_offset()] = r as u16;
465                    rgba[dst_chans.get_g_channel_offset()] = g as u16;
466                    rgba[dst_chans.get_b_channel_offset()] = b as u16;
467                    if dst_chans.has_alpha() {
468                        rgba[dst_chans.get_a_channel_offset()] = max_range_p16 as u16;
469                    }
470                }
471            }
472        });
473    } else if chroma_subsampling == YuvChromaSubsampling::Yuv422 {
474        let iter;
475        #[cfg(feature = "rayon")]
476        {
477            iter = rgba16
478                .par_chunks_exact_mut(rgba_stride as usize)
479                .zip(image.y_plane.par_chunks_exact(image.y_stride as usize))
480                .zip(image.u_plane.par_chunks_exact(image.u_stride as usize))
481                .zip(image.v_plane.par_chunks_exact(image.v_stride as usize));
482        }
483        #[cfg(not(feature = "rayon"))]
484        {
485            iter = rgba16
486                .chunks_exact_mut(rgba_stride as usize)
487                .zip(image.y_plane.chunks_exact(image.y_stride as usize))
488                .zip(image.u_plane.chunks_exact(image.u_stride as usize))
489                .zip(image.v_plane.chunks_exact(image.v_stride as usize));
490        }
491        iter.for_each(|(((rgba, y_plane), u_plane), v_plane)| {
492            process_halved_chroma_row(
493                &y_plane[0..image.width as usize],
494                &u_plane[0..(image.width as usize).div_ceil(2)],
495                &v_plane[0..(image.width as usize).div_ceil(2)],
496                &mut rgba[0..image.width as usize * channels],
497            );
498        });
499    } else if chroma_subsampling == YuvChromaSubsampling::Yuv420 {
500        let iter;
501        #[cfg(feature = "rayon")]
502        {
503            iter = rgba16
504                .par_chunks_exact_mut(rgba_stride as usize * 2)
505                .zip(image.y_plane.par_chunks_exact(image.y_stride as usize * 2))
506                .zip(image.u_plane.par_chunks_exact(image.u_stride as usize))
507                .zip(image.v_plane.par_chunks_exact(image.v_stride as usize));
508        }
509        #[cfg(not(feature = "rayon"))]
510        {
511            iter = rgba16
512                .chunks_exact_mut(rgba_stride as usize * 2)
513                .zip(image.y_plane.chunks_exact(image.y_stride as usize * 2))
514                .zip(image.u_plane.chunks_exact(image.u_stride as usize))
515                .zip(image.v_plane.chunks_exact(image.v_stride as usize));
516        }
517        iter.for_each(|(((rgba, y_plane), u_plane), v_plane)| {
518            for (rgba, y_plane) in rgba
519                .chunks_exact_mut(rgba_stride as usize)
520                .zip(y_plane.chunks_exact(image.y_stride as usize))
521            {
522                process_halved_chroma_row(
523                    &y_plane[0..image.width as usize],
524                    &u_plane[0..(image.width as usize).div_ceil(2)],
525                    &v_plane[0..(image.width as usize).div_ceil(2)],
526                    &mut rgba[0..image.width as usize * channels],
527                );
528            }
529        });
530
531        if image.height & 1 != 0 {
532            let rgba = rgba16
533                .chunks_exact_mut(rgba_stride as usize)
534                .last()
535                .unwrap();
536            let u_plane = image
537                .u_plane
538                .chunks_exact(image.u_stride as usize)
539                .last()
540                .unwrap();
541            let v_plane = image
542                .v_plane
543                .chunks_exact(image.v_stride as usize)
544                .last()
545                .unwrap();
546            let y_plane = image
547                .y_plane
548                .chunks_exact(image.y_stride as usize)
549                .last()
550                .unwrap();
551            process_halved_chroma_row(
552                &y_plane[0..image.width as usize],
553                &u_plane[0..(image.width as usize).div_ceil(2)],
554                &v_plane[0..(image.width as usize).div_ceil(2)],
555                &mut rgba[0..image.width as usize * channels],
556            );
557        }
558    } else {
559        unreachable!();
560    }
561
562    Ok(())
563}
564
565macro_rules! d_cnv {
566    ($method: ident, $px_fmt: expr, $sampling: expr, $endian: expr, $sampling_written: expr, $px_written: expr, $px_written_small: expr, $bit_depth: expr) => {
567        #[doc = concat!("
568Convert ",$sampling_written, " planar format with ", stringify!($bit_depth), " bit pixel format to ", $px_written," ", stringify!($bit_depth), " bit-depth format.
569
570This function takes ", $sampling_written, " planar data with ", stringify!($bit_depth), " bit precision.
571and converts it to ", $px_written," format with ", stringify!($bit_depth), " bit-depth precision per channel
572
573# Arguments
574
575* `planar_image` - Source ",$sampling_written," planar image.
576* `", $px_written_small, "` - A mutable slice to store the converted ", $px_written," ", stringify!($bit_depth), " bit-depth data.
577* `", $px_written_small, "_stride` - The stride (components per row) for ", $px_written," ", stringify!($bit_depth), " bit-depth data.
578* `range` - The YUV range (limited or full).
579* `matrix` - The YUV standard matrix (BT.601 or BT.709 or BT.2020 or other).
580* `endianness` - The endianness of stored bytes
581* `bytes_packing` - see [YuvBytesPacking] for more info.
582* `bit_depth` - Bit depth of source YUV planes, only 10 and 12 is supported.
583
584# Panics
585
586This function panics if the lengths of the planes or the input ", $px_written," data are not valid based
587on the specified width, height, and strides, or if invalid YUV range or matrix is provided.")]
588        pub fn $method(
589            planar_image: &YuvPlanarImage<u16>,
590            dst: &mut [u16],
591            dst_stride: u32,
592            range: YuvRange,
593            matrix: YuvStandardMatrix,
594        ) -> Result<(), YuvError> {
595            yuv_p16_to_image_p16_ant::<{ $px_fmt as u8 },
596                            { $sampling as u8 },
597                            { $endian as u8 },
598                            { YuvBytesPacking::LeastSignificantBytes as u8 }, $bit_depth>(
599                planar_image, dst, dst_stride, range, matrix)
600        }
601    };
602}
603
604d_cnv!(
605    i010_to_rgba10,
606    YuvSourceChannels::Rgba,
607    YuvChromaSubsampling::Yuv420,
608    YuvEndianness::LittleEndian,
609    "I010",
610    "RGBA",
611    "rgba",
612    10
613);
614#[cfg(feature = "big_endian")]
615d_cnv!(
616    i010_be_to_rgba10,
617    YuvSourceChannels::Rgba,
618    YuvChromaSubsampling::Yuv420,
619    YuvEndianness::BigEndian,
620    "I010BE",
621    "RGBA",
622    "rgba",
623    10
624);
625d_cnv!(
626    i010_to_rgb10,
627    YuvSourceChannels::Rgb,
628    YuvChromaSubsampling::Yuv420,
629    YuvEndianness::LittleEndian,
630    "I010",
631    "RGB",
632    "rgb",
633    10
634);
635#[cfg(feature = "big_endian")]
636d_cnv!(
637    i010_be_to_rgb10,
638    YuvSourceChannels::Rgb,
639    YuvChromaSubsampling::Yuv420,
640    YuvEndianness::BigEndian,
641    "I010BE",
642    "RGB",
643    "rgb",
644    10
645);
646d_cnv!(
647    i210_to_rgba10,
648    YuvSourceChannels::Rgba,
649    YuvChromaSubsampling::Yuv422,
650    YuvEndianness::LittleEndian,
651    "I210",
652    "RGBA",
653    "rgba",
654    10
655);
656#[cfg(feature = "big_endian")]
657d_cnv!(
658    i210_be_to_rgba10,
659    YuvSourceChannels::Rgba,
660    YuvChromaSubsampling::Yuv422,
661    YuvEndianness::BigEndian,
662    "I210BE",
663    "RGBA",
664    "rgba",
665    10
666);
667d_cnv!(
668    i210_to_rgb10,
669    YuvSourceChannels::Rgb,
670    YuvChromaSubsampling::Yuv422,
671    YuvEndianness::LittleEndian,
672    "I210",
673    "RGB",
674    "rgb",
675    10
676);
677#[cfg(feature = "big_endian")]
678d_cnv!(
679    i210_be_to_rgb10,
680    YuvSourceChannels::Rgb,
681    YuvChromaSubsampling::Yuv422,
682    YuvEndianness::BigEndian,
683    "I210BE",
684    "RGB",
685    "rgb",
686    10
687);
688d_cnv!(
689    i410_to_rgba10,
690    YuvSourceChannels::Rgba,
691    YuvChromaSubsampling::Yuv444,
692    YuvEndianness::LittleEndian,
693    "I410",
694    "RGBA",
695    "rgba",
696    10
697);
698#[cfg(feature = "big_endian")]
699d_cnv!(
700    i410_be_to_rgba10,
701    YuvSourceChannels::Rgba,
702    YuvChromaSubsampling::Yuv444,
703    YuvEndianness::BigEndian,
704    "I410BE",
705    "RGBA",
706    "rgba",
707    10
708);
709d_cnv!(
710    i410_to_rgb10,
711    YuvSourceChannels::Rgb,
712    YuvChromaSubsampling::Yuv444,
713    YuvEndianness::LittleEndian,
714    "I410",
715    "RGB",
716    "rgb",
717    10
718);
719#[cfg(feature = "big_endian")]
720d_cnv!(
721    i410_be_to_rgb10,
722    YuvSourceChannels::Rgb,
723    YuvChromaSubsampling::Yuv444,
724    YuvEndianness::BigEndian,
725    "I410BE",
726    "RGB",
727    "rgb",
728    10
729);
730
731d_cnv!(
732    i012_to_rgba12,
733    YuvSourceChannels::Rgba,
734    YuvChromaSubsampling::Yuv420,
735    YuvEndianness::LittleEndian,
736    "I012",
737    "RGBA",
738    "rgba",
739    12
740);
741#[cfg(feature = "big_endian")]
742d_cnv!(
743    i012_be_to_rgba12,
744    YuvSourceChannels::Rgba,
745    YuvChromaSubsampling::Yuv420,
746    YuvEndianness::BigEndian,
747    "I012BE",
748    "RGBA",
749    "rgba",
750    12
751);
752d_cnv!(
753    i012_to_rgb12,
754    YuvSourceChannels::Rgb,
755    YuvChromaSubsampling::Yuv420,
756    YuvEndianness::LittleEndian,
757    "I012",
758    "RGB",
759    "rgb",
760    12
761);
762#[cfg(feature = "big_endian")]
763d_cnv!(
764    i012_be_to_rgb12,
765    YuvSourceChannels::Rgb,
766    YuvChromaSubsampling::Yuv420,
767    YuvEndianness::BigEndian,
768    "I012BE",
769    "RGB",
770    "rgb",
771    12
772);
773d_cnv!(
774    i212_to_rgba12,
775    YuvSourceChannels::Rgba,
776    YuvChromaSubsampling::Yuv422,
777    YuvEndianness::LittleEndian,
778    "I212",
779    "RGBA",
780    "rgba",
781    12
782);
783#[cfg(feature = "big_endian")]
784d_cnv!(
785    i212_be_to_rgba12,
786    YuvSourceChannels::Rgba,
787    YuvChromaSubsampling::Yuv422,
788    YuvEndianness::BigEndian,
789    "I212BE",
790    "RGBA",
791    "rgba",
792    12
793);
794d_cnv!(
795    i212_to_rgb12,
796    YuvSourceChannels::Rgb,
797    YuvChromaSubsampling::Yuv422,
798    YuvEndianness::LittleEndian,
799    "I212",
800    "RGB",
801    "rgb",
802    12
803);
804#[cfg(feature = "big_endian")]
805d_cnv!(
806    i212_be_to_rgb12,
807    YuvSourceChannels::Rgb,
808    YuvChromaSubsampling::Yuv422,
809    YuvEndianness::BigEndian,
810    "I212BE",
811    "RGB",
812    "rgb",
813    12
814);
815d_cnv!(
816    i412_to_rgba12,
817    YuvSourceChannels::Rgba,
818    YuvChromaSubsampling::Yuv444,
819    YuvEndianness::LittleEndian,
820    "I412",
821    "RGBA",
822    "rgba",
823    12
824);
825#[cfg(feature = "big_endian")]
826d_cnv!(
827    i412_be_to_rgba12,
828    YuvSourceChannels::Rgba,
829    YuvChromaSubsampling::Yuv444,
830    YuvEndianness::BigEndian,
831    "I412BE",
832    "RGBA",
833    "rgba",
834    12
835);
836d_cnv!(
837    i412_to_rgb12,
838    YuvSourceChannels::Rgb,
839    YuvChromaSubsampling::Yuv444,
840    YuvEndianness::LittleEndian,
841    "I412",
842    "RGB",
843    "rgb",
844    12
845);
846#[cfg(feature = "big_endian")]
847d_cnv!(
848    i412_be_to_rgb12,
849    YuvSourceChannels::Rgb,
850    YuvChromaSubsampling::Yuv444,
851    YuvEndianness::BigEndian,
852    "I412BE",
853    "RGB",
854    "rgb",
855    12
856);
857
858// 4:2:0, 14 bit
859
860d_cnv!(
861    i014_to_rgba14,
862    YuvSourceChannels::Rgba,
863    YuvChromaSubsampling::Yuv420,
864    YuvEndianness::LittleEndian,
865    "I014",
866    "RGBA",
867    "rgba",
868    14
869);
870#[cfg(feature = "big_endian")]
871d_cnv!(
872    i014_be_to_rgba14,
873    YuvSourceChannels::Rgba,
874    YuvChromaSubsampling::Yuv420,
875    YuvEndianness::BigEndian,
876    "I014BE",
877    "RGBA",
878    "rgba",
879    14
880);
881d_cnv!(
882    i014_to_rgb14,
883    YuvSourceChannels::Rgb,
884    YuvChromaSubsampling::Yuv420,
885    YuvEndianness::LittleEndian,
886    "I014",
887    "RGB",
888    "rgb",
889    14
890);
891#[cfg(feature = "big_endian")]
892d_cnv!(
893    i014_be_to_rgb14,
894    YuvSourceChannels::Rgb,
895    YuvChromaSubsampling::Yuv420,
896    YuvEndianness::BigEndian,
897    "I014BE",
898    "RGB",
899    "rgb",
900    14
901);
902
903// 14-bit, 4:2:2
904
905d_cnv!(
906    i214_to_rgba14,
907    YuvSourceChannels::Rgba,
908    YuvChromaSubsampling::Yuv422,
909    YuvEndianness::LittleEndian,
910    "I214",
911    "RGBA",
912    "rgba",
913    14
914);
915#[cfg(feature = "big_endian")]
916d_cnv!(
917    i214_be_to_rgba14,
918    YuvSourceChannels::Rgba,
919    YuvChromaSubsampling::Yuv422,
920    YuvEndianness::BigEndian,
921    "I214BE",
922    "RGBA",
923    "rgba",
924    14
925);
926d_cnv!(
927    i214_to_rgb14,
928    YuvSourceChannels::Rgb,
929    YuvChromaSubsampling::Yuv422,
930    YuvEndianness::LittleEndian,
931    "I214",
932    "RGB",
933    "rgb",
934    14
935);
936#[cfg(feature = "big_endian")]
937d_cnv!(
938    i214_be_to_rgb14,
939    YuvSourceChannels::Rgb,
940    YuvChromaSubsampling::Yuv422,
941    YuvEndianness::BigEndian,
942    "I214BE",
943    "RGB",
944    "rgb",
945    14
946);
947
948// 14-bit, 4:4:4
949
950d_cnv!(
951    i414_to_rgba14,
952    YuvSourceChannels::Rgba,
953    YuvChromaSubsampling::Yuv444,
954    YuvEndianness::LittleEndian,
955    "I414",
956    "RGBA",
957    "rgba",
958    14
959);
960#[cfg(feature = "big_endian")]
961d_cnv!(
962    i414_be_to_rgba14,
963    YuvSourceChannels::Rgba,
964    YuvChromaSubsampling::Yuv444,
965    YuvEndianness::BigEndian,
966    "I414BE",
967    "RGBA",
968    "rgba",
969    14
970);
971d_cnv!(
972    i414_to_rgb14,
973    YuvSourceChannels::Rgb,
974    YuvChromaSubsampling::Yuv444,
975    YuvEndianness::LittleEndian,
976    "I414",
977    "RGB",
978    "rgb",
979    14
980);
981#[cfg(feature = "big_endian")]
982d_cnv!(
983    i414_be_to_rgb14,
984    YuvSourceChannels::Rgb,
985    YuvChromaSubsampling::Yuv444,
986    YuvEndianness::BigEndian,
987    "I414BE",
988    "RGB",
989    "rgb",
990    14
991);
992
993// 4:2:0, 16 bit
994
995d_cnv!(
996    i016_to_rgba16,
997    YuvSourceChannels::Rgba,
998    YuvChromaSubsampling::Yuv420,
999    YuvEndianness::LittleEndian,
1000    "I016",
1001    "RGBA",
1002    "rgba",
1003    16
1004);
1005#[cfg(feature = "big_endian")]
1006d_cnv!(
1007    i016_be_to_rgba16,
1008    YuvSourceChannels::Rgba,
1009    YuvChromaSubsampling::Yuv420,
1010    YuvEndianness::BigEndian,
1011    "I016BE",
1012    "RGBA",
1013    "rgba",
1014    16
1015);
1016d_cnv!(
1017    i016_to_rgb16,
1018    YuvSourceChannels::Rgb,
1019    YuvChromaSubsampling::Yuv420,
1020    YuvEndianness::LittleEndian,
1021    "I016",
1022    "RGB",
1023    "rgb",
1024    16
1025);
1026#[cfg(feature = "big_endian")]
1027d_cnv!(
1028    i016_be_to_rgb16,
1029    YuvSourceChannels::Rgb,
1030    YuvChromaSubsampling::Yuv420,
1031    YuvEndianness::BigEndian,
1032    "I016BE",
1033    "RGB",
1034    "rgb",
1035    16
1036);
1037
1038// 16-bit, 4:2:2
1039
1040d_cnv!(
1041    i216_to_rgba16,
1042    YuvSourceChannels::Rgba,
1043    YuvChromaSubsampling::Yuv422,
1044    YuvEndianness::LittleEndian,
1045    "I216",
1046    "RGBA",
1047    "rgba",
1048    16
1049);
1050#[cfg(feature = "big_endian")]
1051d_cnv!(
1052    i216_be_to_rgba16,
1053    YuvSourceChannels::Rgba,
1054    YuvChromaSubsampling::Yuv422,
1055    YuvEndianness::BigEndian,
1056    "I216BE",
1057    "RGBA",
1058    "rgba",
1059    16
1060);
1061d_cnv!(
1062    i216_to_rgb16,
1063    YuvSourceChannels::Rgb,
1064    YuvChromaSubsampling::Yuv422,
1065    YuvEndianness::LittleEndian,
1066    "I216",
1067    "RGB",
1068    "rgb",
1069    16
1070);
1071#[cfg(feature = "big_endian")]
1072d_cnv!(
1073    i216_be_to_rgb16,
1074    YuvSourceChannels::Rgb,
1075    YuvChromaSubsampling::Yuv422,
1076    YuvEndianness::BigEndian,
1077    "I216BE",
1078    "RGB",
1079    "rgb",
1080    16
1081);
1082
1083// 16-bit, 4:4:4
1084
1085d_cnv!(
1086    i416_to_rgba16,
1087    YuvSourceChannels::Rgba,
1088    YuvChromaSubsampling::Yuv444,
1089    YuvEndianness::LittleEndian,
1090    "I416",
1091    "RGBA",
1092    "rgba",
1093    16
1094);
1095#[cfg(feature = "big_endian")]
1096d_cnv!(
1097    i416_be_to_rgba16,
1098    YuvSourceChannels::Rgba,
1099    YuvChromaSubsampling::Yuv444,
1100    YuvEndianness::BigEndian,
1101    "I416BE",
1102    "RGBA",
1103    "rgba",
1104    16
1105);
1106d_cnv!(
1107    i416_to_rgb16,
1108    YuvSourceChannels::Rgb,
1109    YuvChromaSubsampling::Yuv444,
1110    YuvEndianness::LittleEndian,
1111    "I416",
1112    "RGB",
1113    "rgb",
1114    16
1115);
1116#[cfg(feature = "big_endian")]
1117d_cnv!(
1118    i416_be_to_rgb16,
1119    YuvSourceChannels::Rgb,
1120    YuvChromaSubsampling::Yuv444,
1121    YuvEndianness::BigEndian,
1122    "I416BE",
1123    "RGB",
1124    "rgb",
1125    16
1126);
1127
1128#[cfg(test)]
1129mod tests {
1130    use super::*;
1131    use crate::{rgb10_to_i010, rgb10_to_i210, rgb10_to_i410, YuvPlanarImageMut};
1132    use rand::Rng;
1133
1134    #[test]
1135    fn test_yuv444_p16_round_trip_full_range() {
1136        let image_width = 256usize;
1137        let image_height = 256usize;
1138
1139        let random_point_x = rand::rng().random_range(0..image_width);
1140        let random_point_y = rand::rng().random_range(0..image_height);
1141
1142        const CHANNELS: usize = 3;
1143
1144        let pixel_points = [
1145            [0, 0],
1146            [image_width - 1, image_height - 1],
1147            [image_width - 1, 0],
1148            [0, image_height - 1],
1149            [(image_width - 1) / 2, (image_height - 1) / 2],
1150            [image_width / 5, image_height / 5],
1151            [0, image_height / 5],
1152            [image_width / 5, 0],
1153            [image_width / 5 * 3, image_height / 5],
1154            [image_width / 5 * 3, image_height / 5 * 3],
1155            [image_width / 5, image_height / 5 * 3],
1156            [random_point_x, random_point_y],
1157        ];
1158        let mut image_rgb = vec![0u16; image_width * image_height * 3];
1159
1160        let or = rand::rng().random_range(0..1024) as u16;
1161        let og = rand::rng().random_range(0..1024) as u16;
1162        let ob = rand::rng().random_range(0..1024) as u16;
1163
1164        for point in &pixel_points {
1165            image_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1166            image_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1167            image_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1168        }
1169
1170        let mut planar_image = YuvPlanarImageMut::<u16>::alloc(
1171            image_width as u32,
1172            image_height as u32,
1173            YuvChromaSubsampling::Yuv444,
1174        );
1175
1176        rgb10_to_i410(
1177            &mut planar_image,
1178            &image_rgb,
1179            image_width as u32 * CHANNELS as u32,
1180            YuvRange::Full,
1181            YuvStandardMatrix::Bt709,
1182        )
1183        .unwrap();
1184
1185        image_rgb.fill(0);
1186
1187        let fixed_planar = planar_image.to_fixed();
1188
1189        i410_to_rgb10(
1190            &fixed_planar,
1191            &mut image_rgb,
1192            image_width as u32 * CHANNELS as u32,
1193            YuvRange::Full,
1194            YuvStandardMatrix::Bt709,
1195        )
1196        .unwrap();
1197
1198        for point in &pixel_points {
1199            let x = point[0];
1200            let y = point[1];
1201            let r = image_rgb[x * CHANNELS + y * image_width * CHANNELS];
1202            let g = image_rgb[x * CHANNELS + y * image_width * CHANNELS + 1];
1203            let b = image_rgb[x * CHANNELS + y * image_width * CHANNELS + 2];
1204
1205            let diff_r = (r as i32 - or as i32).abs();
1206            let diff_g = (g as i32 - og as i32).abs();
1207            let diff_b = (b as i32 - ob as i32).abs();
1208
1209            assert!(
1210                diff_r <= 4,
1211                "Original RGB {:?}, Round-tripped RGB {:?}",
1212                [or, og, ob],
1213                [r, g, b]
1214            );
1215            assert!(
1216                diff_g <= 4,
1217                "Original RGB {:?}, Round-tripped RGB {:?}",
1218                [or, og, ob],
1219                [r, g, b]
1220            );
1221            assert!(
1222                diff_b <= 4,
1223                "Original RGB {:?}, Round-tripped RGB {:?}",
1224                [or, og, ob],
1225                [r, g, b]
1226            );
1227        }
1228    }
1229
1230    #[test]
1231    fn test_yuv444_p10_round_trip_limited_range() {
1232        let image_width = 256usize;
1233        let image_height = 256usize;
1234
1235        let random_point_x = rand::rng().random_range(0..image_width);
1236        let random_point_y = rand::rng().random_range(0..image_height);
1237
1238        const CHANNELS: usize = 3;
1239
1240        let pixel_points = [
1241            [0, 0],
1242            [image_width - 1, image_height - 1],
1243            [image_width - 1, 0],
1244            [0, image_height - 1],
1245            [(image_width - 1) / 2, (image_height - 1) / 2],
1246            [image_width / 5, image_height / 5],
1247            [0, image_height / 5],
1248            [image_width / 5, 0],
1249            [image_width / 5 * 3, image_height / 5],
1250            [image_width / 5 * 3, image_height / 5 * 3],
1251            [image_width / 5, image_height / 5 * 3],
1252            [random_point_x, random_point_y],
1253        ];
1254        let mut image_rgb = vec![0u16; image_width * image_height * 3];
1255
1256        let or = rand::rng().random_range(0..1024) as u16;
1257        let og = rand::rng().random_range(0..1024) as u16;
1258        let ob = rand::rng().random_range(0..1024) as u16;
1259
1260        for point in &pixel_points {
1261            image_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1262            image_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1263            image_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1264        }
1265
1266        let mut planar_image = YuvPlanarImageMut::<u16>::alloc(
1267            image_width as u32,
1268            image_height as u32,
1269            YuvChromaSubsampling::Yuv444,
1270        );
1271
1272        rgb10_to_i410(
1273            &mut planar_image,
1274            &image_rgb,
1275            image_width as u32 * CHANNELS as u32,
1276            YuvRange::Limited,
1277            YuvStandardMatrix::Bt709,
1278        )
1279        .unwrap();
1280
1281        image_rgb.fill(0);
1282
1283        let fixed_planar = planar_image.to_fixed();
1284
1285        i410_to_rgb10(
1286            &fixed_planar,
1287            &mut image_rgb,
1288            image_width as u32 * CHANNELS as u32,
1289            YuvRange::Limited,
1290            YuvStandardMatrix::Bt709,
1291        )
1292        .unwrap();
1293
1294        for point in &pixel_points {
1295            let x = point[0];
1296            let y = point[1];
1297            let r = image_rgb[x * CHANNELS + y * image_width * CHANNELS];
1298            let g = image_rgb[x * CHANNELS + y * image_width * CHANNELS + 1];
1299            let b = image_rgb[x * CHANNELS + y * image_width * CHANNELS + 2];
1300
1301            let diff_r = (r as i32 - or as i32).abs();
1302            let diff_g = (g as i32 - og as i32).abs();
1303            let diff_b = (b as i32 - ob as i32).abs();
1304
1305            assert!(
1306                diff_r <= 280,
1307                "Diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1308                diff_r,
1309                [or, og, ob],
1310                [r, g, b]
1311            );
1312            assert!(
1313                diff_g <= 280,
1314                "Diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1315                diff_g,
1316                [or, og, ob],
1317                [r, g, b]
1318            );
1319            assert!(
1320                diff_b <= 280,
1321                "Diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1322                diff_b,
1323                [or, og, ob],
1324                [r, g, b]
1325            );
1326        }
1327    }
1328
1329    #[test]
1330    fn test_yuv422_p16_round_trip_limited_range() {
1331        let image_width = 256usize;
1332        let image_height = 256usize;
1333
1334        let random_point_x = rand::rng().random_range(0..image_width);
1335        let random_point_y = rand::rng().random_range(0..image_height);
1336
1337        const CHANNELS: usize = 3;
1338
1339        let pixel_points = [
1340            [0, 0],
1341            [image_width - 1, image_height - 1],
1342            [image_width - 1, 0],
1343            [0, image_height - 1],
1344            [(image_width - 1) / 2, (image_height - 1) / 2],
1345            [image_width / 5, image_height / 5],
1346            [0, image_height / 5],
1347            [image_width / 5, 0],
1348            [image_width / 5 * 3, image_height / 5],
1349            [image_width / 5 * 3, image_height / 5 * 3],
1350            [image_width / 5, image_height / 5 * 3],
1351            [random_point_x, random_point_y],
1352        ];
1353
1354        let mut source_rgb = vec![0u16; image_width * image_height * CHANNELS];
1355
1356        let or = rand::rng().random_range(0..1024) as u16;
1357        let og = rand::rng().random_range(0..1024) as u16;
1358        let ob = rand::rng().random_range(0..1024) as u16;
1359
1360        for point in &pixel_points {
1361            source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1362            source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1363            source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1364
1365            let nx = (point[0] + 1).min(image_width - 1);
1366            let ny = point[1].min(image_height - 1);
1367
1368            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1369            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1370            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1371
1372            let nx = point[0].saturating_sub(1).min(image_width - 1);
1373            let ny = point[1].min(image_height - 1);
1374
1375            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1376            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1377            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1378        }
1379
1380        let mut planar_image = YuvPlanarImageMut::<u16>::alloc(
1381            image_width as u32,
1382            image_height as u32,
1383            YuvChromaSubsampling::Yuv422,
1384        );
1385
1386        rgb10_to_i210(
1387            &mut planar_image,
1388            &source_rgb,
1389            image_width as u32 * CHANNELS as u32,
1390            YuvRange::Limited,
1391            YuvStandardMatrix::Bt709,
1392        )
1393        .unwrap();
1394
1395        let mut dest_rgb = vec![0u16; image_width * image_height * CHANNELS];
1396
1397        let fixed_planar = planar_image.to_fixed();
1398
1399        i210_to_rgb10(
1400            &fixed_planar,
1401            &mut dest_rgb,
1402            image_width as u32 * CHANNELS as u32,
1403            YuvRange::Limited,
1404            YuvStandardMatrix::Bt709,
1405        )
1406        .unwrap();
1407
1408        for point in &pixel_points {
1409            let x = point[0];
1410            let y = point[1];
1411            let px = x * CHANNELS + y * image_width * CHANNELS;
1412
1413            let r = dest_rgb[px];
1414            let g = dest_rgb[px + 1];
1415            let b = dest_rgb[px + 2];
1416
1417            let diff_r = r as i32 - or as i32;
1418            let diff_g = g as i32 - og as i32;
1419            let diff_b = b as i32 - ob as i32;
1420
1421            assert!(
1422                diff_r <= 260,
1423                "Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1424                diff_r,
1425                [or, og, ob],
1426                [r, g, b]
1427            );
1428            assert!(
1429                diff_g <= 260,
1430                "Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1431                diff_g,
1432                [or, og, ob],
1433                [r, g, b]
1434            );
1435            assert!(
1436                diff_b <= 260,
1437                "Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1438                diff_b,
1439                [or, og, ob],
1440                [r, g, b]
1441            );
1442        }
1443    }
1444
1445    #[test]
1446    fn test_yuv420_p16_round_trip_limited_range() {
1447        let image_width = 256usize;
1448        let image_height = 256usize;
1449
1450        let random_point_x = rand::rng().random_range(0..image_width);
1451        let random_point_y = rand::rng().random_range(0..image_height);
1452
1453        const CHANNELS: usize = 3;
1454
1455        let pixel_points = [
1456            [0, 0],
1457            [image_width - 1, image_height - 1],
1458            [image_width - 1, 0],
1459            [0, image_height - 1],
1460            [(image_width - 1) / 2, (image_height - 1) / 2],
1461            [image_width / 5, image_height / 5],
1462            [0, image_height / 5],
1463            [image_width / 5, 0],
1464            [image_width / 5 * 3, image_height / 5],
1465            [image_width / 5 * 3, image_height / 5 * 3],
1466            [image_width / 5, image_height / 5 * 3],
1467            [random_point_x, random_point_y],
1468        ];
1469
1470        let mut source_rgb = vec![0u16; image_width * image_height * CHANNELS];
1471
1472        let or = rand::rng().random_range(0..1024) as u16;
1473        let og = rand::rng().random_range(0..1024) as u16;
1474        let ob = rand::rng().random_range(0..1024) as u16;
1475
1476        for point in &pixel_points {
1477            source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS] = or;
1478            source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 1] = og;
1479            source_rgb[point[0] * CHANNELS + point[1] * image_width * CHANNELS + 2] = ob;
1480
1481            let nx = (point[0] + 1).min(image_width - 1);
1482            let ny = point[1].min(image_height - 1);
1483
1484            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1485            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1486            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1487
1488            let nx = (point[0] + 1).min(image_width - 1);
1489            let ny = (point[1] + 1).min(image_height - 1);
1490
1491            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1492            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1493            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1494
1495            let nx = point[0].min(image_width - 1);
1496            let ny = (point[1] + 1).min(image_height - 1);
1497
1498            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1499            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1500            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1501
1502            let nx = point[0].saturating_sub(1).min(image_width - 1);
1503            let ny = point[1].saturating_sub(1).min(image_height - 1);
1504
1505            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1506            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1507            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1508
1509            let nx = point[0].min(image_width - 1);
1510            let ny = point[1].saturating_sub(1).min(image_height - 1);
1511
1512            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1513            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1514            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1515
1516            let nx = point[0].saturating_sub(1).min(image_width - 1);
1517            let ny = point[1].min(image_height - 1);
1518
1519            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS] = or;
1520            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 1] = og;
1521            source_rgb[nx * CHANNELS + ny * image_width * CHANNELS + 2] = ob;
1522        }
1523
1524        let mut planar_image = YuvPlanarImageMut::<u16>::alloc(
1525            image_width as u32,
1526            image_height as u32,
1527            YuvChromaSubsampling::Yuv420,
1528        );
1529
1530        rgb10_to_i010(
1531            &mut planar_image,
1532            &source_rgb,
1533            image_width as u32 * CHANNELS as u32,
1534            YuvRange::Limited,
1535            YuvStandardMatrix::Bt709,
1536        )
1537        .unwrap();
1538
1539        let mut dest_rgb = vec![0u16; image_width * image_height * CHANNELS];
1540
1541        let fixed_planar = planar_image.to_fixed();
1542
1543        i010_to_rgb10(
1544            &fixed_planar,
1545            &mut dest_rgb,
1546            image_width as u32 * CHANNELS as u32,
1547            YuvRange::Limited,
1548            YuvStandardMatrix::Bt709,
1549        )
1550        .unwrap();
1551
1552        for point in &pixel_points {
1553            let x = point[0];
1554            let y = point[1];
1555            let px = x * CHANNELS + y * image_width * CHANNELS;
1556
1557            let r = dest_rgb[px];
1558            let g = dest_rgb[px + 1];
1559            let b = dest_rgb[px + 2];
1560
1561            let diff_r = r as i32 - or as i32;
1562            let diff_g = g as i32 - og as i32;
1563            let diff_b = b as i32 - ob as i32;
1564
1565            assert!(
1566                diff_r <= 310,
1567                "Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1568                diff_r,
1569                [or, og, ob],
1570                [r, g, b]
1571            );
1572            assert!(
1573                diff_g <= 310,
1574                "Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1575                diff_g,
1576                [or, og, ob],
1577                [r, g, b]
1578            );
1579            assert!(
1580                diff_b <= 310,
1581                "Actual diff {}, Original RGB {:?}, Round-tripped RGB {:?}",
1582                diff_b,
1583                [or, og, ob],
1584                [r, g, b]
1585            );
1586        }
1587    }
1588}