Skip to content

Commit dc4e360

Browse files
committed
Add sixel backend
1 parent a64db30 commit dc4e360

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

Diff for: src/image_backends/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use image::DynamicImage;
22

33
#[cfg(target_os = "linux")]
44
mod kitty;
5+
#[cfg(target_os = "linux")]
6+
mod sixel;
57

68
pub trait ImageBackend {
79
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String;
@@ -11,6 +13,8 @@ pub trait ImageBackend {
1113
pub fn get_best_backend() -> Option<Box<dyn ImageBackend>> {
1214
if kitty::KittyBackend::supported() {
1315
Some(Box::new(kitty::KittyBackend::new()))
16+
} else if sixel::SixelBackend::supported() {
17+
Some(Box::new(sixel::SixelBackend::new()))
1418
} else {
1519
None
1620
}

Diff for: src/image_backends/sixel.rs

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use image::{
2+
imageops::{colorops, FilterType},
3+
math::nq::NeuQuant,
4+
DynamicImage, GenericImageView, ImageBuffer, Pixel, Rgb,
5+
};
6+
use libc::{
7+
c_void, ioctl, poll, pollfd, read, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON,
8+
POLLIN, STDIN_FILENO, STDOUT_FILENO, TCSANOW, TIOCGWINSZ,
9+
};
10+
use std::time::Instant;
11+
12+
pub struct SixelBackend {}
13+
14+
impl SixelBackend {
15+
pub fn new() -> Self {
16+
Self {}
17+
}
18+
19+
pub fn supported() -> bool {
20+
// save terminal attributes and disable canonical input processing mode
21+
let old_attributes = unsafe {
22+
let mut old_attributes: termios = std::mem::zeroed();
23+
tcgetattr(STDIN_FILENO, &mut old_attributes);
24+
25+
let mut new_attributes = old_attributes;
26+
new_attributes.c_lflag &= !ICANON;
27+
new_attributes.c_lflag &= !ECHO;
28+
tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes);
29+
old_attributes
30+
};
31+
32+
// ask for the primary device attribute string
33+
println!("\x1B[c");
34+
35+
let start_time = Instant::now();
36+
let mut stdin_pollfd = pollfd {
37+
fd: STDIN_FILENO,
38+
events: POLLIN,
39+
revents: 0,
40+
};
41+
let mut buf = Vec::<u8>::new();
42+
loop {
43+
// check for timeout while polling to avoid blocking the main thread
44+
while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
45+
if start_time.elapsed().as_millis() > 50 {
46+
unsafe {
47+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
48+
}
49+
return false;
50+
}
51+
}
52+
let mut byte = 0;
53+
unsafe {
54+
read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
55+
}
56+
buf.push(byte);
57+
if buf.starts_with(&[0x1B, b'[', b'?']) && buf.ends_with(&[b'c']) {
58+
unsafe {
59+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
60+
}
61+
return true;
62+
}
63+
}
64+
}
65+
}
66+
67+
impl super::ImageBackend for SixelBackend {
68+
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String {
69+
let tty_size = unsafe {
70+
let tty_size: winsize = std::mem::zeroed();
71+
ioctl(STDOUT_FILENO, TIOCGWINSZ, &tty_size);
72+
tty_size
73+
};
74+
let width_ratio = tty_size.ws_col as f64 / tty_size.ws_xpixel as f64;
75+
let height_ratio = tty_size.ws_row as f64 / tty_size.ws_ypixel as f64;
76+
77+
// resize image to fit the text height with the Lanczos3 algorithm
78+
let image = image.resize(
79+
u32::max_value(),
80+
(lines.len() as f64 / height_ratio) as u32,
81+
FilterType::Lanczos3,
82+
);
83+
let image_columns = width_ratio * image.width() as f64;
84+
let image_rows = height_ratio * image.height() as f64;
85+
86+
let rgba_image = image.to_rgba(); // convert the image to rgba samples
87+
let flat_samples = rgba_image.as_flat_samples();
88+
let mut rgba_image = rgba_image.clone();
89+
// reduce the amount of colors using dithering
90+
colorops::dither(
91+
&mut rgba_image,
92+
&NeuQuant::new(10, 16, flat_samples.image_slice().unwrap()),
93+
);
94+
let rgb_image = ImageBuffer::from_fn(rgba_image.width(), rgba_image.height(), |x, y| {
95+
let rgba_pixel = rgba_image.get_pixel(x, y);
96+
let mut rgb_pixel = rgba_pixel.to_rgb();
97+
for subpixel in &mut rgb_pixel.0 {
98+
*subpixel = (*subpixel as f32 / 255.0 * rgba_pixel[3] as f32) as u8;
99+
}
100+
rgb_pixel
101+
});
102+
103+
let mut image_data = Vec::<u8>::new();
104+
image_data.extend("\x1BPq".as_bytes()); // start sixel data
105+
image_data.extend(format!("\"1;1;{};{}", image.width(), image.height()).as_bytes());
106+
107+
let mut colors = std::collections::HashMap::<Rgb<u8>, u8>::new();
108+
// subtract 1 -> divide -> add 1 to round up the integer division
109+
for i in 0..((rgb_image.height() - 1) / 6 + 1) {
110+
let sixel_row = rgb_image.view(
111+
0,
112+
i * 6,
113+
rgb_image.width(),
114+
std::cmp::min(6, rgb_image.height() - i * 6),
115+
);
116+
for (_, _, pixel) in sixel_row.pixels() {
117+
if !colors.contains_key(&pixel) {
118+
// sixel uses percentages for rgb values
119+
let color_multiplier = 100.0 / 255.0;
120+
image_data.extend(
121+
format!(
122+
"#{};2;{};{};{};",
123+
colors.len(),
124+
(pixel[0] as f32 * color_multiplier) as u32,
125+
(pixel[1] as f32 * color_multiplier) as u32,
126+
(pixel[2] as f32 * color_multiplier) as u32
127+
)
128+
.as_bytes(),
129+
);
130+
colors.insert(pixel, colors.len() as u8);
131+
}
132+
}
133+
for (color, color_index) in &colors {
134+
let mut sixel_samples = Vec::<u8>::with_capacity(sixel_row.width() as usize);
135+
sixel_samples.resize(sixel_row.width() as usize, 0);
136+
for (x, y, pixel) in sixel_row.pixels() {
137+
if color == &pixel {
138+
sixel_samples[x as usize] = sixel_samples[x as usize] | (1 << y);
139+
}
140+
}
141+
image_data.extend(format!("#{}", color_index).bytes());
142+
image_data.extend(sixel_samples.iter().map(|x| x + 0x3F));
143+
image_data.push('$' as u8);
144+
}
145+
image_data.push('-' as u8);
146+
}
147+
image_data.extend("\x1B\\".as_bytes());
148+
149+
image_data.extend(format!("\x1B[{}A", image_rows as u32 + 2).as_bytes()); // move cursor to top-left corner
150+
image_data.extend(format!("\x1B[{}C", image_columns as u32 + 1).as_bytes()); // move cursor to top-right corner of image
151+
let mut i = 0;
152+
for line in &lines {
153+
image_data.extend(format!("\x1B[s{}\x1B[u\x1B[1B", line).as_bytes());
154+
i += 1;
155+
}
156+
image_data
157+
.extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); // move cursor to end of image
158+
159+
String::from_utf8(image_data).unwrap()
160+
}
161+
}

0 commit comments

Comments
 (0)