1mod 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#[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#[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 pub(crate) dst_format: PixelFormat,
58 pub(crate) src_format: PixelFormat,
60 pub(crate) yuv_tables: Option<YuvTables>,
71 }
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 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 let dst_format_bpp = dst_format.bits_per_pixel();
195
196 #[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 #[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 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 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
328pub(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 if src_format == PixelFormat::YUV420P
339 && 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 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 PixelFormat::YUVJ422P => PixelFormat::YUV422P,
388 PixelFormat::YUVJ444P => PixelFormat::YUV444P,
389 _ => pix_fmt,
405 }
406 }