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 "] 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 DynImage { /// 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, [u8; 4]: PFrom, Image<&'a [u8], N>: kitty::Data + WritePng, + Image<&'a [u8], N>: bloc::Scaled, { /// 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, [u8; 4]: PFrom, + Image<&'a [u8], N>: bloc::Scaled, 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, [u8; 4]: PFrom, + Image<&'a [u8], N>: bloc::Scaled, 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, [u8; 4]: PFrom, [u8; 3]: PFrom, Image<&'a [u8], N>: kitty::Data + WritePng, @@ -86,6 +90,7 @@ impl<'a, const N: usize> Display<'a, N> where [u8; 4]: PFrom, [u8; 3]: PFrom, + Image<&'a [u8], N>: bloc::Scaled, 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, const N: usize> std::ops::Deref for Bloc { impl, const N: usize> Display for Bloc where + Image: Scaled, [u8; 3]: PFrom, { fn fmt(&self, f: &mut Formatter<'_>) -> Result { @@ -23,6 +24,7 @@ where impl, const N: usize> Debug for Bloc where + Image: Scaled, [u8; 3]: PFrom, { fn fmt(&self, f: &mut Formatter<'_>) -> Result { @@ -30,9 +32,36 @@ where } } +#[doc(hidden)] +pub trait Scaled { + fn scaled(&self, to: (u32, u32)) -> Image, N>; +} + +macro_rules! n { + ($n:literal) => { + impl> Scaled<$n> for Image { + fn scaled(&self, (w, h): (u32, u32)) -> Image, $n> { + self.scale::(w, h) + } + } + }; + (o $n:literal) => { + impl> Scaled<$n> for Image { + fn scaled(&self, (w, h): (u32, u32)) -> Image, $n> { + self.as_ref().to_owned().scale::(w, h) + } + } + }; +} +n!(1); +n!(o 2); +n!(3); +n!(o 4); + impl, const N: usize> Bloc where [u8; 3]: PFrom, + Image: Scaled, { /// 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>::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 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::::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 { + let x = std::process::Command::new("tput").arg(arg).output().ok()?; + String::from_utf8(x.stdout) + .ok() + .and_then(|x| x.parse::().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)) + } +}