Add error types and core image types: ImageFormat, ImageSource, Dimensions, QualityPreset

This commit is contained in:
2026-03-06 01:37:39 +02:00
parent dab049b0d3
commit 3e176e3d65
4 changed files with 303 additions and 2 deletions

View File

@@ -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>;

View File

@@ -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",
}
}
}

View 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"));
}

View 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());
}