1use crate::get_dimensions;
2use anyhow::{Context, Result};
3use color_quant::NeuQuant;
4use image::{
5 imageops::{colorops, FilterType},
6 DynamicImage, GenericImageView, ImageBuffer, Pixel, Rgb,
7};
8use libc::{
9 c_void, poll, pollfd, read, tcgetattr, tcsetattr, termios, ECHO, ICANON, POLLIN, STDIN_FILENO,
10 TCSANOW,
11};
12use std::io::{stdout, Write};
13use std::time::Instant;
14
15pub struct SixelBackend {}
16
17impl SixelBackend {
18 pub fn new() -> Self {
19 Self {}
20 }
21
22 pub fn supported() -> bool {
23 let old_attributes = unsafe {
25 let mut old_attributes: termios = std::mem::zeroed();
26 tcgetattr(STDIN_FILENO, &mut old_attributes);
27
28 let mut new_attributes = old_attributes;
29 new_attributes.c_lflag &= !ICANON;
30 new_attributes.c_lflag &= !ECHO;
31 tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes);
32 old_attributes
33 };
34
35 print!("\x1B[c");
37 stdout().flush().unwrap();
38
39 let start_time = Instant::now();
40 let mut stdin_pollfd = pollfd {
41 fd: STDIN_FILENO,
42 events: POLLIN,
43 revents: 0,
44 };
45 let mut buf = Vec::<u8>::new();
46 loop {
47 while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
49 if start_time.elapsed().as_millis() > 50 {
50 unsafe {
51 tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
52 }
53 return false;
54 }
55 }
56 let mut byte = 0;
57 unsafe {
58 read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
59 }
60 buf.push(byte);
61 if buf.starts_with(&[0x1B, b'[', b'?']) && buf.ends_with(b"c") {
62 for attribute in buf[3..(buf.len() - 1)].split(|x| *x == b';') {
63 if attribute == [b'4'] {
64 unsafe {
65 tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
66 }
67 return true;
68 }
69 }
70 }
71 }
72 }
73}
74
75impl Default for SixelBackend {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl super::ImageBackend for SixelBackend {
82 #[allow(clippy::map_entry)]
83 fn add_image(&self, lines: Vec<String>, image: &DynamicImage, colors: usize) -> Result<String> {
84 let tty_size = unsafe { get_dimensions() };
85 let cw = tty_size.ws_xpixel / tty_size.ws_col;
86 let lh = tty_size.ws_ypixel / tty_size.ws_row;
87 let width_ratio = 1.0 / cw as f64;
88 let height_ratio = 1.0 / lh as f64;
89
90 let image = image.resize(
92 u32::MAX,
93 (lines.len() as f64 / height_ratio) as u32,
94 FilterType::Lanczos3,
95 );
96 let image_columns = width_ratio * image.width() as f64;
97 let image_rows = height_ratio * image.height() as f64;
98
99 let rgba_image = image.to_rgba8(); let flat_samples = rgba_image.as_flat_samples();
101 let mut rgba_image = rgba_image.clone();
102 let pixels = flat_samples
104 .image_slice()
105 .context("Error while slicing the image")?;
106 colorops::dither(&mut rgba_image, &NeuQuant::new(10, colors, pixels));
107
108 let rgb_image = ImageBuffer::from_fn(rgba_image.width(), rgba_image.height(), |x, y| {
109 let rgba_pixel = rgba_image.get_pixel(x, y);
110 let mut rgb_pixel = rgba_pixel.to_rgb();
111 for subpixel in &mut rgb_pixel.0 {
112 *subpixel = (*subpixel as f32 / 255.0 * rgba_pixel[3] as f32) as u8;
113 }
114 rgb_pixel
115 });
116
117 let mut image_data = Vec::<u8>::new();
118 image_data.extend(b"\x1BPq"); image_data.extend(format!("\"1;1;{};{}", image.width(), image.height()).as_bytes());
120
121 let mut colors = std::collections::HashMap::<Rgb<u8>, u8>::new();
122 for i in 0..((rgb_image.height() - 1) / 6 + 1) {
124 let sixel_row = rgb_image.view(
125 0,
126 i * 6,
127 rgb_image.width(),
128 std::cmp::min(6, rgb_image.height() - i * 6),
129 );
130 for (_, _, pixel) in sixel_row.pixels() {
131 if !colors.contains_key(&pixel) {
132 let color_multiplier = 100.0 / 255.0;
134 image_data.extend(
135 format!(
136 "#{};2;{};{};{}",
137 colors.len(),
138 (pixel[0] as f32 * color_multiplier) as u32,
139 (pixel[1] as f32 * color_multiplier) as u32,
140 (pixel[2] as f32 * color_multiplier) as u32
141 )
142 .as_bytes(),
143 );
144 colors.insert(pixel, colors.len() as u8);
145 }
146 }
147 for (color, color_index) in &colors {
148 let mut sixel_samples = vec![0; sixel_row.width() as usize];
149 sixel_samples.resize(sixel_row.width() as usize, 0);
150 for (x, y, pixel) in sixel_row.pixels() {
151 if color == &pixel {
152 sixel_samples[x as usize] |= 1 << y;
153 }
154 }
155 image_data.extend(format!("#{color_index}").bytes());
156 image_data.extend(sixel_samples.iter().map(|x| x + 0x3F));
157 image_data.push(b'$');
158 }
159 image_data.push(b'-');
160 }
161 image_data.extend(b"\x1B\\");
162
163 image_data.extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); image_data.extend(format!("\x1B[{}C", image_columns as u32 + 1).as_bytes()); let mut i = 0;
166 for line in &lines {
167 image_data.extend(format!("\x1B[s{line}\x1B[u\x1B[1B").as_bytes());
168 i += 1;
169 }
170 image_data
171 .extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); Ok(String::from_utf8(image_data)?)
174 }
175}