fast image operations
commit crimes
bendn 2024-03-12
parent 418c13b · commit b464d4b
-rw-r--r--Cargo.toml4
-rw-r--r--src/dyn/mod.rs19
-rw-r--r--src/lib.rs5
-rw-r--r--src/term.rs83
-rw-r--r--src/term/bloc.rs76
-rw-r--r--src/term/iterm2.rs27
-rw-r--r--src/term/kitty.rs66
-rw-r--r--src/term/sixel.rs19
-rw-r--r--src/term/size/unix.rs3
9 files changed, 171 insertions, 131 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 51b4638..f973a48 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fimg"
-version = "0.4.39"
+version = "0.4.41"
authors = ["bend-n <[email protected]>"]
license = "MIT"
edition = "2021"
@@ -67,7 +67,7 @@ text = ["fontdue"]
blur = ["slur"]
term = ["qwant", "save", "scale", "windows"]
real-show = ["minifb", "text"]
-default = ["save", "scale", "term"]
+default = ["save", "scale"]
wgpu-convert = ["dep:wgpu"]
[profile.release]
diff --git a/src/dyn/mod.rs b/src/dyn/mod.rs
index 52feaea..ccf19f9 100644
--- a/src/dyn/mod.rs
+++ b/src/dyn/mod.rs
@@ -40,9 +40,24 @@ macro_rules! e {
}
use e;
-impl<'a> std::fmt::Display for DynImage<&'a [u8]> {
+#[cfg(feature = "term")]
+impl<T: AsRef<[u8]>> std::fmt::Display for crate::term::Display<DynImage<T>> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- e!(self, |x| crate::term::Display(*x).write(f))
+ e!(&self.0, |x| crate::term::Display(x.as_ref()).write(f))
+ }
+}
+
+#[cfg(feature = "term")]
+impl<T: AsRef<[u8]>> std::fmt::Debug for crate::term::Display<DynImage<T>> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ e!(&self.0, |x| crate::term::Display(x.as_ref()).write(f))
+ }
+}
+
+#[cfg(feature = "term")]
+impl<T: AsRef<[u8]>> std::fmt::Display for DynImage<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ e!(&self, |x| crate::term::Display(x.as_ref()).write(f))
}
}
diff --git a/src/lib.rs b/src/lib.rs
index b642d36..336d3f4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -57,6 +57,7 @@
generic_const_exprs,
iter_array_chunks,
split_at_checked,
+ core_intrinsics,
slice_as_chunks,
unchecked_math,
slice_flatten,
@@ -82,7 +83,8 @@
clippy::zero_prefixed_literal,
mixed_script_confusables,
incomplete_features,
- confusable_idents
+ confusable_idents,
+ internal_features
)]
use std::{hint::assert_unchecked, num::NonZeroU32, ops::Range};
@@ -208,6 +210,7 @@ impl Image<&[u8], 3> {
/// A image with a variable number of channels, and a nonzero size.
#[derive(Debug, PartialEq, Eq)]
+#[repr(C)]
pub struct Image<T, const CHANNELS: usize> {
/// column order 2d slice/vec
buffer: T,
diff --git a/src/term.rs b/src/term.rs
index 0e54219..5f2df4c 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -12,23 +12,34 @@ mod bloc;
mod kitty;
mod sixel;
mod size;
+use crate::Image;
pub use bloc::Bloc;
pub use iterm2::Iterm2;
pub use kitty::Kitty;
pub use sixel::Sixel;
use std::fmt::{Result, Write};
-use crate::{pixels::convert::PFrom, Image, WritePng};
+mod seal {
+ pub trait Sealed {}
+}
+use seal::Sealed;
+#[doc(hidden)]
+pub trait Basic: Sealed {}
+impl Sealed for [(); 1] {}
+impl Basic for [(); 1] {}
+impl Sealed for [(); 2] {}
+impl Basic for [(); 2] {}
+impl Sealed for [(); 3] {}
+impl Basic for [(); 3] {}
+impl Sealed for [(); 4] {}
+impl Basic for [(); 4] {}
mod b64;
mod iterm2;
impl<'a, const N: usize> std::fmt::Display for Image<&'a [u8], N>
where
- [u8; 3]: PFrom<N>,
- [u8; 4]: PFrom<N>,
- Image<&'a [u8], N>: kitty::Data + WritePng,
- Image<&'a [u8], N>: bloc::Scaled<N>,
+ [(); N]: Basic,
{
/// Display an image in the terminal.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result {
@@ -39,12 +50,10 @@ where
/// Print an image in the terminal.
///
/// This is a wrapper for `print!("{}", term::Display(image))`
-pub fn print<'a, const N: usize>(i: Image<&'a [u8], N>)
+pub fn print<T: AsRef<[u8]>, const N: usize>(i: Image<T, N>)
where
- [u8; 3]: PFrom<N>,
- [u8; 4]: PFrom<N>,
- Image<&'a [u8], N>: bloc::Scaled<N>,
- Image<&'a [u8], N>: kitty::Data + WritePng,
+ [(); N]: Basic,
+ Display<Image<T, N>>: std::fmt::Display,
{
print!("{}", Display(i))
}
@@ -52,75 +61,59 @@ where
#[derive(Copy, Clone)]
/// Display an image in the terminal.
/// This type implements [`Display`](std::fmt::Display) and [`Debug`](std::fmt::Debug).
-pub struct Display<'a, const N: usize>(pub Image<&'a [u8], N>);
+pub struct Display<T>(pub T);
-impl<'a, const N: usize> std::ops::Deref for Display<'a, N> {
- type Target = Image<&'a [u8], N>;
+impl<T> std::ops::Deref for Display<T> {
+ type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
-impl<'a, const N: usize> std::fmt::Debug for Display<'a, N>
+impl<T: AsRef<[u8]>, const N: usize> std::fmt::Debug for Display<Image<T, N>>
where
- [u8; 3]: PFrom<N>,
- [u8; 4]: PFrom<N>,
- Image<&'a [u8], N>: bloc::Scaled<N>,
- Image<&'a [u8], N>: kitty::Data + WritePng,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result {
- self.write(f)
- }
-}
-
-impl<'a, const N: usize> std::fmt::Display for Display<'a, N>
-where
- Image<&'a [u8], N>: bloc::Scaled<N>,
- [u8; 4]: PFrom<N>,
- [u8; 3]: PFrom<N>,
- Image<&'a [u8], N>: kitty::Data + WritePng,
-{
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.write(f)
+ Display(self.as_ref()).write(f)
}
}
-impl<'a, const N: usize> Display<'a, N>
+impl<const N: usize> Display<Image<&[u8], N>>
where
- [u8; 4]: PFrom<N>,
- [u8; 3]: PFrom<N>,
- Image<&'a [u8], N>: bloc::Scaled<N>,
- Image<&'a [u8], N>: kitty::Data + WritePng,
+ [(); N]: Basic,
{
/// Write $TERM protocol encoded image data.
pub fn write(self, f: &mut impl Write) -> Result {
if let Ok(term) = std::env::var("TERM") {
match &*term {
- "mlterm" | "yaft-256color" => return Sixel(*self).write(f),
- x if x.contains("kitty") => return Kitty(*self).write(f),
+ "mlterm" | "yaft-256color" => return Sixel(self.0).write(f),
+ x if x.contains("kitty") => return Kitty(self.0).write(f),
_ => (),
}
}
if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
match &*term_program {
- "MacTerm" => return Sixel(*self).write(f),
- "iTerm" | "WezTerm" => return Iterm2(*self).write(f),
+ "MacTerm" => return Sixel(self.0).write(f),
+ "iTerm" | "WezTerm" => return Iterm2(self.0).write(f),
_ => (),
}
}
if let Ok("iTerm") = std::env::var("LC_TERMINAL").as_deref() {
- return Iterm2(*self).write(f);
+ return Iterm2(self.0).write(f);
}
#[cfg(unix)]
- return self.guess_harder(f).unwrap_or_else(|| Bloc(*self).write(f));
+ return self
+ .guess_harder(f)
+ .unwrap_or_else(|| Bloc(self.0).write(f));
#[cfg(not(unix))]
return Bloc(*self).write(f);
}
#[cfg(unix)]
// https://github.com/benjajaja/ratatui-image/blob/master/src/picker.rs#L226
- fn guess_harder(self, to: &mut impl Write) -> Option<Result> {
+ fn guess_harder(&self, to: &mut impl Write) -> Option<Result> {
extern crate libc;
use std::{io::Read, mem::MaybeUninit};
@@ -171,13 +164,13 @@ where
unsafe { libc::tcsetattr(0, libc::TCSADRAIN, &termios) };
if buf.contains("_Gi=31;OK") {
- Some(Kitty(*self).write(to))
+ Some(Kitty(self.as_ref()).write(to))
} else if buf.contains(";4;")
|| buf.contains("?4;")
|| buf.contains(";4c")
|| buf.contains("?4c")
{
- Some(Sixel(*self).write(to))
+ Some(Sixel(self.as_ref()).write(to))
} else {
None
}
diff --git a/src/term/bloc.rs b/src/term/bloc.rs
index 8601388..5f6347b 100644
--- a/src/term/bloc.rs
+++ b/src/term/bloc.rs
@@ -1,4 +1,6 @@
+use super::Basic;
use crate::{pixels::convert::PFrom, scale, term::size::fit, Image};
+use core::intrinsics::transmute_unchecked as transmute;
use std::fmt::{Debug, Display, Formatter, Result, Write};
/// Colored `▀`s. The simple, stupid solution.
@@ -14,8 +16,7 @@ impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Bloc<T, N> {
impl<T: AsRef<[u8]>, const N: usize> Display for Bloc<T, N>
where
- Image<T, N>: Scaled<N>,
- [u8; 3]: PFrom<N>,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
@@ -24,44 +25,16 @@ where
impl<T: AsRef<[u8]>, const N: usize> Debug for Bloc<T, N>
where
- Image<T, N>: Scaled<N>,
- [u8; 3]: PFrom<N>,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
-#[doc(hidden)]
-pub trait Scaled<const N: usize> {
- fn scaled(&self, to: (u32, u32)) -> Image<Box<[u8]>, N>;
-}
-
-macro_rules! n {
- ($n:literal) => {
- impl<T: AsRef<[u8]>> Scaled<$n> for Image<T, $n> {
- fn scaled(&self, (w, h): (u32, u32)) -> Image<Box<[u8]>, $n> {
- self.scale::<scale::Nearest>(w, h)
- }
- }
- };
- (o $n:literal) => {
- impl<T: AsRef<[u8]>> Scaled<$n> for Image<T, $n> {
- fn scaled(&self, (w, h): (u32, u32)) -> Image<Box<[u8]>, $n> {
- self.as_ref().to_owned().scale::<scale::Nearest>(w, h)
- }
- }
- };
-}
-n!(1);
-n!(o 2);
-n!(3);
-n!(o 4);
-
impl<T: AsRef<[u8]>, const N: usize> Bloc<T, N>
where
- [u8; 3]: PFrom<N>,
- Image<T, N>: Scaled<N>,
+ [(); N]: Basic,
{
/// Write out halfblocks.
pub fn write(&self, to: &mut impl Write) -> Result {
@@ -74,7 +47,31 @@ where
}
let buf;
let i = if !cfg!(test) {
- buf = self.scaled(fit((self.width(), self.height())));
+ let (w, h) = fit((self.width(), self.height()));
+ macro_rules! n {
+ ($n:literal) => {
+ transmute::<Image<Box<[u8]>, $n>, Image<Box<[u8]>, N>>(
+ transmute::<Image<&[u8], N>, Image<&[u8], $n>>(self.as_ref())
+ .scale::<scale::Nearest>(w, h),
+ )
+ };
+ (o $n:literal) => {
+ transmute::<Image<Box<[u8]>, 1>, Image<Box<[u8]>, N>>(
+ transmute::<Image<Vec<u8>, N>, Image<&[u8], 1>>(self.as_ref().to_owned())
+ .scale::<scale::Nearest>(w, h),
+ )
+ };
+ }
+ // SAFETY: #[allow(clippy::undocumented_unsafe_blocks)]
+ buf = unsafe {
+ match N {
+ 1 => n![1],
+ 2 => n![o 2],
+ 3 => n![3],
+ 4 => n![o 4],
+ _ => unreachable!(),
+ }
+ };
buf.as_ref()
} else {
self.as_ref()
@@ -83,7 +80,18 @@ where
for [a, b] in i
.flatten()
.chunks_exact(i.width() as _)
- .map(|x| x.iter().copied().map(<[u8; 3] as PFrom<N>>::pfrom))
+ .map(|x| {
+ #[allow(clippy::undocumented_unsafe_blocks)]
+ x.iter().copied().map(|x| unsafe {
+ match N {
+ 1 => <[u8; 3] as PFrom<1>>::pfrom(transmute(x)),
+ 2 => <[u8; 3] as PFrom<2>>::pfrom(transmute(x)),
+ 3 => <[u8; 3] as PFrom<3>>::pfrom(transmute(x)),
+ 4 => <[u8; 3] as PFrom<4>>::pfrom(transmute(x)),
+ _ => unreachable!(),
+ }
+ })
+ })
.array_chunks::<2>()
{
for (a, b) in a.zip(b) {
diff --git a/src/term/iterm2.rs b/src/term/iterm2.rs
index b6a9663..a6e5bac 100644
--- a/src/term/iterm2.rs
+++ b/src/term/iterm2.rs
@@ -1,5 +1,6 @@
-use super::b64;
+use super::{b64, Basic};
use crate::{Image, WritePng};
+use core::intrinsics::transmute_unchecked as transmute;
use std::fmt::{Debug, Display, Formatter, Result, Write};
/// Outputs [Iterm2 Inline image protocol](https://iterm2.com/documentation-images.html) encoded data.
@@ -14,7 +15,7 @@ impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Iterm2<T, N> {
impl<T: AsRef<[u8]>, const N: usize> Display for Iterm2<T, N>
where
- Image<T, N>: WritePng,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
@@ -23,7 +24,7 @@ where
impl<T: AsRef<[u8]>, const N: usize> Debug for Iterm2<T, N>
where
- Image<T, N>: WritePng,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
@@ -32,12 +33,28 @@ where
impl<T: AsRef<[u8]>, const N: usize> Iterm2<T, N>
where
- Image<T, N>: WritePng,
+ [(); N]: Basic,
{
/// Write out kitty gfx data.
pub fn write(&self, to: &mut impl Write) -> Result {
let mut d = Vec::with_capacity(1024);
- WritePng::write(&**self, &mut d).unwrap();
+ macro_rules! n {
+ ($n:literal) => {
+ WritePng::write(
+ // SAFETY: ... i renounce traits
+ &unsafe { transmute::<Image<&[u8], N>, Image<&[u8], $n>>(self.as_ref()) },
+ &mut d,
+ )
+ .unwrap()
+ };
+ }
+ match N {
+ 1 => n![1],
+ 2 => n![2],
+ 3 => n![3],
+ 4 => n![4],
+ _ => unreachable!(),
+ }
let mut e = Vec::with_capacity(b64::size(&d));
b64::encode(&d, &mut e).unwrap();
writeln!(
diff --git a/src/term/kitty.rs b/src/term/kitty.rs
index b1e4797..16ea2f8 100644
--- a/src/term/kitty.rs
+++ b/src/term/kitty.rs
@@ -1,5 +1,6 @@
-use super::b64;
+use super::{b64, Basic};
use crate::Image;
+use core::intrinsics::transmute_unchecked as transmute;
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter, Result, Write};
@@ -16,7 +17,7 @@ impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Kitty<T, N> {
impl<T: AsRef<[u8]>, const N: usize> Display for Kitty<T, N>
where
- Image<T, N>: Data,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
@@ -25,52 +26,43 @@ where
impl<T: AsRef<[u8]>, const N: usize> Debug for Kitty<T, N>
where
- Image<T, N>: Data,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
-mod seal {
- pub trait Sealed {}
-}
-use seal::Sealed;
-
-pub trait Data: Sealed {
- #[doc(hidden)]
- fn get(&self) -> (Cow<[u8]>, &'static str);
-}
-macro_rules! imp {
- ($n:literal, $f:expr) => {
- impl<T: AsRef<[u8]>> Sealed for Image<T, $n> {}
- impl<T: AsRef<[u8]>> Data for Image<T, $n> {
- fn get(&self) -> (Cow<[u8]>, &'static str) {
- const fn castor<
- T: AsRef<[u8]>,
- F: FnMut(&Image<T, $n>) -> (Cow<[u8]>, &'static str),
- >(
- f: F,
- ) -> F {
- f
- }
- castor($f)(self)
- }
- }
- };
-}
-imp! { 4, |x| (Cow::from(x.bytes()), "32") }
-imp! { 3, |x| (Cow::from(x.bytes()), "24") }
-imp! { 2, |x| (Cow::Owned(<Image<Box<[u8]>, 3>>::from(x.as_ref()).take_buffer().to_vec()), "24") }
-imp! { 1, |x| (Cow::Owned(<Image<Box<[u8]>, 3>>::from(x.as_ref()).take_buffer().to_vec()), "24") }
-
impl<T: AsRef<[u8]>, const N: usize> Kitty<T, N> {
/// Write out kitty gfx data.
pub fn write(&self, to: &mut impl Write) -> Result
where
- Image<T, N>: Data,
+ [(); N]: Basic,
{
- let (bytes, dtype) = self.get();
+ macro_rules! cast {
+ ($n:literal) => {
+ (
+ Cow::Owned(
+ <Image<Box<[u8]>, 3>>::from({
+ // SAFETY: ...
+ unsafe { transmute::<Image<&[u8], N>, Image<&[u8], $n>>(self.as_ref()) }
+ })
+ .take_buffer()
+ .to_vec(),
+ ),
+ "24",
+ )
+ };
+ }
+ let (bytes, dtype) = {
+ match N {
+ 1 => cast!(1),
+ 2 => cast!(2),
+ 3 => (Cow::from(self.bytes()), "24"),
+ 4 => (Cow::from(self.bytes()), "32"),
+ _ => unreachable!(),
+ }
+ };
let (w, h) = (self.width(), self.height());
let mut enc = Vec::with_capacity(b64::size(&bytes));
diff --git a/src/term/sixel.rs b/src/term/sixel.rs
index 46f2b55..7091968 100644
--- a/src/term/sixel.rs
+++ b/src/term/sixel.rs
@@ -1,7 +1,10 @@
+use core::intrinsics::transmute_unchecked as transmute;
use std::fmt::{Debug, Display, Formatter, Result, Write};
use crate::{pixels::convert::PFrom, Image};
+use super::Basic;
+
/// Outputs [sixel](https://en.wikipedia.org/wiki/Sixel) encoded data in its [`Display`] and [`Debug`] implementations, for easy visual debugging.
pub struct Sixel<T: AsRef<[u8]>, const N: usize>(pub Image<T, N>);
@@ -15,7 +18,7 @@ impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Sixel<T, N> {
impl<T: AsRef<[u8]>, const N: usize> Display for Sixel<T, N>
where
- [u8; 4]: PFrom<N>,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
@@ -24,7 +27,7 @@ where
impl<T: AsRef<[u8]>, const N: usize> Debug for Sixel<T, N>
where
- [u8; 4]: PFrom<N>,
+ [(); N]: Basic,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
@@ -35,7 +38,7 @@ impl<T: AsRef<[u8]>, const N: usize> Sixel<T, N> {
/// Write out sixel data.
pub fn write(&self, to: &mut impl Write) -> Result
where
- [u8; 4]: PFrom<N>,
+ [(); N]: Basic,
{
to.write_str("Pq")?;
write!(to, r#""1;1;{};{}"#, self.width(), self.height())?;
@@ -47,7 +50,15 @@ impl<T: AsRef<[u8]>, const N: usize> Sixel<T, N> {
buf = self
.chunked()
.copied()
- .map(<[u8; 4] as PFrom<N>>::pfrom)
+ // SAFETY: #[allow(clippy::undocumented_unsafe_blocks)]
+ .map(|x| unsafe {
+ match N {
+ 1 => <[u8; 4] as PFrom<1>>::pfrom(transmute(x)),
+ 2 => <[u8; 4] as PFrom<2>>::pfrom(transmute(x)),
+ 3 => <[u8; 4] as PFrom<3>>::pfrom(transmute(x)),
+ _ => unreachable!(),
+ }
+ })
.collect::<Vec<_>>();
&*buf
};
diff --git a/src/term/size/unix.rs b/src/term/size/unix.rs
index 718373e..2fefc8f 100644
--- a/src/term/size/unix.rs
+++ b/src/term/size/unix.rs
@@ -10,6 +10,7 @@ struct Fd(i32, bool);
impl Drop for Fd {
fn drop(&mut self) {
if self.1 {
+ // SAFETY: #[allow(clippy::undocumented_unsafe_blocks)]
unsafe { close(self.0) };
}
}
@@ -43,7 +44,7 @@ pub fn size() -> Option<(u16, u16)> {
*File::open("/dev/tty")
.map(Fd::new)
.unwrap_or(Fd::from(STDIN_FILENO)),
- TIOCGWINSZ.into(),
+ TIOCGWINSZ,
size.as_mut_ptr(),
) != -1
{