Skip to main content

ratatui_image/
protocol.rs

1//! Protocol backends for the widgets
2
3use std::{
4    collections::hash_map::DefaultHasher,
5    fmt::Write,
6    hash::{Hash, Hasher},
7    num::NonZeroU16,
8};
9
10use image::{DynamicImage, ImageBuffer, Rgba, imageops};
11use ratatui::{
12    buffer::{Buffer, CellDiffOption},
13    layout::{Rect, Size},
14};
15
16use self::{
17    halfblocks::Halfblocks,
18    iterm2::Iterm2,
19    kitty::{Kitty, StatefulKitty},
20    sixel::Sixel,
21};
22use crate::{FontSize, ResizeEncodeRender, Result};
23
24use super::Resize;
25
26pub mod halfblocks;
27pub mod iterm2;
28pub mod kitty;
29pub mod sixel;
30
31const UNIT_WIDTH: CellDiffOption = CellDiffOption::ForcedWidth(NonZeroU16::new(1).unwrap());
32
33pub(crate) trait ProtocolTrait: Send + Sync {
34    /// Render the currently resized and encoded data to the buffer.
35    fn render(&self, area: Rect, buf: &mut Buffer);
36
37    // Get the size of the image.
38    fn size(&self) -> Size;
39}
40
41trait StatefulProtocolTrait: ProtocolTrait {
42    /// Resize the image and encode it for rendering. The result should be stored statefully so
43    /// that next call for the given area does not need to redo the work.
44    ///
45    /// This can be done in a background thread, and the result is stored in this [StatefulProtocol].
46    fn resize_encode(&mut self, img: DynamicImage, size: Size) -> Result<()>;
47}
48
49/// A fixed-size image protocol for the [crate::Image] widget.
50#[derive(Clone)]
51pub enum Protocol {
52    Halfblocks(Halfblocks),
53    Sixel(Sixel),
54    Kitty(Kitty),
55    ITerm2(Iterm2),
56}
57
58impl Protocol {
59    pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
60        let inner: &dyn ProtocolTrait = match self {
61            Self::Halfblocks(halfblocks) => halfblocks,
62            Self::Sixel(sixel) => sixel,
63            Self::Kitty(kitty) => kitty,
64            Self::ITerm2(iterm2) => iterm2,
65        };
66        inner.render(area, buf);
67    }
68    // Get the size of the image.
69    pub fn size(&self) -> Size {
70        let inner: &dyn ProtocolTrait = match self {
71            Self::Halfblocks(halfblocks) => halfblocks,
72            Self::Sixel(sixel) => sixel,
73            Self::Kitty(kitty) => kitty,
74            Self::ITerm2(iterm2) => iterm2,
75        };
76        inner.size()
77    }
78
79    /// Returns a placeholder area, if the image will not render into the given area, or `None`.
80    ///
81    /// The returned [`ratatui::layout::Rect`] is the area the image would cover, constrained by
82    /// the size of `area` argument, if the image does not fit.
83    ///
84    /// Kitty and Halfblocks can always render partially, so they always return `None`.
85    pub fn needs_placeholder(&self, area: Rect) -> Option<Rect> {
86        let image_size = self.size();
87        if area.width < image_size.width
88            || area.height < image_size.height
89                && (matches!(self, Self::Sixel(_)) || matches!(self, Self::Halfblocks(_)))
90        {
91            let mut placeholder_area = area;
92            placeholder_area.width = placeholder_area.width.min(image_size.width);
93            placeholder_area.height = placeholder_area.height.min(image_size.height);
94            return Some(placeholder_area);
95        }
96        // Kitty and Halfblocks can render into a smaller area.
97        None
98    }
99}
100
101/// A stateful resizing image protocol for the [crate::StatefulImage] widget.
102///
103/// The [crate::thread::ThreadProtocol] widget also uses this, and is the reason why resizing is
104/// split from rendering.
105pub struct StatefulProtocol {
106    source: ImageSource,
107    font_size: FontSize,
108    hash: u64,
109    protocol_type: StatefulProtocolType,
110    last_encoding_result: Option<Result<()>>,
111}
112
113#[derive(Clone)]
114pub enum StatefulProtocolType {
115    Halfblocks(Halfblocks),
116    Sixel(Sixel),
117    Kitty(StatefulKitty),
118    ITerm2(Iterm2),
119}
120
121impl StatefulProtocolType {
122    fn inner_trait(&self) -> &dyn StatefulProtocolTrait {
123        match self {
124            Self::Halfblocks(halfblocks) => halfblocks,
125            Self::Sixel(sixel) => sixel,
126            Self::Kitty(kitty) => kitty,
127            Self::ITerm2(iterm2) => iterm2,
128        }
129    }
130    fn inner_trait_mut(&mut self) -> &mut dyn StatefulProtocolTrait {
131        match self {
132            Self::Halfblocks(halfblocks) => halfblocks,
133            Self::Sixel(sixel) => sixel,
134            Self::Kitty(kitty) => kitty,
135            Self::ITerm2(iterm2) => iterm2,
136        }
137    }
138}
139
140impl StatefulProtocol {
141    pub fn new(
142        image: DynamicImage,
143        font_size: FontSize,
144        background_color: Option<Rgba<u8>>,
145        protocol_type: StatefulProtocolType,
146    ) -> Self {
147        let source = ImageSource::new(image, font_size, background_color);
148        Self {
149            source,
150            font_size,
151            hash: u64::default(),
152            protocol_type,
153            last_encoding_result: None,
154        }
155    }
156
157    // Calculate the area that this image will ultimately render to, inside the given area.
158    pub fn size_for(&self, resize: Resize, size: Size) -> Size {
159        resize.size_for(&self.source.image, self.font_size, size)
160    }
161
162    pub fn protocol_type(&self) -> &StatefulProtocolType {
163        &self.protocol_type
164    }
165
166    pub fn protocol_type_owned(self) -> StatefulProtocolType {
167        self.protocol_type
168    }
169
170    /// This returns the latest Result returned when encoding, and none if there was no encoding since the last result read. It is encouraged but not required to handle it
171    pub fn last_encoding_result(&mut self) -> Option<Result<()>> {
172        self.last_encoding_result.take()
173    }
174
175    // Get the background color that fills in when resizing.
176    pub fn background_color(&self) -> Option<Rgba<u8>> {
177        self.source.background_color
178    }
179
180    fn last_encoding_area(&self) -> Size {
181        self.protocol_type.inner_trait().size()
182    }
183}
184
185impl ResizeEncodeRender for StatefulProtocol {
186    fn resize_encode(&mut self, resize: &Resize, size: Size) {
187        if size.width == 0 || size.height == 0 {
188            return;
189        }
190
191        let img = resize.resize(
192            &self.source.image,
193            self.font_size,
194            size,
195            self.background_color(),
196        );
197
198        // TODO: save err in struct
199        let result = self
200            .protocol_type
201            .inner_trait_mut()
202            .resize_encode(img, size);
203
204        if result.is_ok() {
205            self.hash = self.source.hash
206        }
207
208        self.last_encoding_result = Some(result)
209    }
210
211    fn render(&mut self, area: Rect, buf: &mut Buffer) {
212        self.protocol_type.inner_trait_mut().render(area, buf);
213    }
214
215    fn needs_resize(&self, resize: &Resize, size: Size) -> Option<Size> {
216        resize.needs_resize(
217            &self.source.image,
218            Some(self.source.desired),
219            self.font_size,
220            Some(self.last_encoding_area()),
221            size,
222            self.source.hash != self.hash,
223        )
224    }
225}
226
227#[derive(Clone)]
228/// Image source for [crate::protocol::StatefulProtocol]s
229///
230/// A `[StatefulProtocol]` needs to resize the ImageSource to its state when the available area
231/// changes. A `[Protocol]` only needs it once.
232///
233/// # Examples
234/// ```text
235/// use image::{DynamicImage, ImageBuffer, Rgb};
236/// use ratatui_image::ImageSource;
237///
238/// let image: ImageBuffer::from_pixel(300, 200, Rgb::<u8>([255, 0, 0])).into();
239/// let source = ImageSource::new(image, "filename.png", (7, 14));
240/// assert_eq!((43, 14), (source.rect.width, source.rect.height));
241/// ```
242///
243struct ImageSource {
244    /// The original image without resizing.
245    pub image: DynamicImage,
246    /// The area that the [`ImageSource::image`] covers, but not necessarily fills.
247    pub desired: Size,
248    /// TODO: document this; when image changes but it doesn't need a resize, force a render.
249    pub hash: u64,
250    /// The background color that should be used for padding or background when resizing.
251    pub background_color: Option<Rgba<u8>>,
252}
253
254impl ImageSource {
255    /// Create a new image source
256    pub fn new(
257        mut image: DynamicImage,
258        font_size: FontSize,
259        background_color: Option<Rgba<u8>>,
260    ) -> ImageSource {
261        let desired = Resize::round_pixel_size_to_cells(image.width(), image.height(), font_size);
262
263        let mut state = DefaultHasher::new();
264        image.as_bytes().hash(&mut state);
265        let hash = state.finish();
266
267        // We only need to underlay the background color here if it's not completely transparent.
268        if let Some(background_color) = background_color
269            && background_color.0[3] != 0
270        {
271            let mut bg: DynamicImage =
272                ImageBuffer::from_pixel(image.width(), image.height(), background_color).into();
273            imageops::overlay(&mut bg, &image, 0, 0);
274            image = bg;
275        }
276
277        ImageSource {
278            image,
279            desired,
280            hash,
281            background_color,
282        }
283    }
284}
285
286// Transparency needs explicit erasing of stale characters, or they stay behind the rendered
287// image due to skipping of the following characters _in the terminal buffer_.
288// DECERA does not work in WezTerm, however ECH and and cursor CUD and CUU do.
289// For each line, erase `width` characters, then move back and place image.
290pub(crate) fn clear_area(data: &mut String, escape: &str, width: u16, height: u16) {
291    if height == 1 {
292        // If the image is a single row then we don't need to move the cursor around at all.
293        write!(data, "{escape}[{width}X").unwrap();
294    } else {
295        for _ in 0..height {
296            write!(data, "{escape}[{width}X{escape}[1B").unwrap();
297        }
298        write!(data, "{escape}[{height}A").unwrap();
299    }
300}