use dotmax::image::{DitheringMethod, ImageRenderer};
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
fn env_f32(key: &str, default: f32) -> f32 {
std::env::var(key)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
fn env_u8(key: &str, default: u8) -> Option<u8> {
match std::env::var(key).ok().as_deref() {
Some("auto" | "AUTO" | "") => None,
Some(s) => s.parse().ok(),
None => Some(default),
}
}
fn env_bool(key: &str, default: bool) -> bool {
match std::env::var(key).ok().as_deref() {
Some("1" | "true" | "TRUE" | "yes") => true,
Some("0" | "false" | "FALSE" | "no") => false,
_ => default,
}
}
fn env_dither(key: &str) -> DitheringMethod {
match std::env::var(key)
.ok()
.as_deref()
.map(str::to_lowercase)
.as_deref()
{
Some("floyd" | "floydsteinberg") => DitheringMethod::FloydSteinberg,
Some("bayer") => DitheringMethod::Bayer,
Some("atkinson") => DitheringMethod::Atkinson,
_ => DitheringMethod::None,
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let image_path = args
.next()
.ok_or("usage: render_cuda_dojo <image> <out_dir>")?;
let out_dir: PathBuf = args
.next()
.ok_or("usage: render_cuda_dojo <image> <out_dir>")?
.into();
fs::create_dir_all(&out_dir)?;
let dithering = env_dither("DITHER");
let threshold = env_u8("THRESHOLD", 128);
let brightness = env_f32("BRIGHTNESS", 1.0);
let contrast = env_f32("CONTRAST", 1.3);
let gamma = env_f32("GAMMA", 1.0);
let invert = env_bool("INVERT", true);
let font_px = env_f32("FONT_PX", 16.0);
println!(
"settings: dither={:?} threshold={:?} brightness={} contrast={} gamma={} invert={}",
dithering, threshold, brightness, contrast, gamma, invert
);
let src = image::open(&image_path)?;
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 sizes: &[(&str, usize, usize)] = &[
("01_xs_30x10", 30, 10),
("02_sm_60x20", 60, 20),
("03_md_96x32", 96, 32),
("04_lg_144x48", 144, 48),
("05_xl_210x70", 210, 70),
];
for (name, w, h) in sizes {
let mut builder = ImageRenderer::new()
.load_from_rgba(rgba.clone())
.resize(*w, *h, true)?
.dithering(dithering);
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 unicode = grid.to_unicode_grid();
let mut text = String::with_capacity((grid.width() + 1) * grid.height());
for row in &unicode {
for ch in row {
text.push(*ch);
}
text.push('\n');
}
let txt_path = out_dir.join(format!("{}.txt", name));
fs::write(&txt_path, &text)?;
let char_w = font_px * 0.55;
let row_h = font_px;
let svg_w = grid.width() as f32 * char_w;
let svg_h = grid.height() as f32 * row_h;
let mut svg = String::with_capacity(4096);
let _ = writeln!(
svg,
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {svg_w:.2} {svg_h:.2}\" width=\"{svg_w:.0}\" height=\"{svg_h:.0}\" font-family=\"ui-monospace,Menlo,Consolas,monospace\" font-size=\"{font_px}\">"
);
svg.push_str("<rect width=\"100%\" height=\"100%\" fill=\"#ffffff\"/>\n");
for (row_idx, row) in unicode.iter().enumerate() {
let line: String = row.iter().collect();
let y = (row_idx as f32 + 0.8) * row_h;
let escaped = line
.replace('&', "&")
.replace('<', "<")
.replace('>', ">");
let _ = writeln!(
svg,
"<text x=\"0\" y=\"{y:.2}\" xml:space=\"preserve\" textLength=\"{svg_w:.2}\" lengthAdjust=\"spacingAndGlyphs\">{escaped}</text>"
);
}
svg.push_str("</svg>\n");
let svg_path = out_dir.join(format!("{}.svg", name));
fs::write(&svg_path, svg)?;
println!(
"wrote {} + {}.svg ({}x{} chars)",
txt_path.display(),
name,
grid.width(),
grid.height()
);
}
println!("done — files in {}", out_dir.display());
Ok(())
}