use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
use crate::codec::{EncodeOptions, ImageData, get_codec};
use crate::crop::{self, CropMode};
use crate::error::{Error, Result};
use crate::extend::{self, ExtendMode, FillColor};
use crate::format::Format;
use crate::resize::{self, ResizeMode};
#[derive(Debug, Clone)]
pub struct PipelineOptions {
pub format: Format,
pub quality: u8,
pub resize: Option<ResizeMode>,
pub crop: Option<CropMode>,
pub extend: Option<ExtendMode>,
pub fill_color: Option<FillColor>,
}
#[derive(Debug, Clone)]
pub struct PipelineResult {
pub data: Vec<u8>,
pub format: Format,
pub width: u32,
pub height: u32,
}
impl PipelineResult {
pub fn save(&self, path: &Path) -> Result<()> {
fs::write(path, &self.data)?;
Ok(())
}
}
pub fn decode(data: &[u8]) -> Result<(ImageData, Format)> {
let format = Format::from_magic_bytes(data)
.ok_or_else(|| Error::UnknownFormat("unrecognised magic bytes".to_string()))?;
let codec = get_codec(format);
let image = codec.decode(data)?;
Ok((image, format))
}
pub fn decode_file(path: &Path) -> Result<(ImageData, Format)> {
let data = fs::read(path)?;
decode(&data)
}
pub fn convert(image: &ImageData, options: &PipelineOptions) -> Result<PipelineResult> {
if !options.format.can_encode() {
return Err(Error::EncodingNotSupported(options.format));
}
let image = match &options.crop {
Some(mode) => Cow::Owned(crop::crop(image, mode)?),
None => Cow::Borrowed(image),
};
let image = match &options.extend {
Some(mode) => {
let fill = options
.fill_color
.unwrap_or(FillColor::Solid([255, 255, 255, 255]));
Cow::Owned(extend::extend(&image, mode, &fill)?)
}
None => image,
};
let image = match &options.resize {
Some(mode) => Cow::Owned(resize::resize(&image, mode)?),
None => image,
};
let codec = get_codec(options.format);
let encode_opts = EncodeOptions {
quality: options.quality,
};
let data = codec.encode(&image, &encode_opts)?;
Ok(PipelineResult {
data,
format: options.format,
width: image.width,
height: image.height,
})
}
pub fn optimize(data: &[u8], quality: u8) -> Result<PipelineResult> {
let (image, format) = decode(data)?;
if !format.can_encode() {
return Err(Error::EncodingNotSupported(format));
}
let codec = get_codec(format);
let encode_opts = EncodeOptions { quality };
let encoded = codec.encode(&image, &encode_opts)?;
Ok(PipelineResult {
data: encoded,
format,
width: image.width,
height: image.height,
})
}
pub fn output_path(input: &Path, format: Format, output: Option<&Path>) -> PathBuf {
let new_ext = format.extension();
match output {
None => input.with_extension(new_ext),
Some(out) if out.is_dir() => {
let stem = input.file_stem().unwrap_or_default();
out.join(stem).with_extension(new_ext)
}
Some(out) => out.to_path_buf(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn output_path_changes_extension() {
let result = output_path(Path::new("/tmp/photo.jpg"), Format::WebP, None);
assert_eq!(result, PathBuf::from("/tmp/photo.webp"));
}
#[test]
fn output_path_with_explicit_output() {
let result = output_path(
Path::new("/tmp/photo.jpg"),
Format::Png,
Some(Path::new("/out/result.png")),
);
assert_eq!(result, PathBuf::from("/out/result.png"));
}
#[test]
fn optimize_reencodes_same_format() {
let image = ImageData::new(4, 4, vec![200u8; 64]);
let options = PipelineOptions {
format: Format::Qoi,
quality: 80,
resize: None,
crop: None,
extend: None,
fill_color: None,
};
let encoded = convert(&image, &options).unwrap();
let result = optimize(&encoded.data, 80).unwrap();
assert_eq!(result.format, Format::Qoi);
assert_eq!(result.width, 4);
assert_eq!(result.height, 4);
assert!(!result.data.is_empty());
}
#[test]
fn optimize_unknown_format_returns_error() {
assert!(optimize(&[0u8; 16], 80).is_err());
}
#[test]
fn pipeline_result_save_writes_file() {
let dir = std::env::temp_dir().join("slimg-pipeline-save-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("out.bin");
let result = PipelineResult {
data: vec![1, 2, 3],
format: Format::Qoi,
width: 1,
height: 1,
};
result.save(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), vec![1, 2, 3]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn jxl_encode_succeeds() {
let image = ImageData::new(2, 2, vec![128u8; 16]);
let options = PipelineOptions {
format: Format::Jxl,
quality: 80,
resize: None,
crop: None,
extend: None,
fill_color: None,
};
let result = convert(&image, &options);
assert!(result.is_ok(), "converting to JXL should succeed");
}
}