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.rounding { apply_rounding(img, cfg.radius) } else { img }; if cfg.shadow { apply_shadow( img, cfg.shadow_blur, cfg.shadow_offset_x, cfg.shadow_offset_y, cfg.shadow_color, ) } else { img } } pub fn apply_rounding(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 dst_paint = PixmapPaint { blend_mode: BlendMode::DestinationIn, ..Default::default() }; pixmap.draw_pixmap(0, 0, mask.as_ref(), &dst_paint, Transform::identity(), None); pixmap_to_rgba_image(pixmap) } pub fn apply_shadow( img: RgbaImage, blur: f32, offset_x: f32, offset_y: f32, shadow_color: [u8; 4], ) -> RgbaImage { let (iw, ih) = img.dimensions(); let br = blur.ceil() as u32; let bp = 1; 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; let extra_right = spread + offset_x.max(0.0).ceil() as u32; let extra_bottom = spread + offset_y.max(0.0).ceil() 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 sp = PixmapPaint { blend_mode: BlendMode::Source, ..Default::default() }; 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, bp); let blurred_pixmap = rgba_image_to_pixmap(&blurred); let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap"); let p = PixmapPaint { blend_mode: BlendMode::Source, ..Default::default() }; canvas.draw_pixmap( 0, 0, blurred_pixmap.as_ref(), &p, Transform::identity(), None, ); let p2 = PixmapPaint { blend_mode: BlendMode::SourceOver, ..Default::default() }; 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") } // 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"); 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 } // Shadow 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 } // Shadow blur fn box_blur_rgba(img: &RgbaImage, radius: u32, bp: u32) -> RgbaImage { if radius == 0 { return img.clone(); } let mut buf = img.clone(); for _ in 0..bp { 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 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; 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 }