diff --git a/Cargo.toml b/Cargo.toml index 563e8de..521ed4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,4 @@ [workspace.package] -name = "sss" version = "0.0.1" edition = "2021" license = "MIT OR Apache-2.0" @@ -22,4 +21,3 @@ codegen-units = 1 [workspace.dependencies] log = "0.4.20" env_logger = "0.10.0" -thiserror = "1.0.50" diff --git a/crates/sss_cli/Cargo.toml b/crates/sss_cli/Cargo.toml index 490a91a..5237493 100644 --- a/crates/sss_cli/Cargo.toml +++ b/crates/sss_cli/Cargo.toml @@ -3,7 +3,12 @@ name = "sss_cli" version = "0.1.0" edition = "2021" +[[bin]] +name = "sss" +path = "./src/main.rs" + [dependencies] +sss_lib = { path = "../sss_lib" } clap = { version = "4.4.8", features = [ "derive", "color", @@ -11,3 +16,4 @@ clap = { version = "4.4.8", features = [ "suggestions", "wrap_help", ] } +screenshots = "0.8.6" diff --git a/crates/sss_cli/src/main.rs b/crates/sss_cli/src/main.rs index e7a11a9..c2e9f16 100644 --- a/crates/sss_cli/src/main.rs +++ b/crates/sss_cli/src/main.rs @@ -1,3 +1,21 @@ +use sss_lib::{generate_image, DynImageContent, GenerationSettings, Shadow}; + +struct Screenshot; + +impl DynImageContent for Screenshot { + fn content(&self) -> sss_lib::image::DynamicImage { + let binding = screenshots::Screen::all().unwrap(); + let screens = binding.last().unwrap(); + let img = screens.capture().unwrap(); + sss_lib::image::DynamicImage::ImageRgba8(img) + } +} + fn main() { - println!("Hello, world!"); + let img = generate_image(GenerationSettings { + shadow: Some(Shadow::default()), + ..Default::default() + }, Screenshot); + + img.save("./algo.png").unwrap(); } diff --git a/crates/sss_lib/Cargo.toml b/crates/sss_lib/Cargo.toml index 0b8f2f0..921ea66 100644 --- a/crates/sss_lib/Cargo.toml +++ b/crates/sss_lib/Cargo.toml @@ -10,3 +10,4 @@ font-kit = "0.12.0" imageproc = "0.23.0" arboard = { version = "3.3.0", features = ["wayland-data-control"] } image = { version = "0.24.7", default-features = false, features = ["png", "jpeg", "jpeg_rayon", "webp"] } +rayon = "1.8.0" diff --git a/crates/sss_lib/src/blur.rs b/crates/sss_lib/src/blur.rs new file mode 100644 index 0000000..7c4edcd --- /dev/null +++ b/crates/sss_lib/src/blur.rs @@ -0,0 +1,376 @@ +//! Fast (linear time) implementation of the Gaussian Blur algorithm in Rust +//! +//! This file is originally from https://github.com/fschutt/fastblur +//! Edited by aloxaf to process RgbaImage + +use std::cmp::min; + +use image::RgbaImage; +use rayon::iter::ParallelIterator; +use rayon::iter::IntoParallelIterator; + +#[derive(Copy, Clone)] +struct SharedMutPtr(*mut [[u8; 4]]); + +unsafe impl Sync for SharedMutPtr {} + +impl SharedMutPtr { + #[allow(clippy::mut_from_ref)] + unsafe fn get(&self) -> &mut [[u8; 4]] { + &mut *self.0 + } +} + +pub fn gaussian_blur(image: RgbaImage, sigma: f32) -> RgbaImage { + let (width, height) = image.dimensions(); + let mut raw = image.into_raw(); + let len = raw.len(); + + // fastblur::gaussian_blur only accepts Vec<[u8; 4]> + unsafe { + raw.set_len(len / 4); + + let ptr = &mut *(&mut raw as *mut Vec as *mut Vec<[u8; 4]>); + gaussian_blur_impl(ptr, width as usize, height as usize, sigma); + + raw.set_len(len); + } + + RgbaImage::from_raw(width, height, raw).unwrap() +} + +fn gaussian_blur_impl(data: &mut [[u8; 4]], width: usize, height: usize, blur_radius: f32) { + let bxs = create_box_gauss(blur_radius, 3); + let mut backbuf = data.to_vec(); + + box_blur( + &mut backbuf, + data, + width, + height, + ((bxs[0] - 1) / 2) as usize, + ); + box_blur( + &mut backbuf, + data, + width, + height, + ((bxs[1] - 1) / 2) as usize, + ); + box_blur( + &mut backbuf, + data, + width, + height, + ((bxs[2] - 1) / 2) as usize, + ); +} + +#[inline] +fn create_box_gauss(sigma: f32, n: usize) -> Vec { + let n_float = n as f32; + + // Ideal averaging filter width + let w_ideal = (12.0 * sigma * sigma / n_float).sqrt() + 1.0; + let mut wl: i32 = w_ideal.floor() as i32; + + if wl % 2 == 0 { + wl -= 1; + }; + + let wu = wl + 2; + + let wl_float = wl as f32; + let m_ideal = (12.0 * sigma * sigma + - n_float * wl_float * wl_float + - 4.0 * n_float * wl_float + - 3.0 * n_float) + / (-4.0 * wl_float - 4.0); + let m: usize = m_ideal.round() as usize; + + let mut sizes = Vec::::new(); + + for i in 0..n { + if i < m { + sizes.push(wl); + } else { + sizes.push(wu); + } + } + + sizes +} + +/// Needs 2x the same image +#[inline] +fn box_blur( + backbuf: &mut [[u8; 4]], + frontbuf: &mut [[u8; 4]], + width: usize, + height: usize, + blur_radius: usize, +) { + box_blur_horz(backbuf, frontbuf, width, height, blur_radius); + box_blur_vert(frontbuf, backbuf, width, height, blur_radius); +} + +#[inline] +fn box_blur_vert( + backbuf: &[[u8; 4]], + frontbuf: &mut [[u8; 4]], + width: usize, + height: usize, + blur_radius: usize, +) { + let iarr = 1.0 / (blur_radius + blur_radius + 1) as f32; + + let frontbuf = SharedMutPtr(frontbuf as *mut [[u8; 4]]); + (0..width).into_par_iter().for_each(|i| { + let col_start = i; //inclusive + let col_end = i + width * (height - 1); //inclusive + let mut ti: usize = i; + let mut li: usize = ti; + let mut ri: usize = ti + blur_radius * width; + + let fv: [u8; 4] = backbuf[col_start]; + let lv: [u8; 4] = backbuf[col_end]; + + let mut val_r: isize = (blur_radius as isize + 1) * isize::from(fv[0]); + let mut val_g: isize = (blur_radius as isize + 1) * isize::from(fv[1]); + let mut val_b: isize = (blur_radius as isize + 1) * isize::from(fv[2]); + let mut val_a: isize = (blur_radius as isize + 1) * isize::from(fv[3]); + + // Get the pixel at the specified index, or the first pixel of the column + // if the index is beyond the top edge of the image + let get_top = |i: usize| { + if i < col_start { + fv + } else { + backbuf[i] + } + }; + + // Get the pixel at the specified index, or the last pixel of the column + // if the index is beyond the bottom edge of the image + let get_bottom = |i: usize| { + if i > col_end { + lv + } else { + backbuf[i] + } + }; + + for j in 0..min(blur_radius, height) { + let bb = backbuf[ti + j * width]; + val_r += isize::from(bb[0]); + val_g += isize::from(bb[1]); + val_b += isize::from(bb[2]); + val_a += isize::from(bb[3]); + } + if blur_radius > height { + val_r += (blur_radius - height) as isize * isize::from(lv[0]); + val_g += (blur_radius - height) as isize * isize::from(lv[1]); + val_b += (blur_radius - height) as isize * isize::from(lv[2]); + val_a += (blur_radius - height) as isize * isize::from(lv[3]); + } + + for _ in 0..min(height, blur_radius + 1) { + let bb = get_bottom(ri); + ri += width; + val_r += isize::from(bb[0]) - isize::from(fv[0]); + val_g += isize::from(bb[1]) - isize::from(fv[1]); + val_b += isize::from(bb[2]) - isize::from(fv[2]); + val_a += isize::from(bb[3]) - isize::from(fv[3]); + + let frontbuf = unsafe { frontbuf.get() }; + frontbuf[ti] = [ + round(val_r as f32 * iarr) as u8, + round(val_g as f32 * iarr) as u8, + round(val_b as f32 * iarr) as u8, + round(val_a as f32 * iarr) as u8, + ]; + ti += width; + } + + if height > blur_radius { + // otherwise `(height - blur_radius)` will underflow + for _ in (blur_radius + 1)..(height - blur_radius) { + let bb1 = backbuf[ri]; + ri += width; + let bb2 = backbuf[li]; + li += width; + + val_r += isize::from(bb1[0]) - isize::from(bb2[0]); + val_g += isize::from(bb1[1]) - isize::from(bb2[1]); + val_b += isize::from(bb1[2]) - isize::from(bb2[2]); + val_a += isize::from(bb1[3]) - isize::from(bb2[3]); + + let frontbuf = unsafe { frontbuf.get() }; + frontbuf[ti] = [ + round(val_r as f32 * iarr) as u8, + round(val_g as f32 * iarr) as u8, + round(val_b as f32 * iarr) as u8, + round(val_a as f32 * iarr) as u8, + ]; + ti += width; + } + + for _ in 0..min(height - blur_radius - 1, blur_radius) { + let bb = get_top(li); + li += width; + + val_r += isize::from(lv[0]) - isize::from(bb[0]); + val_g += isize::from(lv[1]) - isize::from(bb[1]); + val_b += isize::from(lv[2]) - isize::from(bb[2]); + val_a += isize::from(lv[3]) - isize::from(bb[3]); + + let frontbuf = unsafe { frontbuf.get() }; + frontbuf[ti] = [ + round(val_r as f32 * iarr) as u8, + round(val_g as f32 * iarr) as u8, + round(val_b as f32 * iarr) as u8, + round(val_a as f32 * iarr) as u8, + ]; + ti += width; + } + } + }); +} + +#[inline] +fn box_blur_horz( + backbuf: &[[u8; 4]], + frontbuf: &mut [[u8; 4]], + width: usize, + height: usize, + blur_radius: usize, +) { + let iarr = 1.0 / (blur_radius + blur_radius + 1) as f32; + + let frontbuf = SharedMutPtr(frontbuf as *mut [[u8; 4]]); + (0..height).into_par_iter().for_each(|i| { + let row_start: usize = i * width; // inclusive + let row_end: usize = (i + 1) * width - 1; // inclusive + let mut ti: usize = i * width; // VERTICAL: $i; + let mut li: usize = ti; + let mut ri: usize = ti + blur_radius; + + let fv: [u8; 4] = backbuf[row_start]; + let lv: [u8; 4] = backbuf[row_end]; // VERTICAL: $backbuf[ti + $width - 1]; + + let mut val_r: isize = (blur_radius as isize + 1) * isize::from(fv[0]); + let mut val_g: isize = (blur_radius as isize + 1) * isize::from(fv[1]); + let mut val_b: isize = (blur_radius as isize + 1) * isize::from(fv[2]); + let mut val_a: isize = (blur_radius as isize + 1) * isize::from(fv[3]); + + // Get the pixel at the specified index, or the first pixel of the row + // if the index is beyond the left edge of the image + let get_left = |i: usize| { + if i < row_start { + fv + } else { + backbuf[i] + } + }; + + // Get the pixel at the specified index, or the last pixel of the row + // if the index is beyond the right edge of the image + let get_right = |i: usize| { + if i > row_end { + lv + } else { + backbuf[i] + } + }; + + for j in 0..min(blur_radius, width) { + let bb = backbuf[ti + j]; // VERTICAL: ti + j * width + val_r += isize::from(bb[0]); + val_g += isize::from(bb[1]); + val_b += isize::from(bb[2]); + val_a += isize::from(bb[3]); + } + if blur_radius > width { + val_r += (blur_radius - height) as isize * isize::from(lv[0]); + val_g += (blur_radius - height) as isize * isize::from(lv[1]); + val_b += (blur_radius - height) as isize * isize::from(lv[2]); + val_a += (blur_radius - height) as isize * isize::from(lv[3]); + } + + // Process the left side where we need pixels from beyond the left edge + for _ in 0..min(width, blur_radius + 1) { + let bb = get_right(ri); + ri += 1; + val_r += isize::from(bb[0]) - isize::from(fv[0]); + val_g += isize::from(bb[1]) - isize::from(fv[1]); + val_b += isize::from(bb[2]) - isize::from(fv[2]); + val_a += isize::from(bb[3]) - isize::from(fv[3]); + + let frontbuf = unsafe { frontbuf.get() }; + frontbuf[ti] = [ + round(val_r as f32 * iarr) as u8, + round(val_g as f32 * iarr) as u8, + round(val_b as f32 * iarr) as u8, + round(val_a as f32 * iarr) as u8, + ]; + ti += 1; // VERTICAL : ti += width, same with the other areas + } + + if width > blur_radius { + // otherwise `(width - blur_radius)` will underflow + // Process the middle where we know we won't bump into borders + // without the extra indirection of get_left/get_right. This is faster. + for _ in (blur_radius + 1)..(width - blur_radius) { + let bb1 = backbuf[ri]; + ri += 1; + let bb2 = backbuf[li]; + li += 1; + + val_r += isize::from(bb1[0]) - isize::from(bb2[0]); + val_g += isize::from(bb1[1]) - isize::from(bb2[1]); + val_b += isize::from(bb1[2]) - isize::from(bb2[2]); + val_a += isize::from(bb1[3]) - isize::from(bb2[3]); + + let frontbuf = unsafe { frontbuf.get() }; + frontbuf[ti] = [ + round(val_r as f32 * iarr) as u8, + round(val_g as f32 * iarr) as u8, + round(val_b as f32 * iarr) as u8, + round(val_a as f32 * iarr) as u8, + ]; + ti += 1; + } + + // Process the right side where we need pixels from beyond the right edge + for _ in 0..min(width - blur_radius - 1, blur_radius) { + let bb = get_left(li); + li += 1; + + val_r += isize::from(lv[0]) - isize::from(bb[0]); + val_g += isize::from(lv[1]) - isize::from(bb[1]); + val_b += isize::from(lv[2]) - isize::from(bb[2]); + val_a += isize::from(lv[3]) - isize::from(bb[3]); + + let frontbuf = unsafe { frontbuf.get() }; + frontbuf[ti] = [ + round(val_r as f32 * iarr) as u8, + round(val_g as f32 * iarr) as u8, + round(val_b as f32 * iarr) as u8, + round(val_a as f32 * iarr) as u8, + ]; + ti += 1; + } + } + }); +} + +#[inline] +/// Fast rounding for x <= 2^23. +/// This is orders of magnitude faster than built-in rounding intrinsic. +/// +/// Source: https://stackoverflow.com/a/42386149/585725 +fn round(mut x: f32) -> f32 { + x += 12_582_912.0; + x -= 12_582_912.0; + x +} diff --git a/crates/sss_lib/src/color.rs b/crates/sss_lib/src/color.rs index 629b851..ed7b609 100644 --- a/crates/sss_lib/src/color.rs +++ b/crates/sss_lib/src/color.rs @@ -1,10 +1,14 @@ +use image::Rgba; + +use crate::error::ParseColor as ParseColorError; + pub trait ToRgba { type Target; fn to_rgba(&self) -> Self::Target; } /// Parse hex color (#RRGGBB or #RRGGBBAA) -impl ToRgba for str { +impl ToRgba for String { type Target = Result, ParseColorError>; fn to_rgba(&self) -> Self::Target { @@ -51,3 +55,11 @@ impl ToRgba for str { } } } + +impl ToRgba for str { + type Target = Result, ParseColorError>; + + fn to_rgba(&self) -> Self::Target { + String::from(self).to_rgba() + } +} diff --git a/crates/sss_lib/src/components/mod.rs b/crates/sss_lib/src/components/mod.rs new file mode 100644 index 0000000..a8666cc --- /dev/null +++ b/crates/sss_lib/src/components/mod.rs @@ -0,0 +1,3 @@ +mod round; + +pub use round::round_corner; diff --git a/crates/sss_lib/src/components/round.rs b/crates/sss_lib/src/components/round.rs new file mode 100644 index 0000000..5a8b2b4 --- /dev/null +++ b/crates/sss_lib/src/components/round.rs @@ -0,0 +1,107 @@ +use image::imageops::{crop_imm, resize, FilterType}; +use image::{DynamicImage, GenericImage, GenericImageView, Rgba, RgbaImage}; +use imageproc::drawing::draw_line_segment_mut; + +/// Round the corner of the image +pub fn round_corner(image: &mut DynamicImage, radius: u32) { + // draw a circle with given foreground on given background + // then split it into four pieces and paste them to the four corner of the image + // + // the circle is drawn on a bigger image to avoid the aliasing + // later it will be scaled to the correct size + // we add +1 (to the radius) to make sure that there is also space for the border to mitigate artefacts when scaling + // note that the +1 isn't added to the radius when drawing the circle + let mut circle = + RgbaImage::from_pixel((radius + 1) * 4, (radius + 1) * 4, Rgba([255, 255, 255, 0])); + + let width = image.width(); + let height = image.height(); + + // use the bottom right pixel to get the color of the foreground + let foreground = image.get_pixel(width - 1, height - 1); + + draw_filled_circle_mut( + &mut circle, + (((radius + 1) * 2) as i32, ((radius + 1) * 2) as i32), + radius as i32 * 2, + foreground, + ); + + // scale down the circle to the correct size + let circle = resize( + &circle, + (radius + 1) * 2, + (radius + 1) * 2, + FilterType::Triangle, + ); + + // top left + let part = crop_imm(&circle, 1, 1, radius, radius); + image.copy_from(&*part, 0, 0).unwrap(); + + // top right + let part = crop_imm(&circle, radius + 1, 1, radius, radius - 1); + image.copy_from(&*part, width - radius, 0).unwrap(); + + // bottom left + let part = crop_imm(&circle, 1, radius + 1, radius, radius); + image.copy_from(&*part, 0, height - radius).unwrap(); + + // bottom right + let part = crop_imm(&circle, radius + 1, radius + 1, radius, radius); + image + .copy_from(&*part, width - radius, height - radius) + .unwrap(); +} + +// `draw_filled_circle_mut` doesn't work well with small radius in imageproc v0.18.0 +// it has been fixed but still have to wait for releasing +// issue: https://github.com/image-rs/imageproc/issues/328 +// PR: https://github.com/image-rs/imageproc/pull/330 +/// Draw as much of a circle, including its contents, as lies inside the image bounds. +pub fn draw_filled_circle_mut(image: &mut I, center: (i32, i32), radius: i32, color: I::Pixel) +where + I: GenericImage, + I::Pixel: 'static, +{ + let mut x = 0i32; + let mut y = radius; + let mut p = 1 - radius; + let x0 = center.0; + let y0 = center.1; + + while x <= y { + draw_line_segment_mut( + image, + ((x0 - x) as f32, (y0 + y) as f32), + ((x0 + x) as f32, (y0 + y) as f32), + color, + ); + draw_line_segment_mut( + image, + ((x0 - y) as f32, (y0 + x) as f32), + ((x0 + y) as f32, (y0 + x) as f32), + color, + ); + draw_line_segment_mut( + image, + ((x0 - x) as f32, (y0 - y) as f32), + ((x0 + x) as f32, (y0 - y) as f32), + color, + ); + draw_line_segment_mut( + image, + ((x0 - y) as f32, (y0 - x) as f32), + ((x0 + y) as f32, (y0 - x) as f32), + color, + ); + + x += 1; + if p < 0 { + p += 2 * x + 1; + } else { + y -= 1; + p += 2 * (x - y) + 1; + } + } +} diff --git a/crates/sss_lib/src/error.rs b/crates/sss_lib/src/error.rs new file mode 100644 index 0000000..724e3ba --- /dev/null +++ b/crates/sss_lib/src/error.rs @@ -0,0 +1,62 @@ +use font_kit::error::{FontLoadingError, SelectionError}; +use std::error::Error; +use std::fmt::{self, Display}; +use std::num::ParseIntError; + +#[derive(Debug)] +pub enum ImagenGeneration { + Font(Font), + Color(ParseColor), +} + +#[derive(Debug)] +pub enum Font { + SelectionError(SelectionError), + FontLoadingError(FontLoadingError), +} + +impl Error for Font {} + +impl Display for Font { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Font::SelectionError(e) => write!(f, "Font error: {}", e), + Font::FontLoadingError(e) => write!(f, "Font error: {}", e), + } + } +} + +impl From for Font { + fn from(e: SelectionError) -> Self { + Font::SelectionError(e) + } +} + +impl From for Font { + fn from(e: FontLoadingError) -> Self { + Font::FontLoadingError(e) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum ParseColor { + InvalidLength, + InvalidDigit, +} + +impl Error for ParseColor {} + +impl Display for ParseColor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseColor::InvalidDigit => write!(f, "Invalid digit"), + ParseColor::InvalidLength => write!(f, "Invalid length"), + } + } +} + +impl From for ParseColor { + fn from(_e: ParseIntError) -> Self { + ParseColor::InvalidDigit + } +} diff --git a/crates/sss_lib/src/image.rs b/crates/sss_lib/src/image.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/sss_lib/src/img.rs b/crates/sss_lib/src/img.rs new file mode 100644 index 0000000..94b8e68 --- /dev/null +++ b/crates/sss_lib/src/img.rs @@ -0,0 +1,55 @@ +use image::imageops::{resize, FilterType}; +use image::{Rgba, RgbaImage}; + +use crate::color::ToRgba; +use crate::components::round_corner; +// use crate::utils::copy_alpha; +use crate::{DynImageContent, GenerationSettings}; + +#[derive(Clone, Debug)] +pub enum Background { + Solid(Rgba), + Image(RgbaImage), +} + +impl Default for Background { + fn default() -> Self { + Self::Solid("#323232".to_rgba().unwrap()) + } +} + +impl Background { + pub fn to_image(&self, width: u32, height: u32) -> RgbaImage { + match self { + Background::Solid(color) => RgbaImage::from_pixel(width, height, color.to_owned()), + Background::Image(image) => resize(image, width, height, FilterType::Triangle), + } + } +} + +pub fn generate_image( + settings: GenerationSettings, + content: impl DynImageContent, +) -> image::ImageBuffer, Vec> { + let mut inner = content.content(); + let (p_x, p_y) = settings.padding; + let (w, h) = ( + inner.width() + (p_x * 2), + inner.height() + (p_y * 2), + ); + + let mut img = settings.background.to_image(w, h); + + if let Some(radius) = settings.round_corner { + round_corner(&mut inner, radius); + } + + if let Some(shadow) = settings.shadow { + inner = shadow.apply_to(&inner, p_x, p_y); + image::imageops::overlay(&mut img, &inner, 0, 0); + } else { + image::imageops::overlay(&mut img, &inner, p_x.into(), p_y.into()); + } + + img +} diff --git a/crates/sss_lib/src/lib.rs b/crates/sss_lib/src/lib.rs index 89f7298..93f37be 100644 --- a/crates/sss_lib/src/lib.rs +++ b/crates/sss_lib/src/lib.rs @@ -1,26 +1,44 @@ +//! This library is originally inspired from https://github.com/Aloxaf/silicon +use ::image::DynamicImage; + +pub mod blur; mod color; -mod image; +pub mod components; +pub mod error; +mod img; +mod shadow; +pub mod utils; + +pub use image; +pub use img::*; +pub use shadow::Shadow; pub struct GenerationSettings { - background: String, - /// pad between code and edge of code area. + /// Background for image + /// Default: #323232 + pub background: Background, + /// pad between inner immage and edge. /// Default: 25 - padding: (u32, u32), - /// Title bar padding - /// Default: 15 - title_bar_pad: u32, + pub padding: (u32, u32), /// round corner - /// Default: true - round_corner: bool, - /// Shadow adder - shadow_adder: Option, + /// Default: Some(15) + pub round_corner: Option, + /// Shadow + /// Default: None + pub shadow: Option, } -pub trait DynImageContent { - fn content(&self) -> DynamicImage::ImageRgba8; +impl Default for GenerationSettings { + fn default() -> Self { + Self { + background: Background::Solid(image::Rgba([0x32, 0x32, 0x32, 255])), + padding: (80, 100), + round_corner: Some(15), + shadow: None, + } + } } -pub fn generate_image(settings: GenerationSettings, content: impl DynImageContent) { - let mut img = - DynamicImage::ImageRgba8(RgbaImage::from_pixel(size.0, size.1, background.to_rgba())); +pub trait DynImageContent { + fn content(&self) -> DynamicImage; } diff --git a/crates/sss_lib/src/shadow.rs b/crates/sss_lib/src/shadow.rs new file mode 100644 index 0000000..759f714 --- /dev/null +++ b/crates/sss_lib/src/shadow.rs @@ -0,0 +1,48 @@ +use image::{DynamicImage, Rgba}; +use imageproc::drawing::draw_filled_rect_mut; +use imageproc::rect::Rect; + +use crate::color::ToRgba; +use crate::utils::copy_alpha; +use crate::Background; + +/// Add the shadow for image +#[derive(Debug)] +pub struct Shadow { + pub background: Background, + pub shadow_color: Rgba, + pub blur_radius: f32, +} + +impl Default for Shadow { + fn default() -> Self { + Self { + background: Background::default(), + shadow_color: "#707070".to_rgba().unwrap(), + blur_radius: 50.0, + } + } +} + +impl Shadow { + pub fn apply_to(&self, image: &DynamicImage, p_x: u32, p_y: u32) -> DynamicImage { + // the size of the final image + let width = image.width() + p_x * 2; + let height = image.height() + p_y * 2; + + // create the shadow + let mut shadow = self.background.to_image(width, height); + if self.blur_radius > 0.0 { + let rect = Rect::at(p_x as i32, p_y as i32).of_size(image.width(), image.height()); + + draw_filled_rect_mut(&mut shadow, rect, self.shadow_color); + + shadow = crate::blur::gaussian_blur(shadow, self.blur_radius); + } + + // copy the original image to the top of it + copy_alpha(image.as_rgba8().unwrap(), &mut shadow, p_x, p_y); + + DynamicImage::ImageRgba8(shadow) + } +} diff --git a/crates/sss_lib/src/utils.rs b/crates/sss_lib/src/utils.rs new file mode 100644 index 0000000..47b92c5 --- /dev/null +++ b/crates/sss_lib/src/utils.rs @@ -0,0 +1,23 @@ +use image::{Pixel, RgbaImage}; + +/// copy from src to dst, taking into account alpha channels +pub(crate) fn copy_alpha(src: &RgbaImage, dst: &mut RgbaImage, x: u32, y: u32) { + assert!(src.width() + x <= dst.width()); + assert!(src.height() + y <= dst.height()); + for j in 0..src.height() { + for i in 0..src.width() { + let s = src.get_pixel(i, j); + let mut d = dst.get_pixel(i + x, j + y).clone(); + match s.0[3] { + 255 => { + d = *s; + } + 0 => (/* do nothing */), + _ => { + d.blend(s); + } + } + dst.put_pixel(i + x, j + y, d.clone()); + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1900e47 --- /dev/null +++ b/flake.lock @@ -0,0 +1,187 @@ +{ + "nodes": { + "crane": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1704300976, + "narHash": "sha256-QLMpTrHxsND2T8+khAhLCqzOY/h2SzWS0s4Z7N2ds/E=", + "owner": "ipetkov", + "repo": "crane", + "rev": "0efe36f9232e0961512572883ba9c995aa1f54b1", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": "nixpkgs_2", + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1704090261, + "narHash": "sha256-Vti1mv4WhmXHPNcFgUiJyt4OKLvsvLzM2eKS4bEegf0=", + "owner": "nix-community", + "repo": "fenix", + "rev": "66fc1883c34c42df188b83272445aedb26bb64b5", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "monthly", + "repo": "fenix", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1704152458, + "narHash": "sha256-DS+dGw7SKygIWf9w4eNBUZsK+4Ug27NwEWmn2tnbycg=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "88a2cd8166694ba0b6cb374700799cec53aef527", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1704008649, + "narHash": "sha256-rGPSWjXTXTurQN9beuHdyJhB8O761w1Zc5BqSSmHvoM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1703961334, + "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1703637592, + "narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1704194953, + "narHash": "sha256-RtDKd8Mynhe5CFnVT8s0/0yqtWFMM9LmCzXv/YKxnq4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bd645e8668ec6612439a9ee7e71f7eac4099d4f6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "fenix": "fenix", + "flake-parts": "flake-parts", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_3" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1704034202, + "narHash": "sha256-OFBXLWm+aIqG+jNAz8BmB+QpepI11SGLtSY6qEs6EmY=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "cf52c4b2b3367ae7355ef23393e2eae1d37de723", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 4ff5f58..d9719b6 100644 --- a/flake.nix +++ b/flake.nix @@ -7,76 +7,86 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; crane.url = "github:ipetkov/crane"; }; - outputs = inputs @ { - flake-parts, - fenix, - nixpkgs, - flake-utils, - crane, - self, - ... - }: + outputs = + inputs @ { flake-parts + , fenix + , nixpkgs + , flake-utils + , crane + , self + , ... + }: inputs.flake-parts.lib.mkFlake - { - inherit inputs; - } - { - systems = ["x86_64-linux"]; - perSystem = { - config, - pkgs, - system, - ... - }: let - # Toolchain - toolchain = with fenix.packages.${system}; - fromToolchainFile { - file = ./rust-toolchain.toml; - sha256 = "sha256-U2yfueFohJHjif7anmJB5vZbpP7G6bICH4ZsjtufRoU="; - }; - craneLib = crane.lib.${system}.overrideToolchain toolchain; + { + inherit inputs; + } + { + systems = [ "x86_64-linux" ]; + perSystem = + { config + , pkgs + , system + , ... + }: + let + inherit (pkgs) lib; + # Toolchain + toolchain = with fenix.packages.${system}; + fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = "sha256-U2yfueFohJHjif7anmJB5vZbpP7G6bICH4ZsjtufRoU="; + }; + craneLib = crane.lib.${system}.overrideToolchain toolchain; - src = craneLib.cleanCargoSource (craneLib.path ./.); - commonArgs = { - inherit src; - buildInputs = with pkgs; [ + src = craneLib.cleanCargoSource (craneLib.path ./.); + buildInputs = with pkgs; [ pkg-config - ]; - }; - # Compile all artifacts for x86_64-unknown-linux-gnu - linuxArtifacts = craneLib.buildDepsOnly (commonArgs - // { - CARGO_BUILD_TARGET = "x86_64-unknown-linux-gnu"; - doCheck = false; - }); + fontconfig - # Compile app for x86_64-unknown-linux-gnu - linuxApp = craneLib.buildPackage ( - commonArgs - // { - doCheck = false; - cargoArtifacts = linuxArtifacts; - } - ); - in { - # nix build - packages = { - default = linuxApp; - }; + dbus + xorg.libXcursor + xorg.libxcb + ]; + commonArgs = { + inherit src; + inherit buildInputs; + }; + # Compile all artifacts for x86_64-unknown-linux-gnu + linuxArtifacts = craneLib.buildDepsOnly (commonArgs + // { + CARGO_BUILD_TARGET = "x86_64-unknown-linux-gnu"; + doCheck = false; + }); - # nix run - apps = { - default = flake-utils.lib.mkApp { - drv = linuxApp; - }; - }; + # Compile app for x86_64-unknown-linux-gnu + linuxApp = craneLib.buildPackage ( + commonArgs + // { + doCheck = false; + cargoArtifacts = linuxArtifacts; + } + ); + in + { + # nix build + packages = { + default = linuxApp; + }; - # nix develop - devShells.default = craneLib.devShell { - packages = [ - toolchain - ] ++ commonArgs.buildInputs; - }; + # nix run + apps = { + default = flake-utils.lib.mkApp { + drv = linuxApp; + }; + }; + + # nix develop + devShells.default = craneLib.devShell { + packages = [ + toolchain + ] ++ commonArgs.buildInputs; + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; + }; + }; }; - }; } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e7f52bc..43fada6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,11 @@ [toolchain] channel = "1.74.0" profile = "default" +components = [ + "rust-analyzer", + "rustc", + "cargo", + "clippy", + "rustfmt", + "rust-std", +]