From 7c0e8a66c7ce5b07fb7f929995a4095ca21b910c Mon Sep 17 00:00:00 2001 From: bendn Date: Mon, 30 Oct 2023 12:07:29 +0700 Subject: [PATCH] add pixelops, improve text --- src/drawing/circle.rs | 1 - src/drawing/text.rs | 73 +++++++++---------------------- src/lib.rs | 1 + src/pixels/blending.rs | 95 +++++++++++++++++++++++++++++++++++++++++ src/pixels/mod.rs | 8 ++++ src/pixels/utility.rs | 72 +++++++++++++++++++++++++++++++ src/pixels/wam.rs | 37 ++++++++++++++++ src/scale/algorithms.rs | 29 ------------- 8 files changed, 232 insertions(+), 84 deletions(-) create mode 100644 src/pixels/blending.rs create mode 100644 src/pixels/mod.rs create mode 100644 src/pixels/utility.rs create mode 100644 src/pixels/wam.rs diff --git a/src/drawing/circle.rs b/src/drawing/circle.rs index 5f9f105..1adb7e5 100644 --- a/src/drawing/circle.rs +++ b/src/drawing/circle.rs @@ -1,7 +1,6 @@ //! draw 2d circles use crate::Image; - impl + AsRef<[u8]>, const CHANNELS: usize> Image { /// Draws a circle, using the [Bresenham's circle](https://en.wikipedia.org/wiki/Midpoint_circle_algorithm) algorithm. /// ``` diff --git a/src/drawing/text.rs b/src/drawing/text.rs index 3a8502f..3ff0b8d 100644 --- a/src/drawing/text.rs +++ b/src/drawing/text.rs @@ -1,26 +1,29 @@ //! text raster -use crate::Image; +use crate::{ + pixels::{float, Wam}, + Image, +}; use fontdue::{layout::TextStyle, Font}; -use umath::FFloat; +use umath::{generic_float::Constructors, FF32}; -impl + AsRef<[u8]>> Image { +impl + AsRef<[u8]>> Image { /// Draw text. /// /// ``` /// # use fimg::Image; /// let font = fontdue::Font::from_bytes( - /// &include_bytes!("../../tdata/CascadiaCode.ttf")[..], - /// fontdue::FontSettings { - /// scale: 200.0, - /// ..Default::default() - /// }, + /// &include_bytes!("../../tdata/CascadiaCode.ttf")[..], + /// fontdue::FontSettings { + /// scale: 200.0, + /// ..Default::default() + /// }, /// ).unwrap(); /// let mut i: Image<_, 4> = Image::alloc(750, 250).boxed(); /// i.text(50, 10, 200.0, &font, "hello", [0, 0, 0, 255]); - /// # assert_eq!(i.buffer(), include_bytes!("../../tdata/text.imgbuf")); + /// # assert_eq!(&**i.buffer(), include_bytes!("../../tdata/text.imgbuf")); /// ``` - pub fn text(&mut self, x: u32, y: u32, size: f32, font: &Font, text: &str, color: [u8; 4]) { + pub fn text(&mut self, x: u32, y: u32, size: f32, font: &Font, text: &str, color: [u8; N]) { let mut lay = fontdue::layout::Layout::new(fontdue::layout::CoordinateSystem::PositiveYDown); lay.append(&[font], &TextStyle::new(text, size, 0)); @@ -28,10 +31,6 @@ impl + AsRef<[u8]>> Image { let (metrics, bitmap) = font.rasterize(glpyh.parent, size); for i in 0..metrics.width { for j in 0..metrics.height { - // SAFETY: the rasterizer kinda promises that metrics width and height are in bounds - let fg = [color[0], color[1], color[2], unsafe { - *bitmap.get_unchecked(j * metrics.width + i) - }]; let x = x + i as u32 + glpyh.x as u32; if x >= self.width() { continue; @@ -40,49 +39,15 @@ impl + AsRef<[u8]>> Image { if y >= self.height() { continue; } - // SAFETY: we clampin - let bg = unsafe { self.pixel_mut(x, y) }; - blend(bg.try_into().unwrap(), fg); + // SAFETY: the rasterizer kinda promises that metrics width and height are in bounds + let fill = unsafe { float(*bitmap.get_unchecked(j * metrics.width + i)) }; + // SAFETY: we clampin + let bg = unsafe { &mut *(self.pixel_mut(x, y).as_mut_ptr() as *mut [u8; N]) }; + // SAFETY: fill is 0..=1 + *bg = unsafe { bg.wam(color, FF32::one() - fill, fill) }; } } } } } - -pub fn blend(bg: &mut [u8; 4], fg: [u8; 4]) { - if fg[3] == 0 { - return; - } - if fg[3] == 255 { - *bg = fg; - return; - } - #[allow(clippy::multiple_unsafe_ops_per_block)] - // SAFETY: no u8 can possibly become INF / NAN - unsafe { - let max = FFloat::new(255.0); - let bg_a = FFloat::new(bg[3] as f32) / max; - let fg_a = FFloat::new(fg[3] as f32) / max; - let a = bg_a + fg_a - bg_a * fg_a; - if a == 0.0 { - return; - }; - // could turn it into array::map - *bg = [ - *(max - * ((((FFloat::new(fg[0] as f32) / max) * fg_a) - + ((FFloat::new(bg[0] as f32) / max) * bg_a) * (FFloat::new(1.0) - fg_a)) - / a)) as u8, - *(max - * ((((FFloat::new(fg[1] as f32) / max) * fg_a) - + ((FFloat::new(bg[1] as f32) / max) * bg_a) * (FFloat::new(1.0) - fg_a)) - / a)) as u8, - *(max - * ((((FFloat::new(fg[2] as f32) / max) * fg_a) - + ((FFloat::new(bg[2] as f32) / max) * bg_a) * (FFloat::new(1.0) - fg_a)) - / a)) as u8, - *(max * a) as u8, - ] - } -} diff --git a/src/lib.rs b/src/lib.rs index 4489c17..53ae5d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub mod cloner; mod drawing; pub(crate) mod math; mod overlay; +pub mod pixels; #[cfg(feature = "scale")] pub mod scale; use cloner::ImageCloner; diff --git a/src/pixels/blending.rs b/src/pixels/blending.rs new file mode 100644 index 0000000..17ce141 --- /dev/null +++ b/src/pixels/blending.rs @@ -0,0 +1,95 @@ +//! module for pixel blending ops +use super::{unfloat, Floatify, PMap, Trunc, Unfloatify}; +use umath::FF32; + +/// Trait for blending pixels together. +pub trait Blend { + /// blends self with another pixel + fn blend(&mut self, with: [u8; W]); +} + +impl Blend<4> for [u8; 4] { + fn blend(&mut self, fg: [u8; 4]) { + if fg[3] == 0 { + return; + } + if fg[3] == 255 { + *self = fg; + return; + } + let fg = fg.float(); + let bg = self.float(); + let a = bg[3] + fg[3] - bg[3] * fg[3]; + if a == 0.0 { + return; + }; + self[..3].copy_from_slice( + &fg.trunc() + // SAFETY: no u8 can possibly become INF / NAN + .pmap(bg.trunc(), |f, b| unsafe { + (f * fg[3] + b * bg[3] * (FF32::new(1.0) - fg[3])) / a + }) + .unfloat(), + ); + self[3] = unfloat(a); + } +} + +impl Blend<3> for [u8; 3] { + fn blend(&mut self, with: [u8; 3]) { + *self = with; + } +} + +impl Blend<2> for [u8; 2] { + fn blend(&mut self, with: [u8; 2]) { + let bg = self.float(); + let fg = with.float(); + + let a = bg[1] + fg[1] - bg[1] * fg[1]; + if a == 0.0 { + return; + } + *self = [ + // SAFETY: no u8 can do transform bad + (fg[0] * fg[1] + bg[0] * bg[1] * (unsafe { FF32::new(1.0) } - fg[1])) / a, + a, + ] + .unfloat(); + } +} + +impl Blend<1> for [u8; 1] { + fn blend(&mut self, with: [u8; 1]) { + *self = with; + } +} + +#[cfg(test)] +mod blend { + use super::*; + + macro_rules! blend { + ([$($a:literal),+] + [$($b:literal),+] = $what:expr) => { + let mut a = [$($a,)+]; + a.blend([$($b,)+]); + assert_eq!(a, $what); + }; +} + + #[test] + fn test_blend_rgba() { + blend!([255, 255, 255, 255] + [255, 255, 255, 255] = [255, 255, 255, 255]); + blend!([255, 255, 255, 0] + [255, 255, 255, 255] = [255, 255, 255, 255]); + blend!([255, 255, 255, 255] + [255_u8, 255, 255, 0] = [255, 255, 255, 255]); + blend!([255, 255, 255, 0] + [255_u8, 255, 255, 0] = [255, 255, 255, 0]); + } + + #[test] + fn test_blend_ya() { + blend!([255, 255] + [255, 255] = [255, 255]); + blend!([255, 0] + [255, 255] = [255, 255]); + blend!([255, 255] + [255, 0] = [255, 255]); + blend!([255, 0] + [255, 0] = [255, 0]); + } +} diff --git a/src/pixels/mod.rs b/src/pixels/mod.rs new file mode 100644 index 0000000..503115a --- /dev/null +++ b/src/pixels/mod.rs @@ -0,0 +1,8 @@ +//! module for pixels ops. +#![allow(unused_imports)] +pub mod blending; +mod utility; +mod wam; +pub use blending::Blend; +pub(crate) use utility::{float, unfloat, Floatify, PMap, Trunc, Unfloatify}; +pub(crate) use wam::Wam; diff --git a/src/pixels/utility.rs b/src/pixels/utility.rs new file mode 100644 index 0000000..5184f58 --- /dev/null +++ b/src/pixels/utility.rs @@ -0,0 +1,72 @@ +use umath::FF32; +pub trait Unfloatify { + /// computes 255 * n, for all elements + fn unfloat(self) -> [u8; N]; +} + +#[inline(always)] +/// computes 255 * n +pub fn unfloat(n: FF32) -> u8 { + // SAFETY: n is 0..=1 + unsafe { *(FF32::new(255.0) * n) as u8 } +} + +impl Unfloatify for [FF32; N] { + fn unfloat(self) -> [u8; N] { + self.map(unfloat) + } +} + +#[rustfmt::skip] +implUnfloatifyfor[u8; N]{fn unfloat(self)->[u8;N]{self}} + +pub trait Floatify { + /// computes n / 255, for all elements + fn float(self) -> [FF32; N]; +} + +/// computes n / 255 +pub fn float(n: u8) -> FF32 { + // SAFETY: 0..=255 / 0..=255 maynt ever be NAN / INF + unsafe { FF32::new(n as f32) / FF32::new(255.0) } +} + +impl Floatify for [u8; N] { + fn float(self) -> [FF32; N] { + self.map(float) + } +} + +#[rustfmt::skip] +implFloatifyfor[FF32;N]{fn float(self)->[FF32;N]{self}} + +pub trait PMap { + /// think of it like a `a.zip(b).map(f).collect::<[]>()` + fn pmap(self, with: Self, f: impl FnMut(T, T) -> R) -> [R; N]; +} + +impl PMap for [T; N] { + fn pmap(self, with: Self, mut f: impl FnMut(T, T) -> R) -> [R; N] { + let mut iter = self.into_iter().zip(with).map(|(a, b)| f(a, b)); + std::array::from_fn(|_| iter.next().unwrap()) + } +} + +pub trait Trunc { + /// it does `a[..a.len() - 1].try_into().unwrap()``. + fn trunc(&self) -> [T; N - 1]; +} + +impl Trunc for [T; N] { + fn trunc(&self) -> [T; N - 1] { + self[..N - 1].try_into().unwrap() + } +} + +#[test] +fn trunc() { + let x = [1]; + assert_eq!(x.trunc(), []); + let x = [1, 2, 3, 4]; + assert_eq!(x.trunc(), [1, 2, 3]); +} diff --git a/src/pixels/wam.rs b/src/pixels/wam.rs new file mode 100644 index 0000000..30016fd --- /dev/null +++ b/src/pixels/wam.rs @@ -0,0 +1,37 @@ +use umath::{generic_float::Constructors, FF32}; + +use super::{float, unfloat, PMap}; + +pub trait Wam { + /// this function weighs the sides and combines + /// + /// # Safety + /// + /// pls make l = 0..=f32::MAX/2, r = 0..=f32::MAX/2 + unsafe fn wam(self, b: Self, l: FF32, r: FF32) -> Self; +} + +impl Wam for [u8; N] { + unsafe fn wam(self, b: Self, l: FF32, r: FF32) -> Self { + // SAFETY: read [`weigh`] + self.pmap(b, |a, b| unsafe { weigh(a, b, l, r) }) + } +} + +#[inline(always)] +/// # Safety +/// +/// floats must be smart +unsafe fn weigh(a: u8, b: u8, l: FF32, r: FF32) -> u8 { + // SAFETY: float(x) returns 0..=1, 0..=1 * f32::MAX isnt Inf, but if you add 1.0 and then mul by max again, you get inf (big bad, hence unsafe fn) + unsafe { unfloat((float(a) * l + float(b) * r).clamp(FF32::zero(), FF32::one())) } +} + +#[test] +fn weig() { + unsafe { + assert_eq!(weigh(10, 20, FF32::new(0.5), FF32::new(0.5)), 15); + assert_eq!(weigh(10, 20, FF32::new(0.9), FF32::new(0.1)), 11); + assert_eq!(weigh(150, 150, FF32::new(1.8), FF32::new(0.8)), 255); + } +} diff --git a/src/scale/algorithms.rs b/src/scale/algorithms.rs index 55a71d2..eaec38c 100644 --- a/src/scale/algorithms.rs +++ b/src/scale/algorithms.rs @@ -111,32 +111,3 @@ alg!(Hamming); /// [Mitchell–Netravali](https://en.wikipedia.org/wiki/Mitchell%E2%80%93Netravali_filters) bicubic filtering. pub struct Mitchell {} alg!(Mitchell); - -impl Nearest { - /// Resize a image. - /// # Safety - /// - /// `image` must be as big or bigger than `width`, `height. - #[must_use = "function does not modify the original image"] - #[deprecated = "use Image::scale instead (note that Image::scale does not support any N. if there is a N you would like to see supported, please open a issue)"] - pub unsafe fn scale( - image: Image<&[u8], N>, - width: u32, - height: u32, - ) -> Image, N> { - let x_scale = image.width() as f32 / width as f32; - let y_scale = image.height() as f32 / height as f32; - let mut out = Image::alloc(width, height); - for y in 0..height { - for x in 0..width { - let x1 = ((x as f32 + 0.5) * x_scale).floor() as u32; - let y1 = ((y as f32 + 0.5) * y_scale).floor() as u32; - // SAFETY: i asked the caller to make sure its ok - let px = unsafe { image.pixel(x1, y1) }; - // SAFETY: were looping over the width and height of out. its ok. - unsafe { out.set_pixel(x, y, px) }; - } - } - out - } -}