add terminal printing functions

This commit is contained in:
bendn 2024-02-20 11:55:03 +07:00
parent 72be3fe0b0
commit 5100cc28e3
11 changed files with 634 additions and 12 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "fimg"
version = "0.4.36"
version = "0.4.38"
authors = ["bend-n <bend.n@outlook.com>"]
license = "MIT"
edition = "2021"
@ -25,6 +25,7 @@ minifb = { version = "0.25.0", default-features = false, features = [
], optional = true }
wgpu = { version = "0.19.1", default-features = false, optional = true }
atools = "0.1.0"
qwant = { version = "1.0.0", optional = true }
[dev-dependencies]
iai = { git = "https://github.com/bend-n/iai.git" }
@ -59,6 +60,7 @@ scale = ["fr"]
save = ["png"]
text = ["fontdue"]
blur = ["slur"]
term = ["qwant", "save"]
real-show = ["minifb", "text"]
default = ["save", "scale"]
wgpu-convert = ["dep:wgpu"]

View file

@ -47,15 +47,19 @@
//! - `real-show`: [`Image::show`], if the `save` feature is enabled, will, by default, simply open the appropriate image viewing program.
//! if, for some reason, this is inadequate/you dont have a good image viewer, enable the `real-show` feature to make [`Image::show`] open up a window of its own.
//! 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`\].
#![feature(
maybe_uninit_write_slice,
hint_assert_unchecked,
slice_swap_unchecked,
generic_const_exprs,
iter_array_chunks,
split_at_checked,
slice_as_chunks,
unchecked_math,
slice_flatten,
rustc_private,
portable_simd,
array_windows,
doc_auto_cfg,
@ -73,7 +77,12 @@
clippy::use_self,
missing_docs
)]
#![allow(clippy::zero_prefixed_literal, incomplete_features)]
#![allow(
clippy::zero_prefixed_literal,
mixed_script_confusables,
incomplete_features,
confusable_idents
)]
use std::{hint::assert_unchecked, num::NonZeroU32, ops::Range};
mod affine;
@ -100,6 +109,8 @@ pub mod pixels;
pub mod scale;
#[cfg(any(feature = "save", feature = "real-show"))]
mod show;
#[cfg(feature = "term")]
pub mod term;
pub use cloner::ImageCloner;
pub use overlay::{BlendingOverlay, ClonerOverlay, ClonerOverlayAt, Overlay, OverlayAt};
pub use r#dyn::DynImage;
@ -600,18 +611,23 @@ impl<const CHANNELS: usize, T: ?Sized> Image<Box<T>, CHANNELS> {
}
}
#[cfg(feature = "save")]
/// Write a png image.
pub trait WritePng {
/// Write this png image.
fn write(&self, f: &mut impl std::io::Write) -> std::io::Result<()>;
}
/// helper macro for defining the save() method.
macro_rules! save {
($channels:literal == $clr:ident ($clrhuman:literal)) => {
impl<T: AsRef<[u8]>> Image<T, $channels> {
#[cfg(feature = "save")]
#[cfg(feature = "save")]
impl<T: AsRef<[u8]>> WritePng for Image<T, $channels> {
#[doc = "Save this "]
#[doc = $clrhuman]
#[doc = " image."]
pub fn save(&self, f: impl AsRef<std::path::Path>) {
let p = std::fs::File::create(f).unwrap();
let w = &mut std::io::BufWriter::new(p);
let mut enc = png::Encoder::new(w, self.width(), self.height());
fn write(&self, f: &mut impl std::io::Write) -> std::io::Result<()> {
let mut enc = png::Encoder::new(f, self.width(), self.height());
enc.set_color(png::ColorType::$clr);
enc.set_depth(png::BitDepth::Eight);
enc.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2));
@ -621,8 +637,21 @@ macro_rules! save {
(0.30000, 0.60000),
(0.15000, 0.06000),
));
let mut writer = enc.write_header().unwrap();
writer.write_image_data(self.bytes()).unwrap();
let mut writer = enc.write_header()?;
writer.write_image_data(self.bytes())?;
Ok(())
}
}
impl<T: AsRef<[u8]>> Image<T, $channels> {
#[cfg(feature = "save")]
#[doc = "Save this "]
#[doc = $clrhuman]
#[doc = " image."]
pub fn save(&self, f: impl AsRef<std::path::Path>) {
self.write(&mut std::io::BufWriter::new(
std::fs::File::create(f).unwrap(),
))
.unwrap();
}
}
};

169
src/term.rs Normal file
View file

@ -0,0 +1,169 @@
//! terminal outputs
//! produces output for any terminal supporting one of the
//! ```text
//! Kitty Graphics Protocol
//! Iterm2 Inline Image Protocol
//! Sixel Bitmap Graphics Format
//! ```
//! with a fallback for dumb terminals.
//!
//! the (second?) best way to debug your images.
mod bloc;
mod kitty;
mod sixel;
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 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,
{
/// Display an image in the terminal.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result {
Display(*self).write(f)
}
}
/// 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>)
where
[u8; 3]: PFrom<N>,
[u8; 4]: PFrom<N>,
Image<&'a [u8], N>: kitty::Data + WritePng,
{
print!("{}", Display(i))
}
#[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>);
impl<'a, const N: usize> std::ops::Deref for Display<'a, N> {
type Target = Image<&'a [u8], N>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a, const N: usize> std::fmt::Display for Display<'a, N>
where
[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)
}
}
impl<'a, const N: usize> Display<'a, N>
where
[u8; 4]: PFrom<N>,
[u8; 3]: PFrom<N>,
Image<&'a [u8], N>: kitty::Data + WritePng,
{
/// 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),
_ => (),
}
}
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),
_ => (),
}
}
if let Ok("iTerm") = std::env::var("LC_TERMINAL").as_deref() {
return Iterm2(*self).write(f);
}
#[cfg(unix)]
return self.guess_harder(f).unwrap_or_else(|| Bloc(*self).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> {
extern crate libc;
use std::{io::Read, mem::MaybeUninit};
fn r(result: i32) -> Option<()> {
(result != -1).then_some(())
}
let mut termios = MaybeUninit::<libc::termios>::uninit();
// SAFETY: get termios of stdin
r(unsafe { libc::tcgetattr(0, termios.as_mut_ptr()) })?;
// SAFETY: gotten
let termios = unsafe { termios.assume_init() };
// SAFETY: turn off echo and canonical (requires enter before stdin reads) modes
unsafe {
libc::tcsetattr(
0,
libc::TCSADRAIN,
&libc::termios {
c_lflag: termios.c_lflag & !libc::ICANON & !libc::ECHO,
..termios
},
)
};
let buf = {
// contains a kitty gfx and sixel query, the `\x1b[c` is for sixels
println!(r"_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\");
let mut stdin = std::io::stdin();
let mut buf = String::new();
let mut b = [0; 16];
'l: loop {
let n = stdin.read(&mut b).ok()?;
if n == 0 {
continue;
}
for b in b {
buf.push(b as char);
if b == b'c' {
break 'l;
}
}
}
buf
};
// SAFETY: reset attrs to what they were before we became nosy
unsafe { libc::tcsetattr(0, libc::TCSADRAIN, &termios) };
if buf.contains("_Gi=31;OK") {
Some(Kitty(*self).write(to))
} else if buf.contains(";4;")
|| buf.contains("?4;")
|| buf.contains(";4c")
|| buf.contains("?4c")
{
Some(Sixel(*self).write(to))
} else {
None
}
}
}

48
src/term/b64.rs Normal file
View file

@ -0,0 +1,48 @@
#[test]
fn b64() {
fn t(i: &'static str, o: &'static str) {
let mut x = vec![];
encode(i.as_bytes(), &mut x).unwrap();
assert_eq!(x, o.as_bytes());
}
t("Hello World!", "SGVsbG8gV29ybGQh");
t("Hello World", "SGVsbG8gV29ybGQ=");
}
pub fn encode(mut input: &[u8], output: &mut impl std::io::Write) -> std::io::Result<()> {
const Α: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
while let [a, b, c, rest @ ..] = input {
let α = ((*a as usize) << 16) | ((*b as usize) << 8) | *c as usize;
output.write_all(&[
Α[α >> 18],
Α[(α >> 12) & 0x3F],
Α[(α >> 6) & 0x3F],
Α[α & 0x3F],
])?;
input = rest;
}
if !input.is_empty() {
let mut α = (input[0] as usize) << 16;
if input.len() > 1 {
α |= (input[1] as usize) << 8;
}
output.write_all(&[Α[α >> 18], Α[α >> 12 & 0x3F]])?;
if input.len() > 1 {
output.write_all(&[Α[α >> 6 & 0x3f]])?;
} else {
output.write_all(&[b'='])?;
}
output.write_all(&[b'='])?;
}
Ok(())
}
pub const fn size(of: &[u8]) -> usize {
let use_pad = of.len() % 3 != 0;
if use_pad {
4 * (of.len() / 3 + 1)
} else {
4 * (of.len() / 3)
}
}

70
src/term/bloc.rs Normal file
View file

@ -0,0 +1,70 @@
use crate::{pixels::convert::PFrom, Image};
use std::fmt::{Debug, Display, Formatter, Result, Write};
/// Colored `▀`s. The simple, stupid solution.
/// May be too big for your terminal.
pub struct Bloc<T: AsRef<[u8]>, const N: usize>(pub Image<T, N>);
impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Bloc<T, N> {
type Target = Image<T, N>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: AsRef<[u8]>, const N: usize> Display for Bloc<T, N>
where
[u8; 3]: PFrom<N>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
impl<T: AsRef<[u8]>, const N: usize> Debug for Bloc<T, N>
where
[u8; 3]: PFrom<N>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
impl<T: AsRef<[u8]>, const N: usize> Bloc<T, N>
where
[u8; 3]: PFrom<N>,
{
/// Write out halfblocks.
pub fn write(&self, to: &mut impl Write) -> Result {
macro_rules! c {
(fg $fg:expr, bg $bg:expr) => {{
let [fr, fg, fb] = $fg;
let [br, bg, bb] = $bg;
write!(to, "\x1b[38;2;{fr};{fg};{fb};48;2;{br};{bg};{bb}m▀")?;
}};
}
// TODO: scale 2 fit
for [a, b] in self
.flatten()
.chunks_exact(self.width() as _)
.map(|x| x.iter().copied().map(<[u8; 3] as PFrom<N>>::pfrom))
.array_chunks::<2>()
{
for (a, b) in a.zip(b) {
c! { fg a, bg b };
}
writeln!(to)?;
}
write!(to, "\x1b[0m")?;
Ok(())
}
}
#[test]
fn test() {
let x = Image::<_, 3>::open("tdata/small_cat.png");
use std::hash::Hasher;
let mut h = std::hash::DefaultHasher::new();
h.write(Bloc(x).to_string().as_bytes());
assert_eq!(h.finish(), 0x6546104ffee16f77);
}

61
src/term/iterm2.rs Normal file
View file

@ -0,0 +1,61 @@
use super::b64;
use crate::{Image, WritePng};
use std::fmt::{Debug, Display, Formatter, Result, Write};
/// Outputs [Iterm2 Inline image protocol](https://iterm2.com/documentation-images.html) encoded data.
pub struct Iterm2<T: AsRef<[u8]>, const N: usize>(pub Image<T, N>);
impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Iterm2<T, N> {
type Target = Image<T, N>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: AsRef<[u8]>, const N: usize> Display for Iterm2<T, N>
where
Image<T, N>: WritePng,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
impl<T: AsRef<[u8]>, const N: usize> Debug for Iterm2<T, N>
where
Image<T, N>: WritePng,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
impl<T: AsRef<[u8]>, const N: usize> Iterm2<T, N>
where
Image<T, N>: WritePng,
{
/// 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();
let mut e = Vec::with_capacity(b64::size(&d));
b64::encode(&d, &mut e).unwrap();
writeln!(
to,
"]1337;File=inline=1;preserveAspectRatio=1;size={}:{}",
d.len(),
// SAFETY: b64
unsafe { std::str::from_utf8_unchecked(&e) }
)?;
Ok(())
}
}
#[test]
fn test() {
let x = Image::<_, 3>::open("tdata/small_cat.png");
use std::hash::Hasher;
let mut h = std::hash::DefaultHasher::new();
h.write(Iterm2(x).to_string().as_bytes());
assert_eq!(h.finish(), 0x32e81fb3cea8336f);
}

114
src/term/kitty.rs Normal file
View file

@ -0,0 +1,114 @@
use super::b64;
use crate::Image;
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter, Result, Write};
/// Outputs [Kitty Graphics Protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol) encoded data.
pub struct Kitty<T: AsRef<[u8]>, const N: usize>(pub Image<T, N>);
impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Kitty<T, N> {
type Target = Image<T, N>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: AsRef<[u8]>, const N: usize> Display for Kitty<T, N>
where
Image<T, N>: Data,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
impl<T: AsRef<[u8]>, const N: usize> Debug for Kitty<T, N>
where
Image<T, N>: Data,
{
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,
{
let (bytes, dtype) = self.get();
let (w, h) = (self.width(), self.height());
let mut enc = Vec::with_capacity(b64::size(&bytes));
b64::encode(&bytes, &mut enc).unwrap();
let mut chunks = enc
.chunks(4096)
// SAFETY: b64
.map(|x| unsafe { std::str::from_utf8_unchecked(x) });
let last = chunks.len();
const H: &str = "_G";
const MORE: &str = "m";
const DISPLAY: char = 'T';
const ACTION: char = 'a';
const DATATYPE: char = 'f';
const TYPE: char = 't';
const DIRECT: char = 'd';
const E: &str = r"\";
let payload = chunks.next().unwrap();
let more = (last > 1) as u8;
write!(to, "{H}{DATATYPE}={dtype},{ACTION}={DISPLAY},{TYPE}={DIRECT},s={w},v={h},{MORE}={more};{payload}{E}")?;
for (payload, i) in chunks.zip(2..) {
let more = (i != last) as u8;
write!(to, "{H}{MORE}={more};{payload}{E}")?;
}
Ok(())
}
}
#[test]
fn test() {
let x = Image::<_, 3>::open("tdata/cat.png");
use std::hash::Hasher;
let mut h = std::hash::DefaultHasher::new();
h.write(Kitty(x).to_string().as_bytes());
assert_eq!(h.finish(), 0x1cc13114bcf3cc3);
}

128
src/term/sixel.rs Normal file
View file

@ -0,0 +1,128 @@
use std::fmt::{Debug, Display, Formatter, Result, Write};
use crate::{pixels::convert::PFrom, Image};
/// 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>);
impl<T: AsRef<[u8]>, const N: usize> std::ops::Deref for Sixel<T, N> {
type Target = Image<T, N>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: AsRef<[u8]>, const N: usize> Display for Sixel<T, N>
where
[u8; 4]: PFrom<N>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
impl<T: AsRef<[u8]>, const N: usize> Debug for Sixel<T, N>
where
[u8; 4]: PFrom<N>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
self.write(f)
}
}
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>,
{
to.write_str("Pq")?;
write!(to, r#""1;1;{};{}"#, self.width(), self.height())?;
let buf;
let rgba = if N == 4 {
// SAFETY: buffer cannot have half pixels (cant use flatten bcoz N)
unsafe { self.buffer().as_ref().as_chunks_unchecked() }
} else {
buf = self
.chunked()
.copied()
.map(<[u8; 4] as PFrom<N>>::pfrom)
.collect::<Vec<_>>();
&*buf
};
let q = qwant::NeuQuant::new(15, 255, rgba);
// TODO: don't colllect
let pixels: Vec<u8> = rgba.iter().map(|&pix| q.index_of(pix) as u8).collect();
for ([r, g, b], i) in q
.color_map_rgb()
.map(|x| x.map(|x| (x as f32 * (100. / 255.)) as u32))
.zip(0u8..)
{
write!(to, "#{i};2;{r};{g};{b}")?;
}
for sixel_row in pixels.chunks_exact(self.width() as usize * 6).map(|x| {
let mut x = x
.iter()
.zip(0u32..)
.map(|(&p, j)| (p, (j % self.width(), j / self.width())))
.collect::<Vec<_>>();
x.sort_unstable();
x
}) {
// extracted
for samples in Grouped(&sixel_row, |r| r.0) {
write!(to, "#{}", samples[0].0)?;
let mut last = -1;
for (x, byte) in Grouped(samples, |(_, (x, _))| x).map(|v| {
(
v[0].1 .0 as i32,
v.iter()
.map(|&(_, (_, y))| (1 << y))
.fold(0, |acc, x| acc | x),
)
}) {
if last + 1 != x {
write!(to, "!{}?", x - last - 1)?;
}
to.write_char((byte + b'?') as char)?;
last = x;
}
write!(to, "$")?;
}
write!(to, "-")?;
}
write!(to, r"\")?;
Ok(())
}
}
struct Grouped<'a, K: Eq, T, F: Fn(T) -> K>(&'a [T], F);
impl<'a, K: Eq, T: Copy, F: Fn(T) -> K> Iterator for Grouped<'a, K, T, F> {
type Item = &'a [T];
fn next(&mut self) -> Option<Self::Item> {
self.0.first()?;
self.0
.split_at_checked(
self.0
.array_windows::<2>()
.take_while(|&&[a, b]| (self.1)(a) == (self.1)(b))
.count()
+ 1,
)
.inspect(|(_, t)| self.0 = t)
.map(|(h, _)| h)
}
}
#[test]
fn test() {
assert_eq!(
Sixel(Image::<Vec<u8>, 3>::open("tdata/small_cat.png")).to_string(),
include_str!("../../tdata/small_cat.six")
);
}

View file

@ -40,7 +40,7 @@ impl<T: Copy, const CHANNELS: usize> Image<T, CHANNELS> {
let dat = unsafe { self.slice(i) };
// SAFETY: caller
unsafe { assert_unchecked(dat.len() == data.len()) };
MaybeUninit::write_slice(dat, data);
MaybeUninit::copy_from_slice(dat, data);
}
/// Slice the image.

View file

@ -97,7 +97,7 @@ impl Image<Box<[u8]>, 4> {
.chunks_exact(pad)
.zip(out.buf().chunks_exact_mut(row))
{
::core::mem::MaybeUninit::write_slice(pixels, &padded[..row]);
::core::mem::MaybeUninit::copy_from_slice(pixels, &padded[..row]);
}
unsafe { out.assume_init().boxed() }

1
tdata/small_cat.six Normal file

File diff suppressed because one or more lines are too long