sentryshot_convert/
lib.rs

1// SPDX-License-Identifier: MPL-2.0+
2
3mod yub2rgb;
4
5#[cfg(test)]
6mod test;
7
8pub use sentryshot_util::{pixfmt::PixelFormat, ColorRange, Frame};
9
10use crate::yub2rgb::yuv2rgb_get_func;
11use sentryshot_util::{pixfmt::PIX_FMT_FLAG_RGB, ResetBufferError};
12use std::{num::NonZeroU16, panic, sync::Mutex};
13use thiserror::Error;
14use yub2rgb::{yuv2rgb_init_tables, Yuv2RgbInitTablesError, FF_YUV2RGB_COEFFS};
15
16const SWS_CS_DEFAULT: usize = 5;
17
18pub(crate) const YUVRGB_TABLE_HEADROOM: u16 = 512;
19#[allow(clippy::as_conversions)]
20pub(crate) const YUVRGB_TABLE_HEADROOM_USIZE: usize = YUVRGB_TABLE_HEADROOM as usize;
21pub(crate) const YUVRGB_TABLE_SIZE: usize = 256 + 2 * YUVRGB_TABLE_HEADROOM_USIZE;
22pub(crate) const YUVRGB_TABLE_LUMA_HEADROOM: u16 = 512;
23#[allow(clippy::as_conversions)]
24pub(crate) const YUV_TABLE_SIZE: usize = 1024 + 2 * YUVRGB_TABLE_LUMA_HEADROOM as usize;
25
26pub(crate) type ConvertFunc = fn(
27    context: &PixelFormatConverter,
28    src: &[Vec<u8>],
29    src_stride: &mut [usize],
30    src_slice_h: i32,
31    dst: &mut [Vec<u8>],
32    dst_stride: &[usize],
33);
34
35// alignment ensures the offset can be added in a single
36// instruction on e.g. ARM
37// ALIGN = 16.
38#[repr(C, align(16))]
39pub(crate) struct YuvTables {
40    pub(crate) yuv_table: [u8; YUV_TABLE_SIZE],
41    pub(crate) gv: [i16; YUVRGB_TABLE_SIZE],
42    pub(crate) rv: [usize; YUVRGB_TABLE_SIZE],
43    pub(crate) gu: [usize; YUVRGB_TABLE_SIZE],
44    pub(crate) bu: [usize; YUVRGB_TABLE_SIZE],
45}
46
47// This struct should be aligned on at least a 32-byte boundary.
48#[repr(C, align(32))]
49pub struct PixelFormatConverter {
50    pub(crate) convert: ConvertFunc,
51
52    pub(crate) width: NonZeroU16,
53    pub(crate) height: NonZeroU16,
54    color_range: ColorRange,
55
56    // Destination pixel format.
57    pub(crate) dst_format: PixelFormat,
58    // Source pixel format.
59    pub(crate) src_format: PixelFormat,
60    //
61    // Number of bits per pixel o fthe destiantion pixel format.
62    //dst_format_bpp: u8,
63    //int dstFormatBpp;
64    //
65    //frame_src: &'a AVFrame,
66    //AVFrame *frame_src;
67    //frame_dst: &'a AVFrame,
68    //AVFrame *frame_dst;
69    //
70    pub(crate) yuv_tables: Option<YuvTables>,
71    //pub(crate) yuv_table: [u8; YUV_TABLE_SIZE],
72    // alignment ensures the offset can be added in a single
73    // instruction on e.g. ARM
74    // ALIGN = 16.
75    //pub(crate) table_gv: [i16; YUVRGB_TABLE_SIZE],
76    //pub(crate) table_rv: [usize; YUVRGB_TABLE_SIZE],
77    //pub(crate) table_gu: [usize; YUVRGB_TABLE_SIZE],
78    //pub(crate) table_bu: [usize; YUVRGB_TABLE_SIZE],
79    //DECLARE_ALIGNED(16, int32_t, input_rgb2yuv_table)
80    //[16 + 40 * 4]; // This table can contain both C and SIMD formatted values, the
81    // C vales are always at the XY_IDX points
82    //#define RY_IDX 0
83    //#define GY_IDX 1
84    //#define BY_IDX 2
85    //#define RU_IDX 3
86    //#define GU_IDX 4
87    //#define BU_IDX 5
88    //#define RV_IDX 6
89    //#define GV_IDX 7
90    //#define BV_IDX 8
91    //#define RGB2YUV_SHIFT 15
92
93    // Colorspace stuff
94    //int srcColorspaceTable[4];
95    //int dstColorspaceTable[4];
96    //src_range: i32, // 0 = MPG YUV range, 1 = JPG YUV range (source      image).
97
98    //int dstRange; ///< 0 = MPG YUV range, 1 = JPG YUV range (destination image).
99    //int yuv2rgb_y_offset;
100    //int yuv2rgb_y_coeff;
101    //int yuv2rgb_v2r_coeff;
102    //int yuv2rgb_v2g_coeff;
103    //int yuv2rgb_u2g_coeff;
104    //int yuv2rgb_u2b_coeff;
105
106    //DECLARE_ALIGNED(8, uint64_t, yCoeff);
107    //DECLARE_ALIGNED(8, uint64_t, vrCoeff);
108    //DECLARE_ALIGNED(8, uint64_t, ubCoeff);
109    //DECLARE_ALIGNED(8, uint64_t, vgCoeff);
110    //DECLARE_ALIGNED(8, uint64_t, ugCoeff);
111    //DECLARE_ALIGNED(8, uint64_t, yOffset);
112    //DECLARE_ALIGNED(8, uint64_t, uOffset);
113    //DECLARE_ALIGNED(8, uint64_t, vOffset);
114}
115
116#[derive(Debug, Error)]
117#[non_exhaustive]
118pub enum NewConverterError {
119    #[error("odd height is unsupported for yuv->rgb")]
120    OddHeightUnsupported,
121
122    #[error("unsupported pixel formats: {0} -> {1}")]
123    UnsupportedPixelFormat(PixelFormat, PixelFormat),
124
125    #[error("init tables: {0} {0}")]
126    InitTables(Yuv2RgbInitTablesError, PixelFormat),
127}
128
129#[derive(Debug, Error)]
130pub enum ConvertError {
131    #[error("width changed: {0}vs{1}")]
132    WidthChanged(NonZeroU16, NonZeroU16),
133
134    #[error("height changed: {0}vs{1}")]
135    HeightChanged(NonZeroU16, NonZeroU16),
136
137    #[error("pixel format changed: {0}vs{1}")]
138    PixelFormatChanged(PixelFormat, PixelFormat),
139
140    #[error("color range changed: {0}vs{1}")]
141    ColorRangeChanged(ColorRange, ColorRange),
142
143    #[error("reset buffer: {0}")]
144    ResetBuffer(#[from] ResetBufferError),
145
146    #[error("converter paniced: {0}")]
147    Panic(String),
148}
149
150impl PixelFormatConverter {
151    pub fn new(
152        width: NonZeroU16,
153        height: NonZeroU16,
154        color_range: ColorRange,
155        mut src_format: PixelFormat,
156        mut dst_format: PixelFormat,
157    ) -> Result<Self, NewConverterError> {
158        let dst_format2 = dst_format;
159        src_format = handle_jpeg(src_format);
160        dst_format = handle_jpeg(dst_format);
161
162        if dst_format2 != dst_format {
163            return Err(NewConverterError::UnsupportedPixelFormat(
164                src_format,
165                dst_format2,
166            ));
167        }
168
169        //if (!c->dstFormatBpp)
170        //  sws_setColorspaceDetails(c, ff_yuv2rgb_coeffs[SWS_CS_DEFAULT], c->srcRange,
171        //                           ff_yuv2rgb_coeffs[SWS_CS_DEFAULT], c->dstRange, 0,
172        //                           1 << 16, 1 << 16);
173
174        let inv_table = FF_YUV2RGB_COEFFS[SWS_CS_DEFAULT];
175        let brightness = 0;
176        let contrast = 1 << 16;
177        let saturation = 1 << 16;
178
179        //const AVPixFmtDescriptor *desc_dst;
180
181        //dstRange = 0;
182
183        //int need_reinit = 0;
184        /*if (c->dstRange != dstRange ||
185              memcmp(c->dstColorspaceTable, table, sizeof(int) * 4)) {
186            need_reinit = 1;
187        }*/
188
189        //memmove(c->srcColorspaceTable, inv_table, sizeof(int) * 4);
190        //memmove(c->dstColorspaceTable, table, sizeof(int) * 4);
191
192        //c->dstRange = dstRange;
193
194        let dst_format_bpp = dst_format.bits_per_pixel();
195
196        // if (need_reinit) {
197        //    return 0;
198        //}
199
200        #[allow(clippy::if_then_some_else_none)]
201        let yuv_tables = if is_yuv(src_format) && is_rgb(dst_format) {
202            Some(
203                yuv2rgb_init_tables(
204                    dst_format_bpp,
205                    &inv_table,
206                    color_range,
207                    brightness,
208                    contrast,
209                    saturation,
210                )
211                .map_err(|e| NewConverterError::InitTables(e, dst_format))?,
212            )
213        } else {
214            None
215        };
216
217        Ok(PixelFormatConverter {
218            convert: get_convert_func(src_format, dst_format, height)?,
219            width,
220            height,
221            color_range,
222            dst_format,
223            src_format,
224            yuv_tables,
225        })
226    }
227
228    /// Converts `src_frame` to another pixel format and stores it in `dst_frame`.
229    ///
230    /// Will check that `src_frame` matches converter configuration. `dst_frame` will be resized if necessary.
231    //
232    // Assumes planar YUV to be in YUV order instead of YVU.
233    #[allow(clippy::missing_panics_doc)]
234    pub fn convert(
235        &mut self,
236        src_frame: &Frame,
237        dst_frame: &mut Frame,
238    ) -> Result<(), ConvertError> {
239        use ConvertError::*;
240
241        let width = self.width;
242        let height = self.height;
243        let color_range = self.color_range;
244        let src_format = self.src_format;
245        let dst_format = self.dst_format;
246
247        if src_frame.width() != width {
248            return Err(WidthChanged(src_frame.width(), width));
249        }
250        if src_frame.height() != height {
251            return Err(HeightChanged(src_frame.height(), height));
252        }
253        if handle_jpeg(src_frame.pix_fmt()) != src_format {
254            return Err(PixelFormatChanged(
255                handle_jpeg(src_frame.pix_fmt()),
256                src_format,
257            ));
258        }
259        if src_frame.color_range() != color_range {
260            return Err(ColorRangeChanged(src_frame.color_range(), color_range));
261        }
262
263        dst_frame.reset_buffer(width, height, dst_format, 1)?;
264        dst_frame.set_width(width);
265        dst_frame.set_height(height);
266        dst_frame.set_pix_fmt(dst_format);
267        dst_frame.set_color_range(color_range);
268
269        let dst_frame = Mutex::new(dst_frame);
270        let converter = Mutex::new(self);
271
272        #[allow(clippy::unwrap_used)]
273        let result = panic::catch_unwind(|| {
274            let mut dst_frame = dst_frame.lock().unwrap();
275            converter.lock().unwrap().convert_internal(
276                src_frame.data(),
277                src_frame.linesize(),
278                src_frame.height(),
279                &mut dst_frame,
280            );
281        });
282        if result.is_err() {
283            return Err(ConvertError::Panic(format!(
284                "width={width} height={height} src_format={src_format} dst_format={dst_format}",
285            )));
286        }
287        Ok(())
288    }
289
290    fn convert_internal(
291        &mut self,
292        src_slice: &[Vec<u8>],
293        src_stride: &[usize; 8],
294        src_slice_h: NonZeroU16,
295        dst_frame: &mut Frame,
296    ) {
297        // Copy strides, so they can safely be modified.
298        let mut src2: [Vec<u8>; 4] = Default::default();
299        for i in 0..4 {
300            src2[i].extend_from_slice(&src_slice[i]);
301        }
302        //memcpy(src2, srcSlice, sizeof(src2));
303
304        let mut src_stride2: [usize; 8] = src_stride.to_owned();
305
306        let dst_stride = dst_frame.linesize_mut();
307        let dst_stride2: [usize; 8] = *dst_stride;
308
309        (self.convert)(
310            self,
311            &src2,
312            &mut src_stride2,
313            src_slice_h.get().into(),
314            dst_frame.data_mut(),
315            &dst_stride2[..],
316        );
317    }
318}
319
320fn is_yuv(pix_fmt: PixelFormat) -> bool {
321    (pix_fmt.flags() & PIX_FMT_FLAG_RGB == 0) && pix_fmt.comps().len() >= 2
322}
323
324fn is_rgb(pix_fmt: PixelFormat) -> bool {
325    pix_fmt.flags() & PIX_FMT_FLAG_RGB != 0
326}
327
328// Set c.convert_unscaled to an unscaled converter if one exists for the
329// specific source and destination formats, bit depths, flags, etc.
330pub(crate) fn get_convert_func(
331    src_format: PixelFormat,
332    dst_format: PixelFormat,
333    height: NonZeroU16,
334) -> Result<ConvertFunc, NewConverterError> {
335    use NewConverterError::*;
336
337    // yuv2rgb.
338    if src_format == PixelFormat::YUV420P
339        /*|| src_format == PixelFormat::YUV422P
340        || src_format == PixelFormat::YUVA420P*/ && dst_format == PixelFormat::RGB24
341    {
342        if height.get() & 1 != 0 {
343            return Err(NewConverterError::OddHeightUnsupported);
344        }
345        return yuv2rgb_get_func(dst_format).ok_or(UnsupportedPixelFormat(src_format, dst_format));
346    }
347    // yuv2gray.
348    if src_format == PixelFormat::YUV420P && dst_format == PixelFormat::GRAY8 {
349        return yuv2gray_get_func(dst_format).ok_or(UnsupportedPixelFormat(src_format, dst_format));
350    }
351
352    Err(UnsupportedPixelFormat(src_format, dst_format))
353}
354
355fn yuv2gray_get_func(dst_format: PixelFormat) -> Option<ConvertFunc> {
356    match dst_format {
357        PixelFormat::GRAY8 => Some(yuv2gray_8),
358        _ => None,
359    }
360}
361
362#[allow(clippy::unnecessary_wraps)]
363pub(crate) fn yuv2gray_8(
364    _c: &PixelFormatConverter,
365    src: &[Vec<u8>],
366    src_stride: &mut [usize],
367    src_slice_h: i32,
368    dst: &mut [Vec<u8>],
369    dst_stride: &[usize],
370) {
371    dst[0].clear();
372
373    let src_width = src_stride[0];
374    let dst_width = dst_stride[0];
375
376    let mut src: &[u8] = &src[0];
377    for _ in 0..src_slice_h {
378        dst[0].extend_from_slice(&src[..dst_width]);
379        src = &src[src_width..];
380    }
381}
382
383pub(crate) fn handle_jpeg(pix_fmt: PixelFormat) -> PixelFormat {
384    match pix_fmt {
385        PixelFormat::YUVJ420P => PixelFormat::YUV420P,
386        // YUVJ411P.
387        PixelFormat::YUVJ422P => PixelFormat::YUV422P,
388        PixelFormat::YUVJ444P => PixelFormat::YUV444P,
389        // YUVJ440P.
390        /*PixelFormat::GRAY8
391        | PixelFormat::YA8
392        | PixelFormat::GRAY9LE
393        | PixelFormat::GRAY9BE
394        | PixelFormat::GRAY10LE
395        | PixelFormat::GRAY10BE
396        | PixelFormat::GRAY12LE
397        | PixelFormat::GRAY12BE
398        | PixelFormat::GRAY14LE
399        | PixelFormat::GRAY14BE
400        | PixelFormat::GRAY16LE
401        | PixelFormat::GRAY16BE
402        | PixelFormat::YA16BE
403        | PixelFormat::YA16LE => 1,*/
404        _ => pix_fmt,
405    }
406    /*switch (*format) {
407    case AV_PIX_FMT_YUVJ420P:
408      *format = AV_PIX_FMT_YUV420P;
409      return 1;
410    case AV_PIX_FMT_YUVJ411P:
411      *format = AV_PIX_FMT_YUV411P;
412      return 1;
413    case AV_PIX_FMT_YUVJ422P:
414      *format = AV_PIX_FMT_YUV422P;
415      return 1;
416    case AV_PIX_FMT_YUVJ444P:
417      *format = AV_PIX_FMT_YUV444P;
418      return 1;
419    case AV_PIX_FMT_YUVJ440P:
420      *format = AV_PIX_FMT_YUV440P;
421      return 1;
422    case AV_PIX_FMT_GRAY8:
423    case AV_PIX_FMT_YA8:
424    case AV_PIX_FMT_GRAY9LE:
425    case AV_PIX_FMT_GRAY9BE:
426    case AV_PIX_FMT_GRAY10LE:
427    case AV_PIX_FMT_GRAY10BE:
428    case AV_PIX_FMT_GRAY12LE:
429    case AV_PIX_FMT_GRAY12BE:
430    case AV_PIX_FMT_GRAY14LE:
431    case AV_PIX_FMT_GRAY14BE:
432    case AV_PIX_FMT_GRAY16LE:
433    case AV_PIX_FMT_GRAY16BE:
434    case AV_PIX_FMT_YA16BE:
435    case AV_PIX_FMT_YA16LE:
436      return 1;
437    default:
438      return 0;
439    }*/
440}