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}