fast image operations
add text to show()
bendn 2023-11-09
parent 8b0bb9f · commit f7b1c21
-rw-r--r--Cargo.toml4
-rw-r--r--data/CascadiaCode.ttfbin0 -> 7100 bytes
-rw-r--r--src/convert.rs21
-rw-r--r--src/drawing/text.rs43
-rw-r--r--src/lib.rs82
-rw-r--r--src/show.rs50
-rw-r--r--tdata/CascadiaCode.ttfbin648732 -> 0 bytes
-rw-r--r--tdata/blurred_pentagon.imgbufbin90000 -> 90000 bytes
8 files changed, 146 insertions, 54 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 38baac3..6fd5c77 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fimg"
-version = "0.4.23"
+version = "0.4.24"
authors = ["bend-n <[email protected]>"]
license = "MIT"
edition = "2021"
@@ -57,7 +57,7 @@ scale = ["fr"]
save = ["png"]
text = ["fontdue"]
blur = ["stackblur-iter"]
-real-show = ["minifb"]
+real-show = ["minifb", "text"]
default = ["save", "scale"]
[profile.release]
diff --git a/data/CascadiaCode.ttf b/data/CascadiaCode.ttf
new file mode 100644
index 0000000..09a679e
--- /dev/null
+++ b/data/CascadiaCode.ttf
Binary files differ
diff --git a/src/convert.rs b/src/convert.rs
index 8a6d7a4..3c6c54f 100644
--- a/src/convert.rs
+++ b/src/convert.rs
@@ -6,14 +6,15 @@ fn map<const A: usize, const B: usize>(image: Image<&[u8], A>) -> Image<Box<[u8]
where
[u8; B]: PFrom<A>,
{
- let buffer = image
- .chunked()
- .copied()
- .flat_map(<[u8; B] as PFrom<A>>::pfrom)
- .collect::<Vec<_>>()
- .into();
- // SAFETY: ctor
- unsafe { Image::new(image.width, image.height, buffer) }
+ // SAFETY: size unchanged, just change pixels
+ unsafe {
+ image.mapped(|buf| {
+ buf.array_chunks::<A>()
+ .copied()
+ .flat_map(<[u8; B] as PFrom<A>>::pfrom)
+ .collect()
+ })
+ }
}
macro_rules! convert {
@@ -64,12 +65,12 @@ boxconv!(4 => 2);
boxconv!(4 => 3);
#[inline]
-const fn pack([r, g, b, a]: [u8; 4]) -> u32 {
+pub const 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]
-const fn unpack(n: u32) -> [u8; 4] {
+pub const fn unpack(n: u32) -> [u8; 4] {
[
((n >> 16) & 0xFF) as u8,
((n >> 8) & 0xFF) as u8,
diff --git a/src/drawing/text.rs b/src/drawing/text.rs
index 3ff0b8d..192230a 100644
--- a/src/drawing/text.rs
+++ b/src/drawing/text.rs
@@ -1,19 +1,60 @@
//! text raster
use crate::{
+ convert::{pack, unpack},
pixels::{float, Wam},
Image,
};
use fontdue::{layout::TextStyle, Font};
use umath::{generic_float::Constructors, FF32};
+impl Image<&mut [u32], 1> {
+ pub(crate) fn text_u32(
+ &mut self,
+ x: u32,
+ y: u32,
+ size: f32,
+ font: &Font,
+ text: &str,
+ color: [u8; 4],
+ ) {
+ let mut lay =
+ fontdue::layout::Layout::new(fontdue::layout::CoordinateSystem::PositiveYDown);
+ lay.append(&[font], &TextStyle::new(text, size, 0));
+ for glpyh in lay.glyphs() {
+ let (metrics, bitmap) = font.rasterize(glpyh.parent, size);
+ for i in 0..metrics.width {
+ for j in 0..metrics.height {
+ let x = x + i as u32 + glpyh.x as u32;
+ if x >= self.width() {
+ continue;
+ }
+ let y = y + j as u32 + glpyh.y as u32;
+ if y >= self.height() {
+ continue;
+ }
+
+ // 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 { unpack(*self.buffer.get_unchecked(self.at(x, y))) };
+ // SAFETY: see above
+ *unsafe { self.buffer.get_unchecked_mut(self.at(x, y)) } =
+ // SAFETY: fill is 0..=1
+ pack(unsafe { bg.wam(color, FF32::one() - fill, fill) });
+ }
+ }
+ }
+ }
+}
+
impl<const N: usize, T: AsMut<[u8]> + AsRef<[u8]>> Image<T, N> {
/// Draw text.
///
/// ```
/// # use fimg::Image;
/// let font = fontdue::Font::from_bytes(
- /// &include_bytes!("../../tdata/CascadiaCode.ttf")[..],
+ /// &include_bytes!("../../data/CascadiaCode.ttf")[..],
/// fontdue::FontSettings {
/// scale: 200.0,
/// ..Default::default()
diff --git a/src/lib.rs b/src/lib.rs
index 1882368..f4317a2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -56,7 +56,8 @@
array_windows,
doc_auto_cfg,
const_option,
- array_chunks
+ array_chunks,
+ let_chains
)]
#![warn(
clippy::undocumented_unsafe_blocks,
@@ -243,21 +244,52 @@ impl<T, const CHANNELS: usize> Image<T, CHANNELS> {
pub unsafe fn buffer_mut(&mut self) -> &mut T {
&mut self.buffer
}
+
+ /// # Safety
+ ///
+ /// the output index is not guranteed to be in bounds
+ #[inline]
+ fn at(&self, x: u32, y: u32) -> usize {
+ debug_assert!(x < self.width(), "x out of bounds");
+ debug_assert!(y < self.height(), "y out of bounds");
+ #[allow(clippy::multiple_unsafe_ops_per_block)]
+ // SAFETY: me when uncheck math: 😧 (FIXME)
+ let index = unsafe {
+ let w = self.width();
+ // y * w + x
+ let tmp = (y as usize).unchecked_mul(w as usize);
+ tmp.unchecked_add(x as usize)
+ };
+ // SAFETY: 🧐 is unsound? 😖
+ unsafe { index.unchecked_mul(CHANNELS) }
+ }
+
+ /// # Safety
+ /// keep the buffer size the same
+ unsafe fn map<U, const N: usize, F: FnOnce(&T) -> U>(&self, f: F) -> Image<U, N> {
+ // SAFETY: we dont change anything, why check
+ unsafe { Image::new(self.width, self.height, f(self.buffer())) }
+ }
+
+ unsafe fn mapped<U, const N: usize, F: FnOnce(T) -> U>(self, f: F) -> Image<U, N> {
+ // SAFETY: we dont change anything, why check
+ unsafe { Image::new(self.width, self.height, f(self.buffer)) }
+ }
}
impl<const CHANNELS: usize, T: Clone> Image<&[T], CHANNELS> {
/// Allocate a new `Image<Vec<T>>` from this imageref.
pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> {
- // SAFETY: we have been constructed already, so must be valid
- unsafe { Image::new(self.width, self.height, self.buffer.to_vec()) }
+ // SAFETY: size not changed
+ unsafe { self.map(|b| b.to_vec()) }
}
}
impl<const CHANNELS: usize, T: Clone> Image<&mut [T], CHANNELS> {
/// Allocate a new `Image<Vec<T>>` from this mutable imageref.
pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> {
- // SAFETY: we have been constructed already, so must be valid
- unsafe { Image::new(self.width, self.height, self.buffer.to_vec()) }
+ // SAFETY: size not changed
+ unsafe { self.map(|b| b.to_vec()) }
}
}
@@ -301,16 +333,16 @@ impl<const CHANNELS: usize> Image<&[u8], CHANNELS> {
impl<const CHANNELS: usize, const N: usize> Image<[u8; N], CHANNELS> {
/// Box this array image.
pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> {
- // SAFETY: ctor
- unsafe { Image::new(self.width, self.height, Box::new(self.buffer)) }
+ // SAFETY: size not changed
+ unsafe { self.mapped(|b| b.into()) }
}
}
impl<const CHANNELS: usize> Image<&[u8], CHANNELS> {
/// Box this image.
pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> {
- // SAFETY: ctor
- unsafe { Image::new(self.width, self.height, self.buffer.into()) }
+ // SAFETY: size not changed
+ unsafe { self.mapped(|b| b.into()) }
}
}
@@ -318,7 +350,7 @@ impl<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> {
/// Box this owned image.
pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> {
// SAFETY: ctor
- unsafe { Image::new(self.width, self.height, self.buffer.into_boxed_slice()) }
+ unsafe { self.mapped(|b| b.into()) }
}
}
@@ -367,31 +399,11 @@ impl<T: AsRef<[u8]>, const CHANNELS: usize> Image<T, CHANNELS> {
#[inline]
fn slice(&self, x: u32, y: u32) -> impl SliceIndex<[u8], Output = [u8]> {
let index = self.at(x, y);
+ debug_assert!(self.len() > index);
// SAFETY: as long as the buffer isnt wrong, this is 😄
index..unsafe { index.unchecked_add(CHANNELS) }
}
- /// # Safety
- ///
- /// the output index is not guranteed to be in bounds
- #[inline]
- fn at(&self, x: u32, y: u32) -> usize {
- debug_assert!(x < self.width(), "x out of bounds");
- debug_assert!(y < self.height(), "y out of bounds");
- #[allow(clippy::multiple_unsafe_ops_per_block)]
- // SAFETY: me when uncheck math: 😧 (FIXME)
- let index = unsafe {
- let w = self.width();
- // y * w + x
- let tmp = (y as usize).unchecked_mul(w as usize);
- tmp.unchecked_add(x as usize)
- };
- // SAFETY: 🧐 is unsound? 😖
- let index = unsafe { index.unchecked_mul(CHANNELS) };
- debug_assert!(self.len() > index);
- index
- }
-
/// Procure a [`ImageCloner`].
#[must_use = "function does not modify the original image"]
pub fn cloner(&self) -> ImageCloner<'_, CHANNELS> {
@@ -539,8 +551,8 @@ impl<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> {
/// Consumes and leaks this image, returning a reference to the image.
#[must_use = "not using the returned reference is a memory leak"]
pub fn leak(self) -> Image<&'static mut [u8], CHANNELS> {
- // SAFETY: ctor
- unsafe { Image::new(self.width, self.height, self.buffer.leak()) }
+ // SAFETY: size unchanged
+ unsafe { self.mapped(Vec::leak) }
}
}
@@ -548,8 +560,8 @@ impl<const CHANNELS: usize, T: ?Sized> Image<Box<T>, CHANNELS> {
/// Consumes and leaks this image, returning a reference to the image.
#[must_use = "not using the returned reference is a memory leak"]
pub fn leak(self) -> Image<&'static mut T, CHANNELS> {
- // SAFETY: ctor
- unsafe { Image::new(self.width, self.height, Box::leak(self.buffer)) }
+ // SAFETY: size unchanged
+ unsafe { self.mapped(Box::leak) }
}
}
diff --git a/src/show.rs b/src/show.rs
index a7a1220..25040cb 100644
--- a/src/show.rs
+++ b/src/show.rs
@@ -2,10 +2,13 @@ use crate::Image;
#[cfg(feature = "real-show")]
mod real {
- use crate::Image;
+ use crate::{pixels::convert::PFrom, Image};
use minifb::{Key, Window};
- pub fn show(i: Image<&[u32], 1>) {
+ pub fn show<const CHANNELS: usize>(i: Image<&[u8], CHANNELS>)
+ where
+ [u8; 4]: PFrom<CHANNELS>,
+ {
let mut win = Window::new(
"show",
i.width() as usize,
@@ -13,9 +16,44 @@ mod real {
Default::default(),
)
.unwrap();
- win.limit_update_rate(Some(std::time::Duration::from_millis(100)));
+ let font = fontdue::Font::from_bytes(
+ &include_bytes!("../data/CascadiaCode.ttf")[..],
+ fontdue::FontSettings {
+ scale: 12.0,
+ ..Default::default()
+ },
+ )
+ .unwrap();
while win.is_open() && !win.is_key_down(Key::Q) && !win.is_key_down(Key::Escape) {
- win.update_with_buffer(&i.buffer, i.width() as usize, i.height() as usize)
+ let mut buf = Image::<Box<[u32]>, 1>::from(i.as_ref());
+
+ if !win.is_key_down(Key::H)
+ && let Some((x, y)) = win
+ .get_mouse_pos(minifb::MouseMode::Discard)
+ .map(|(x, y)| (x.round() as u32, y.round() as u32))
+ .map(|(x, y)| (x.min(i.width()), y.min(i.height())))
+ {
+ // SAFETY: ctor
+ unsafe { Image::new(buf.width, buf.height, &mut *buf.buffer) }.text_u32(
+ 5,
+ i.height() - 20,
+ 12.0,
+ &font,
+ &format!(
+ "P ({x}, {y}), {}",
+ // SAFETY: clampd
+ match unsafe { &i.pixel(x, y)[..] } {
+ [y] => format!("(Y {y})"),
+ [y, a] => format!("(Y {y} A {a})"),
+ [r, g, b] => format!("(R {r} G {g} B {b})"),
+ [r, g, b, a] => format!("(R {r} G {g} B {b} A {a})"),
+ _ => unreachable!(),
+ }
+ ),
+ [238, 232, 213, 255],
+ )
+ }
+ win.update_with_buffer(&buf.buffer, i.width() as usize, i.height() as usize)
.expect("window update fail");
}
}
@@ -93,7 +131,7 @@ macro_rules! show {
/// if the window is un creatable
pub fn show(self) -> Self {
#[cfg(feature = "real-show")]
- real::show(r(&self.as_ref().into()));
+ real::show(self.as_ref());
#[cfg(not(feature = "real-show"))]
fake::show!(self);
self
@@ -117,7 +155,7 @@ impl Image<Box<[u32]>, 1> {
/// if the window is un creatable
pub fn show(self) -> Self {
#[cfg(feature = "real-show")]
- real::show(r(&self));
+ real::show(Image::<Box<[u8]>, 4>::from(r(&self)).as_ref());
#[cfg(not(feature = "real-show"))]
fake::show!(Image::<Box<[u8]>, 4>::from(r(&self)));
self
diff --git a/tdata/CascadiaCode.ttf b/tdata/CascadiaCode.ttf
deleted file mode 100644
index b47bf63..0000000
--- a/tdata/CascadiaCode.ttf
+++ /dev/null
Binary files differ
diff --git a/tdata/blurred_pentagon.imgbuf b/tdata/blurred_pentagon.imgbuf
index 9580840..a73a99c 100644
--- a/tdata/blurred_pentagon.imgbuf
+++ b/tdata/blurred_pentagon.imgbuf
Binary files differ