Skip to main content

slt/
halfblock.rs

1//! Half-block image rendering for terminals with truecolor support.
2//!
3//! Uses `▀` (upper half block) with foreground/background colors to render
4//! two vertical pixels per terminal cell, achieving 2x vertical resolution.
5
6#[cfg(feature = "image")]
7use image::DynamicImage;
8
9use crate::style::Color;
10
11/// A terminal-renderable image stored as a grid of [`Color`] values.
12///
13/// Each cell contains a foreground color (upper pixel) and background color
14/// (lower pixel), rendered using the `▀` half-block character.
15///
16/// Create from an [`image::DynamicImage`] with [`HalfBlockImage::from_dynamic`]
17/// (requires `image` feature), or construct manually from raw RGB data with
18/// [`HalfBlockImage::from_rgb`].
19pub struct HalfBlockImage {
20    /// Width in terminal columns.
21    pub width: u32,
22    /// Height in terminal rows (each row = 2 image pixels).
23    pub height: u32,
24    /// Row-major pairs of (upper_color, lower_color) for each cell.
25    pub pixels: Vec<(Color, Color)>,
26}
27
28#[cfg(feature = "image")]
29impl HalfBlockImage {
30    /// Create a half-block image from a [`DynamicImage`], resized to fit
31    /// the given terminal cell dimensions.
32    ///
33    /// The image is resized to `width x (height * 2)` pixels using Lanczos3
34    /// filtering, then each pair of vertically adjacent pixels is packed
35    /// into one terminal cell.
36    pub fn from_dynamic(img: &DynamicImage, width: u32, height: u32) -> Self {
37        let pixel_height = height * 2;
38        let resized = img.resize_exact(width, pixel_height, image::imageops::FilterType::Lanczos3);
39        let rgba = resized.to_rgba8();
40
41        let mut pixels = Vec::with_capacity((width * height) as usize);
42        for row in 0..height {
43            for col in 0..width {
44                let upper_y = row * 2;
45                let lower_y = row * 2 + 1;
46
47                let up = rgba.get_pixel(col, upper_y);
48                let lo = rgba.get_pixel(col, lower_y);
49
50                let upper = Color::Rgb(up[0], up[1], up[2]);
51                let lower = Color::Rgb(lo[0], lo[1], lo[2]);
52                pixels.push((upper, lower));
53            }
54        }
55
56        Self {
57            width,
58            height,
59            pixels,
60        }
61    }
62}
63
64impl HalfBlockImage {
65    /// Create a half-block image from raw RGB pixel data.
66    ///
67    /// `rgb_data` must contain `width x pixel_height x 3` bytes in row-major
68    /// RGB order, where `pixel_height = height * 2`.
69    pub fn from_rgb(rgb_data: &[u8], width: u32, height: u32) -> Self {
70        let pixel_height = height * 2;
71        let stride = (width * 3) as usize;
72        let mut pixels = Vec::with_capacity((width * height) as usize);
73
74        for row in 0..height {
75            for col in 0..width {
76                let upper_y = (row * 2) as usize;
77                let lower_y = (row * 2 + 1) as usize;
78                let x = (col * 3) as usize;
79
80                let (ur, ug, ub) = if upper_y < pixel_height as usize {
81                    let offset = upper_y * stride + x;
82                    if offset + 2 < rgb_data.len() {
83                        (rgb_data[offset], rgb_data[offset + 1], rgb_data[offset + 2])
84                    } else {
85                        (0, 0, 0)
86                    }
87                } else {
88                    (0, 0, 0)
89                };
90
91                let (lr, lg, lb) = if lower_y < pixel_height as usize {
92                    let offset = lower_y * stride + x;
93                    if offset + 2 < rgb_data.len() {
94                        (rgb_data[offset], rgb_data[offset + 1], rgb_data[offset + 2])
95                    } else {
96                        (0, 0, 0)
97                    }
98                } else {
99                    (0, 0, 0)
100                };
101
102                pixels.push((Color::Rgb(ur, ug, ub), Color::Rgb(lr, lg, lb)));
103            }
104        }
105
106        Self {
107            width,
108            height,
109            pixels,
110        }
111    }
112}