From c02376b96bf13a7a0b4dab7b5bd1863849971a2f Mon Sep 17 00:00:00 2001 From: bendn Date: Wed, 1 Nov 2023 09:09:20 +0700 Subject: [PATCH] blur --- Cargo.toml | 4 ++- README.md | 3 +- src/blur.rs | 51 ++++++++++++++++++++++++++++++++ src/convert.rs | 54 ++++++++++++++++++++++++++++++++++ src/lib.rs | 3 ++ src/pixels/convert.rs | 2 +- tdata/blurred_pentagon.imgbuf | Bin 0 -> 90000 bytes 7 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/blur.rs create mode 100644 tdata/blurred_pentagon.imgbuf diff --git a/Cargo.toml b/Cargo.toml index 721b047..ed1b715 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fimg" -version = "0.4.22" +version = "0.4.23" authors = ["bend-n "] license = "MIT" edition = "2021" @@ -17,6 +17,7 @@ fontdue = { version = "0.7.3", optional = true } vecto = "0.1.0" umath = "0.0.7" fr = { version = "0.1.1", package = "fer", optional = true } +stackblur-iter = { version = "0.2.0", features = ["simd"], optional = true } [dev-dependencies] iai = { git = "https://github.com/bend-n/iai.git" } @@ -50,6 +51,7 @@ harness = false scale = ["fr"] save = ["png"] text = ["fontdue"] +blur = ["stackblur-iter"] default = ["save", "scale"] [profile.release] diff --git a/README.md b/README.md index 7ad63a6..90be93f 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,5 @@ quick simple image operations - [x] box drawing - [x] polygon drawing - [x] circle drawing -- [x] text drawing \ No newline at end of file +- [x] text drawing +- [x] blur \ No newline at end of file diff --git a/src/blur.rs b/src/blur.rs new file mode 100644 index 0000000..62ec9fa --- /dev/null +++ b/src/blur.rs @@ -0,0 +1,51 @@ +use stackblur_iter::imgref::ImgRefMut; + +use crate::{pixels::convert::PFrom, Image}; + +impl + AsRef<[u32]>> Image { + /// Blur a image of packed 32 bit integers, `[0xAARRGGBB]`. + pub fn blur_argb(&mut self, radius: usize) { + let w = self.width() as usize; + let h = self.height() as usize; + stackblur_iter::simd_blur_argb::<4>(&mut ImgRefMut::new(self.buffer.as_mut(), w, h), radius) + } +} + +impl Image, N> +where + [u8; 4]: PFrom, + [u8; N]: PFrom<4>, +{ + /// Blur a image. + /// ``` + /// # use fimg::Image; + /// let mut i = Image::alloc(300, 300).boxed(); + /// // draw a lil pentagon + /// i.poly((150., 150.), 5, 100.0, 0.0, [255]); + /// // give it some blur + /// i.blur(25); + /// assert_eq!(include_bytes!("../tdata/blurred_pentagon.imgbuf"), i.bytes()) + /// ``` + pub fn blur(&mut self, radius: usize) { + // you know, i optimized blurslice a fair bit, and yet, despite all the extra bit twiddling stackblur-iter is faster. + let mut argb = Image::, 1>::from(self.as_ref()); + argb.blur_argb(radius); + for (i, n) in crate::convert::unpack_all(&argb.buffer).enumerate() { + *unsafe { self.buffer.get_unchecked_mut(i) } = n; + } + } +} + +impl Image<&[u8], N> +where + [u8; 4]: PFrom, + [u8; N]: PFrom<4>, +{ + /// Blur a image. + pub fn blur(self, radius: usize) -> Image, N> { + let mut argb = Image::, 1>::from(self); + argb.blur_argb(radius); + // SAFETY: ctor + unsafe { Image::new(argb.width, argb.height, &**argb.buffer()) }.into() + } +} diff --git a/src/convert.rs b/src/convert.rs index 5704f26..6f44116 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -62,3 +62,57 @@ boxconv!(3 => 4); boxconv!(4 => 1); boxconv!(4 => 2); boxconv!(4 => 3); + +#[inline] +fn pack([r, g, b, a]: [u8; 4]) -> u32 { + ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) +} + +#[inline] +fn unpack(n: u32) -> [u8; 4] { + [ + ((n >> 16) & 0xFF) as u8, + ((n >> 8) & 0xFF) as u8, + (n & 0xFF) as u8, + ((n >> 24) & 0xFF) as u8, + ] +} + +impl From> for Image, 1> +where + [u8; 4]: PFrom, +{ + /// Pack into ARGB. + fn from(value: Image<&[u8], N>) -> Self { + let buf = value + .chunked() + .copied() + .map(PFrom::pfrom) + .map(pack) + .collect(); + // SAFETY: ctor + unsafe { Self::new(value.width, value.height, buf) } + } +} + +pub fn unpack_all(buffer: &[u32]) -> impl Iterator + '_ +where + [u8; N]: PFrom<4>, +{ + buffer + .iter() + .copied() + .map(unpack) + .flat_map(<[u8; N] as PFrom<4>>::pfrom) +} + +impl From> for Image, N> +where + [u8; N]: PFrom<4>, +{ + fn from(value: Image<&[u32], 1>) -> Self { + let buf = unpack_all(value.buffer).collect(); + // SAFETY: ctor + unsafe { Self::new(value.width, value.height, buf) } + } +} diff --git a/src/lib.rs b/src/lib.rs index e325c6c..52bc7ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ //! Misc image ops: //! - [`Image::repeated`] //! - [`Image::overlay`](Overlay), [`Image::overlay_at`](OverlayAt), [`Image::overlay_blended`](BlendingOverlay) +//! - [`Image::blur`] #![feature( slice_swap_unchecked, generic_const_exprs, @@ -60,6 +61,8 @@ use std::{num::NonZeroU32, slice::SliceIndex}; mod affine; +#[cfg(feature = "blur")] +mod blur; #[doc(hidden)] pub mod builder; #[doc(hidden)] diff --git a/src/pixels/convert.rs b/src/pixels/convert.rs index 55c9338..13b7e62 100644 --- a/src/pixels/convert.rs +++ b/src/pixels/convert.rs @@ -24,7 +24,7 @@ impl PFrom<2> for Y { impl PFrom<3> for Y { fn pfrom([r, g, b]: RGB) -> Self { - [((2126 * r as u16 + 7152 * g as u16 + 722 * b as u16) / 10000) as u8] + [((2126 * r as u32 + 7152 * g as u32 + 722 * b as u32) / 10000) as u8] } } diff --git a/tdata/blurred_pentagon.imgbuf b/tdata/blurred_pentagon.imgbuf new file mode 100644 index 0000000000000000000000000000000000000000..9580840e88172b03291aeb18d70269e521645ab1 GIT binary patch literal 90000 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 O0Sy5S0Sy7!5%~Y7&|7K% literal 0 HcmV?d00001