use std::path::{Path, PathBuf};
use image::{DynamicImage, RgbaImage};
use crate::error::Error;
fn to_io_error(err: image::ImageError) -> Error {
Error::Parse(err.to_string())
}
fn save_rgba(image: &RgbaImage, path: &Path) -> crate::Result<()> {
let is_jpeg = matches!(
path.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.as_deref(),
Some("jpg") | Some("jpeg")
);
if is_jpeg {
DynamicImage::ImageRgba8(image.clone())
.to_rgb8()
.save(path)
.map_err(to_io_error)
} else {
image.save(path).map_err(to_io_error)
}
}
#[derive(Debug, Clone)]
pub struct ImageSink {
target_dir: PathBuf,
prefix: String,
extension: String,
padding: usize,
overwrite: bool,
next_index: usize,
}
impl ImageSink {
pub fn new(target_dir: impl Into<PathBuf>) -> crate::Result<Self> {
let target_dir = target_dir.into();
std::fs::create_dir_all(&target_dir)?;
Ok(Self {
target_dir,
prefix: "image".to_string(),
extension: "png".to_string(),
padding: 5,
overwrite: false,
next_index: 0,
})
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn with_extension(mut self, extension: impl Into<String>) -> Self {
self.extension = extension.into();
self
}
pub fn with_padding(mut self, padding: usize) -> Self {
self.padding = padding;
self
}
pub fn overwrite(mut self, overwrite: bool) -> Self {
self.overwrite = overwrite;
self
}
pub fn save_image(&mut self, image: &RgbaImage) -> crate::Result<PathBuf> {
loop {
let name = format!(
"{}_{:0width$}.{}",
self.prefix,
self.next_index,
self.extension,
width = self.padding
);
let path = self.target_dir.join(name);
self.next_index += 1;
if !self.overwrite && path.exists() {
continue;
}
save_rgba(image, &path)?;
return Ok(path);
}
}
pub fn save_image_as(&self, image: &RgbaImage, file_name: &str) -> crate::Result<PathBuf> {
let path = self.target_dir.join(file_name);
save_rgba(image, &path)?;
Ok(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn save_image_uses_incrementing_zero_padded_names() {
let dir = tempfile::tempdir().unwrap();
let mut sink = ImageSink::new(dir.path()).unwrap();
let image = RgbaImage::new(2, 2);
let first = sink.save_image(&image).unwrap();
let second = sink.save_image(&image).unwrap();
assert_eq!(first.file_name().unwrap(), "image_00000.png");
assert_eq!(second.file_name().unwrap(), "image_00001.png");
assert!(first.exists());
assert!(second.exists());
}
#[test]
fn with_prefix_extension_and_padding_customize_the_name() {
let dir = tempfile::tempdir().unwrap();
let mut sink = ImageSink::new(dir.path())
.unwrap()
.with_prefix("frame")
.with_extension("jpg")
.with_padding(3);
let image = RgbaImage::new(2, 2);
let path = sink.save_image(&image).unwrap();
assert_eq!(path.file_name().unwrap(), "frame_000.jpg");
}
#[test]
fn without_overwrite_skips_past_existing_files() {
let dir = tempfile::tempdir().unwrap();
let image = RgbaImage::new(2, 2);
image.save(dir.path().join("image_00000.png")).unwrap();
let mut sink = ImageSink::new(dir.path()).unwrap();
let path = sink.save_image(&image).unwrap();
assert_eq!(path.file_name().unwrap(), "image_00001.png");
}
#[test]
fn with_overwrite_reuses_an_existing_name() {
let dir = tempfile::tempdir().unwrap();
let image = RgbaImage::new(2, 2);
image.save(dir.path().join("image_00000.png")).unwrap();
let mut sink = ImageSink::new(dir.path()).unwrap().overwrite(true);
let path = sink.save_image(&image).unwrap();
assert_eq!(path.file_name().unwrap(), "image_00000.png");
}
#[test]
fn save_image_as_bypasses_the_counter() {
let dir = tempfile::tempdir().unwrap();
let sink = ImageSink::new(dir.path()).unwrap();
let image = RgbaImage::new(2, 2);
let path = sink.save_image_as(&image, "custom.png").unwrap();
assert_eq!(path.file_name().unwrap(), "custom.png");
assert!(path.exists());
}
}