Add error types and core image types: ImageFormat, ImageSource, Dimensions, QualityPreset
This commit is contained in:
@@ -1 +1,27 @@
|
|||||||
// Error types
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum PixstripError {
|
||||||
|
#[error("Failed to load image '{path}': {reason}")]
|
||||||
|
ImageLoad { path: PathBuf, reason: String },
|
||||||
|
|
||||||
|
#[error("Unsupported format: {format}")]
|
||||||
|
UnsupportedFormat { format: String },
|
||||||
|
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Output file already exists: {path}")]
|
||||||
|
OutputExists { path: PathBuf },
|
||||||
|
|
||||||
|
#[error("Processing error in '{operation}': {reason}")]
|
||||||
|
Processing { operation: String, reason: String },
|
||||||
|
|
||||||
|
#[error("Preset error: {0}")]
|
||||||
|
Preset(String),
|
||||||
|
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
Config(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, PixstripError>;
|
||||||
|
|||||||
@@ -1 +1,147 @@
|
|||||||
// Core domain types for image processing
|
use std::fmt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum ImageFormat {
|
||||||
|
Jpeg,
|
||||||
|
Png,
|
||||||
|
WebP,
|
||||||
|
Avif,
|
||||||
|
Gif,
|
||||||
|
Tiff,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageFormat {
|
||||||
|
pub fn from_extension(ext: &str) -> Option<Self> {
|
||||||
|
match ext.to_lowercase().as_str() {
|
||||||
|
"jpg" | "jpeg" => Some(Self::Jpeg),
|
||||||
|
"png" => Some(Self::Png),
|
||||||
|
"webp" => Some(Self::WebP),
|
||||||
|
"avif" => Some(Self::Avif),
|
||||||
|
"gif" => Some(Self::Gif),
|
||||||
|
"tiff" | "tif" => Some(Self::Tiff),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extension(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Jpeg => "jpg",
|
||||||
|
Self::Png => "png",
|
||||||
|
Self::WebP => "webp",
|
||||||
|
Self::Avif => "avif",
|
||||||
|
Self::Gif => "gif",
|
||||||
|
Self::Tiff => "tiff",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ImageSource {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub original_format: Option<ImageFormat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageSource {
|
||||||
|
pub fn from_path(path: impl AsRef<Path>) -> Self {
|
||||||
|
let path = path.as_ref().to_path_buf();
|
||||||
|
let original_format = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.and_then(ImageFormat::from_extension);
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
original_format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Dimensions {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dimensions {
|
||||||
|
pub fn aspect_ratio(&self) -> f64 {
|
||||||
|
self.width as f64 / self.height as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fit_within(self, max: Dimensions, allow_upscale: bool) -> Dimensions {
|
||||||
|
if !allow_upscale && self.width <= max.width && self.height <= max.height {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale_w = max.width as f64 / self.width as f64;
|
||||||
|
let scale_h = max.height as f64 / self.height as f64;
|
||||||
|
let scale = scale_w.min(scale_h);
|
||||||
|
|
||||||
|
if !allow_upscale && scale >= 1.0 {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dimensions {
|
||||||
|
width: (self.width as f64 * scale).round() as u32,
|
||||||
|
height: (self.height as f64 * scale).round() as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Dimensions {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}x{}", self.width, self.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum QualityPreset {
|
||||||
|
Maximum,
|
||||||
|
High,
|
||||||
|
Medium,
|
||||||
|
Low,
|
||||||
|
WebOptimized,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QualityPreset {
|
||||||
|
pub fn jpeg_quality(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Maximum => 95,
|
||||||
|
Self::High => 85,
|
||||||
|
Self::Medium => 72,
|
||||||
|
Self::Low => 55,
|
||||||
|
Self::WebOptimized => 65,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn png_level(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Maximum => 2,
|
||||||
|
Self::High => 3,
|
||||||
|
Self::Medium => 4,
|
||||||
|
Self::Low => 6,
|
||||||
|
Self::WebOptimized => 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn webp_quality(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
Self::Maximum => 95.0,
|
||||||
|
Self::High => 85.0,
|
||||||
|
Self::Medium => 72.0,
|
||||||
|
Self::Low => 50.0,
|
||||||
|
Self::WebOptimized => 75.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Maximum => "Maximum",
|
||||||
|
Self::High => "High",
|
||||||
|
Self::Medium => "Medium",
|
||||||
|
Self::Low => "Low",
|
||||||
|
Self::WebOptimized => "Web-optimized",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
46
pixstrip-core/tests/error_tests.rs
Normal file
46
pixstrip-core/tests/error_tests.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use pixstrip_core::error::PixstripError;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_display_image_load() {
|
||||||
|
let err = PixstripError::ImageLoad {
|
||||||
|
path: "/tmp/bad.jpg".into(),
|
||||||
|
reason: "corrupt header".into(),
|
||||||
|
};
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(msg.contains("/tmp/bad.jpg"));
|
||||||
|
assert!(msg.contains("corrupt header"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_display_unsupported_format() {
|
||||||
|
let err = PixstripError::UnsupportedFormat {
|
||||||
|
format: "bmp".into(),
|
||||||
|
};
|
||||||
|
assert!(err.to_string().contains("bmp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_display_io() {
|
||||||
|
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
|
||||||
|
let err = PixstripError::Io(io_err);
|
||||||
|
assert!(err.to_string().contains("file missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_display_output_exists() {
|
||||||
|
let err = PixstripError::OutputExists {
|
||||||
|
path: "/tmp/out.jpg".into(),
|
||||||
|
};
|
||||||
|
assert!(err.to_string().contains("/tmp/out.jpg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_display_processing() {
|
||||||
|
let err = PixstripError::Processing {
|
||||||
|
operation: "resize".into(),
|
||||||
|
reason: "invalid dimensions".into(),
|
||||||
|
};
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(msg.contains("resize"));
|
||||||
|
assert!(msg.contains("invalid dimensions"));
|
||||||
|
}
|
||||||
83
pixstrip-core/tests/types_tests.rs
Normal file
83
pixstrip-core/tests/types_tests.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use pixstrip_core::types::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_format_from_extension() {
|
||||||
|
assert_eq!(ImageFormat::from_extension("jpg"), Some(ImageFormat::Jpeg));
|
||||||
|
assert_eq!(ImageFormat::from_extension("JPEG"), Some(ImageFormat::Jpeg));
|
||||||
|
assert_eq!(ImageFormat::from_extension("png"), Some(ImageFormat::Png));
|
||||||
|
assert_eq!(ImageFormat::from_extension("webp"), Some(ImageFormat::WebP));
|
||||||
|
assert_eq!(ImageFormat::from_extension("avif"), Some(ImageFormat::Avif));
|
||||||
|
assert_eq!(ImageFormat::from_extension("gif"), Some(ImageFormat::Gif));
|
||||||
|
assert_eq!(ImageFormat::from_extension("tiff"), Some(ImageFormat::Tiff));
|
||||||
|
assert_eq!(ImageFormat::from_extension("tif"), Some(ImageFormat::Tiff));
|
||||||
|
assert_eq!(ImageFormat::from_extension("xyz"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_format_extension_string() {
|
||||||
|
assert_eq!(ImageFormat::Jpeg.extension(), "jpg");
|
||||||
|
assert_eq!(ImageFormat::Png.extension(), "png");
|
||||||
|
assert_eq!(ImageFormat::WebP.extension(), "webp");
|
||||||
|
assert_eq!(ImageFormat::Avif.extension(), "avif");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_source_from_path() {
|
||||||
|
let source = ImageSource::from_path("/tmp/photo.jpg");
|
||||||
|
assert_eq!(source.path, PathBuf::from("/tmp/photo.jpg"));
|
||||||
|
assert_eq!(source.original_format, Some(ImageFormat::Jpeg));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_source_unknown_format() {
|
||||||
|
let source = ImageSource::from_path("/tmp/file.xyz");
|
||||||
|
assert_eq!(source.original_format, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dimensions_display() {
|
||||||
|
let dims = Dimensions { width: 1920, height: 1080 };
|
||||||
|
assert_eq!(dims.to_string(), "1920x1080");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dimensions_aspect_ratio() {
|
||||||
|
let dims = Dimensions { width: 1920, height: 1080 };
|
||||||
|
let ratio = dims.aspect_ratio();
|
||||||
|
assert!((ratio - 16.0 / 9.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dimensions_fit_within() {
|
||||||
|
let original = Dimensions { width: 4000, height: 3000 };
|
||||||
|
let max_box = Dimensions { width: 1200, height: 1200 };
|
||||||
|
let fitted = original.fit_within(max_box, false);
|
||||||
|
assert_eq!(fitted.width, 1200);
|
||||||
|
assert_eq!(fitted.height, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dimensions_fit_within_no_upscale() {
|
||||||
|
let original = Dimensions { width: 800, height: 600 };
|
||||||
|
let max_box = Dimensions { width: 1200, height: 1200 };
|
||||||
|
let fitted = original.fit_within(max_box, false);
|
||||||
|
assert_eq!(fitted.width, 800);
|
||||||
|
assert_eq!(fitted.height, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dimensions_fit_within_allow_upscale() {
|
||||||
|
let original = Dimensions { width: 800, height: 600 };
|
||||||
|
let max_box = Dimensions { width: 1200, height: 1200 };
|
||||||
|
let fitted = original.fit_within(max_box, true);
|
||||||
|
assert_eq!(fitted.width, 1200);
|
||||||
|
assert_eq!(fitted.height, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quality_preset_values() {
|
||||||
|
assert!(QualityPreset::Maximum.jpeg_quality() > QualityPreset::High.jpeg_quality());
|
||||||
|
assert!(QualityPreset::High.jpeg_quality() > QualityPreset::Medium.jpeg_quality());
|
||||||
|
assert!(QualityPreset::Medium.jpeg_quality() > QualityPreset::Low.jpeg_quality());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user