fast image operations
-rw-r--r--Cargo.toml11
-rw-r--r--src/dyn/mod.rs6
-rw-r--r--src/lib.rs1
-rw-r--r--src/term.rs7
-rw-r--r--src/term/bloc.rs44
-rw-r--r--src/term/size.rs28
-rw-r--r--src/term/size/unix.rs67
-rw-r--r--src/term/size/windows.rs25
8 files changed, 181 insertions, 8 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 707981d..51b4638 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fimg"
-version = "0.4.38"
+version = "0.4.39"
authors = ["bend-n <[email protected]>"]
license = "MIT"
edition = "2021"
@@ -27,6 +27,11 @@ wgpu = { version = "0.19.1", default-features = false, optional = true }
atools = "0.1.0"
qwant = { version = "1.0.0", optional = true }
+[target.'cfg(windows)'.dependencies]
+windows = { version = "0.53.0", features = [
+ "Win32_System_Console",
+], optional = true }
+
[dev-dependencies]
iai = { git = "https://github.com/bend-n/iai.git" }
@@ -60,9 +65,9 @@ scale = ["fr"]
save = ["png"]
text = ["fontdue"]
blur = ["slur"]
-term = ["qwant", "save"]
+term = ["qwant", "save", "scale", "windows"]
real-show = ["minifb", "text"]
-default = ["save", "scale"]
+default = ["save", "scale", "term"]
wgpu-convert = ["dep:wgpu"]
[profile.release]
diff --git a/src/dyn/mod.rs b/src/dyn/mod.rs
index 184047a..52feaea 100644
--- a/src/dyn/mod.rs
+++ b/src/dyn/mod.rs
@@ -40,6 +40,12 @@ macro_rules! e {
}
use e;
+impl<'a> std::fmt::Display for DynImage<&'a [u8]> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ e!(self, |x| crate::term::Display(*x).write(f))
+ }
+}
+
impl<T> DynImage<T> {
/// Get the width of this image.
pub const fn width(&self) -> u32 {
diff --git a/src/lib.rs b/src/lib.rs
index dd9b44e..b642d36 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -49,6 +49,7 @@
//! without the `real-show` feature, [`Image::show`] will save itself to your temp directory, which you may not want.
//! - `term`: [`term::print`]. this enables printing images directly to the terminal, if you don't want to open a window or something. supports `{iterm2, kitty, sixel, fallback}` graphics.
//! - `default`: \[`save`, `scale`\].
+#![cfg_attr(all(feature = "term", windows), windows_subsystem = "console")]
#![feature(
maybe_uninit_write_slice,
hint_assert_unchecked,
diff --git a/src/term.rs b/src/term.rs
index 6512179..0e54219 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -11,7 +11,7 @@
mod bloc;
mod kitty;
mod sixel;
-
+mod size;
pub use bloc::Bloc;
pub use iterm2::Iterm2;
pub use kitty::Kitty;
@@ -28,6 +28,7 @@ where
[u8; 3]: PFrom<N>,
[u8; 4]: PFrom<N>,
Image<&'a [u8], N>: kitty::Data + WritePng,
+ Image<&'a [u8], N>: bloc::Scaled<N>,
{
/// Display an image in the terminal.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result {
@@ -42,6 +43,7 @@ pub fn print<'a, const N: usize>(i: Image<&'a [u8], N>)
where
[u8; 3]: PFrom<N>,
[u8; 4]: PFrom<N>,
+ Image<&'a [u8], N>: bloc::Scaled<N>,
Image<&'a [u8], N>: kitty::Data + WritePng,
{
print!("{}", Display(i))
@@ -64,6 +66,7 @@ impl<'a, const N: usize> std::fmt::Debug for Display<'a, N>
where
[u8; 3]: PFrom<N>,
[u8; 4]: PFrom<N>,
+ Image<&'a [u8], N>: bloc::Scaled<N>,
Image<&'a [u8], N>: kitty::Data + WritePng,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result {
@@ -73,6 +76,7 @@ where
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,
@@ -86,6 +90,7 @@ impl<'a, const N: usize> Display<'a, N>
where
[u8; 4]: PFrom<N>,
[u8; 3]: PFrom<N>,
+ Image<&'a [u8], N>: bloc::Scaled<N>,
Image<&'a [u8], N>: kitty::Data + WritePng,
{
/// Write $TERM protocol encoded image data.
diff --git a/src/term/bloc.rs b/src/term/bloc.rs
index 2be3e25..8601388 100644
--- a/src/term/bloc.rs
+++ b/src/term/bloc.rs
@@ -1,4 +1,4 @@
-use crate::{pixels::convert::PFrom, Image};
+use crate::{pixels::convert::PFrom, scale, term::size::fit, Image};
use std::fmt::{Debug, Display, Formatter, Result, Write};
/// Colored `â–€`s. The simple, stupid solution.
@@ -14,6 +14,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>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
@@ -23,6 +24,7 @@ where
impl<T: AsRef<[u8]>, const N: usize> Debug for Bloc<T, N>
where
+ Image<T, N>: Scaled<N>,
[u8; 3]: PFrom<N>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
@@ -30,9 +32,36 @@ where
}
}
+#[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>,
{
/// Write out halfblocks.
pub fn write(&self, to: &mut impl Write) -> Result {
@@ -43,10 +72,17 @@ where
write!(to, "\x1b[38;2;{fr};{fg};{fb};48;2;{br};{bg};{bb}mâ–€")?;
}};
}
- // TODO: scale 2 fit
- for [a, b] in self
+ let buf;
+ let i = if !cfg!(test) {
+ buf = self.scaled(fit((self.width(), self.height())));
+ buf.as_ref()
+ } else {
+ self.as_ref()
+ };
+
+ for [a, b] in i
.flatten()
- .chunks_exact(self.width() as _)
+ .chunks_exact(i.width() as _)
.map(|x| x.iter().copied().map(<[u8; 3] as PFrom<N>>::pfrom))
.array_chunks::<2>()
{
diff --git a/src/term/size.rs b/src/term/size.rs
new file mode 100644
index 0000000..c0a3c04
--- /dev/null
+++ b/src/term/size.rs
@@ -0,0 +1,28 @@
+#[cfg(unix)]
+mod unix;
+#[cfg(windows)]
+mod windows;
+use std::cmp::max;
+
+#[cfg(unix)]
+pub use unix::size;
+#[cfg(windows)]
+pub use windows::size;
+#[cfg(all(not(unix), not(windows)))]
+pub fn size() -> Option<(u16, u16)> {
+ #[cfg(debug_assertions)]
+ eprintln!("unable to get terminal size");
+ None
+}
+
+pub fn fit((w, h): (u32, u32)) -> (u32, u32) {
+ if let Some((mw, mh)) = size().map(|(a, b)| (a as u32, b as u32)) {
+ match () {
+ () if w <= mw && h <= 2 * mh => (w, 2 * max(1, h / 2 + h % 2) - h % 2),
+ () if mw * h <= w * 2 * mh => (mw, 2 * max(1, h * mw / w / 2) - h % 2),
+ () => (w * 2 * mh / h, 2 * max(1, 2 * mh / 2) - h % 2),
+ }
+ } else {
+ (w, h)
+ }
+}
diff --git a/src/term/size/unix.rs b/src/term/size/unix.rs
new file mode 100644
index 0000000..718373e
--- /dev/null
+++ b/src/term/size/unix.rs
@@ -0,0 +1,67 @@
+extern crate libc;
+use std::fs::File;
+use std::mem::MaybeUninit as MU;
+use std::ops::Deref;
+use std::os::fd::IntoRawFd;
+
+use libc::*;
+
+struct Fd(i32, bool);
+impl Drop for Fd {
+ fn drop(&mut self) {
+ if self.1 {
+ unsafe { close(self.0) };
+ }
+ }
+}
+impl Fd {
+ pub fn new(x: File) -> Self {
+ Self(x.into_raw_fd(), true)
+ }
+}
+
+impl From<i32> for Fd {
+ fn from(value: i32) -> Self {
+ Self(value, false)
+ }
+}
+
+impl Deref for Fd {
+ type Target = i32;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+pub fn size() -> Option<(u16, u16)> {
+ // SAFETY: SYS
+ unsafe {
+ let mut size = MU::<winsize>::uninit();
+
+ if ioctl(
+ *File::open("/dev/tty")
+ .map(Fd::new)
+ .unwrap_or(Fd::from(STDIN_FILENO)),
+ TIOCGWINSZ.into(),
+ size.as_mut_ptr(),
+ ) != -1
+ {
+ let winsize { ws_col, ws_row, .. } = size.assume_init();
+ return Some((ws_col as _, ws_row as _)).filter(|&(w, h)| w != 0 && h != 0);
+ }
+ tput("cols").and_then(|w| tput("lines").map(|h| (w, h)))
+ }
+}
+
+pub fn tput(arg: &'static str) -> Option<u16> {
+ let x = std::process::Command::new("tput").arg(arg).output().ok()?;
+ String::from_utf8(x.stdout)
+ .ok()
+ .and_then(|x| x.parse::<u16>().ok())
+}
+
+#[test]
+fn t() {
+ println!("{:?}", size().unwrap());
+}
diff --git a/src/term/size/windows.rs b/src/term/size/windows.rs
new file mode 100644
index 0000000..b7499cc
--- /dev/null
+++ b/src/term/size/windows.rs
@@ -0,0 +1,25 @@
+use std::mem::MaybeUninit as MU;
+use windows::Win32::System::Console::{
+ GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO as winsize,
+ SMALL_RECT as rect, STD_OUTPUT_HANDLE,
+};
+
+pub fn size() -> Option<(u16, u16)> {
+ // SAFETY: SYS
+ unsafe {
+ let mut info = MU::uninit();
+ GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE).ok()?, info.as_mut_ptr())
+ .ok()?;
+ let winsize {
+ srWindow:
+ rect {
+ Top,
+ Left,
+ Right,
+ Bottom,
+ },
+ ..
+ } = info.assume_init();
+ Some(((Bottom - Top - 1) as u16, (Right - Left - 1) as u16))
+ }
+}