diff --git a/Cargo.lock b/Cargo.lock index 145df7b..9943e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,12 +260,6 @@ dependencies = [ "syn", ] -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "exr" version = "1.74.0" @@ -345,12 +339,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - [[package]] name = "image" version = "0.25.10" @@ -391,16 +379,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "interpolate_name" version = "0.2.4" @@ -421,6 +399,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jobserver" version = "0.1.34" @@ -813,8 +797,8 @@ dependencies = [ "anyhow", "image", "serde", + "serde_json", "tiny-skia", - "toml", ] [[package]] @@ -854,12 +838,16 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.9" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "itoa", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] @@ -972,47 +960,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -1090,15 +1037,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.57.1" @@ -1131,6 +1069,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zune-core" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index d4d0cf5..1d8f6a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ tiny-skia = "0.11" # Config serialization serde = { version = "1", features = ["derive"] } -toml = "0.8" # Error handling anyhow = "1" +serde_json = "1.0.149" # ── Build profiles ──────────────────────────────────────────────────────────── diff --git a/src/config.rs b/src/config.rs index 1244593..c997238 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,35 +2,20 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -/// Top-level application configuration. -/// Serialized to/from ~/.config/rs-pictures/config.toml #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { - /// Visual effects applied to images. + #[serde(rename = "screenshot")] pub effects: EffectsConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EffectsConfig { - /// Apply rounded corners to the image. pub rounded_corners: bool, - - /// Radius in pixels for rounded corners. pub corner_radius: f32, - - /// Apply a drop shadow beneath the image. pub drop_shadow: bool, - - /// Blur radius for the drop shadow (higher = softer shadow). pub shadow_blur_radius: f32, - - /// Horizontal offset of the shadow in pixels. pub shadow_offset_x: f32, - - /// Vertical offset of the shadow in pixels. pub shadow_offset_y: f32, - - /// Shadow color as [R, G, B, A] in 0..=255. pub shadow_color: [u8; 4], } @@ -57,52 +42,23 @@ impl Default for Config { } impl Config { - /// Returns the path to the config file: ~/.config/rs-pictures/config.toml pub fn config_path() -> Option { let home = std::env::var("HOME").ok()?; Some( PathBuf::from(home) .join(".config") - .join("rs-pictures") - .join("config.toml"), + .join("zshell") + .join("config.json"), ) } - /// Load config from disk, or write and return the default config if the file doesn't exist. pub fn load() -> Result { - let path = match Self::config_path() { - Some(p) => p, - None => return Ok(Self::default()), - }; - - if !path.exists() { - let config = Self::default(); - config.save()?; - return Ok(config); - } + let path = Self::config_path().context("Could not determine HOME directory")?; let raw = std::fs::read_to_string(&path) .with_context(|| format!("Failed to read config at {}", path.display()))?; - toml::from_str(&raw) - .with_context(|| format!("Failed to parse config at {}", path.display())) - } - - /// Persist the current config to disk. - pub fn save(&self) -> Result<()> { - let path = - Self::config_path().context("Could not determine config directory (HOME not set)")?; - - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create config dir {}", parent.display()))?; - } - - let serialized = toml::to_string_pretty(self).context("Failed to serialize config")?; - - std::fs::write(&path, serialized) - .with_context(|| format!("Failed to write config to {}", path.display()))?; - - Ok(()) + serde_json::from_str(&raw) + .with_context(|| format!("Failed to parse JSON config at {}", path.display())) } } diff --git a/src/effects.rs b/src/effects.rs index bae227f..e6d41f6 100644 --- a/src/effects.rs +++ b/src/effects.rs @@ -1,26 +1,9 @@ -//! Post-capture image effects: rounded corners and drop shadow. -//! -//! Pipeline: -//! RgbaImage (captured) -//! → apply_rounded_corners() – clips corners to transparent via tiny-skia mask -//! → apply_drop_shadow() – composites a blurred shadow beneath the image -//! → final RgbaImage (may be larger when shadow is added) -//! -//! Performance notes: -//! - Box blur uses a sliding-window algorithm: O(W*H) regardless of radius. -//! Three passes of the box filter approximate a Gaussian. -//! - Pixel format conversions between RgbaImage and tiny-skia Pixmap are done -//! with a single pass each way. -//! - This module is called from a background thread in review.rs so the UI -//! never blocks. - use crate::config::EffectsConfig; use image::RgbaImage; use tiny_skia::{ BlendMode, Color, FillRule, Paint, Path, PathBuilder, Pixmap, PixmapPaint, Transform, }; -/// Apply all configured effects in order. Returns a new image. pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage { let img = if cfg.rounded_corners { apply_rounded_corners(img, cfg.corner_radius) @@ -40,8 +23,6 @@ pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage { } } -// ─── Rounded corners ───────────────────────────────────────────────────────── - pub fn apply_rounded_corners(img: RgbaImage, radius: f32) -> RgbaImage { let (w, h) = img.dimensions(); let mut mask = Pixmap::new(w, h).expect("mask pixmap"); @@ -64,8 +45,6 @@ pub fn apply_rounded_corners(img: RgbaImage, radius: f32) -> RgbaImage { pixmap_to_rgba_image(pixmap) } -// ─── Drop shadow ───────────────────────────────────────────────────────────── - pub fn apply_drop_shadow( img: RgbaImage, blur_radius: f32, @@ -84,7 +63,6 @@ pub fn apply_drop_shadow( let canvas_w = iw + extra_left + extra_right; let canvas_h = ih + extra_top + extra_bottom; - // 1. Place the image silhouette at the shadow position. let mut shadow_pixmap = Pixmap::new(canvas_w, canvas_h).expect("shadow pixmap"); let img_pixmap = rgba_image_to_pixmap(&img); let shadow_x = (extra_left as f32 + offset_x) as i32; @@ -101,15 +79,12 @@ pub fn apply_drop_shadow( None, ); - // 2. Tint the silhouette with the shadow colour. tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color); - // 3. Blur the shadow (sliding-window box blur, 3 passes). let shadow_img = pixmap_to_rgba_image(shadow_pixmap); let blurred = box_blur_rgba(&shadow_img, br); let blurred_pixmap = rgba_image_to_pixmap(&blurred); - // 4. Composite: shadow first, image on top. let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap"); let mut p = PixmapPaint::default(); p.blend_mode = BlendMode::Source; @@ -136,8 +111,6 @@ pub fn apply_drop_shadow( pixmap_to_rgba_image(canvas) } -// ─── Helpers ───────────────────────────────────────────────────────────────── - fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Path { let r = r.min(w / 2.0).min(h / 2.0); let mut pb = PathBuilder::new(); @@ -211,18 +184,10 @@ fn tint_pixmap_as_shadow(pixmap: &mut Pixmap, color: [u8; 4]) { } } -// ─── Sliding-window box blur (O(W*H) regardless of radius) ─────────────────── -// -// Classic algorithm: maintain a running sum over a window of (2r+1) pixels. -// When the window slides by one pixel, subtract the pixel leaving the window -// and add the pixel entering it. Three passes (H→V→H or H→V→H) approximate -// a Gaussian kernel. - fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage { if radius == 0 { return img.clone(); } - // Three passes of H+V to approximate a Gaussian. let mut buf = sliding_horizontal(img, radius); buf = sliding_vertical(&buf, radius); buf = sliding_horizontal(&buf, radius); @@ -230,7 +195,6 @@ fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage { buf } -/// Horizontal sliding-window box blur, single pass. fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage { let (w, h) = img.dimensions(); let r = radius as i32; @@ -238,13 +202,11 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage { let mut out = RgbaImage::new(w, h); for y in 0..h { - // Accumulator for the current window. let mut sr = 0u32; let mut sg = 0u32; let mut sb = 0u32; let mut sa = 0u32; - // Seed the window around x=0. for dx in -r..=r { let sx = dx.clamp(0, w as i32 - 1) as u32; let p = img.get_pixel(sx, y).0; @@ -266,7 +228,6 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage { ]), ); - // Slide: remove left edge, add right edge. let remove_x = (x as i32 - r).clamp(0, w as i32 - 1) as u32; let add_x = (x as i32 + r + 1).clamp(0, w as i32 - 1) as u32; let rp = img.get_pixel(remove_x, y).0; @@ -280,7 +241,6 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage { out } -/// Vertical sliding-window box blur, single pass. fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage { let (w, h) = img.dimensions(); let r = radius as i32; diff --git a/src/main.rs b/src/main.rs index 71e0321..d332e53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use std::io::Write as _; use std::process::{Command, Stdio}; fn main() -> Result<()> { - // ── 1. Collect image paths from CLI arguments ───────────────────────────── let paths: Vec = std::env::args().skip(1).collect(); if paths.is_empty() { @@ -15,10 +14,8 @@ fn main() -> Result<()> { std::process::exit(1); } - // ── 2. Load config (generates default if missing) ───────────────────────── let config = config::Config::load().context("Failed to load config")?; - // ── 3. Process each image ───────────────────────────────────────────────── for path in &paths { if let Err(e) = process_image(path, &config) { eprintln!("Error processing '{}': {e:#}", path); @@ -29,15 +26,12 @@ fn main() -> Result<()> { } fn process_image(path: &str, config: &config::Config) -> Result<()> { - // ── a. Load image ───────────────────────────────────────────────────────── let img = image::open(path) .with_context(|| format!("Failed to open image '{path}'"))? .into_rgba8(); - // ── b. Apply effects (rounded corners + drop shadow) ───────────────────── let processed = effects::apply_effects(img, &config.effects); - // ── c. Encode as PNG into memory ────────────────────────────────────────── let mut png_bytes: Vec = Vec::new(); image::DynamicImage::ImageRgba8(processed) .write_to( @@ -45,9 +39,7 @@ fn process_image(path: &str, config: &config::Config) -> Result<()> { image::ImageFormat::Png, ) .context("Failed to encode processed image as PNG")?; - // Re-unwrap into RgbaImage — not needed further, png_bytes is ready - // ── d. Pipe PNG bytes to `swappy -f -` ──────────────────────────────────── let mut child = Command::new("swappy") .args(["-f", "-"]) .stdin(Stdio::piped())