feat: Implement Unicode block renderer

This commit is contained in:
Tony Du 2025-03-05 21:49:22 -08:00
commit 8f3e6ef835
Signed by: tony
SSH Key Fingerprint: SHA256:mhFElod7vS6ugYoXNATYTUGD3tYvW5yvXbXjP2uWXVw
7 changed files with 1481 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.direnv

1256
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "picsl"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.31", features = ["derive"] }
image = "0.25.5"
termion = "4.0.4"

46
flake.lock generated Normal file
View File

@ -0,0 +1,46 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1739736696,
"narHash": "sha256-zON2GNBkzsIyALlOCFiEBcIjI4w38GYOb+P+R4S8Jsw=",
"rev": "d74a2335ac9c133d6bbec9fc98d91a77f1604c1f",
"revCount": 754461,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.754461%2Brev-d74a2335ac9c133d6bbec9fc98d91a77f1604c1f/01951426-5a87-7b75-8413-1a0d9ec5ff04/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1739845646,
"narHash": "sha256-UGQVBU/yDn6u0kAE4z1PYrOaaf3wl+gAAv5rui2TkFQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "ab2cd2b8b25ab3f65b8ce4aa701a6d69fbb0210f",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

57
flake.nix Normal file
View File

@ -0,0 +1,57 @@
{
description = "A Nix-flake-based Rust development environment";
inputs = {
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, rust-overlay }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default self.overlays.default ];
};
});
in
{
overlays.default = final: prev: {
rustToolchain =
let
rust = prev.rust-bin;
in
if builtins.pathExists ./rust-toolchain.toml then
rust.fromRustupToolchainFile ./rust-toolchain.toml
else if builtins.pathExists ./rust-toolchain then
rust.fromRustupToolchainFile ./rust-toolchain
else
rust.stable.latest.default.override {
extensions = [ "rust-src" "rustfmt" ];
};
};
devShells = forEachSupportedSystem ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [
rustToolchain
openssl
pkg-config
cargo-deny
cargo-edit
cargo-watch
rust-analyzer
];
env = {
# Required by rust-analyzer
RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library";
};
};
});
};
}

110
src/main.rs Normal file
View File

@ -0,0 +1,110 @@
use clap::{Parser, Subcommand};
use image::{imageops, DynamicImage, GenericImageView, ImageReader, Pixel};
use termion::{color, terminal_size};
trait TextImageRenderer {
fn render(&self, image: DynamicImage, width: u32, height: u32) -> String;
}
struct UnicodeBlockRendererConfig {
alpha_threshold: u8,
block_char: String,
}
struct UnicodeBlockRenderer {
config: UnicodeBlockRendererConfig,
}
impl TextImageRenderer for UnicodeBlockRenderer {
fn render(&self, image: DynamicImage, width: u32, height: u32) -> String {
let scaled_image = image.resize(width, height, imageops::FilterType::Gaussian);
// This might not match `(environment.width, environment.height)` because resize preserves
// aspect ratio on the scaled image
let (scaled_x, scaled_y) = scaled_image.dimensions();
let mut out = String::new();
for y in 0..scaled_y {
for x in 0..scaled_x {
let pix = scaled_image.get_pixel(x.into(), y.into()).to_rgba();
if pix[3] >= self.config.alpha_threshold {
out.push_str(&format!(
"{}{}",
color::Fg(color::Rgb(pix[0], pix[1], pix[2])),
self.config.block_char,
));
} else {
out.push_str(&format!("{} ", color::Fg(color::Reset)));
}
}
out.push_str("\n");
}
out
}
}
#[derive(Parser, Debug)]
#[command(name = "picsl")]
#[command(version = "0.1")]
#[command(about = "Render images in the terminal", long_about = None)]
struct Cli {
/// Path to image
#[arg(short, long)]
image: String,
#[command(subcommand)]
renderer: RendererCommands,
}
#[derive(Subcommand, Debug)]
enum RendererCommands {
UnicodeBlock {
/// A pixel's alpha channel must be higher than this threshold to be rendered.
#[arg(long, default_value_t = 0)]
alpha_threshold: u8,
/// The (potentially unicode) character to use for filled-in values
#[arg(short, long, default_value_t = String::from(""))]
block_char: String,
},
}
fn get_renderer(renderer_command: RendererCommands) -> impl TextImageRenderer {
match renderer_command {
RendererCommands::UnicodeBlock {
alpha_threshold,
block_char,
} => UnicodeBlockRenderer {
config: UnicodeBlockRendererConfig {
alpha_threshold,
block_char,
},
},
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Cli::parse();
let image_path = args.image;
// Load the image and resize it to terminal size
let (term_x, term_y) = terminal_size().expect("Terminal size");
let image_buf = ImageReader::open(image_path)?.decode()?;
let renderer = get_renderer(args.renderer);
let rendered_image_text = renderer.render(image_buf, term_x.into(), term_y.into());
print!(
"{}{}{}{}{}",
// Blank canvas
termion::clear::All,
// Go to top
termion::cursor::Goto(1, 1),
// Display image
rendered_image_text,
// Reset colors in case we do something else later
color::Fg(color::Reset),
color::Bg(color::Reset),
);
Ok(())
}