ratatui_image/
protocol.rs

1//! Protocol backends for the widgets
2
3use std::{
4    collections::hash_map::DefaultHasher,
5    hash::{Hash, Hasher},
6};
7
8use image::{imageops, DynamicImage, ImageBuffer, Rgba};
9use ratatui::{buffer::Buffer, layout::Rect};
10
11use self::{
12    halfblocks::Halfblocks,
13    iterm2::Iterm2,
14    kitty::{Kitty, StatefulKitty},
15    sixel::Sixel,
16};
17use crate::{FontSize, Result};
18
19use super::Resize;
20
21pub mod halfblocks;
22pub mod iterm2;
23pub mod kitty;
24pub mod sixel;
25
26trait ProtocolTrait: Send + Sync {
27    /// Render the currently resized and encoded data to the buffer.
28    fn render(&mut self, area: Rect, buf: &mut Buffer);
29
30    // Get the area of the image.
31    #[allow(dead_code)]
32    fn area(&self) -> Rect;
33}
34
35trait StatefulProtocolTrait: ProtocolTrait {
36    /// Resize the image and encode it for rendering. The result should be stored statefully so
37    /// that next call for the given area does not need to redo the work.
38    ///
39    /// This can be done in a background thread, and the result is stored in this [StatefulProtocol].
40    fn resize_encode(&mut self, img: DynamicImage, area: Rect) -> Result<()>;
41}
42
43/// A fixed-size image protocol for the [crate::Image] widget.
44#[derive(Clone)]
45pub enum Protocol {
46    Halfblocks(Halfblocks),
47    Sixel(Sixel),
48    Kitty(Kitty),
49    ITerm2(Iterm2),
50}
51
52impl Protocol {
53    pub(crate) fn render(&mut self, area: Rect, buf: &mut Buffer) {
54        let inner: &mut dyn ProtocolTrait = match self {
55            Self::Halfblocks(halfblocks) => halfblocks,
56            Self::Sixel(sixel) => sixel,
57            Self::Kitty(kitty) => kitty,
58            Self::ITerm2(iterm2) => iterm2,
59        };
60        inner.render(area, buf);
61    }
62    pub fn area(&self) -> Rect {
63        let inner: &dyn ProtocolTrait = match self {
64            Self::Halfblocks(halfblocks) => halfblocks,
65            Self::Sixel(sixel) => sixel,
66            Self::Kitty(kitty) => kitty,
67            Self::ITerm2(iterm2) => iterm2,
68        };
69        inner.area()
70    }
71}
72
73/// A stateful resizing image protocol for the [crate::StatefulImage] widget.
74///
75/// The [create::thread::ThreadImage] widget also uses this, and is the reason why resizing is
76/// split from rendering.
77pub struct StatefulProtocol {
78    source: ImageSource,
79    font_size: FontSize,
80    hash: u64,
81    protocol_type: StatefulProtocolType,
82    last_encoding_result: Option<Result<()>>,
83}
84
85#[derive(Clone)]
86pub enum StatefulProtocolType {
87    Halfblocks(Halfblocks),
88    Sixel(Sixel),
89    Kitty(StatefulKitty),
90    ITerm2(Iterm2),
91}
92
93impl StatefulProtocolType {
94    fn inner_trait(&self) -> &dyn StatefulProtocolTrait {
95        match self {
96            Self::Halfblocks(halfblocks) => halfblocks,
97            Self::Sixel(sixel) => sixel,
98            Self::Kitty(kitty) => kitty,
99            Self::ITerm2(iterm2) => iterm2,
100        }
101    }
102    fn inner_trait_mut(&mut self) -> &mut dyn StatefulProtocolTrait {
103        match self {
104            Self::Halfblocks(halfblocks) => halfblocks,
105            Self::Sixel(sixel) => sixel,
106            Self::Kitty(kitty) => kitty,
107            Self::ITerm2(iterm2) => iterm2,
108        }
109    }
110}
111
112impl StatefulProtocol {
113    pub fn new(
114        source: ImageSource,
115        font_size: FontSize,
116        protocol_type: StatefulProtocolType,
117    ) -> Self {
118        Self {
119            source,
120            font_size,
121            hash: u64::default(),
122            protocol_type,
123            last_encoding_result: None,
124        }
125    }
126
127    pub fn protocol_type(&self) -> &StatefulProtocolType {
128        &self.protocol_type
129    }
130
131    pub fn protocol_type_owned(self) -> StatefulProtocolType {
132        self.protocol_type
133    }
134
135    /// 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
136    pub fn last_encoding_result(&mut self) -> Option<Result<()>> {
137        self.last_encoding_result.take()
138    }
139
140    // Get the background color that fills in when resizing.
141    pub fn background_color(&self) -> Rgba<u8> {
142        self.source.background_color
143    }
144
145    /// Resize and encode if necessary, and render immediately.
146    ///
147    /// This blocks the UI thread but requires neither threads nor async.
148    pub fn resize_encode_render(&mut self, resize: &Resize, area: Rect, buf: &mut Buffer) {
149        if let Some(rect) = self.needs_resize(resize, area) {
150            self.resize_encode(resize, rect);
151        }
152        self.render(area, buf);
153    }
154
155    /// Check if the current image state would need resizing (grow or shrink) for the given area.
156    ///
157    /// This can be called by the UI thread to check if this [StatefulProtocol] should be sent off
158    /// to some background thread/task to do the resizing and encoding, instead of rendering. The
159    /// thread should then return the [StatefulProtocol] so that it can be rendered.protoco
160    pub fn needs_resize(&mut self, resize: &Resize, area: Rect) -> Option<Rect> {
161        resize.needs_resize(
162            &self.source,
163            self.font_size,
164            self.last_encoding_area(),
165            area,
166            self.source.hash != self.hash,
167        )
168    }
169
170    /// Resize the image and encode it for rendering. The result should be stored statefully so
171    /// that next call for the given area does not need to redo the work.
172    ///
173    /// This can be done in a background thread, and the result is stored in this [StatefulProtocol].
174    pub fn resize_encode(&mut self, resize: &Resize, area: Rect) {
175        if area.width == 0 || area.height == 0 {
176            return;
177        }
178
179        let img = resize.resize(&self.source, self.font_size, area, self.background_color());
180
181        // TODO: save err in struct
182        let result = self
183            .protocol_type
184            .inner_trait_mut()
185            .resize_encode(img, area);
186
187        if result.is_ok() {
188            self.hash = self.source.hash
189        }
190
191        self.last_encoding_result = Some(result)
192    }
193
194    /// Render the currently resized and encoded data to the buffer.
195    pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
196        self.protocol_type.inner_trait_mut().render(area, buf);
197    }
198
199    pub fn size_for(&self, resize: &Resize, area: Rect) -> Rect {
200        resize.render_area(&self.source, self.font_size, area)
201    }
202
203    fn last_encoding_area(&self) -> Rect {
204        self.protocol_type.inner_trait().area()
205    }
206}
207
208#[derive(Clone)]
209/// Image source for [crate::protocol::StatefulProtocol]s
210///
211/// A `[StatefulProtocol]` needs to resize the ImageSource to its state when the available area
212/// changes. A `[Protocol]` only needs it once.
213///
214/// # Examples
215/// ```text
216/// use image::{DynamicImage, ImageBuffer, Rgb};
217/// use ratatui_image::ImageSource;
218///
219/// let image: ImageBuffer::from_pixel(300, 200, Rgb::<u8>([255, 0, 0])).into();
220/// let source = ImageSource::new(image, "filename.png", (7, 14));
221/// assert_eq!((43, 14), (source.rect.width, source.rect.height));
222/// ```
223///
224pub struct ImageSource {
225    /// The original image without resizing.
226    pub image: DynamicImage,
227    /// The area that the [`ImageSource::image`] covers, but not necessarily fills.
228    pub desired: Rect,
229    /// TODO: document this; when image changes but it doesn't need a resize, force a render.
230    pub hash: u64,
231    /// The background color that should be used for padding or background when resizing.
232    pub background_color: Rgba<u8>,
233}
234
235impl ImageSource {
236    /// Create a new image source
237    pub fn new(
238        mut image: DynamicImage,
239        font_size: FontSize,
240        background_color: Rgba<u8>,
241    ) -> ImageSource {
242        let desired =
243            ImageSource::round_pixel_size_to_cells(image.width(), image.height(), font_size);
244
245        let mut state = DefaultHasher::new();
246        image.as_bytes().hash(&mut state);
247        let hash = state.finish();
248
249        // We only need to underlay the background color here if it's not completely transparent.
250        if background_color.0[3] != 0 {
251            let mut bg: DynamicImage =
252                ImageBuffer::from_pixel(image.width(), image.height(), background_color).into();
253            imageops::overlay(&mut bg, &image, 0, 0);
254            image = bg;
255        }
256
257        ImageSource {
258            image,
259            desired,
260            hash,
261            background_color,
262        }
263    }
264    /// Round an image pixel size to the nearest matching cell size, given a font size.
265    pub fn round_pixel_size_to_cells(
266        img_width: u32,
267        img_height: u32,
268        (char_width, char_height): FontSize,
269    ) -> Rect {
270        let width = (img_width as f32 / char_width as f32).ceil() as u16;
271        let height = (img_height as f32 / char_height as f32).ceil() as u16;
272        Rect::new(0, 0, width, height)
273    }
274}