scale to fit

This commit is contained in:
bendn 2024-03-12 08:47:48 +07:00
parent 6a26b785cd
commit 418c13be5d
8 changed files with 181 additions and 8 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "fimg"
version = "0.4.38"
version = "0.4.39"
authors = ["bend-n <bend.n@outlook.com>"]
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]

View file

@ -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 {

View file

@ -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,

View file

@ -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.

View file

@ -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>()
{

28
src/term/size.rs Normal file
View file

@ -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)
}
}

67
src/term/size/unix.rs Normal file
View file

@ -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());
}

25
src/term/size/windows.rs Normal file
View file

@ -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))
}
}