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}