Skip to main content

ff_decode/image/
builder.rs

1//! Image decoder builder for constructing image decoders.
2//!
3//! This module provides the [`ImageDecoderBuilder`] type which enables fluent
4//! configuration of image decoders. Use [`ImageDecoder::open()`] to start building.
5
6use std::path::{Path, PathBuf};
7
8use ff_format::VideoFrame;
9
10use crate::error::DecodeError;
11use crate::image::decoder_inner::ImageDecoderInner;
12
13/// Builder for configuring and constructing an [`ImageDecoder`].
14///
15/// Created by calling [`ImageDecoder::open()`]. Call [`build()`](Self::build)
16/// to open the file and prepare for decoding.
17///
18/// # Examples
19///
20/// ```ignore
21/// use ff_decode::ImageDecoder;
22///
23/// let frame = ImageDecoder::open("photo.png").build()?.decode()?;
24/// println!("{}x{}", frame.width(), frame.height());
25/// ```
26#[derive(Debug)]
27pub struct ImageDecoderBuilder {
28    path: PathBuf,
29}
30
31impl ImageDecoderBuilder {
32    pub(crate) fn new(path: PathBuf) -> Self {
33        Self { path }
34    }
35
36    /// Opens the image file and returns an [`ImageDecoder`] ready to decode.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`DecodeError`] if the file cannot be opened, contains no
41    /// video stream, or uses an unsupported codec.
42    pub fn build(self) -> Result<ImageDecoder, DecodeError> {
43        if !self.path.exists() {
44            return Err(DecodeError::FileNotFound {
45                path: self.path.clone(),
46            });
47        }
48        let inner = ImageDecoderInner::new(&self.path)?;
49        let width = inner.width();
50        let height = inner.height();
51        Ok(ImageDecoder {
52            inner: Some(inner),
53            width,
54            height,
55        })
56    }
57}
58
59/// Decodes a single still image into a [`VideoFrame`].
60///
61/// Supports common image formats: JPEG, PNG, BMP, TIFF, WebP.
62///
63/// # Construction
64///
65/// Use [`ImageDecoder::open()`] to create a builder, then call
66/// [`ImageDecoderBuilder::build()`]:
67///
68/// ```ignore
69/// use ff_decode::ImageDecoder;
70///
71/// let frame = ImageDecoder::open("photo.png").build()?.decode()?;
72/// println!("{}x{}", frame.width(), frame.height());
73/// ```
74///
75/// # Frame Decoding
76///
77/// The image can be decoded as a single frame or via an iterator:
78///
79/// ```ignore
80/// // Single frame (consuming)
81/// let frame = decoder.decode()?;
82///
83/// // Via iterator (for API consistency with VideoDecoder / AudioDecoder)
84/// for frame in decoder.frames() {
85///     let frame = frame?;
86/// }
87/// ```
88pub struct ImageDecoder {
89    /// Inner `FFmpeg` state; `None` after the frame has been decoded.
90    inner: Option<ImageDecoderInner>,
91    /// Cached width so it remains accessible after `decode_one` consumes `inner`.
92    width: u32,
93    /// Cached height so it remains accessible after `decode_one` consumes `inner`.
94    height: u32,
95}
96
97impl ImageDecoder {
98    /// Creates a builder for the specified image file path.
99    ///
100    /// # Note
101    ///
102    /// This method does not validate that the file exists or is a valid image.
103    /// Validation occurs when [`ImageDecoderBuilder::build()`] is called.
104    pub fn open(path: impl AsRef<Path>) -> ImageDecoderBuilder {
105        ImageDecoderBuilder::new(path.as_ref().to_path_buf())
106    }
107
108    /// Returns the image width in pixels.
109    #[must_use]
110    pub fn width(&self) -> u32 {
111        self.width
112    }
113
114    /// Returns the image height in pixels.
115    #[must_use]
116    pub fn height(&self) -> u32 {
117        self.height
118    }
119
120    /// Decodes the image frame.
121    ///
122    /// Returns `Ok(Some(frame))` on the first call, then `Ok(None)` on
123    /// subsequent calls (the underlying `FFmpeg` context is consumed on first
124    /// decode).
125    ///
126    /// # Errors
127    ///
128    /// Returns [`DecodeError`] if `FFmpeg` fails to decode the image.
129    pub fn decode_one(&mut self) -> Result<Option<VideoFrame>, DecodeError> {
130        let Some(inner) = self.inner.take() else {
131            return Ok(None);
132        };
133        Ok(Some(inner.decode()?))
134    }
135
136    /// Returns an iterator that yields the single decoded image frame.
137    ///
138    /// This method exists for API consistency with [`VideoDecoder`] and
139    /// [`AudioDecoder`].  The iterator yields at most one item.
140    ///
141    /// [`VideoDecoder`]: crate::VideoDecoder
142    /// [`AudioDecoder`]: crate::AudioDecoder
143    pub fn frames(&mut self) -> ImageFrameIterator<'_> {
144        ImageFrameIterator { decoder: self }
145    }
146
147    /// Decodes the image, consuming `self` and returning the [`VideoFrame`].
148    ///
149    /// This is a convenience wrapper around [`decode_one`](Self::decode_one)
150    /// for the common single-frame use-case.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`DecodeError`] if the image cannot be decoded or was already
155    /// decoded.
156    pub fn decode(mut self) -> Result<VideoFrame, DecodeError> {
157        self.decode_one()?.ok_or_else(|| DecodeError::Ffmpeg {
158            code: 0,
159            message: "Image already decoded".to_string(),
160        })
161    }
162}
163
164/// Iterator over the decoded image frame.
165///
166/// Created by calling [`ImageDecoder::frames()`]. Yields exactly one item —
167/// the decoded [`VideoFrame`] — then returns `None`.
168///
169/// This type exists for API consistency with [`VideoFrameIterator`] and
170/// [`AudioFrameIterator`].
171///
172/// [`VideoFrameIterator`]: crate::VideoFrameIterator
173/// [`AudioFrameIterator`]: crate::AudioFrameIterator
174pub struct ImageFrameIterator<'a> {
175    decoder: &'a mut ImageDecoder,
176}
177
178impl Iterator for ImageFrameIterator<'_> {
179    type Item = Result<VideoFrame, DecodeError>;
180
181    fn next(&mut self) -> Option<Self::Item> {
182        match self.decoder.decode_one() {
183            Ok(Some(frame)) => Some(Ok(frame)),
184            Ok(None) => None,
185            Err(e) => Some(Err(e)),
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::path::PathBuf;
194
195    #[test]
196    fn open_nonexistent_file_should_return_file_not_found() {
197        let result = ImageDecoder::open("nonexistent_image_12345.png").build();
198        assert!(result.is_err());
199        assert!(matches!(result, Err(DecodeError::FileNotFound { .. })));
200    }
201
202    #[test]
203    fn builder_new_should_store_path() {
204        let builder = ImageDecoderBuilder::new(PathBuf::from("photo.png"));
205        assert_eq!(builder.path, PathBuf::from("photo.png"));
206    }
207}