//! Raster effects on premultiplied RGBA8 pixmaps: blur (3× box ≈ Gaussian), dilate/erode (spread), //! alpha extraction and tinting. use tiny_skia::Pixmap; /// Box radii for three passes approximating a Gaussian of `sigma` (Kovesi). fn box_sizes(sigma: f64) -> [usize; 3] { let n = 3.0; let w_ideal = (12.0 * sigma * sigma / n + 1.0).sqrt(); let mut wl = w_ideal.floor() as i64; if wl % 2 == 0 { wl -= 1; } let wu = wl + 2; let m_ideal = (12.0 * sigma * sigma - n * (wl * wl) as f64 - 4.0 * n * wl as f64 - 3.0 * n) / (-4.0 * wl as f64 - 4.0); let m = m_ideal.round() as i64; let r = |i: i64| -> usize { let w = if i < m { wl } else { wu }; ((w.max(1) - 1) / 2) as usize }; [r(0), r(1), r(2)] } fn box_blur_h(src: &[u8], dst: &mut [u8], w: usize, h: usize, r: usize) { if r == 0 { dst.copy_from_slice(src); return; } let div = (2 * r + 1) as u32; for y in 0..h { let row = y * w * 4; let mut acc = [0u32; 4]; // Edge: transparent outside. for x in 0..=r.min(w.saturating_sub(1)) { for c in 0..4 { acc[c] += u32::from(src.get(row + x * 4 + c).copied().unwrap_or(0)); } } for x in 0..w { for c in 0..4 { if let Some(d) = dst.get_mut(row + x * 4 + c) { *d = (acc[c] / div) as u8; } } let add = x + r + 1; if add < w { for c in 0..4 { acc[c] += u32::from(src.get(row + add * 4 + c).copied().unwrap_or(0)); } } if x >= r { let sub = x - r; for c in 0..4 { acc[c] = acc[c].saturating_sub(u32::from(src.get(row + sub * 4 + c).copied().unwrap_or(0))); } } } } } fn transpose(src: &[u8], dst: &mut [u8], w: usize, h: usize) { for y in 0..h { for x in 0..w { let s = (y * w + x) * 4; let d = (x * h + y) * 4; if let (Some(sp), Some(dp)) = (src.get(s..s + 4), dst.get_mut(d..d + 4)) { dp.copy_from_slice(sp); } } } } /// The largest blur standard deviation drawn (pixels): far wider than any layer (at most 16,384 /// pixels a side), so wider blurs look the same (everything averages out to nothing). The box /// passes cost the same at any radius, so the documented blur radii (up to 10,000 units) draw /// in full at any zoom. pub const MAX_SIGMA: f64 = 1_000_000.0; /// Gaussian-like blur of the whole pixmap with standard deviation `sigma` (pixels). pub fn blur(p: &mut Pixmap, sigma: f64) { if !(sigma.is_finite() && sigma > 0.3) { return; } let sigma = sigma.min(MAX_SIGMA); let (w, h) = (p.width() as usize, p.height() as usize); let radii = box_sizes(sigma); let mut a = p.data().to_vec(); let mut b = vec![0u8; a.len()]; for r in radii { box_blur_h(&a, &mut b, w, h, r); std::mem::swap(&mut a, &mut b); } transpose(&a, &mut b, w, h); std::mem::swap(&mut a, &mut b); for r in radii { box_blur_h(&a, &mut b, h, w, r); std::mem::swap(&mut a, &mut b); } transpose(&a, &mut b, h, w); p.data_mut().copy_from_slice(&b); } /// Grow (`r > 0`) or shrink (`r < 0`) the alpha of a pixmap by `r` pixels (square structuring /// element, separable max/min). Colour channels follow alpha (tinted afterwards anyway). pub fn morph(p: &mut Pixmap, r: i32) { if r == 0 { return; } let grow = r > 0; let r = r.unsigned_abs().min(400) as usize; let (w, h) = (p.width() as usize, p.height() as usize); let alpha: Vec = p.data().as_chunks::<4>().0.iter().map(|c| c.get(3).copied().unwrap_or(0)).collect(); let pass = |src: &[u8], w: usize, h: usize, horizontal: bool| -> Vec { let mut out = vec![0u8; src.len()]; for y in 0..h { for x in 0..w { let mut v: u8 = if grow { 0 } else { 255 }; let (lo, hi) = (x.saturating_sub(r), (x + r).min(w.saturating_sub(1))); for k in lo..=hi { let idx = if horizontal { y * w + k } else { k * h + y }; let s = src.get(idx).copied().unwrap_or(0); v = if grow { v.max(s) } else { v.min(s) }; } // Shrinking treats outside as empty. if !grow && (x < r || x + r >= w) { v = 0; } let o = if horizontal { y * w + x } else { x * h + y }; if let Some(d) = out.get_mut(o) { *d = v; } } } out }; let a1 = pass(&alpha, w, h, true); // Vertical pass: treat columns as rows by swapping roles. let mut a2 = vec![0u8; a1.len()]; for x in 0..w { for y in 0..h { let mut v: u8 = if grow { 0 } else { 255 }; let (lo, hi) = (y.saturating_sub(r), (y + r).min(h.saturating_sub(1))); for k in lo..=hi { let s = a1.get(k * w + x).copied().unwrap_or(0); v = if grow { v.max(s) } else { v.min(s) }; } if !grow && (y < r || y + r >= h) { v = 0; } if let Some(d) = a2.get_mut(y * w + x) { *d = v; } } } for (px, a) in p.data_mut().as_chunks_mut::<4>().0.iter_mut().zip(a2) { px.copy_from_slice(&[a, a, a, a]); } } /// Replace every pixel with `color` (non-premultiplied RGBA 0..1) at the pixel's alpha × color alpha. pub fn tint(p: &mut Pixmap, color: [f32; 4]) { let [r, g, b, a] = color; for px in p.data_mut().as_chunks_mut::<4>().0 { let alpha = f32::from(px.get(3).copied().unwrap_or(0)) / 255.0 * a; let q = |c: f32| (c * alpha * 255.0).round().clamp(0.0, 255.0) as u8; px.copy_from_slice(&[q(r), q(g), q(b), (alpha * 255.0).round().clamp(0.0, 255.0) as u8]); } } /// Invert the alpha (premultiplied white with alpha 1-a): used for inner shadows. pub fn invert_alpha(p: &mut Pixmap) { for px in p.data_mut().as_chunks_mut::<4>().0 { let a = 255 - px.get(3).copied().unwrap_or(0); px.copy_from_slice(&[a, a, a, a]); } } /// Multiply every pixel of `p` by the alpha of `m` (same size); `invert` uses 1-alpha. pub fn mask_by(p: &mut Pixmap, m: &Pixmap, invert: bool) { for (px, mp) in p.data_mut().as_chunks_mut::<4>().0.iter_mut().zip(m.data().chunks_exact(4)) { let ma = u32::from(mp.get(3).copied().unwrap_or(0)); let ma = if invert { 255 - ma } else { ma }; for c in px.iter_mut() { *c = ((u32::from(*c) * ma + 127) / 255) as u8; } } } /// Call `f(dst_row, src_row, row, first_col)` for each row where `src`, with its top-left at `at` /// on `dst`, overlaps it (premultiplied RGBA bytes of the overlapping columns; `row` and /// `first_col` in `src`'s pixels). Nothing past either edge is visited. pub(crate) fn overlap_rows(dst: &mut Pixmap, at: (i32, i32), src: &Pixmap, mut f: impl FnMut(&mut [u8], &[u8], i64, i64)) { let (dw, dh) = (i64::from(dst.width()), i64::from(dst.height())); let (sw, sh) = (i64::from(src.width()), i64::from(src.height())); let (ax, ay) = (i64::from(at.0), i64::from(at.1)); let (c0, c1) = ((-ax).max(0), sw.min(dw - ax)); let (r0, r1) = ((-ay).max(0), sh.min(dh - ay)); if c1 <= c0 || r1 <= r0 { return; } let len = (c1 - c0) as usize * 4; let s = src.data(); let d = dst.data_mut(); for r in r0..r1 { let (si, di) = ((r * sw + c0) as usize * 4, ((r + ay) * dw + ax + c0) as usize * 4); if let (Some(sp), Some(dp)) = (s.get(si..si + len), d.get_mut(di..di + len)) { f(dp, sp, r, c0); } } } /// Composite `src` at `opacity` onto `dst` (top-left at `at`) with plus-darker (linear burn: /// `max(0, backdrop + source - 1)`, through the W3C general compositing formula), which /// tiny-skia lacks. pub fn plus_darker(dst: &mut Pixmap, at: (i32, i32), src: &Pixmap, opacity: f32) { let o = if opacity.is_finite() { opacity.clamp(0.0, 1.0) } else { 0.0 }; overlap_rows(dst, at, src, |dp, sp, _, _| { for (d, s) in dp.chunks_exact_mut(4).zip(sp.chunks_exact(4)) { let f = |v: u8| f32::from(v) / 255.0; let sa = f(s[3]) * o; if sa <= 0.0 { continue; } let da = f(d[3]); for c in 0..3 { let (sc, dc) = (f(s[c]) * o, f(d[c])); let v = sc * (1.0 - da) + dc * (1.0 - sa) + (sa * dc + da * sc - sa * da).max(0.0); d[c] = (v.clamp(0.0, 1.0) * 255.0).round() as u8; } d[3] = ((sa + da - sa * da).clamp(0.0, 1.0) * 255.0).round() as u8; } }); } #[cfg(test)] mod tests { use super::*; #[test] fn blur_spreads_and_conserves() { let mut p = Pixmap::new(64, 64).unwrap(); let i = (32 * 64 + 32) * 4; p.data_mut()[i..i + 4].copy_from_slice(&[255, 255, 255, 255]); let before: u32 = p.data().iter().map(|v| u32::from(*v)).sum(); blur(&mut p, 3.0); let after: u32 = p.data().iter().map(|v| u32::from(*v)).sum(); assert!(p.data()[i + 3] < 255); // Integer rounding loses a little. assert!(after <= before && after > 0, "{before} {after}"); } #[test] fn morph_grows() { let mut p = Pixmap::new(20, 20).unwrap(); let i = (10 * 20 + 10) * 4; p.data_mut()[i..i + 4].copy_from_slice(&[255, 255, 255, 255]); morph(&mut p, 2); let count = p.data().as_chunks::<4>().0.iter().filter(|c| c[3] == 255).count(); assert_eq!(count, 25); morph(&mut p, -2); let count = p.data().as_chunks::<4>().0.iter().filter(|c| c[3] == 255).count(); assert_eq!(count, 1); } #[test] fn box_sizes_sane() { for s in [0.5, 1.0, 4.0, 50.0] { let b = box_sizes(s); assert!(b.iter().all(|r| *r < 1000)); } } }