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 encoder_inner::encode_image(&self.path, frame, &opts)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn create_should_return_builder() {
176 let _builder = ImageEncoder::create("out.png");
177 }
178
179 #[test]
180 fn new_should_return_builder() {
181 let _builder = ImageEncoder::new("out.png");
182 }
183
184 #[test]
185 fn build_with_unsupported_extension_should_return_error() {
186 let result = ImageEncoder::create("out.avi").build();
187 assert!(
188 matches!(result, Err(EncodeError::UnsupportedCodec { .. })),
189 "expected UnsupportedCodec, got {result:?}"
190 );
191 }
192
193 #[test]
194 fn build_with_no_extension_should_return_error() {
195 let result = ImageEncoder::create("out_no_ext").build();
196 assert!(
197 matches!(result, Err(EncodeError::InvalidConfig { .. })),
198 "expected InvalidConfig, got {result:?}"
199 );
200 }
201
202 #[test]
203 fn build_with_zero_width_should_return_error() {
204 let result = ImageEncoder::create("out.png").width(0).build();
205 assert!(
206 matches!(result, Err(EncodeError::InvalidConfig { .. })),
207 "expected InvalidConfig for zero width, got {result:?}"
208 );
209 }
210
211 #[test]
212 fn build_with_zero_height_should_return_error() {
213 let result = ImageEncoder::create("out.png").height(0).build();
214 assert!(
215 matches!(result, Err(EncodeError::InvalidConfig { .. })),
216 "expected InvalidConfig for zero height, got {result:?}"
217 );
218 }
219
220 #[test]
221 fn width_setter_should_store_value() {
222 let encoder = ImageEncoder::create("out.png").width(320).build().unwrap();
223 assert_eq!(encoder.width, Some(320));
224 }
225
226 #[test]
227 fn height_setter_should_store_value() {
228 let encoder = ImageEncoder::create("out.png").height(240).build().unwrap();
229 assert_eq!(encoder.height, Some(240));
230 }
231
232 #[test]
233 fn quality_setter_should_store_value() {
234 let encoder = ImageEncoder::create("out.png").quality(75).build().unwrap();
235 assert_eq!(encoder.quality, Some(75));
236 }
237
238 #[test]
239 fn pixel_format_setter_should_store_value() {
240 let encoder = ImageEncoder::create("out.png")
241 .pixel_format(PixelFormat::Rgb24)
242 .build()
243 .unwrap();
244 assert_eq!(encoder.pixel_format, Some(PixelFormat::Rgb24));
245 }
246
247 #[test]
248 fn build_with_only_width_should_succeed() {
249 let result = ImageEncoder::create("out.png").width(128).build();
251 assert!(result.is_ok(), "expected Ok, got {result:?}");
252 }
253
254 #[test]
255 fn build_with_all_options_should_succeed() {
256 let result = ImageEncoder::create("out.jpg")
257 .width(320)
258 .height(240)
259 .quality(80)
260 .pixel_format(PixelFormat::Yuv420p)
261 .build();
262 assert!(result.is_ok(), "expected Ok, got {result:?}");
263 }
264}