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