From e7142604d48b994067517964e236f810338c64a3 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 01:42:35 +0200 Subject: [PATCH] Add Preset type with 8 built-in presets and JSON serialization All 5 preset tests passing. --- pixstrip-core/src/preset.rs | 218 +++++++++++++++++++++++++++- pixstrip-core/tests/preset_tests.rs | 59 ++++++++ 2 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 pixstrip-core/tests/preset_tests.rs diff --git a/pixstrip-core/src/preset.rs b/pixstrip-core/src/preset.rs index bf2e962..37720ad 100644 --- a/pixstrip-core/src/preset.rs +++ b/pixstrip-core/src/preset.rs @@ -1 +1,217 @@ -// Preset management +use serde::{Deserialize, Serialize}; + +use crate::operations::*; +use crate::pipeline::ProcessingJob; +use crate::types::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Preset { + pub name: String, + pub description: String, + pub icon: String, + pub is_custom: bool, + pub resize: Option, + pub rotation: Option, + pub flip: Option, + pub convert: Option, + pub compress: Option, + pub metadata: Option, + pub watermark: Option, + pub rename: Option, +} + +impl Preset { + pub fn to_job( + &self, + input_dir: impl Into, + output_dir: impl Into, + ) -> ProcessingJob { + ProcessingJob { + input_dir: input_dir.into(), + output_dir: output_dir.into(), + sources: Vec::new(), + resize: self.resize.clone(), + rotation: self.rotation, + flip: self.flip, + convert: self.convert.clone(), + compress: self.compress.clone(), + metadata: self.metadata.clone(), + watermark: self.watermark.clone(), + rename: self.rename.clone(), + preserve_directory_structure: false, + } + } + + pub fn all_builtins() -> Vec { + vec![ + Self::builtin_blog_photos(), + Self::builtin_social_media(), + Self::builtin_web_optimization(), + Self::builtin_email_friendly(), + Self::builtin_privacy_clean(), + Self::builtin_photographer_export(), + Self::builtin_archive_compress(), + Self::builtin_fediverse_ready(), + ] + } + + pub fn builtin_blog_photos() -> Preset { + Preset { + name: "Blog Photos".into(), + description: "Resize 1200px wide, JPEG quality High, strip all metadata".into(), + icon: "image-x-generic-symbolic".into(), + is_custom: false, + resize: Some(ResizeConfig::ByWidth(1200)), + rotation: None, + flip: None, + convert: None, + compress: Some(CompressConfig::Preset(QualityPreset::High)), + metadata: Some(MetadataConfig::StripAll), + watermark: None, + rename: None, + } + } + + pub fn builtin_social_media() -> Preset { + Preset { + name: "Social Media".into(), + description: "Resize to fit 1080x1080, compress Medium, strip metadata".into(), + icon: "system-users-symbolic".into(), + is_custom: false, + resize: Some(ResizeConfig::FitInBox { + max: Dimensions { + width: 1080, + height: 1080, + }, + allow_upscale: false, + }), + rotation: None, + flip: None, + convert: None, + compress: Some(CompressConfig::Preset(QualityPreset::Medium)), + metadata: Some(MetadataConfig::StripAll), + watermark: None, + rename: None, + } + } + + pub fn builtin_web_optimization() -> Preset { + Preset { + name: "Web Optimization".into(), + description: "Convert to WebP, compress High, sequential rename".into(), + icon: "web-browser-symbolic".into(), + is_custom: false, + resize: None, + rotation: None, + flip: None, + convert: Some(ConvertConfig::SingleFormat(ImageFormat::WebP)), + compress: Some(CompressConfig::Preset(QualityPreset::High)), + metadata: Some(MetadataConfig::StripAll), + watermark: None, + rename: Some(RenameConfig { + prefix: String::new(), + suffix: String::new(), + counter_start: 1, + counter_padding: 3, + template: None, + }), + } + } + + pub fn builtin_email_friendly() -> Preset { + Preset { + name: "Email Friendly".into(), + description: "Resize 800px wide, JPEG quality Medium".into(), + icon: "mail-unread-symbolic".into(), + is_custom: false, + resize: Some(ResizeConfig::ByWidth(800)), + rotation: None, + flip: None, + convert: None, + compress: Some(CompressConfig::Preset(QualityPreset::Medium)), + metadata: Some(MetadataConfig::StripAll), + watermark: None, + rename: None, + } + } + + pub fn builtin_privacy_clean() -> Preset { + Preset { + name: "Privacy Clean".into(), + description: "Strip all metadata, no other changes".into(), + icon: "security-high-symbolic".into(), + is_custom: false, + resize: None, + rotation: None, + flip: None, + convert: None, + compress: None, + metadata: Some(MetadataConfig::StripAll), + watermark: None, + rename: None, + } + } + + pub fn builtin_photographer_export() -> Preset { + Preset { + name: "Photographer Export".into(), + description: "Resize 2048px, compress High, privacy metadata, rename by date".into(), + icon: "camera-photo-symbolic".into(), + is_custom: false, + resize: Some(ResizeConfig::ByWidth(2048)), + rotation: None, + flip: None, + convert: None, + compress: Some(CompressConfig::Preset(QualityPreset::High)), + metadata: Some(MetadataConfig::Privacy), + watermark: None, + rename: Some(RenameConfig { + prefix: String::new(), + suffix: String::new(), + counter_start: 1, + counter_padding: 4, + template: Some("{exif_date}_{name}_{counter:4}".into()), + }), + } + } + + pub fn builtin_archive_compress() -> Preset { + Preset { + name: "Archive Compress".into(), + description: "Lossless compression, preserve metadata".into(), + icon: "folder-symbolic".into(), + is_custom: false, + resize: None, + rotation: None, + flip: None, + convert: None, + compress: Some(CompressConfig::Preset(QualityPreset::Maximum)), + metadata: Some(MetadataConfig::KeepAll), + watermark: None, + rename: None, + } + } + + pub fn builtin_fediverse_ready() -> Preset { + Preset { + name: "Fediverse Ready".into(), + description: "Resize 1920x1080, convert to WebP, compress High, strip metadata".into(), + icon: "network-server-symbolic".into(), + is_custom: false, + resize: Some(ResizeConfig::FitInBox { + max: Dimensions { + width: 1920, + height: 1080, + }, + allow_upscale: false, + }), + rotation: None, + flip: None, + convert: Some(ConvertConfig::SingleFormat(ImageFormat::WebP)), + compress: Some(CompressConfig::Preset(QualityPreset::High)), + metadata: Some(MetadataConfig::StripAll), + watermark: None, + rename: None, + } + } +} diff --git a/pixstrip-core/tests/preset_tests.rs b/pixstrip-core/tests/preset_tests.rs new file mode 100644 index 0000000..12c17bc --- /dev/null +++ b/pixstrip-core/tests/preset_tests.rs @@ -0,0 +1,59 @@ +use pixstrip_core::preset::*; +use pixstrip_core::operations::*; + +#[test] +fn builtin_preset_blog_photos() { + let preset = Preset::builtin_blog_photos(); + assert_eq!(preset.name, "Blog Photos"); + assert!(preset.resize.is_some()); + assert!(preset.compress.is_some()); + assert!(preset.metadata.is_some()); + assert!(!preset.is_custom); +} + +#[test] +fn builtin_preset_privacy_clean() { + let preset = Preset::builtin_privacy_clean(); + assert_eq!(preset.name, "Privacy Clean"); + assert!(preset.resize.is_none()); + assert!(preset.compress.is_none()); + assert!(preset.metadata.is_some()); + if let Some(MetadataConfig::StripAll) = &preset.metadata { + // correct + } else { + panic!("Expected StripAll metadata config"); + } +} + +#[test] +fn preset_serialization_roundtrip() { + let preset = Preset::builtin_blog_photos(); + let json = serde_json::to_string_pretty(&preset).unwrap(); + let deserialized: Preset = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.name, preset.name); + assert_eq!(deserialized.is_custom, preset.is_custom); +} + +#[test] +fn all_builtin_presets() { + let presets = Preset::all_builtins(); + assert_eq!(presets.len(), 8); + let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect(); + assert!(names.contains(&"Blog Photos")); + assert!(names.contains(&"Social Media")); + assert!(names.contains(&"Web Optimization")); + assert!(names.contains(&"Email Friendly")); + assert!(names.contains(&"Privacy Clean")); + assert!(names.contains(&"Photographer Export")); + assert!(names.contains(&"Archive Compress")); + assert!(names.contains(&"Fediverse Ready")); +} + +#[test] +fn preset_to_processing_job() { + let preset = Preset::builtin_blog_photos(); + let job = preset.to_job("/tmp/input/", "/tmp/output/"); + assert!(job.resize.is_some()); + assert!(job.compress.is_some()); + assert!(job.metadata.is_some()); +}