mod preview_inner;
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::EncodeError;
pub struct SpriteSheet {
input: PathBuf,
cols: u32,
rows: u32,
frame_width: u32,
frame_height: u32,
output: PathBuf,
}
impl SpriteSheet {
pub fn new(input: impl AsRef<Path>) -> Self {
Self {
input: input.as_ref().to_path_buf(),
cols: 10,
rows: 10,
frame_width: 160,
frame_height: 90,
output: PathBuf::new(),
}
}
#[must_use]
pub fn cols(self, n: u32) -> Self {
Self { cols: n, ..self }
}
#[must_use]
pub fn rows(self, n: u32) -> Self {
Self { rows: n, ..self }
}
#[must_use]
pub fn frame_width(self, w: u32) -> Self {
Self {
frame_width: w,
..self
}
}
#[must_use]
pub fn frame_height(self, h: u32) -> Self {
Self {
frame_height: h,
..self
}
}
#[must_use]
pub fn output(self, path: impl AsRef<Path>) -> Self {
Self {
output: path.as_ref().to_path_buf(),
..self
}
}
pub fn run(self) -> Result<(), EncodeError> {
if self.cols == 0 || self.rows == 0 {
return Err(EncodeError::MediaOperationFailed {
reason: "cols/rows must be > 0".to_string(),
});
}
if self.frame_width == 0 || self.frame_height == 0 {
return Err(EncodeError::MediaOperationFailed {
reason: "frame_width/frame_height must be > 0".to_string(),
});
}
if self.output.as_os_str().is_empty() {
return Err(EncodeError::MediaOperationFailed {
reason: "output path not set".to_string(),
});
}
preview_inner::generate_sprite_sheet(
&self.input,
self.cols,
self.rows,
self.frame_width,
self.frame_height,
&self.output,
)
}
}
pub struct GifPreview {
input: PathBuf,
start: Duration,
duration: Duration,
fps: f64,
width: u32,
output: PathBuf,
}
impl GifPreview {
pub fn new(input: impl AsRef<Path>) -> Self {
Self {
input: input.as_ref().to_path_buf(),
start: Duration::ZERO,
duration: Duration::from_secs(3),
fps: 10.0,
width: 320,
output: PathBuf::new(),
}
}
#[must_use]
pub fn start(self, t: Duration) -> Self {
Self { start: t, ..self }
}
#[must_use]
pub fn duration(self, d: Duration) -> Self {
Self {
duration: d,
..self
}
}
#[must_use]
pub fn fps(self, fps: f64) -> Self {
Self { fps, ..self }
}
#[must_use]
pub fn width(self, w: u32) -> Self {
Self { width: w, ..self }
}
#[must_use]
pub fn output(self, path: impl AsRef<Path>) -> Self {
Self {
output: path.as_ref().to_path_buf(),
..self
}
}
pub fn run(self) -> Result<(), EncodeError> {
if self.output.as_os_str().is_empty() {
return Err(EncodeError::MediaOperationFailed {
reason: "output path not set".to_string(),
});
}
if self.output.extension().and_then(|e| e.to_str()) != Some("gif") {
return Err(EncodeError::MediaOperationFailed {
reason: "output path must have .gif extension".to_string(),
});
}
if self.fps <= 0.0 {
return Err(EncodeError::MediaOperationFailed {
reason: "fps must be positive".to_string(),
});
}
if self.width == 0 {
return Err(EncodeError::MediaOperationFailed {
reason: "width must be > 0".to_string(),
});
}
preview_inner::generate_gif_preview(
&self.input,
self.start,
self.duration,
self.fps,
self.width,
&self.output,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sprite_sheet_zero_cols_should_return_media_operation_failed() {
let result = SpriteSheet::new("irrelevant.mp4")
.cols(0)
.output("out.png")
.run();
assert!(
matches!(result, Err(EncodeError::MediaOperationFailed { .. })),
"expected MediaOperationFailed for cols=0, got {result:?}"
);
}
#[test]
fn sprite_sheet_zero_frame_width_should_return_media_operation_failed() {
let result = SpriteSheet::new("irrelevant.mp4")
.frame_width(0)
.output("out.png")
.run();
assert!(
matches!(result, Err(EncodeError::MediaOperationFailed { .. })),
"expected MediaOperationFailed for frame_width=0, got {result:?}"
);
}
#[test]
fn sprite_sheet_missing_output_should_return_media_operation_failed() {
let result = SpriteSheet::new("irrelevant.mp4").run();
assert!(
matches!(result, Err(EncodeError::MediaOperationFailed { .. })),
"expected MediaOperationFailed for empty output path, got {result:?}"
);
}
#[test]
fn gif_preview_non_gif_extension_should_return_media_operation_failed() {
let result = GifPreview::new("irrelevant.mp4").output("out.mp4").run();
assert!(
matches!(result, Err(EncodeError::MediaOperationFailed { .. })),
"expected MediaOperationFailed for non-.gif extension, got {result:?}"
);
}
#[test]
fn gif_preview_missing_output_should_return_media_operation_failed() {
let result = GifPreview::new("irrelevant.mp4").run();
assert!(
matches!(result, Err(EncodeError::MediaOperationFailed { .. })),
"expected MediaOperationFailed for missing output path, got {result:?}"
);
}
#[test]
fn gif_preview_zero_fps_should_return_media_operation_failed() {
let result = GifPreview::new("irrelevant.mp4")
.fps(0.0)
.output("out.gif")
.run();
assert!(
matches!(result, Err(EncodeError::MediaOperationFailed { .. })),
"expected MediaOperationFailed for fps=0, got {result:?}"
);
}
}