zshell-img-tools crate reduction to 53, process release fix, blur-passes, scale impl, settings passes setting, scale only avail in config

This commit is contained in:
2026-05-25 23:15:00 +02:00
parent ba9926af18
commit de11767d3b
9 changed files with 143 additions and 884 deletions
+4 -2
View File
@@ -11,13 +11,15 @@ pub struct Config {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectsConfig {
pub mode: String,
pub rounded_corners: bool,
pub corner_radius: f32,
pub drop_shadow: bool,
pub rounded_corners: bool,
pub scale: f32,
pub shadow_blur_radius: f32,
pub shadow_blur_passes: u32,
pub shadow_color: [u8; 4],
pub shadow_offset_x: f32,
pub shadow_offset_y: f32,
pub shadow_color: [u8; 4],
}
impl Config {
+37 -24
View File
@@ -16,6 +16,7 @@ pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage {
cfg.shadow_blur_radius,
cfg.shadow_offset_x,
cfg.shadow_offset_y,
cfg.shadow_blur_passes,
cfg.shadow_color,
)
} else {
@@ -52,11 +53,16 @@ pub fn apply_drop_shadow(
blur_radius: f32,
offset_x: f32,
offset_y: f32,
blur_passes: u32,
shadow_color: [u8; 4],
) -> RgbaImage {
let (iw, ih) = img.dimensions();
let br = blur_radius.ceil() as u32;
let spread = br * 2;
let bp = blur_passes;
// Original idea
// let spread = br * bp;
// Claude is hallucinating but let's try it **Worked btw**
let spread = (br as f32 * (bp as f32).sqrt() * 2.0).ceil() as u32;
let extra_left = spread + (-offset_x).max(0.0).ceil() as u32;
let extra_top = spread + (-offset_y).max(0.0).ceil() as u32;
@@ -86,8 +92,11 @@ pub fn apply_drop_shadow(
tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color);
// Shadow
let shadow_img = pixmap_to_rgba_image(shadow_pixmap);
let blurred = box_blur_rgba(&shadow_img, br);
// Shadow blur
let blurred = box_blur_rgba(&shadow_img, br, bp);
// Shadow pos
let blurred_pixmap = rgba_image_to_pixmap(&blurred);
let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap");
@@ -136,6 +145,7 @@ fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Path {
pb.finish().expect("rounded rect path")
}
// Shadow pos
fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap {
let (w, h) = img.dimensions();
let mut pixmap = Pixmap::new(w, h).expect("pixmap alloc");
@@ -154,6 +164,7 @@ fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap {
pixmap
}
// Shadow
fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage {
let (w, h) = (pixmap.width(), pixmap.height());
let mut out = RgbaImage::new(w, h);
@@ -176,31 +187,16 @@ fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage {
out
}
fn tint_pixmap_as_shadow(pixmap: &mut Pixmap, color: [u8; 4]) {
let [sr, sg, sb, _] = color;
for px in pixmap.pixels_mut() {
let a = px.alpha();
if a > 0 {
let af = a as f32 / 255.0;
*px = tiny_skia::PremultipliedColorU8::from_rgba(
(sr as f32 * af) as u8,
(sg as f32 * af) as u8,
(sb as f32 * af) as u8,
a,
)
.unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
}
}
}
fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage {
// Shadow blur
fn box_blur_rgba(img: &RgbaImage, radius: u32, bp: u32) -> RgbaImage {
if radius == 0 {
return img.clone();
}
let mut buf = sliding_horizontal(img, radius);
buf = sliding_vertical(&buf, radius);
buf = sliding_horizontal(&buf, radius);
buf = sliding_vertical(&buf, radius);
let mut buf = img.clone();
for _ in 0..bp {
buf = sliding_horizontal(&buf, radius);
buf = sliding_vertical(&buf, radius);
}
buf
}
@@ -250,6 +246,23 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
out
}
fn tint_pixmap_as_shadow(pixmap: &mut Pixmap, color: [u8; 4]) {
let [sr, sg, sb, _] = color;
for px in pixmap.pixels_mut() {
let a = px.alpha();
if a > 0 {
let af = a as f32 / 255.0;
*px = tiny_skia::PremultipliedColorU8::from_rgba(
(sr as f32 * af) as u8,
(sg as f32 * af) as u8,
(sb as f32 * af) as u8,
a,
)
.unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
}
}
}
fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage {
let (w, h) = img.dimensions();
let r = radius as i32;
+78 -41
View File
@@ -1,21 +1,21 @@
mod config;
mod effects;
use anyhow::{Context, Result, bail};
use anyhow::{bail, Context, Result};
use std::io::Write as _;
use std::process::{Command, Stdio};
/// CLI overrides that map 1:1 to `EffectsConfig` fields.
/// All fields are `Option<T>` so we can tell "not supplied" from any concrete value.
#[derive(Default)]
struct CliOverrides {
rounded_corners: Option<bool>,
corner_radius: Option<f32>,
drop_shadow: Option<bool>,
scale: Option<f32>,
shadow_blur_radius: Option<f32>,
shadow_blur_passes: Option<u32>,
shadow_offset_x: Option<f32>,
shadow_offset_y: Option<f32>,
/// Accepted as four comma-separated u8 values, e.g. `255,0,0,200`
// Accepted as four comma-separated u8 values, e.g. `255,0,0,200`
shadow_color: Option<[u8; 4]>,
}
@@ -30,24 +30,24 @@ fn parse_bool(s: &str) -> Result<bool> {
fn parse_shadow_color(s: &str) -> Result<[u8; 4]> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 4 {
bail!("--shadow_color expects four comma-separated u8 values, e.g. 255,0,0,200");
bail!("--shadow-color expects four comma-separated u8 values, e.g. 255,0,0,200");
}
let r = parts[0]
.trim()
.parse::<u8>()
.context("shadow_color red channel")?;
.context("shadow-color red channel")?;
let g = parts[1]
.trim()
.parse::<u8>()
.context("shadow_color green channel")?;
.context("shadow-color green channel")?;
let b = parts[2]
.trim()
.parse::<u8>()
.context("shadow_color blue channel")?;
.context("shadow-color blue channel")?;
let a = parts[3]
.trim()
.parse::<u8>()
.context("shadow_color alpha channel")?;
.context("shadow-color alpha channel")?;
Ok([r, g, b, a])
}
@@ -68,67 +68,82 @@ fn main() -> Result<()> {
.context("Expected a path after --image")?,
);
}
"--rounded_corners" => {
"--rounded-corners" => {
i += 1;
let val = args
.get(i)
.context("Expected true/false after --rounded_corners")?;
.context("Expected true/false after --rounded-corners")?;
overrides.rounded_corners = Some(parse_bool(val)?);
}
"--corner_radius" => {
"--corner-radius" => {
i += 1;
let val = args
.get(i)
.context("Expected a number after --corner_radius")?;
.context("Expected a number after --corner-radius")?;
overrides.corner_radius = Some(
val.parse::<f32>()
.context("--corner_radius must be a number")?,
.context("--corner-radius must be a number")?,
);
}
"--drop_shadow" => {
"--drop-shadow" => {
i += 1;
let val = args
.get(i)
.context("Expected true/false after --drop_shadow")?;
.context("Expected true/false after --drop-shadow")?;
overrides.drop_shadow = Some(parse_bool(val)?);
}
"--shadow_blur_radius" => {
"--shadow-blur-radius" => {
i += 1;
let val = args
.get(i)
.context("Expected a number after --shadow_blur_radius")?;
.context("Expected a number after --shadow-blur-radius")?;
overrides.shadow_blur_radius = Some(
val.parse::<f32>()
.context("--shadow_blur_radius must be a number")?,
.context("--shadow-blur-radius must be a number")?,
);
}
"--shadow_offset_x" => {
"--shadow-offset-x" => {
i += 1;
let val = args
.get(i)
.context("Expected a number after --shadow_offset_x")?;
.context("Expected a number after --shadow-offset-x")?;
overrides.shadow_offset_x = Some(
val.parse::<f32>()
.context("--shadow_offset_x must be a number")?,
.context("--shadow-offset-x must be a number")?,
);
}
"--shadow_offset_y" => {
"--shadow-offset-y" => {
i += 1;
let val = args
.get(i)
.context("Expected a number after --shadow_offset_y")?;
.context("Expected a number after --shadow-offset-y")?;
overrides.shadow_offset_y = Some(
val.parse::<f32>()
.context("--shadow_offset_y must be a number")?,
.context("--shadow-offset-y must be a number")?,
);
}
"--shadow_color" => {
"--shadow-blur-passes" => {
i += 1;
let val = args
.get(i)
.context("Expected r,g,b,a after --shadow_color")?;
.context("Expected a number after --shadow-blur-passes")?;
overrides.shadow_blur_passes = Some(
val.parse::<u32>()
.context("--shadow-blur-passes must be a number")?,
);
}
"--shadow-color" => {
i += 1;
let val = args
.get(i)
.context("Expected r,g,b,a after --shadow-color")?;
overrides.shadow_color = Some(parse_shadow_color(val)?);
}
"--scale" => {
i += 1;
let val = args.get(i).context("Expected a number after --scale")?;
overrides.scale = Some(val.parse::<f32>().context("--scale must be a number")?);
}
unknown => bail!("Unknown argument: {unknown}"),
}
i += 1;
@@ -158,9 +173,22 @@ fn main() -> Result<()> {
if let Some(v) = overrides.shadow_offset_y {
effects.shadow_offset_y = v;
}
if let Some(v) = overrides.shadow_blur_passes {
effects.shadow_blur_passes = v;
}
if let Some(v) = overrides.shadow_color {
effects.shadow_color = v;
}
if let Some(v) = overrides.scale {
effects.scale = v;
}
}
if effects.scale != 1.0 {
effects.corner_radius *= effects.scale;
effects.shadow_blur_radius *= effects.scale;
effects.shadow_offset_x *= effects.scale;
effects.shadow_offset_y *= effects.scale;
}
if let Err(e) = process_image(&image_path, &effects) {
@@ -191,21 +219,30 @@ fn process_image(path: &str, effects: &config::EffectsConfig) -> Result<()> {
.spawn()
.context("Failed to spawn swappy. Is it installed and in PATH?")?;
child
.stdin
.take()
.context("Failed to get swappy stdin")?
.write_all(&png_bytes)
.context("Failed to write image data to swappy")?;
let status = child.wait().context("Failed to wait for swappy")?;
if !status.success() {
eprintln!(
"swappy exited with non-zero status for '{}': {}",
path, status
);
// Writes the PNG bytes to swappy's stdin and then closes
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(&png_bytes)
.context("Failed to write image data to swappy")?;
}
// Writes the PNG bytes to swappy's stdin and waits for swappy to close
// child
// .stdin
// .take()
// .context("Failed to get swappy stdin")?
// .write_all(&png_bytes)
// .context("Failed to write image data to swappy")?
// .spawn();
//
// let status = child.await().context("Failed to wait for swappy")?;
//
// if !status.success() {
// eprintln!(
// "swappy exited with non-zero status for '{}': {}",
// path, status
// );
// }
Ok(())
}