add text to show()

This commit is contained in:
bendn 2023-11-09 07:28:37 +07:00
parent 8b0bb9f804
commit f7b1c2100f
No known key found for this signature in database
GPG key ID: 0D9D3A2A3B2A93D6
8 changed files with 146 additions and 54 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "fimg" name = "fimg"
version = "0.4.23" version = "0.4.24"
authors = ["bend-n <bend.n@outlook.com>"] authors = ["bend-n <bend.n@outlook.com>"]
license = "MIT" license = "MIT"
edition = "2021" edition = "2021"
@ -57,7 +57,7 @@ scale = ["fr"]
save = ["png"] save = ["png"]
text = ["fontdue"] text = ["fontdue"]
blur = ["stackblur-iter"] blur = ["stackblur-iter"]
real-show = ["minifb"] real-show = ["minifb", "text"]
default = ["save", "scale"] default = ["save", "scale"]
[profile.release] [profile.release]

BIN
data/CascadiaCode.ttf Normal file

Binary file not shown.

View file

@ -6,14 +6,15 @@ fn map<const A: usize, const B: usize>(image: Image<&[u8], A>) -> Image<Box<[u8]
where where
[u8; B]: PFrom<A>, [u8; B]: PFrom<A>,
{ {
let buffer = image // SAFETY: size unchanged, just change pixels
.chunked() unsafe {
image.mapped(|buf| {
buf.array_chunks::<A>()
.copied() .copied()
.flat_map(<[u8; B] as PFrom<A>>::pfrom) .flat_map(<[u8; B] as PFrom<A>>::pfrom)
.collect::<Vec<_>>() .collect()
.into(); })
// SAFETY: ctor }
unsafe { Image::new(image.width, image.height, buffer) }
} }
macro_rules! convert { macro_rules! convert {
@ -64,12 +65,12 @@ boxconv!(4 => 2);
boxconv!(4 => 3); boxconv!(4 => 3);
#[inline] #[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) ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
} }
#[inline] #[inline]
const fn unpack(n: u32) -> [u8; 4] { pub const fn unpack(n: u32) -> [u8; 4] {
[ [
((n >> 16) & 0xFF) as u8, ((n >> 16) & 0xFF) as u8,
((n >> 8) & 0xFF) as u8, ((n >> 8) & 0xFF) as u8,

View file

@ -1,19 +1,60 @@
//! text raster //! text raster
use crate::{ use crate::{
convert::{pack, unpack},
pixels::{float, Wam}, pixels::{float, Wam},
Image, Image,
}; };
use fontdue::{layout::TextStyle, Font}; use fontdue::{layout::TextStyle, Font};
use umath::{generic_float::Constructors, FF32}; 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> { impl<const N: usize, T: AsMut<[u8]> + AsRef<[u8]>> Image<T, N> {
/// Draw text. /// Draw text.
/// ///
/// ``` /// ```
/// # use fimg::Image; /// # use fimg::Image;
/// let font = fontdue::Font::from_bytes( /// let font = fontdue::Font::from_bytes(
/// &include_bytes!("../../tdata/CascadiaCode.ttf")[..], /// &include_bytes!("../../data/CascadiaCode.ttf")[..],
/// fontdue::FontSettings { /// fontdue::FontSettings {
/// scale: 200.0, /// scale: 200.0,
/// ..Default::default() /// ..Default::default()

View file

@ -56,7 +56,8 @@
array_windows, array_windows,
doc_auto_cfg, doc_auto_cfg,
const_option, const_option,
array_chunks array_chunks,
let_chains
)] )]
#![warn( #![warn(
clippy::undocumented_unsafe_blocks, 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 { pub unsafe fn buffer_mut(&mut self) -> &mut T {
&mut self.buffer &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> { impl<const CHANNELS: usize, T: Clone> Image<&[T], CHANNELS> {
/// Allocate a new `Image<Vec<T>>` from this imageref. /// Allocate a new `Image<Vec<T>>` from this imageref.
pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> { pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> {
// SAFETY: we have been constructed already, so must be valid // SAFETY: size not changed
unsafe { Image::new(self.width, self.height, self.buffer.to_vec()) } unsafe { self.map(|b| b.to_vec()) }
} }
} }
impl<const CHANNELS: usize, T: Clone> Image<&mut [T], CHANNELS> { impl<const CHANNELS: usize, T: Clone> Image<&mut [T], CHANNELS> {
/// Allocate a new `Image<Vec<T>>` from this mutable imageref. /// Allocate a new `Image<Vec<T>>` from this mutable imageref.
pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> { pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> {
// SAFETY: we have been constructed already, so must be valid // SAFETY: size not changed
unsafe { Image::new(self.width, self.height, self.buffer.to_vec()) } 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> { impl<const CHANNELS: usize, const N: usize> Image<[u8; N], CHANNELS> {
/// Box this array image. /// Box this array image.
pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> { pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> {
// SAFETY: ctor // SAFETY: size not changed
unsafe { Image::new(self.width, self.height, Box::new(self.buffer)) } unsafe { self.mapped(|b| b.into()) }
} }
} }
impl<const CHANNELS: usize> Image<&[u8], CHANNELS> { impl<const CHANNELS: usize> Image<&[u8], CHANNELS> {
/// Box this image. /// Box this image.
pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> { pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> {
// SAFETY: ctor // SAFETY: size not changed
unsafe { Image::new(self.width, self.height, self.buffer.into()) } unsafe { self.mapped(|b| b.into()) }
} }
} }
@ -318,7 +350,7 @@ impl<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> {
/// Box this owned image. /// Box this owned image.
pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> { pub fn boxed(self) -> Image<Box<[u8]>, CHANNELS> {
// SAFETY: ctor // 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] #[inline]
fn slice(&self, x: u32, y: u32) -> impl SliceIndex<[u8], Output = [u8]> { fn slice(&self, x: u32, y: u32) -> impl SliceIndex<[u8], Output = [u8]> {
let index = self.at(x, y); let index = self.at(x, y);
debug_assert!(self.len() > index);
// SAFETY: as long as the buffer isnt wrong, this is 😄 // SAFETY: as long as the buffer isnt wrong, this is 😄
index..unsafe { index.unchecked_add(CHANNELS) } 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`]. /// Procure a [`ImageCloner`].
#[must_use = "function does not modify the original image"] #[must_use = "function does not modify the original image"]
pub fn cloner(&self) -> ImageCloner<'_, CHANNELS> { 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. /// Consumes and leaks this image, returning a reference to the image.
#[must_use = "not using the returned reference is a memory leak"] #[must_use = "not using the returned reference is a memory leak"]
pub fn leak(self) -> Image<&'static mut [u8], CHANNELS> { pub fn leak(self) -> Image<&'static mut [u8], CHANNELS> {
// SAFETY: ctor // SAFETY: size unchanged
unsafe { Image::new(self.width, self.height, self.buffer.leak()) } 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. /// Consumes and leaks this image, returning a reference to the image.
#[must_use = "not using the returned reference is a memory leak"] #[must_use = "not using the returned reference is a memory leak"]
pub fn leak(self) -> Image<&'static mut T, CHANNELS> { pub fn leak(self) -> Image<&'static mut T, CHANNELS> {
// SAFETY: ctor // SAFETY: size unchanged
unsafe { Image::new(self.width, self.height, Box::leak(self.buffer)) } unsafe { self.mapped(Box::leak) }
} }
} }

View file

@ -2,10 +2,13 @@ use crate::Image;
#[cfg(feature = "real-show")] #[cfg(feature = "real-show")]
mod real { mod real {
use crate::Image; use crate::{pixels::convert::PFrom, Image};
use minifb::{Key, Window}; 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( let mut win = Window::new(
"show", "show",
i.width() as usize, i.width() as usize,
@ -13,9 +16,44 @@ mod real {
Default::default(), Default::default(),
) )
.unwrap(); .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) { 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"); .expect("window update fail");
} }
} }
@ -93,7 +131,7 @@ macro_rules! show {
/// if the window is un creatable /// if the window is un creatable
pub fn show(self) -> Self { pub fn show(self) -> Self {
#[cfg(feature = "real-show")] #[cfg(feature = "real-show")]
real::show(r(&self.as_ref().into())); real::show(self.as_ref());
#[cfg(not(feature = "real-show"))] #[cfg(not(feature = "real-show"))]
fake::show!(self); fake::show!(self);
self self
@ -117,7 +155,7 @@ impl Image<Box<[u32]>, 1> {
/// if the window is un creatable /// if the window is un creatable
pub fn show(self) -> Self { pub fn show(self) -> Self {
#[cfg(feature = "real-show")] #[cfg(feature = "real-show")]
real::show(r(&self)); real::show(Image::<Box<[u8]>, 4>::from(r(&self)).as_ref());
#[cfg(not(feature = "real-show"))] #[cfg(not(feature = "real-show"))]
fake::show!(Image::<Box<[u8]>, 4>::from(r(&self))); fake::show!(Image::<Box<[u8]>, 4>::from(r(&self)));
self self

Binary file not shown.

Binary file not shown.