mirror of
https://github.com/bend-n/fimg.git
synced 2024-12-22 10:28:21 -06:00
add terminal printing functions
This commit is contained in:
parent
72be3fe0b0
commit
5100cc28e3
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "fimg"
|
name = "fimg"
|
||||||
version = "0.4.36"
|
version = "0.4.38"
|
||||||
authors = ["bend-n <bend.n@outlook.com>"]
|
authors = ["bend-n <bend.n@outlook.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@ -25,6 +25,7 @@ minifb = { version = "0.25.0", default-features = false, features = [
|
||||||
], optional = true }
|
], optional = true }
|
||||||
wgpu = { version = "0.19.1", default-features = false, optional = true }
|
wgpu = { version = "0.19.1", default-features = false, optional = true }
|
||||||
atools = "0.1.0"
|
atools = "0.1.0"
|
||||||
|
qwant = { version = "1.0.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
iai = { git = "https://github.com/bend-n/iai.git" }
|
iai = { git = "https://github.com/bend-n/iai.git" }
|
||||||
|
@ -59,6 +60,7 @@ scale = ["fr"]
|
||||||
save = ["png"]
|
save = ["png"]
|
||||||
text = ["fontdue"]
|
text = ["fontdue"]
|
||||||
blur = ["slur"]
|
blur = ["slur"]
|
||||||
|
term = ["qwant", "save"]
|
||||||
real-show = ["minifb", "text"]
|
real-show = ["minifb", "text"]
|
||||||
default = ["save", "scale"]
|
default = ["save", "scale"]
|
||||||
wgpu-convert = ["dep:wgpu"]
|
wgpu-convert = ["dep:wgpu"]
|
||||||
|
|
47
src/lib.rs
47
src/lib.rs
|
@ -47,15 +47,19 @@
|
||||||
//! - `real-show`: [`Image::show`], if the `save` feature is enabled, will, by default, simply open the appropriate image viewing program.
|
//! - `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.
|
//! 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.
|
//! 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`\].
|
//! - `default`: \[`save`, `scale`\].
|
||||||
#![feature(
|
#![feature(
|
||||||
maybe_uninit_write_slice,
|
maybe_uninit_write_slice,
|
||||||
hint_assert_unchecked,
|
hint_assert_unchecked,
|
||||||
slice_swap_unchecked,
|
slice_swap_unchecked,
|
||||||
generic_const_exprs,
|
generic_const_exprs,
|
||||||
|
iter_array_chunks,
|
||||||
|
split_at_checked,
|
||||||
slice_as_chunks,
|
slice_as_chunks,
|
||||||
unchecked_math,
|
unchecked_math,
|
||||||
slice_flatten,
|
slice_flatten,
|
||||||
|
rustc_private,
|
||||||
portable_simd,
|
portable_simd,
|
||||||
array_windows,
|
array_windows,
|
||||||
doc_auto_cfg,
|
doc_auto_cfg,
|
||||||
|
@ -73,7 +77,12 @@
|
||||||
clippy::use_self,
|
clippy::use_self,
|
||||||
missing_docs
|
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};
|
use std::{hint::assert_unchecked, num::NonZeroU32, ops::Range};
|
||||||
|
|
||||||
mod affine;
|
mod affine;
|
||||||
|
@ -100,6 +109,8 @@ pub mod pixels;
|
||||||
pub mod scale;
|
pub mod scale;
|
||||||
#[cfg(any(feature = "save", feature = "real-show"))]
|
#[cfg(any(feature = "save", feature = "real-show"))]
|
||||||
mod show;
|
mod show;
|
||||||
|
#[cfg(feature = "term")]
|
||||||
|
pub mod term;
|
||||||
pub use cloner::ImageCloner;
|
pub use cloner::ImageCloner;
|
||||||
pub use overlay::{BlendingOverlay, ClonerOverlay, ClonerOverlayAt, Overlay, OverlayAt};
|
pub use overlay::{BlendingOverlay, ClonerOverlay, ClonerOverlayAt, Overlay, OverlayAt};
|
||||||
pub use r#dyn::DynImage;
|
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.
|
/// helper macro for defining the save() method.
|
||||||
macro_rules! save {
|
macro_rules! save {
|
||||||
($channels:literal == $clr:ident ($clrhuman:literal)) => {
|
($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 = "Save this "]
|
||||||
#[doc = $clrhuman]
|
#[doc = $clrhuman]
|
||||||
#[doc = " image."]
|
#[doc = " image."]
|
||||||
pub fn save(&self, f: impl AsRef<std::path::Path>) {
|
fn write(&self, f: &mut impl std::io::Write) -> std::io::Result<()> {
|
||||||
let p = std::fs::File::create(f).unwrap();
|
let mut enc = png::Encoder::new(f, self.width(), self.height());
|
||||||
let w = &mut std::io::BufWriter::new(p);
|
|
||||||
let mut enc = png::Encoder::new(w, self.width(), self.height());
|
|
||||||
enc.set_color(png::ColorType::$clr);
|
enc.set_color(png::ColorType::$clr);
|
||||||
enc.set_depth(png::BitDepth::Eight);
|
enc.set_depth(png::BitDepth::Eight);
|
||||||
enc.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2));
|
enc.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2));
|
||||||
|
@ -621,8 +637,21 @@ macro_rules! save {
|
||||||
(0.30000, 0.60000),
|
(0.30000, 0.60000),
|
||||||
(0.15000, 0.06000),
|
(0.15000, 0.06000),
|
||||||
));
|
));
|
||||||
let mut writer = enc.write_header().unwrap();
|
let mut writer = enc.write_header()?;
|
||||||
writer.write_image_data(self.bytes()).unwrap();
|
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
169
src/term.rs
Normal 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\[c");
|
||||||
|
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
48
src/term/b64.rs
Normal 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
70
src/term/bloc.rs
Normal 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
61
src/term/iterm2.rs
Normal 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
114
src/term/kitty.rs
Normal 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
128
src/term/sixel.rs
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
|
@ -40,7 +40,7 @@ impl<T: Copy, const CHANNELS: usize> Image<T, CHANNELS> {
|
||||||
let dat = unsafe { self.slice(i) };
|
let dat = unsafe { self.slice(i) };
|
||||||
// SAFETY: caller
|
// SAFETY: caller
|
||||||
unsafe { assert_unchecked(dat.len() == data.len()) };
|
unsafe { assert_unchecked(dat.len() == data.len()) };
|
||||||
MaybeUninit::write_slice(dat, data);
|
MaybeUninit::copy_from_slice(dat, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Slice the image.
|
/// Slice the image.
|
||||||
|
|
|
@ -97,7 +97,7 @@ impl Image<Box<[u8]>, 4> {
|
||||||
.chunks_exact(pad)
|
.chunks_exact(pad)
|
||||||
.zip(out.buf().chunks_exact_mut(row))
|
.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() }
|
unsafe { out.assume_init().boxed() }
|
||||||
|
|
1
tdata/small_cat.six
Normal file
1
tdata/small_cat.six
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue