Skip to main content

ratatui_image/protocol/
halfblocks.rs

1//! Halfblocks protocol implementations.
2//!
3//! Uses the unicode character `▀` combined with foreground and background color. Assumes that the
4//! font aspect ratio is roughly 1:2. Should work in all terminals.
5//!
6//! If chafa is available, uses chafa for much richer rendering than primitive halfblocks:
7//! - `chafa-static`: statically linked at compile time (requires static libchafa.a)
8//! - `chafa-dyn`: dynamically linked at compile time via pkg-config
9
10// Ensure only one chafa feature is enabled at a time
11#[cfg(all(feature = "chafa-static", feature = "chafa-dyn"))]
12compile_error!("features `chafa-static` and `chafa-dyn` are mutually exclusive");
13
14use image::DynamicImage;
15use ratatui::{
16    buffer::{Buffer, Cell},
17    layout::Rect,
18    style::Color,
19};
20
21use super::{ProtocolTrait, StatefulProtocolTrait};
22use crate::Result;
23
24#[cfg(any(feature = "chafa-dyn", feature = "chafa-static",))]
25mod chafa;
26
27#[cfg(not(any(feature = "chafa-dyn", feature = "chafa-static",)))]
28mod primitive;
29
30/// Fixed Halfblocks protocol
31#[derive(Clone, Default)]
32pub struct Halfblocks {
33    data: Vec<HalfBlock>,
34    area: Rect,
35}
36
37#[derive(Clone, Debug)]
38pub(crate) struct HalfBlock {
39    pub upper: Color,
40    pub lower: Color,
41    pub char: char,
42}
43
44impl HalfBlock {
45    fn set_cell(&self, cell: &mut Cell) {
46        cell.set_fg(self.upper)
47            .set_bg(self.lower)
48            .set_char(self.char);
49    }
50}
51
52impl Halfblocks {
53    /// Create a FixedHalfblocks from an image.
54    ///
55    /// The "resolution" is determined by the font size of the terminal. Smaller fonts will result
56    /// in more half-blocks for the same image size. To get a size independent of the font size,
57    /// the image could be resized in relation to the font size beforehand.
58    /// Also note that the font-size is probably just some arbitrary size with a 1:2 ratio when the
59    /// protocol is Halfblocks, and not the actual font size of the terminal.
60    pub fn new(image: DynamicImage, area: Rect) -> Result<Self> {
61        let data = encode(&image, area);
62        Ok(Self { data, area })
63    }
64}
65
66// chafa-static and chafa-dyn: always use chafa (no fallback needed/possible)
67#[cfg(any(feature = "chafa-static", feature = "chafa-dyn"))]
68fn encode(img: &DynamicImage, rect: Rect) -> Vec<HalfBlock> {
69    chafa::encode(img, rect).expect("chafa is always available with compile-time linking")
70}
71
72// no chafa feature: use primitive only
73#[cfg(not(any(feature = "chafa-dyn", feature = "chafa-static")))]
74fn encode(img: &DynamicImage, rect: Rect) -> Vec<HalfBlock> {
75    primitive::encode(img, rect)
76}
77
78impl ProtocolTrait for Halfblocks {
79    fn render(&self, area: Rect, buf: &mut Buffer) {
80        for (i, hb) in self.data.iter().enumerate() {
81            let x = i as u16 % self.area.width;
82            let y = i as u16 / self.area.width;
83            if x >= area.width || y >= area.height {
84                continue;
85            }
86
87            if let Some(cell) = buf.cell_mut((area.x + x, area.y + y)) {
88                hb.set_cell(cell);
89            }
90        }
91    }
92    fn area(&self) -> Rect {
93        self.area
94    }
95}
96
97impl StatefulProtocolTrait for Halfblocks {
98    fn resize_encode(&mut self, img: DynamicImage, area: Rect) -> Result<()> {
99        let data = encode(&img, area);
100        *self = Halfblocks { data, area };
101        Ok(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use image::{Rgb, RgbImage};
108    use insta::assert_snapshot;
109    use ratatui::{Terminal, backend::TestBackend, layout::Rect};
110
111    use crate::{
112        Image,
113        protocol::{Protocol, halfblocks::Halfblocks},
114    };
115
116    #[test]
117    fn render_image() {
118        let mut img = RgbImage::new(2, 2);
119        img.put_pixel(0, 0, Rgb([255, 0, 0])); // red
120        img.put_pixel(1, 0, Rgb([0, 255, 0])); // green
121        img.put_pixel(0, 1, Rgb([0, 0, 255])); // blue
122        img.put_pixel(1, 1, Rgb([255, 255, 0])); // yellow
123
124        let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
125        terminal
126            .draw(|frame| {
127                let image = image::ImageReader::open("./assets/NixOS.png")
128                    .unwrap()
129                    .decode()
130                    .unwrap();
131                let area = Rect::new(0, 0, 40, 20);
132                let hbs = Halfblocks::new(image, area).unwrap();
133                frame.render_widget(Image::new(&Protocol::Halfblocks(hbs)), frame.area());
134            })
135            .unwrap();
136
137        #[cfg(any(feature = "chafa-static", feature = "chafa-dyn",))]
138        {
139            assert_snapshot!("chafa", terminal.backend());
140        }
141        #[cfg(not(any(feature = "chafa-static", feature = "chafa-dyn",)))]
142        assert_snapshot!("halfblocks", terminal.backend());
143        #[cfg(any(feature = "chafa-static", feature = "chafa-dyn",))]
144        assert_snapshot!("chafa", terminal.backend());
145    }
146}