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