Skip to main content

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::{DynamicImage, ImageBuffer, Rgba, imageops};
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, ResizeEncodeRender, 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(&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(&self, area: Rect, buf: &mut Buffer) {
54        let inner: &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 [crate::thread::ThreadProtocol] 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    // Calculate the area that this image will ultimately render to, inside the given area.
128    pub fn size_for(&self, resize: Resize, area: Rect) -> Rect {
129        resize.render_area(&self.source, self.font_size, area)
130    }
131
132    pub fn protocol_type(&self) -> &StatefulProtocolType {
133        &self.protocol_type
134    }
135
136    pub fn protocol_type_owned(self) -> StatefulProtocolType {
137        self.protocol_type
138    }
139
140    /// 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
141    pub fn last_encoding_result(&mut self) -> Option<Result<()>> {
142        self.last_encoding_result.take()
143    }
144
145    // Get the background color that fills in when resizing.
146    pub fn background_color(&self) -> Rgba<u8> {
147        self.source.background_color
148    }
149
150    fn last_encoding_area(&self) -> Rect {
151        self.protocol_type.inner_trait().area()
152    }
153}
154
155impl ResizeEncodeRender for StatefulProtocol {
156    fn resize_encode(&mut self, resize: &Resize, area: Rect) {
157        if area.width == 0 || area.height == 0 {
158            return;
159        }
160
161        let img = resize.resize(&self.source, self.font_size, area, self.background_color());
162
163        // TODO: save err in struct
164        let result = self
165            .protocol_type
166            .inner_trait_mut()
167            .resize_encode(img, area);
168
169        if result.is_ok() {
170            self.hash = self.source.hash
171        }
172
173        self.last_encoding_result = Some(result)
174    }
175
176    fn render(&mut self, area: Rect, buf: &mut Buffer) {
177        self.protocol_type.inner_trait_mut().render(area, buf);
178    }
179
180    fn needs_resize(&self, resize: &Resize, area: Rect) -> Option<Rect> {
181        resize.needs_resize(
182            &self.source,
183            self.font_size,
184            self.last_encoding_area(),
185            area,
186            self.source.hash != self.hash,
187        )
188    }
189}
190#[derive(Clone)]
191/// Image source for [crate::protocol::StatefulProtocol]s
192///
193/// A `[StatefulProtocol]` needs to resize the ImageSource to its state when the available area
194/// changes. A `[Protocol]` only needs it once.
195///
196/// # Examples
197/// ```text
198/// use image::{DynamicImage, ImageBuffer, Rgb};
199/// use ratatui_image::ImageSource;
200///
201/// let image: ImageBuffer::from_pixel(300, 200, Rgb::<u8>([255, 0, 0])).into();
202/// let source = ImageSource::new(image, "filename.png", (7, 14));
203/// assert_eq!((43, 14), (source.rect.width, source.rect.height));
204/// ```
205///
206pub struct ImageSource {
207    /// The original image without resizing.
208    pub image: DynamicImage,
209    /// The area that the [`ImageSource::image`] covers, but not necessarily fills.
210    pub desired: Rect,
211    /// TODO: document this; when image changes but it doesn't need a resize, force a render.
212    pub hash: u64,
213    /// The background color that should be used for padding or background when resizing.
214    pub background_color: Rgba<u8>,
215}
216
217impl ImageSource {
218    /// Create a new image source
219    pub fn new(
220        mut image: DynamicImage,
221        font_size: FontSize,
222        background_color: Rgba<u8>,
223    ) -> ImageSource {
224        let desired =
225            ImageSource::round_pixel_size_to_cells(image.width(), image.height(), font_size);
226
227        let mut state = DefaultHasher::new();
228        image.as_bytes().hash(&mut state);
229        let hash = state.finish();
230
231        // We only need to underlay the background color here if it's not completely transparent.
232        if background_color.0[3] != 0 {
233            let mut bg: DynamicImage =
234                ImageBuffer::from_pixel(image.width(), image.height(), background_color).into();
235            imageops::overlay(&mut bg, &image, 0, 0);
236            image = bg;
237        }
238
239        ImageSource {
240            image,
241            desired,
242            hash,
243            background_color,
244        }
245    }
246    /// Round an image pixel size to the nearest matching cell size, given a font size.
247    pub fn round_pixel_size_to_cells(
248        img_width: u32,
249        img_height: u32,
250        (char_width, char_height): FontSize,
251    ) -> Rect {
252        let width = (img_width as f32 / char_width as f32).ceil() as u16;
253        let height = (img_height as f32 / char_height as f32).ceil() as u16;
254        Rect::new(0, 0, width, height)
255    }
256}