Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0608f1f1aa | |||
| cf634a76f2 | |||
| 075cd42064 | |||
| 2903819626 | |||
| 200e76b899 | |||
| 0e6fdb9719 | |||
| 7fe112293e |
@@ -9,10 +9,3 @@ target/
|
|||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
# RustRover
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|||||||
Generated
+1100
File diff suppressed because it is too large
Load Diff
+27
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "iwaku"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "iwaku"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
image = { version = "0.25", features = ["png"] }
|
||||||
|
tiny-skia = "0.11"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
anyhow = "1"
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# What_That_Claude_DO?
|
# What_That_Claude_DO?
|
||||||
|
|
||||||
What That Claude Do? (WTCD)
|
What That Claude Do? (WTCD)
|
||||||
A repository of random things I ask Claude to do for me.
|
A repository of random things I ask Claude to do for me.
|
||||||
|
|
||||||
|
In this case it is creating a screenshot tool
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(rename = "screenshot")]
|
||||||
|
pub screenshot: EffectsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EffectsConfig {
|
||||||
|
pub mode: String,
|
||||||
|
pub rounded_corners: bool,
|
||||||
|
pub corner_radius: f32,
|
||||||
|
pub drop_shadow: bool,
|
||||||
|
pub shadow_blur_radius: f32,
|
||||||
|
pub shadow_offset_x: f32,
|
||||||
|
pub shadow_offset_y: f32,
|
||||||
|
pub shadow_color: [u8; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn config_path() -> Option<PathBuf> {
|
||||||
|
let home = std::env::var("HOME").ok()?;
|
||||||
|
Some(
|
||||||
|
PathBuf::from(home)
|
||||||
|
.join(".config")
|
||||||
|
.join("zshell")
|
||||||
|
.join("config.json"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Result<Self> {
|
||||||
|
let path = Self::config_path().context("Could not determine HOME directory")?;
|
||||||
|
Self::load_from(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from(path: &PathBuf) -> Result<Self> {
|
||||||
|
let raw = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("Failed to read config at {}", path.display()))?;
|
||||||
|
serde_json::from_str(&raw)
|
||||||
|
.with_context(|| format!("Failed to parse JSON config at {}", path.display()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+288
@@ -0,0 +1,288 @@
|
|||||||
|
use crate::config::EffectsConfig;
|
||||||
|
use image::RgbaImage;
|
||||||
|
use tiny_skia::{
|
||||||
|
BlendMode, Color, FillRule, Paint, Path, PathBuilder, Pixmap, PixmapPaint, Transform,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage {
|
||||||
|
let img = if cfg.rounded_corners {
|
||||||
|
apply_rounded_corners(img, cfg.corner_radius)
|
||||||
|
} else {
|
||||||
|
img
|
||||||
|
};
|
||||||
|
if cfg.drop_shadow {
|
||||||
|
apply_drop_shadow(
|
||||||
|
img,
|
||||||
|
cfg.shadow_blur_radius,
|
||||||
|
cfg.shadow_offset_x,
|
||||||
|
cfg.shadow_offset_y,
|
||||||
|
cfg.shadow_color,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
let path = rounded_rect_path(0.0, 0.0, w as f32, h as f32, radius);
|
||||||
|
let mut paint = Paint::default();
|
||||||
|
paint.set_color(Color::WHITE);
|
||||||
|
paint.anti_alias = true;
|
||||||
|
mask.fill_path(
|
||||||
|
&path,
|
||||||
|
&paint,
|
||||||
|
FillRule::Winding,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut pixmap = rgba_image_to_pixmap(&img);
|
||||||
|
let mut dst_paint = PixmapPaint::default();
|
||||||
|
dst_paint.blend_mode = BlendMode::DestinationIn;
|
||||||
|
pixmap.draw_pixmap(0, 0, mask.as_ref(), &dst_paint, Transform::identity(), None);
|
||||||
|
pixmap_to_rgba_image(pixmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_drop_shadow(
|
||||||
|
img: RgbaImage,
|
||||||
|
blur_radius: f32,
|
||||||
|
offset_x: f32,
|
||||||
|
offset_y: f32,
|
||||||
|
shadow_color: [u8; 4],
|
||||||
|
) -> RgbaImage {
|
||||||
|
let (iw, ih) = img.dimensions();
|
||||||
|
let br = blur_radius.ceil() as u32;
|
||||||
|
|
||||||
|
let extra_left = br.saturating_sub((-offset_x).max(0.0) as u32);
|
||||||
|
let extra_top = br.saturating_sub((-offset_y).max(0.0) as u32);
|
||||||
|
let extra_right = br + offset_x.max(0.0) as u32;
|
||||||
|
let extra_bottom = br + offset_y.max(0.0) as u32;
|
||||||
|
|
||||||
|
let canvas_w = iw + extra_left + extra_right;
|
||||||
|
let canvas_h = ih + extra_top + extra_bottom;
|
||||||
|
|
||||||
|
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;
|
||||||
|
let shadow_y = (extra_top as f32 + offset_y) as i32;
|
||||||
|
|
||||||
|
let mut sp = PixmapPaint::default();
|
||||||
|
sp.blend_mode = BlendMode::Source;
|
||||||
|
shadow_pixmap.draw_pixmap(
|
||||||
|
shadow_x,
|
||||||
|
shadow_y,
|
||||||
|
img_pixmap.as_ref(),
|
||||||
|
&sp,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap");
|
||||||
|
let mut p = PixmapPaint::default();
|
||||||
|
p.blend_mode = BlendMode::Source;
|
||||||
|
canvas.draw_pixmap(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
blurred_pixmap.as_ref(),
|
||||||
|
&p,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut p2 = PixmapPaint::default();
|
||||||
|
p2.blend_mode = BlendMode::SourceOver;
|
||||||
|
canvas.draw_pixmap(
|
||||||
|
extra_left as i32,
|
||||||
|
extra_top as i32,
|
||||||
|
img_pixmap.as_ref(),
|
||||||
|
&p2,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
pixmap_to_rgba_image(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
pb.move_to(x + r, y);
|
||||||
|
pb.line_to(x + w - r, y);
|
||||||
|
pb.quad_to(x + w, y, x + w, y + r);
|
||||||
|
pb.line_to(x + w, y + h - r);
|
||||||
|
pb.quad_to(x + w, y + h, x + w - r, y + h);
|
||||||
|
pb.line_to(x + r, y + h);
|
||||||
|
pb.quad_to(x, y + h, x, y + h - r);
|
||||||
|
pb.line_to(x, y + r);
|
||||||
|
pb.quad_to(x, y, x + r, y);
|
||||||
|
pb.close();
|
||||||
|
pb.finish().expect("rounded rect path")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap {
|
||||||
|
let (w, h) = img.dimensions();
|
||||||
|
let mut pixmap = Pixmap::new(w, h).expect("pixmap alloc");
|
||||||
|
let pixels = pixmap.pixels_mut();
|
||||||
|
for (i, px) in img.pixels().enumerate() {
|
||||||
|
let [r, g, b, a] = px.0;
|
||||||
|
let af = a as f32 / 255.0;
|
||||||
|
pixels[i] = tiny_skia::PremultipliedColorU8::from_rgba(
|
||||||
|
(r as f32 * af) as u8,
|
||||||
|
(g as f32 * af) as u8,
|
||||||
|
(b as f32 * af) as u8,
|
||||||
|
a,
|
||||||
|
)
|
||||||
|
.unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
|
||||||
|
}
|
||||||
|
pixmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage {
|
||||||
|
let (w, h) = (pixmap.width(), pixmap.height());
|
||||||
|
let mut out = RgbaImage::new(w, h);
|
||||||
|
for (i, px) in pixmap.pixels().iter().enumerate() {
|
||||||
|
let x = (i as u32) % w;
|
||||||
|
let y = (i as u32) / w;
|
||||||
|
let a = px.alpha();
|
||||||
|
let (r, g, b) = if a == 0 {
|
||||||
|
(0, 0, 0)
|
||||||
|
} else {
|
||||||
|
let af = a as f32 / 255.0;
|
||||||
|
(
|
||||||
|
(px.red() as f32 / af).round().min(255.0) as u8,
|
||||||
|
(px.green() as f32 / af).round().min(255.0) as u8,
|
||||||
|
(px.blue() as f32 / af).round().min(255.0) as u8,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
out.put_pixel(x, y, image::Rgba([r, g, b, a]));
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
|
||||||
|
let (w, h) = img.dimensions();
|
||||||
|
let r = radius as i32;
|
||||||
|
let diam = (2 * r + 1) as u32;
|
||||||
|
let mut out = RgbaImage::new(w, h);
|
||||||
|
|
||||||
|
for y in 0..h {
|
||||||
|
let mut sr = 0u32;
|
||||||
|
let mut sg = 0u32;
|
||||||
|
let mut sb = 0u32;
|
||||||
|
let mut sa = 0u32;
|
||||||
|
|
||||||
|
for dx in -r..=r {
|
||||||
|
let sx = dx.clamp(0, w as i32 - 1) as u32;
|
||||||
|
let p = img.get_pixel(sx, y).0;
|
||||||
|
sr += p[0] as u32;
|
||||||
|
sg += p[1] as u32;
|
||||||
|
sb += p[2] as u32;
|
||||||
|
sa += p[3] as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
for x in 0..w {
|
||||||
|
out.put_pixel(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
image::Rgba([
|
||||||
|
(sr / diam) as u8,
|
||||||
|
(sg / diam) as u8,
|
||||||
|
(sb / diam) as u8,
|
||||||
|
(sa / diam) as u8,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
let ap = img.get_pixel(add_x, y).0;
|
||||||
|
sr = sr.saturating_sub(rp[0] as u32) + ap[0] as u32;
|
||||||
|
sg = sg.saturating_sub(rp[1] as u32) + ap[1] as u32;
|
||||||
|
sb = sb.saturating_sub(rp[2] as u32) + ap[2] as u32;
|
||||||
|
sa = sa.saturating_sub(rp[3] as u32) + ap[3] as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage {
|
||||||
|
let (w, h) = img.dimensions();
|
||||||
|
let r = radius as i32;
|
||||||
|
let diam = (2 * r + 1) as u32;
|
||||||
|
let mut out = RgbaImage::new(w, h);
|
||||||
|
|
||||||
|
for x in 0..w {
|
||||||
|
let mut sr = 0u32;
|
||||||
|
let mut sg = 0u32;
|
||||||
|
let mut sb = 0u32;
|
||||||
|
let mut sa = 0u32;
|
||||||
|
|
||||||
|
for dy in -r..=r {
|
||||||
|
let sy = dy.clamp(0, h as i32 - 1) as u32;
|
||||||
|
let p = img.get_pixel(x, sy).0;
|
||||||
|
sr += p[0] as u32;
|
||||||
|
sg += p[1] as u32;
|
||||||
|
sb += p[2] as u32;
|
||||||
|
sa += p[3] as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
for y in 0..h {
|
||||||
|
out.put_pixel(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
image::Rgba([
|
||||||
|
(sr / diam) as u8,
|
||||||
|
(sg / diam) as u8,
|
||||||
|
(sb / diam) as u8,
|
||||||
|
(sa / diam) as u8,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let remove_y = (y as i32 - r).clamp(0, h as i32 - 1) as u32;
|
||||||
|
let add_y = (y as i32 + r + 1).clamp(0, h as i32 - 1) as u32;
|
||||||
|
let rp = img.get_pixel(x, remove_y).0;
|
||||||
|
let ap = img.get_pixel(x, add_y).0;
|
||||||
|
sr = sr.saturating_sub(rp[0] as u32) + ap[0] as u32;
|
||||||
|
sg = sg.saturating_sub(rp[1] as u32) + ap[1] as u32;
|
||||||
|
sb = sb.saturating_sub(rp[2] as u32) + ap[2] as u32;
|
||||||
|
sa = sa.saturating_sub(rp[3] as u32) + ap[3] as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
mod config;
|
||||||
|
mod effects;
|
||||||
|
|
||||||
|
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>,
|
||||||
|
shadow_blur_radius: Option<f32>,
|
||||||
|
shadow_offset_x: Option<f32>,
|
||||||
|
shadow_offset_y: Option<f32>,
|
||||||
|
/// Accepted as four comma-separated u8 values, e.g. `255,0,0,200`
|
||||||
|
shadow_color: Option<[u8; 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_bool(s: &str) -> Result<bool> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"true" | "1" | "yes" => Ok(true),
|
||||||
|
"false" | "0" | "no" => Ok(false),
|
||||||
|
other => bail!("Expected a boolean (true/false), got '{other}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
let r = parts[0]
|
||||||
|
.trim()
|
||||||
|
.parse::<u8>()
|
||||||
|
.context("shadow_color red channel")?;
|
||||||
|
let g = parts[1]
|
||||||
|
.trim()
|
||||||
|
.parse::<u8>()
|
||||||
|
.context("shadow_color green channel")?;
|
||||||
|
let b = parts[2]
|
||||||
|
.trim()
|
||||||
|
.parse::<u8>()
|
||||||
|
.context("shadow_color blue channel")?;
|
||||||
|
let a = parts[3]
|
||||||
|
.trim()
|
||||||
|
.parse::<u8>()
|
||||||
|
.context("shadow_color alpha channel")?;
|
||||||
|
Ok([r, g, b, a])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
|
||||||
|
let mut image_path: Option<String> = None;
|
||||||
|
let mut overrides = CliOverrides::default();
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--image" => {
|
||||||
|
i += 1;
|
||||||
|
image_path = Some(
|
||||||
|
args.get(i)
|
||||||
|
.cloned()
|
||||||
|
.context("Expected a path after --image")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--rounded_corners" => {
|
||||||
|
i += 1;
|
||||||
|
let val = args
|
||||||
|
.get(i)
|
||||||
|
.context("Expected true/false after --rounded_corners")?;
|
||||||
|
overrides.rounded_corners = Some(parse_bool(val)?);
|
||||||
|
}
|
||||||
|
"--corner_radius" => {
|
||||||
|
i += 1;
|
||||||
|
let val = args
|
||||||
|
.get(i)
|
||||||
|
.context("Expected a number after --corner_radius")?;
|
||||||
|
overrides.corner_radius = Some(
|
||||||
|
val.parse::<f32>()
|
||||||
|
.context("--corner_radius must be a number")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--drop_shadow" => {
|
||||||
|
i += 1;
|
||||||
|
let val = args
|
||||||
|
.get(i)
|
||||||
|
.context("Expected true/false after --drop_shadow")?;
|
||||||
|
overrides.drop_shadow = Some(parse_bool(val)?);
|
||||||
|
}
|
||||||
|
"--shadow_blur_radius" => {
|
||||||
|
i += 1;
|
||||||
|
let val = args
|
||||||
|
.get(i)
|
||||||
|
.context("Expected a number after --shadow_blur_radius")?;
|
||||||
|
overrides.shadow_blur_radius = Some(
|
||||||
|
val.parse::<f32>()
|
||||||
|
.context("--shadow_blur_radius must be a number")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--shadow_offset_x" => {
|
||||||
|
i += 1;
|
||||||
|
let val = args
|
||||||
|
.get(i)
|
||||||
|
.context("Expected a number after --shadow_offset_x")?;
|
||||||
|
overrides.shadow_offset_x = Some(
|
||||||
|
val.parse::<f32>()
|
||||||
|
.context("--shadow_offset_x must be a number")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"--shadow_offset_y" => {
|
||||||
|
i += 1;
|
||||||
|
let val = args
|
||||||
|
.get(i)
|
||||||
|
.context("Expected a number after --shadow_offset_y")?;
|
||||||
|
overrides.shadow_offset_y = Some(
|
||||||
|
val.parse::<f32>()
|
||||||
|
.context("--shadow_offset_y 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)?);
|
||||||
|
}
|
||||||
|
unknown => bail!("Unknown argument: {unknown}"),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image_path = image_path.context("Missing --image <path>")?;
|
||||||
|
|
||||||
|
let config = config::Config::load().context("Failed to load config")?;
|
||||||
|
|
||||||
|
let mut effects = config.screenshot;
|
||||||
|
if effects.mode == "auto" {
|
||||||
|
if let Some(v) = overrides.rounded_corners {
|
||||||
|
effects.rounded_corners = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = overrides.corner_radius {
|
||||||
|
effects.corner_radius = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = overrides.drop_shadow {
|
||||||
|
effects.drop_shadow = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = overrides.shadow_blur_radius {
|
||||||
|
effects.shadow_blur_radius = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = overrides.shadow_offset_x {
|
||||||
|
effects.shadow_offset_x = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = overrides.shadow_offset_y {
|
||||||
|
effects.shadow_offset_y = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = overrides.shadow_color {
|
||||||
|
effects.shadow_color = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = process_image(&image_path, &effects) {
|
||||||
|
eprintln!("Error processing '{}': {e:#}", image_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_image(path: &str, effects: &config::EffectsConfig) -> Result<()> {
|
||||||
|
let img = image::open(path)
|
||||||
|
.with_context(|| format!("Failed to open image '{path}'"))?
|
||||||
|
.into_rgba8();
|
||||||
|
|
||||||
|
let processed = effects::apply_effects(img, effects);
|
||||||
|
|
||||||
|
let mut png_bytes: Vec<u8> = Vec::new();
|
||||||
|
image::DynamicImage::ImageRgba8(processed)
|
||||||
|
.write_to(
|
||||||
|
&mut std::io::Cursor::new(&mut png_bytes),
|
||||||
|
image::ImageFormat::Png,
|
||||||
|
)
|
||||||
|
.context("Failed to encode processed image as PNG")?;
|
||||||
|
|
||||||
|
let mut child = Command::new("swappy")
|
||||||
|
.args(["-f", "-"])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user