255 lines
8.0 KiB
Rust
255 lines
8.0 KiB
Rust
mod config;
|
|
mod effects;
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use std::io::Write as _;
|
|
use std::process::{Command, Stdio};
|
|
|
|
#[derive(Default)]
|
|
struct CliOverrides {
|
|
rounding: Option<bool>,
|
|
radius: Option<f32>,
|
|
shadow: Option<bool>,
|
|
shadow_blur: Option<f32>,
|
|
shadow_offset_x: Option<f32>,
|
|
shadow_offset_y: Option<f32>,
|
|
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 extract_image_path() -> Option<String> {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
args.windows(2)
|
|
.find(|w| w[0] == "--image")
|
|
.map(|w| w[1].clone())
|
|
}
|
|
|
|
fn main() {
|
|
// Fundamental issue when supplying args it won't give output unless --image is used.
|
|
// Will have to be fixed in a later patch upcoming week
|
|
if let Some(path) = extract_image_path()
|
|
&& let Err(e) = run()
|
|
{
|
|
eprintln!("Error: {}", e);
|
|
push_image(&path).ok();
|
|
}
|
|
}
|
|
|
|
fn run() -> 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 scale: Option<f32> = None;
|
|
|
|
let mut i = 0;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--image" => {
|
|
image_path = Some(next_arg(&args, &mut i, "--image")?);
|
|
}
|
|
"--rounding" => {
|
|
let val = next_arg(&args, &mut i, "--rounding")?;
|
|
overrides.rounding = Some(parse_bool(&val)?);
|
|
}
|
|
"--radius" => {
|
|
let val = next_arg(&args, &mut i, "--radius")?;
|
|
overrides.radius = Some(val.parse::<f32>().context("--radius must be a number")?);
|
|
}
|
|
"--shadow" => {
|
|
let val = next_arg(&args, &mut i, "--shadow")?;
|
|
overrides.shadow = Some(parse_bool(&val)?);
|
|
}
|
|
"--shadow-blur" => {
|
|
let val = next_arg(&args, &mut i, "--shadow-blur")?;
|
|
overrides.shadow_blur = Some(
|
|
val.parse::<f32>()
|
|
.context("--shadow-blur must be a number")?,
|
|
);
|
|
}
|
|
"--shadow-offset-x" => {
|
|
let val = next_arg(&args, &mut i, "--shadow-offset-x")?;
|
|
overrides.shadow_offset_x = Some(
|
|
val.parse::<f32>()
|
|
.context("--shadow-offset-x must be a number")?,
|
|
);
|
|
}
|
|
"--shadow-offset-y" => {
|
|
let val = next_arg(&args, &mut i, "--shadow-offset-y")?;
|
|
overrides.shadow_offset_y = Some(
|
|
val.parse::<f32>()
|
|
.context("--shadow-offset-y must be a number")?,
|
|
);
|
|
}
|
|
"--shadow-color" => {
|
|
let val = next_arg(&args, &mut i, "--shadow-color")?;
|
|
overrides.shadow_color = Some(parse_shadow_color(&val)?);
|
|
}
|
|
"--scale" => {
|
|
let val = next_arg(&args, &mut i, "--scale")?;
|
|
scale = Some(val.parse::<f32>().context("--scale must be a number")?);
|
|
}
|
|
unknown => {
|
|
let unknown_args = unknown.to_string();
|
|
println!("Warning: Unknown argument '{}'", unknown);
|
|
next_arg(&args, &mut i, &unknown_args)?;
|
|
}
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
let image_path = image_path.context("Missing --image <path>")?;
|
|
|
|
let cli_args_provided = overrides.rounding.is_some()
|
|
|| overrides.radius.is_some()
|
|
|| overrides.shadow.is_some()
|
|
|| overrides.shadow_blur.is_some()
|
|
|| overrides.shadow_offset_x.is_some()
|
|
|| overrides.shadow_offset_y.is_some()
|
|
|| overrides.shadow_color.is_some();
|
|
let mut effects = if cli_args_provided {
|
|
let rounding = overrides.rounding.context("Missing --rounding")?;
|
|
let radius = overrides.radius.context("Missing --radius")?;
|
|
let shadow = overrides.shadow.context("Missing --shadow")?;
|
|
let shadow_blur = overrides.shadow_blur.context("Missing --shadow-blur")?;
|
|
let shadow_offset_x = overrides
|
|
.shadow_offset_x
|
|
.context("Missing --shadow-offset-x")?;
|
|
let shadow_offset_y = overrides
|
|
.shadow_offset_y
|
|
.context("Missing --shadow-offset-y")?;
|
|
let shadow_color = overrides.shadow_color.context("Missing --shadow-color")?;
|
|
config::EffectsConfig {
|
|
rounding,
|
|
radius,
|
|
shadow,
|
|
shadow_blur,
|
|
shadow_offset_x,
|
|
shadow_offset_y,
|
|
shadow_color,
|
|
}
|
|
} else {
|
|
let config = config::Config::load()?;
|
|
config.screenshot
|
|
};
|
|
|
|
if let Some(scale) = scale.filter(|&s| s != 1.0) {
|
|
effects.radius *= scale;
|
|
effects.shadow_blur *= scale;
|
|
effects.shadow_offset_x *= scale;
|
|
effects.shadow_offset_y *= scale;
|
|
}
|
|
|
|
if let Err(e) = process_image(&image_path, &effects) {
|
|
eprintln!("Error processing '{}': {e:#}", image_path);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn next_arg(args: &[String], i: &mut usize, flag: &str) -> Result<String> {
|
|
*i += 1;
|
|
|
|
let val = args
|
|
.get(*i)
|
|
.context(format!("Expected value after {}", flag))?;
|
|
|
|
if val.starts_with('-') {
|
|
bail!("Expected value after {}, found flag {}", flag, val);
|
|
}
|
|
|
|
Ok(val.clone())
|
|
}
|
|
|
|
fn push_image(path: &str) -> Result<()> {
|
|
let img = image::open(path)
|
|
.with_context(|| format!("Failed to open image '{path}'"))?
|
|
.into_rgba8();
|
|
|
|
let mut png_bytes: Vec<u8> = Vec::new();
|
|
image::DynamicImage::ImageRgba8(img)
|
|
.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?")?;
|
|
|
|
// 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")?;
|
|
}
|
|
|
|
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?")?;
|
|
|
|
// 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")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|