mirror of
https://github.com/bend-n/fimg.git
synced 2024-12-22 02:28:19 -06:00
add terminal printing functions
This commit is contained in:
parent
72be3fe0b0
commit
5100cc28e3
|
@ -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"]
|
||||
|
|
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.
|
||||
//! 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
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) };
|
||||
// SAFETY: caller
|
||||
unsafe { assert_unchecked(dat.len() == data.len()) };
|
||||
MaybeUninit::write_slice(dat, data);
|
||||
MaybeUninit::copy_from_slice(dat, data);
|
||||
}
|
||||
|
||||
/// Slice the image.
|
||||
|
|
|
@ -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
1
tdata/small_cat.six
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue