use std::path::{Path, PathBuf};
use ff_format::{PixelFormat, VideoFrame};
use crate::EncodeError;
use super::encoder_inner;
#[derive(Debug)]
pub struct ImageEncoderBuilder {
path: PathBuf,
width: Option<u32>,
height: Option<u32>,
quality: Option<u32>,
pixel_format: Option<PixelFormat>,
}
impl ImageEncoderBuilder {
pub(crate) fn new(path: PathBuf) -> Self {
Self {
path,
width: None,
height: None,
quality: None,
pixel_format: None,
}
}
#[must_use]
pub fn width(mut self, w: u32) -> Self {
self.width = Some(w);
self
}
#[must_use]
pub fn height(mut self, h: u32) -> Self {
self.height = Some(h);
self
}
#[must_use]
pub fn quality(mut self, q: u32) -> Self {
self.quality = Some(q);
self
}
#[must_use]
pub fn pixel_format(mut self, fmt: PixelFormat) -> Self {
self.pixel_format = Some(fmt);
self
}
pub fn build(self) -> Result<ImageEncoder, EncodeError> {
encoder_inner::codec_from_extension(&self.path)?;
if let Some(0) = self.width {
return Err(EncodeError::InvalidConfig {
reason: "width must be non-zero".to_string(),
});
}
if let Some(0) = self.height {
return Err(EncodeError::InvalidConfig {
reason: "height must be non-zero".to_string(),
});
}
Ok(ImageEncoder {
path: self.path,
width: self.width,
height: self.height,
quality: self.quality,
pixel_format: self.pixel_format,
})
}
}
#[derive(Debug)]
pub struct ImageEncoder {
path: PathBuf,
width: Option<u32>,
height: Option<u32>,
quality: Option<u32>,
pixel_format: Option<PixelFormat>,
}
impl ImageEncoder {
pub fn create(path: impl AsRef<Path>) -> ImageEncoderBuilder {
ImageEncoderBuilder::new(path.as_ref().to_path_buf())
}
#[allow(clippy::new_ret_no_self)]
pub fn new(path: impl AsRef<Path>) -> ImageEncoderBuilder {
ImageEncoderBuilder::new(path.as_ref().to_path_buf())
}
pub fn encode(self, frame: &VideoFrame) -> Result<(), EncodeError> {
let opts = encoder_inner::ImageEncodeOptions {
width: self.width,
height: self.height,
quality: self.quality,
pixel_format: self.pixel_format,
};
encoder_inner::encode_image(&self.path, frame, &opts)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_should_return_builder() {
let _builder = ImageEncoder::create("out.png");
}
#[test]
fn new_should_return_builder() {
let _builder = ImageEncoder::new("out.png");
}
#[test]
fn build_with_unsupported_extension_should_return_error() {
let result = ImageEncoder::create("out.avi").build();
assert!(
matches!(result, Err(EncodeError::UnsupportedCodec { .. })),
"expected UnsupportedCodec, got {result:?}"
);
}
#[test]
fn build_with_no_extension_should_return_error() {
let result = ImageEncoder::create("out_no_ext").build();
assert!(
matches!(result, Err(EncodeError::InvalidConfig { .. })),
"expected InvalidConfig, got {result:?}"
);
}
#[test]
fn build_with_zero_width_should_return_error() {
let result = ImageEncoder::create("out.png").width(0).build();
assert!(
matches!(result, Err(EncodeError::InvalidConfig { .. })),
"expected InvalidConfig for zero width, got {result:?}"
);
}
#[test]
fn build_with_zero_height_should_return_error() {
let result = ImageEncoder::create("out.png").height(0).build();
assert!(
matches!(result, Err(EncodeError::InvalidConfig { .. })),
"expected InvalidConfig for zero height, got {result:?}"
);
}
#[test]
fn width_setter_should_store_value() {
let encoder = ImageEncoder::create("out.png").width(320).build().unwrap();
assert_eq!(encoder.width, Some(320));
}
#[test]
fn height_setter_should_store_value() {
let encoder = ImageEncoder::create("out.png").height(240).build().unwrap();
assert_eq!(encoder.height, Some(240));
}
#[test]
fn quality_setter_should_store_value() {
let encoder = ImageEncoder::create("out.png").quality(75).build().unwrap();
assert_eq!(encoder.quality, Some(75));
}
#[test]
fn pixel_format_setter_should_store_value() {
let encoder = ImageEncoder::create("out.png")
.pixel_format(PixelFormat::Rgb24)
.build()
.unwrap();
assert_eq!(encoder.pixel_format, Some(PixelFormat::Rgb24));
}
#[test]
fn build_with_only_width_should_succeed() {
let result = ImageEncoder::create("out.png").width(128).build();
assert!(result.is_ok(), "expected Ok, got {result:?}");
}
#[test]
fn build_with_all_options_should_succeed() {
let result = ImageEncoder::create("out.jpg")
.width(320)
.height(240)
.quality(80)
.pixel_format(PixelFormat::Yuv420p)
.build();
assert!(result.is_ok(), "expected Ok, got {result:?}");
}
}