use dotmax::image::{DitheringMethod, ImageRenderer};
use std::io::Write;
fn env_str(key: &str) -> Option<String> {
std::env::var(key).ok()
}
fn env_f32(key: &str, default: f32) -> f32 {
env_str(key).and_then(|s| s.parse().ok()).unwrap_or(default)
}
fn env_u8(key: &str, default: u8) -> Option<u8> {
match env_str(key).as_deref() {
Some("auto" | "AUTO" | "") => None,
Some(s) => s.parse().ok(),
None => Some(default),
}
}
fn env_u64(key: &str, default: u64) -> u64 {
env_str(key).and_then(|s| s.parse().ok()).unwrap_or(default)
}
fn env_bool(key: &str, default: bool) -> bool {
match env_str(key).as_deref() {
Some("1" | "true" | "TRUE" | "yes" | "YES") => true,
Some("0" | "false" | "FALSE" | "no" | "NO") => false,
_ => default,
}
}
fn env_dither() -> DitheringMethod {
match env_str("DOTMAX_DITHER")
.as_deref()
.map(str::to_lowercase)
.as_deref()
{
Some("floyd" | "floydsteinberg" | "floyd_steinberg") => DitheringMethod::FloydSteinberg,
Some("bayer") => DitheringMethod::Bayer,
Some("none") => DitheringMethod::None,
_ => DitheringMethod::Atkinson,
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let path = args
.next()
.ok_or("usage: dotmax-braille <input.png> <width_cells> <height_cells>")?;
let width: usize = args
.next()
.ok_or("missing width argument")?
.parse()
.map_err(|e| format!("invalid width: {e}"))?;
let height: usize = args
.next()
.ok_or("missing height argument")?
.parse()
.map_err(|e| format!("invalid height: {e}"))?;
let dither = env_dither();
let threshold = env_u8("DOTMAX_THRESHOLD", 128);
let brightness = env_f32("DOTMAX_BRIGHTNESS", 1.0);
let contrast = env_f32("DOTMAX_CONTRAST", 1.3);
let gamma = env_f32("DOTMAX_GAMMA", 1.0);
let invert = env_bool("DOTMAX_INVERT", true);
let invert_output = env_bool("DOTMAX_INVERT_OUTPUT", false);
let ambient = env_f32("DOTMAX_AMBIENT", 0.55).clamp(0.0, 1.0);
let seed = env_u64("DOTMAX_SEED", 0);
let frame = env_u64("DOTMAX_FRAME", 0);
let src = image::open(&path).map_err(|e| format!("failed to open {path}: {e}"))?;
let mut rgba = src.to_rgba8();
if invert {
for px in rgba.pixels_mut() {
px.0[0] = 255 - px.0[0];
px.0[1] = 255 - px.0[1];
px.0[2] = 255 - px.0[2];
}
}
let mut builder = ImageRenderer::new()
.load_from_rgba(rgba)
.resize(width, height, true)?
.dithering(dither)
.ambient(ambient)
.ambient_frame(frame);
if seed != 0 {
builder = builder.ambient_seed(seed);
}
if (brightness - 1.0).abs() > f32::EPSILON {
builder = builder.brightness(brightness)?;
}
if (contrast - 1.0).abs() > f32::EPSILON {
builder = builder.contrast(contrast)?;
}
if (gamma - 1.0).abs() > f32::EPSILON {
builder = builder.gamma(gamma)?;
}
if let Some(t) = threshold {
builder = builder.threshold(t);
}
let grid = builder.render()?;
let mut unicode = grid.to_unicode_grid();
if invert_output {
for row in &mut unicode {
for ch in row.iter_mut() {
let bits = (*ch as u32).saturating_sub(0x2800) as u8;
*ch = char::from_u32(0x2800 + (!bits as u32)).unwrap_or(*ch);
}
}
}
let stdout = std::io::stdout();
let mut out = stdout.lock();
for row in &unicode {
for ch in row {
let mut buf = [0u8; 4];
out.write_all(ch.encode_utf8(&mut buf).as_bytes())?;
}
out.write_all(b"\n")?;
}
out.flush()?;
Ok(())
}