Skip to main content

ff_decode/image/
decoder_inner.rs

1//! Internal image decoder implementation using FFmpeg.
2//!
3//! This module contains the low-level decoder logic that directly interacts
4//! with FFmpeg's C API through the ff-sys crate. It is not exposed publicly.
5
6// Allow unsafe code in this module as it's necessary for FFmpeg FFI
7#![allow(unsafe_code)]
8// Allow specific clippy lints for FFmpeg FFI code
9#![allow(clippy::similar_names)]
10#![allow(clippy::too_many_lines)]
11#![allow(clippy::cast_sign_loss)]
12#![allow(clippy::cast_possible_truncation)]
13#![allow(clippy::cast_possible_wrap)]
14#![allow(clippy::module_name_repetitions)]
15#![allow(clippy::ptr_as_ptr)]
16#![allow(clippy::doc_markdown)]
17#![allow(clippy::unnecessary_cast)]
18#![allow(clippy::cast_precision_loss)]
19#![allow(clippy::cast_lossless)]
20
21use std::path::Path;
22use std::ptr;
23
24use ff_format::time::{Rational, Timestamp};
25use ff_format::{PixelFormat, PooledBuffer, VideoFrame};
26use ff_sys::{
27    AVCodecContext, AVCodecID, AVFormatContext, AVFrame, AVMediaType_AVMEDIA_TYPE_VIDEO, AVPacket,
28    AVPixelFormat,
29};
30
31use crate::error::DecodeError;
32
33// ── RAII guards ────────────────────────────────────────────────────────────────
34
35/// RAII guard for `AVFormatContext` to ensure proper cleanup.
36struct AvFormatContextGuard(*mut AVFormatContext);
37
38impl AvFormatContextGuard {
39    unsafe fn new(path: &Path) -> Result<Self, DecodeError> {
40        // SAFETY: Caller ensures FFmpeg is initialized and path is valid
41        let format_ctx = unsafe {
42            ff_sys::avformat::open_input(path).map_err(|e| DecodeError::Ffmpeg {
43                code: e,
44                message: format!("Failed to open file: {}", ff_sys::av_error_string(e)),
45            })?
46        };
47        Ok(Self(format_ctx))
48    }
49
50    const fn as_ptr(&self) -> *mut AVFormatContext {
51        self.0
52    }
53
54    fn into_raw(self) -> *mut AVFormatContext {
55        let ptr = self.0;
56        std::mem::forget(self);
57        ptr
58    }
59}
60
61impl Drop for AvFormatContextGuard {
62    fn drop(&mut self) {
63        if !self.0.is_null() {
64            // SAFETY: self.0 is valid and owned by this guard
65            unsafe {
66                ff_sys::avformat::close_input(&mut (self.0 as *mut _));
67            }
68        }
69    }
70}
71
72/// RAII guard for `AVCodecContext` to ensure proper cleanup.
73struct AvCodecContextGuard(*mut AVCodecContext);
74
75impl AvCodecContextGuard {
76    unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
77        // SAFETY: Caller ensures codec pointer is valid
78        let codec_ctx = unsafe {
79            ff_sys::avcodec::alloc_context3(codec).map_err(|e| DecodeError::Ffmpeg {
80                code: e,
81                message: format!("Failed to allocate codec context: {e}"),
82            })?
83        };
84        Ok(Self(codec_ctx))
85    }
86
87    const fn as_ptr(&self) -> *mut AVCodecContext {
88        self.0
89    }
90
91    fn into_raw(self) -> *mut AVCodecContext {
92        let ptr = self.0;
93        std::mem::forget(self);
94        ptr
95    }
96}
97
98impl Drop for AvCodecContextGuard {
99    fn drop(&mut self) {
100        if !self.0.is_null() {
101            // SAFETY: self.0 is valid and owned by this guard
102            unsafe {
103                ff_sys::avcodec::free_context(&mut (self.0 as *mut _));
104            }
105        }
106    }
107}
108
109// ── ImageDecoderInner ─────────────────────────────────────────────────────────
110
111/// Internal state for the image decoder.
112///
113/// Holds raw FFmpeg pointers and is responsible for proper cleanup in `Drop`.
114pub(crate) struct ImageDecoderInner {
115    /// Format context for reading the image file.
116    format_ctx: *mut AVFormatContext,
117    /// Codec context for decoding the image.
118    codec_ctx: *mut AVCodecContext,
119    /// Video stream index in the format context.
120    stream_index: usize,
121    /// Reusable packet for reading from file.
122    packet: *mut AVPacket,
123    /// Reusable frame for decoding.
124    frame: *mut AVFrame,
125}
126
127// SAFETY: `ImageDecoderInner` owns all FFmpeg contexts exclusively.
128//         FFmpeg contexts are not safe for concurrent access (not Sync),
129//         but ownership transfer between threads is safe.
130unsafe impl Send for ImageDecoderInner {}
131
132impl ImageDecoderInner {
133    /// Opens an image file and prepares the decoder.
134    ///
135    /// Performs the full FFmpeg initialization sequence:
136    /// 1. `avformat_open_input`
137    /// 2. `avformat_find_stream_info`
138    /// 3. `av_find_best_stream(AVMEDIA_TYPE_VIDEO)`
139    /// 4. `avcodec_find_decoder`
140    /// 5. `avcodec_alloc_context3`
141    /// 6. `avcodec_parameters_to_context`
142    /// 7. `avcodec_open2`
143    pub(crate) fn new(path: &Path) -> Result<Self, DecodeError> {
144        ff_sys::ensure_initialized();
145
146        // 1. avformat_open_input
147        // SAFETY: Path is valid; AvFormatContextGuard ensures cleanup on error.
148        let format_ctx_guard = unsafe { AvFormatContextGuard::new(path)? };
149        let format_ctx = format_ctx_guard.as_ptr();
150
151        // 2. avformat_find_stream_info
152        // SAFETY: format_ctx is valid and owned by the guard.
153        unsafe {
154            ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
155                code: e,
156                message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
157            })?;
158        }
159
160        // 3. Find the video stream.
161        // SAFETY: format_ctx is valid.
162        let (stream_index, codec_id) =
163            unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
164                DecodeError::NoVideoStream {
165                    path: path.to_path_buf(),
166                }
167            })?;
168
169        // 4. avcodec_find_decoder
170        // SAFETY: codec_id comes from FFmpeg.
171        let codec = unsafe {
172            ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
173                DecodeError::UnsupportedCodec {
174                    codec: format!("codec_id={codec_id:?}"),
175                }
176            })?
177        };
178
179        // 5. avcodec_alloc_context3
180        // SAFETY: codec pointer is valid; AvCodecContextGuard ensures cleanup.
181        let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
182        let codec_ctx = codec_ctx_guard.as_ptr();
183
184        // 6. avcodec_parameters_to_context
185        // SAFETY: All pointers are valid; stream_index was validated above.
186        unsafe {
187            let stream = (*format_ctx).streams.add(stream_index);
188            let codecpar = (*(*stream)).codecpar;
189            ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
190                DecodeError::Ffmpeg {
191                    code: e,
192                    message: format!(
193                        "Failed to copy codec parameters: {}",
194                        ff_sys::av_error_string(e)
195                    ),
196                }
197            })?;
198        }
199
200        // 7. avcodec_open2
201        // SAFETY: codec_ctx and codec are valid; no hardware acceleration for images.
202        unsafe {
203            ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
204                DecodeError::Ffmpeg {
205                    code: e,
206                    message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
207                }
208            })?;
209        }
210
211        // Allocate packet and frame.
212        // SAFETY: FFmpeg is initialized.
213        let packet = unsafe { ff_sys::av_packet_alloc() };
214        if packet.is_null() {
215            return Err(DecodeError::Ffmpeg {
216                code: 0,
217                message: "Failed to allocate packet".to_string(),
218            });
219        }
220        let frame = unsafe { ff_sys::av_frame_alloc() };
221        if frame.is_null() {
222            unsafe { ff_sys::av_packet_free(&mut (packet as *mut _)) };
223            return Err(DecodeError::Ffmpeg {
224                code: 0,
225                message: "Failed to allocate frame".to_string(),
226            });
227        }
228
229        Ok(Self {
230            format_ctx: format_ctx_guard.into_raw(),
231            codec_ctx: codec_ctx_guard.into_raw(),
232            stream_index,
233            packet,
234            frame,
235        })
236    }
237
238    /// Returns the image width in pixels.
239    pub(crate) fn width(&self) -> u32 {
240        // SAFETY: codec_ctx is valid for the lifetime of `self`.
241        unsafe { (*self.codec_ctx).width as u32 }
242    }
243
244    /// Returns the image height in pixels.
245    pub(crate) fn height(&self) -> u32 {
246        // SAFETY: codec_ctx is valid for the lifetime of `self`.
247        unsafe { (*self.codec_ctx).height as u32 }
248    }
249
250    /// Decodes the image, consuming `self` and returning a [`VideoFrame`].
251    ///
252    /// Follows the sequence:
253    /// 1. `av_read_frame`
254    /// 2. `avcodec_send_packet`
255    /// 3. `avcodec_receive_frame`
256    /// 4. Convert to [`VideoFrame`]
257    pub(crate) fn decode(self) -> Result<VideoFrame, DecodeError> {
258        // 1. av_read_frame
259        // SAFETY: format_ctx and packet are valid.
260        let ret = unsafe { ff_sys::av_read_frame(self.format_ctx, self.packet) };
261        if ret < 0 {
262            return Err(DecodeError::Ffmpeg {
263                code: ret,
264                message: format!("Failed to read frame: {}", ff_sys::av_error_string(ret)),
265            });
266        }
267
268        // 2. avcodec_send_packet
269        // SAFETY: codec_ctx and packet are valid; packet contains image data.
270        let ret = unsafe { ff_sys::avcodec_send_packet(self.codec_ctx, self.packet) };
271        unsafe { ff_sys::av_packet_unref(self.packet) };
272        if ret < 0 {
273            return Err(DecodeError::Ffmpeg {
274                code: ret,
275                message: format!(
276                    "Failed to send packet to decoder: {}",
277                    ff_sys::av_error_string(ret)
278                ),
279            });
280        }
281
282        // 3. avcodec_receive_frame
283        // SAFETY: codec_ctx and frame are valid.
284        let ret = unsafe { ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame) };
285        if ret < 0 {
286            return Err(DecodeError::Ffmpeg {
287                code: ret,
288                message: format!(
289                    "Failed to receive decoded frame: {}",
290                    ff_sys::av_error_string(ret)
291                ),
292            });
293        }
294
295        // 4. Convert to VideoFrame.
296        // SAFETY: frame is valid and contains decoded image data.
297        let video_frame = unsafe { self.av_frame_to_video_frame(self.frame)? };
298        Ok(video_frame)
299    }
300
301    /// Finds the first video stream in the format context.
302    ///
303    /// # Safety
304    ///
305    /// `format_ctx` must be a valid, fully initialized `AVFormatContext`.
306    unsafe fn find_video_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
307        // SAFETY: Caller ensures format_ctx is valid.
308        unsafe {
309            let nb_streams = (*format_ctx).nb_streams as usize;
310            for i in 0..nb_streams {
311                let stream = (*format_ctx).streams.add(i);
312                let codecpar = (*(*stream)).codecpar;
313                if (*codecpar).codec_type == AVMediaType_AVMEDIA_TYPE_VIDEO {
314                    return Some((i, (*codecpar).codec_id));
315                }
316            }
317        }
318        None
319    }
320
321    /// Maps an `AVPixelFormat` value to our [`PixelFormat`] enum.
322    ///
323    /// Image decoders commonly produce YUVJ formats (full-range YUV), which
324    /// have the same plane layout as the corresponding YUV formats but with a
325    /// different color range flag.  We map them to their YUV equivalents here
326    /// and rely on the colour-range metadata to distinguish them if needed.
327    fn convert_pixel_format(fmt: AVPixelFormat) -> PixelFormat {
328        if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
329            || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P
330        {
331            PixelFormat::Yuv420p
332        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P
333            || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ422P
334        {
335            PixelFormat::Yuv422p
336        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P
337            || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ444P
338        {
339            PixelFormat::Yuv444p
340        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24 {
341            PixelFormat::Rgb24
342        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24 {
343            PixelFormat::Bgr24
344        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA {
345            PixelFormat::Rgba
346        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA {
347            PixelFormat::Bgra
348        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8 {
349            PixelFormat::Gray8
350        } else {
351            log::warn!(
352                "pixel_format unsupported, falling back to Rgb24 requested={fmt} fallback=Rgb24"
353            );
354            PixelFormat::Rgb24
355        }
356    }
357
358    /// Converts a decoded `AVFrame` to a [`VideoFrame`].
359    ///
360    /// # Safety
361    ///
362    /// `frame` must be a valid, fully decoded `AVFrame` owned by `self`.
363    unsafe fn av_frame_to_video_frame(
364        &self,
365        frame: *const AVFrame,
366    ) -> Result<VideoFrame, DecodeError> {
367        // SAFETY: Caller ensures frame is valid.
368        unsafe {
369            let width = (*frame).width as u32;
370            let height = (*frame).height as u32;
371            let format = Self::convert_pixel_format((*frame).format);
372
373            // Extract timestamp (images often have no meaningful PTS).
374            let pts = (*frame).pts;
375            let timestamp = if pts == ff_sys::AV_NOPTS_VALUE {
376                Timestamp::default()
377            } else {
378                let stream = (*self.format_ctx).streams.add(self.stream_index);
379                let time_base = (*(*stream)).time_base;
380                Timestamp::new(
381                    pts as i64,
382                    Rational::new(time_base.num as i32, time_base.den as i32),
383                )
384            };
385
386            let (planes, strides) = Self::extract_planes_and_strides(frame, width, height, format)?;
387
388            // Images are always key frames.
389            VideoFrame::new(planes, strides, width, height, format, timestamp, true).map_err(|e| {
390                DecodeError::Ffmpeg {
391                    code: 0,
392                    message: format!("Failed to create VideoFrame: {e}"),
393                }
394            })
395        }
396    }
397
398    /// Extracts pixel data from an `AVFrame` into [`PooledBuffer`] planes.
399    ///
400    /// Copies data row-by-row to strip any FFmpeg padding from line strides.
401    ///
402    /// # Safety
403    ///
404    /// `frame` must be a valid, fully decoded `AVFrame` with `format` matching
405    /// the actual pixel format of the frame.
406    unsafe fn extract_planes_and_strides(
407        frame: *const AVFrame,
408        width: u32,
409        height: u32,
410        format: PixelFormat,
411    ) -> Result<(Vec<PooledBuffer>, Vec<usize>), DecodeError> {
412        // SAFETY: Caller ensures frame is valid and format matches.
413        unsafe {
414            let w = width as usize;
415            let h = height as usize;
416            let mut planes: Vec<PooledBuffer> = Vec::new();
417            let mut strides: Vec<usize> = Vec::new();
418
419            match format {
420                PixelFormat::Rgba | PixelFormat::Bgra => {
421                    let bytes_per_pixel = 4_usize;
422                    let stride = (*frame).linesize[0] as usize;
423                    let row_w = w * bytes_per_pixel;
424                    let mut buf = vec![0u8; row_w * h];
425                    let src = (*frame).data[0];
426                    if src.is_null() {
427                        return Err(DecodeError::Ffmpeg {
428                            code: 0,
429                            message: "Null plane data for packed format".to_string(),
430                        });
431                    }
432                    for row in 0..h {
433                        ptr::copy_nonoverlapping(
434                            src.add(row * stride),
435                            buf[row * row_w..].as_mut_ptr(),
436                            row_w,
437                        );
438                    }
439                    planes.push(PooledBuffer::standalone(buf));
440                    strides.push(row_w);
441                }
442                PixelFormat::Rgb24 | PixelFormat::Bgr24 => {
443                    let bytes_per_pixel = 3_usize;
444                    let stride = (*frame).linesize[0] as usize;
445                    let row_w = w * bytes_per_pixel;
446                    let mut buf = vec![0u8; row_w * h];
447                    let src = (*frame).data[0];
448                    if src.is_null() {
449                        return Err(DecodeError::Ffmpeg {
450                            code: 0,
451                            message: "Null plane data for packed format".to_string(),
452                        });
453                    }
454                    for row in 0..h {
455                        ptr::copy_nonoverlapping(
456                            src.add(row * stride),
457                            buf[row * row_w..].as_mut_ptr(),
458                            row_w,
459                        );
460                    }
461                    planes.push(PooledBuffer::standalone(buf));
462                    strides.push(row_w);
463                }
464                PixelFormat::Gray8 => {
465                    let stride = (*frame).linesize[0] as usize;
466                    let mut buf = vec![0u8; w * h];
467                    let src = (*frame).data[0];
468                    if src.is_null() {
469                        return Err(DecodeError::Ffmpeg {
470                            code: 0,
471                            message: "Null plane data for Gray8".to_string(),
472                        });
473                    }
474                    for row in 0..h {
475                        ptr::copy_nonoverlapping(
476                            src.add(row * stride),
477                            buf[row * w..].as_mut_ptr(),
478                            w,
479                        );
480                    }
481                    planes.push(PooledBuffer::standalone(buf));
482                    strides.push(w);
483                }
484                PixelFormat::Yuv420p | PixelFormat::Nv12 | PixelFormat::Nv21 => {
485                    // Y plane (full size).
486                    let y_stride = (*frame).linesize[0] as usize;
487                    let mut y_buf = vec![0u8; w * h];
488                    let y_src = (*frame).data[0];
489                    if y_src.is_null() {
490                        return Err(DecodeError::Ffmpeg {
491                            code: 0,
492                            message: "Null Y plane".to_string(),
493                        });
494                    }
495                    for row in 0..h {
496                        ptr::copy_nonoverlapping(
497                            y_src.add(row * y_stride),
498                            y_buf[row * w..].as_mut_ptr(),
499                            w,
500                        );
501                    }
502                    planes.push(PooledBuffer::standalone(y_buf));
503                    strides.push(w);
504
505                    if matches!(format, PixelFormat::Nv12 | PixelFormat::Nv21) {
506                        // Interleaved UV plane (half height).
507                        let uv_h = h / 2;
508                        let uv_stride = (*frame).linesize[1] as usize;
509                        let mut uv_buf = vec![0u8; w * uv_h];
510                        let uv_src = (*frame).data[1];
511                        if !uv_src.is_null() {
512                            for row in 0..uv_h {
513                                ptr::copy_nonoverlapping(
514                                    uv_src.add(row * uv_stride),
515                                    uv_buf[row * w..].as_mut_ptr(),
516                                    w,
517                                );
518                            }
519                        }
520                        planes.push(PooledBuffer::standalone(uv_buf));
521                        strides.push(w);
522                    } else {
523                        // YUV 4:2:0 — separate U and V planes (half width, half height).
524                        let uv_w = w / 2;
525                        let uv_h = h / 2;
526                        for plane_idx in 1..=2usize {
527                            let uv_stride = (*frame).linesize[plane_idx] as usize;
528                            let mut uv_buf = vec![0u8; uv_w * uv_h];
529                            let uv_src = (*frame).data[plane_idx];
530                            if !uv_src.is_null() {
531                                for row in 0..uv_h {
532                                    ptr::copy_nonoverlapping(
533                                        uv_src.add(row * uv_stride),
534                                        uv_buf[row * uv_w..].as_mut_ptr(),
535                                        uv_w,
536                                    );
537                                }
538                            }
539                            planes.push(PooledBuffer::standalone(uv_buf));
540                            strides.push(uv_w);
541                        }
542                    }
543                }
544                PixelFormat::Yuv422p => {
545                    // Y plane (full size), U and V planes (half width, full height).
546                    let uv_w = w / 2;
547                    let plane_dims = [(w, h), (uv_w, h), (uv_w, h)];
548                    for (plane_idx, (pw, ph)) in plane_dims.iter().enumerate() {
549                        let stride = (*frame).linesize[plane_idx] as usize;
550                        let mut buf = vec![0u8; pw * ph];
551                        let src = (*frame).data[plane_idx];
552                        if !src.is_null() {
553                            for row in 0..*ph {
554                                ptr::copy_nonoverlapping(
555                                    src.add(row * stride),
556                                    buf[row * pw..].as_mut_ptr(),
557                                    *pw,
558                                );
559                            }
560                        }
561                        planes.push(PooledBuffer::standalone(buf));
562                        strides.push(*pw);
563                    }
564                }
565                PixelFormat::Yuv444p => {
566                    // All three planes are full size.
567                    for plane_idx in 0..3usize {
568                        let stride = (*frame).linesize[plane_idx] as usize;
569                        let mut buf = vec![0u8; w * h];
570                        let src = (*frame).data[plane_idx];
571                        if !src.is_null() {
572                            for row in 0..h {
573                                ptr::copy_nonoverlapping(
574                                    src.add(row * stride),
575                                    buf[row * w..].as_mut_ptr(),
576                                    w,
577                                );
578                            }
579                        }
580                        planes.push(PooledBuffer::standalone(buf));
581                        strides.push(w);
582                    }
583                }
584                _ => {
585                    return Err(DecodeError::Ffmpeg {
586                        code: 0,
587                        message: format!("Unsupported pixel format for image decoding: {format:?}"),
588                    });
589                }
590            }
591
592            Ok((planes, strides))
593        }
594    }
595}
596
597impl Drop for ImageDecoderInner {
598    fn drop(&mut self) {
599        // SAFETY: All pointers are exclusively owned by this struct and were
600        // allocated by the corresponding FFmpeg alloc functions.
601        unsafe {
602            if !self.frame.is_null() {
603                ff_sys::av_frame_free(&mut (self.frame as *mut _));
604            }
605            if !self.packet.is_null() {
606                ff_sys::av_packet_free(&mut (self.packet as *mut _));
607            }
608            if !self.codec_ctx.is_null() {
609                ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
610            }
611            if !self.format_ctx.is_null() {
612                ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
613            }
614        }
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621
622    #[test]
623    fn convert_pixel_format_yuv420p_should_map_to_yuv420p() {
624        assert_eq!(
625            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
626            PixelFormat::Yuv420p
627        );
628    }
629
630    #[test]
631    fn convert_pixel_format_yuvj420p_should_map_to_yuv420p() {
632        assert_eq!(
633            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P),
634            PixelFormat::Yuv420p
635        );
636    }
637
638    #[test]
639    fn convert_pixel_format_rgb24_should_map_to_rgb24() {
640        assert_eq!(
641            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
642            PixelFormat::Rgb24
643        );
644    }
645
646    #[test]
647    fn convert_pixel_format_rgba_should_map_to_rgba() {
648        assert_eq!(
649            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
650            PixelFormat::Rgba
651        );
652    }
653
654    #[test]
655    fn convert_pixel_format_gray8_should_map_to_gray8() {
656        assert_eq!(
657            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
658            PixelFormat::Gray8
659        );
660    }
661}