ratatui_image/protocol/
sixel.rs1use icy_sixel::{EncodeOptions, sixel_encode};
9use image::DynamicImage;
10use ratatui::{buffer::Buffer, layout::Rect};
11use std::{cmp::min, fmt::Write};
12
13use super::{ProtocolTrait, StatefulProtocolTrait};
14use crate::{Result, errors::Errors, picker::cap_parser::Parser};
15
16#[derive(Clone, Default)]
18pub struct Sixel {
19 pub data: String,
20 pub area: Rect,
21 pub is_tmux: bool,
22}
23
24impl Sixel {
25 pub fn new(image: DynamicImage, area: Rect, is_tmux: bool) -> Result<Self> {
26 let data = encode(&image, area, is_tmux)?;
27 Ok(Self {
28 data,
29 area,
30 is_tmux,
31 })
32 }
33}
34
35fn encode(img: &DynamicImage, area: Rect, is_tmux: bool) -> Result<String> {
37 let (w, h) = (img.width(), img.height());
38 let img_rgba8 = img.to_rgba8();
39 let bytes = img_rgba8.as_raw();
40 let (start, escape, end) = Parser::escape_tmux(is_tmux);
41
42 let width = area.width;
47 let height = area.height;
48
49 let sixel_data = sixel_encode(bytes, w as usize, h as usize, &EncodeOptions::default())
50 .map_err(|err| Errors::Sixel(format!("sixel encoding error: {err}")))?;
51
52 let mut data = String::new();
53 if is_tmux {
54 if !sixel_data.starts_with('\x1b') {
55 return Err(Errors::Tmux("sixel string did not start with escape"));
56 }
57 data.push_str(start);
60 for _ in 0..height {
61 write!(data, "{escape}[{width}X{escape}[1B").unwrap();
62 }
63 write!(data, "{escape}[{height}A").unwrap();
64 data.push_str(escape);
65 data.push_str(&sixel_data[1..]);
66 data.push_str(end);
67 } else {
68 for _ in 0..height {
69 write!(data, "{escape}[{width}X{escape}[1B").unwrap();
70 }
71 write!(data, "{escape}[{height}A").unwrap();
72 data.push_str(&sixel_data);
73 }
74 Ok(data)
75}
76
77impl ProtocolTrait for Sixel {
78 fn render(&self, area: Rect, buf: &mut Buffer) {
79 render(self.area, &self.data, area, buf, false)
80 }
81
82 fn area(&self) -> Rect {
83 self.area
84 }
85}
86
87fn render(rect: Rect, data: &str, area: Rect, buf: &mut Buffer, overdraw: bool) {
88 let render_area = match render_area(rect, area, overdraw) {
89 None => {
90 return;
108 }
109 Some(r) => r,
110 };
111
112 buf.cell_mut(render_area).map(|cell| cell.set_symbol(data));
113 let mut skip_first = false;
114
115 for y in render_area.top()..render_area.bottom() {
117 for x in render_area.left()..render_area.right() {
118 if !skip_first {
119 skip_first = true;
120 continue;
121 }
122 buf.cell_mut((x, y)).map(|cell| cell.set_skip(true));
123 }
124 }
125}
126
127fn render_area(rect: Rect, area: Rect, overdraw: bool) -> Option<Rect> {
128 if overdraw {
129 return Some(Rect::new(
130 area.x,
131 area.y,
132 min(rect.width, area.width),
133 min(rect.height, area.height),
134 ));
135 }
136
137 if rect.width > area.width || rect.height > area.height {
138 return None;
139 }
140 Some(Rect::new(area.x, area.y, rect.width, rect.height))
141}
142
143impl StatefulProtocolTrait for Sixel {
144 fn resize_encode(&mut self, img: DynamicImage, area: Rect) -> Result<()> {
145 let data = encode(&img, area, self.is_tmux)?;
146 *self = Sixel {
147 data,
148 area,
149 ..*self
150 };
151 Ok(())
152 }
153}