Skip to main content

hdim_render/
lib.rs

1//! High-performance terminal image renderer.
2//!
3//! This crate handles the translation of [DynamicImage] buffers into ANSI escape sequences
4//! suitable for display in a terminal emulator. It uses "half-block" characters (`â–„`)
5//! to effectively double the vertical resolution of the terminal grid.
6
7pub mod pixel;
8pub mod view;
9
10use anyhow::Result;
11use image::DynamicImage;
12use std::fmt::Write;
13
14use self::pixel::get_average_rgb;
15pub use self::view::View;
16
17/// Renders a portion of an image to an ANSI string using half-block characters.
18///
19/// This function maps the source image pixels to terminal cells based on the provided [View].
20/// It calculates the average color for the top and bottom half of each character cell
21/// to determine the foreground and background colors.
22///
23/// # Arguments
24///
25/// * `image` - The source [DynamicImage] to render.
26/// * `view` - The [View] configuration defining the source region and target dimensions.
27///
28/// # Errors
29///
30/// Returns an error if writing to the output string fails (though this is unlikely with `String`).
31///
32/// # Examples
33///
34/// ```no_run
35/// use hdim_render::{render, View};
36/// use image::DynamicImage;
37///
38/// let img = DynamicImage::new_rgba8(100, 100);
39/// let view = View {
40///     source_x: 0,
41///     source_y: 0,
42///     source_width: 100,
43///     source_height: 100,
44///     target_width: 50,
45///     target_height: 25,
46/// };
47/// let output = render(&img, &view).unwrap();
48/// println!("{}", output);
49/// ```
50pub fn render(image: &DynamicImage, view: &View) -> Result<String> {
51    let mut output = String::new();
52    let (x_ratio, y_ratio) = view.calculate_scaling();
53    let (block_width, block_height) = view.calculate_block_size(x_ratio, y_ratio);
54
55    for y in 0..view.target_height {
56        for x in 0..view.target_width {
57            let (source_x, source_y_top, source_y_bottom) =
58                view.map_to_source(x, y, x_ratio, y_ratio);
59
60            let (top_color, bottom_color) = calculate_cell_colors(
61                image,
62                source_x,
63                source_y_top,
64                source_y_bottom,
65                block_width,
66                block_height,
67            );
68
69            write_ansi_cell(&mut output, top_color, bottom_color)?;
70        }
71        output.push_str("\x1b[0m\n");
72    }
73    Ok(output)
74}
75
76/// Helper to calculate the average RGB values for the top and bottom halves of a character cell.
77fn calculate_cell_colors(
78    image: &DynamicImage,
79    source_x: u32,
80    source_y_top: u32,
81    source_y_bottom: u32,
82    block_width: u32,
83    block_height: u32,
84) -> ([u8; 3], [u8; 3]) {
85    let top_color = get_average_rgb(image, source_x, source_y_top, block_width, block_height);
86    let bottom_color = get_average_rgb(image, source_x, source_y_bottom, block_width, block_height);
87    (top_color, bottom_color)
88}
89
90/// Writes a single ANSI-colored half-block character to the output buffer.
91fn write_ansi_cell(output: &mut String, top: [u8; 3], bottom: [u8; 3]) -> Result<()> {
92    write!(
93        output,
94        "\x1b[48;2;{};{};{}m\x1b[38;2;{};{};{}mâ–„",
95        top[0], top[1], top[2], bottom[0], bottom[1], bottom[2]
96    )?;
97    Ok(())
98}