ff_encode/image/
builder.rs1use std::path::{Path, PathBuf};
4
5use ff_format::{PixelFormat, VideoFrame};
6
7use crate::EncodeError;
8
9use super::encoder_inner;
10
11#[derive(Debug)]
16pub struct ImageEncoderBuilder {
17 path: PathBuf,
18 width: Option<u32>,
19 height: Option<u32>,
20 quality: Option<u32>,
21 pixel_format: Option<PixelFormat>,
22}
23
24impl ImageEncoderBuilder {
25 pub(crate) fn new(path: PathBuf) -> Self {
26 Self {
27 path,
28 width: None,
29 height: None,
30 quality: None,
31 pixel_format: None,
32 }
33 }
34
35 #[must_use]
40 pub fn width(mut self, w: u32) -> Self {
41 self.width = Some(w);
42 self
43 }
44
45 #[must_use]
50 pub fn height(mut self, h: u32) -> Self {
51 self.height = Some(h);
52 self
53 }
54
55 #[must_use]
63 pub fn quality(mut self, q: u32) -> Self {
64 self.quality = Some(q);
65 self
66 }
67
68 #[must_use]
74 pub fn pixel_format(mut self, fmt: PixelFormat) -> Self {
75 self.pixel_format = Some(fmt);
76 self
77 }
78
79 pub fn build(self) -> Result<ImageEncoder, EncodeError> {
86 encoder_inner::codec_from_extension(&self.path)?;
87 if let Some(0) = self.width {
88 return Err(EncodeError::InvalidConfig {
89 reason: "width must be non-zero".to_string(),
90 });
91 }
92 if let Some(0) = self.height {
93 return Err(EncodeError::InvalidConfig {
94 reason: "height must be non-zero".to_string(),
95 });
96 }
97 Ok(ImageEncoder {
98 path: self.path,
99 width: self.width,
100 height: self.height,
101 quality: self.quality,
102 pixel_format: self.pixel_format,
103 })
104 }
105}
106
107#[derive(Debug)]
126pub struct ImageEncoder {
127 path: PathBuf,
128 width: Option<u32>,
129 height: Option<u32>,
130 quality: Option<u32>,
131 pixel_format: Option<PixelFormat>,
132}
133
134impl ImageEncoder {
135 pub fn create(path: impl AsRef<Path>) -> ImageEncoderBuilder {
140 ImageEncoderBuilder::new(path.as_ref().to_path_buf())
141 }
142
143 #[allow(clippy::new_ret_no_self)]
145 pub fn new(path: impl AsRef<Path>) -> ImageEncoderBuilder {
146 ImageEncoderBuilder::new(path.as_ref().to_path_buf())
147 }
148
149 pub fn encode(self, frame: &VideoFrame) -> Result<(), EncodeError> {
160 let opts = encoder_inner::ImageEncodeOptions {
161 width: self.width,
162 height: self.height,
163 quality: self.quality,
164 pixel_format: self.pixel_format,
165 };
166 unsafe { encoder_inner::encode_image(&self.path, frame, &opts) }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn create_should_return_builder() {
178 let _builder = ImageEncoder::create("out.png");
179 }
180
181 #[test]
182 fn new_should_return_builder() {
183 let _builder = ImageEncoder::new("out.png");
184 }
185
186 #[test]
187 fn build_with_unsupported_extension_should_return_error() {
188 let result = ImageEncoder::create("out.avi").build();
189 assert!(
190 matches!(result, Err(EncodeError::UnsupportedCodec { .. })),
191 "expected UnsupportedCodec, got {result:?}"
192 );
193 }
194
195 #[test]
196 fn build_with_no_extension_should_return_error() {
197 let result = ImageEncoder::create("out_no_ext").build();
198 assert!(
199 matches!(result, Err(EncodeError::InvalidConfig { .. })),
200 "expected InvalidConfig, got {result:?}"
201 );
202 }
203
204 #[test]
205 fn build_with_zero_width_should_return_error() {
206 let result = ImageEncoder::create("out.png").width(0).build();
207 assert!(
208 matches!(result, Err(EncodeError::InvalidConfig { .. })),
209 "expected InvalidConfig for zero width, got {result:?}"
210 );
211 }
212
213 #[test]
214 fn build_with_zero_height_should_return_error() {
215 let result = ImageEncoder::create("out.png").height(0).build();
216 assert!(
217 matches!(result, Err(EncodeError::InvalidConfig { .. })),
218 "expected InvalidConfig for zero height, got {result:?}"
219 );
220 }
221
222 #[test]
223 fn width_setter_should_store_value() {
224 let encoder = ImageEncoder::create("out.png").width(320).build().unwrap();
225 assert_eq!(encoder.width, Some(320));
226 }
227
228 #[test]
229 fn height_setter_should_store_value() {
230 let encoder = ImageEncoder::create("out.png").height(240).build().unwrap();
231 assert_eq!(encoder.height, Some(240));
232 }
233
234 #[test]
235 fn quality_setter_should_store_value() {
236 let encoder = ImageEncoder::create("out.png").quality(75).build().unwrap();
237 assert_eq!(encoder.quality, Some(75));
238 }
239
240 #[test]
241 fn pixel_format_setter_should_store_value() {
242 let encoder = ImageEncoder::create("out.png")
243 .pixel_format(PixelFormat::Rgb24)
244 .build()
245 .unwrap();
246 assert_eq!(encoder.pixel_format, Some(PixelFormat::Rgb24));
247 }
248
249 #[test]
250 fn build_with_only_width_should_succeed() {
251 let result = ImageEncoder::create("out.png").width(128).build();
253 assert!(result.is_ok(), "expected Ok, got {result:?}");
254 }
255
256 #[test]
257 fn build_with_all_options_should_succeed() {
258 let result = ImageEncoder::create("out.jpg")
259 .width(320)
260 .height(240)
261 .quality(80)
262 .pixel_format(PixelFormat::Yuv420p)
263 .build();
264 assert!(result.is_ok(), "expected Ok, got {result:?}");
265 }
266}