Skip to main content

px2ansi_rs/
lib.rs

1#![allow(clippy::multiple_crate_versions)]
2use image::{DynamicImage, GenericImageView, Rgba};
3use std::io::Write;
4
5/// This function iterates over the image pixels, processing them in vertical pairs
6/// to utilize the "Upper Half Block" (▀) character.
7/// # Errors
8/// Returns an error if writing to the output stream fails
9pub fn write_ansi_art<W: Write>(img: &DynamicImage, out: &mut W) -> std::io::Result<()> {
10    let (width, height) = img.dimensions();
11
12    // Iterate 2 rows at a time because each ANSI block character represents 2 vertical pixels.
13    for y in (0..height).step_by(2) {
14        for x in 0..width {
15            let px1 = img.get_pixel(x, y);
16
17            // Handle edge case: odd height images have no bottom pixel on the last row
18            let px2 = if y + 1 < height {
19                img.get_pixel(x, y + 1)
20            } else {
21                Rgba([0, 0, 0, 0]) // Treat as transparent
22            };
23
24            write_pixels(out, px1, px2)?;
25        }
26        // Reset color at the end of the row and add a newline
27        writeln!(out, "\x1b[0m")?;
28    }
29
30    Ok(())
31}
32
33/// Helper to write the ANSI sequence for a single character block (2 vertical pixels).
34fn write_pixels<W: Write>(out: &mut W, top: Rgba<u8>, bot: Rgba<u8>) -> std::io::Result<()> {
35    let top_alpha = top[3];
36    let bot_alpha = bot[3];
37
38    if top_alpha > 0 {
39        // CASE 1: Top pixel is visible
40        // Set foreground color to Top Pixel
41        write!(out, "\x1b[38;2;{};{};{}m", top[0], top[1], top[2])?;
42
43        if bot_alpha > 0 {
44            // CASE 1.1: Both pixels visible
45            // Set background to Bottom Pixel and print "Upper Half Block" (▀).
46            write!(out, "\x1b[48;2;{};{};{}m▀", bot[0], bot[1], bot[2])
47        } else {
48            // CASE 1.2: Only top visible
49            // Reset background (transparent) and print "Upper Half Block" (▀).
50            write!(out, "\x1b[49m▀")
51        }
52    } else {
53        // CASE 2: Top pixel is transparent
54        if bot_alpha > 0 {
55            // CASE 2.1: Only bottom visible
56            // Set foreground to Bottom Pixel and print "Lower Half Block" (▄).
57            // We use reset background (\x1b[49m) to ensure top half is transparent.
58            write!(out, "\x1b[38;2;{};{};{}m\x1b[49m▄", bot[0], bot[1], bot[2])
59        } else {
60            // CASE 2.2: Both transparent
61            // Print a space with reset colors
62            write!(out, "\x1b[0m ")
63        }
64    }
65}