Media

ASCII art

Pictures turned into ASCII art: characters from light to dark, plain for terminals and emails, or coloured as HTML.

Version 1.0.0 · 2 functions · 1 crate

#images#text#fun

Functions

What use ascii_art.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use ascii_art, ascii_art_html from ascii_art (Quillo 0.6).

ascii_art(path: str, width: int, style: str, invert: bool) → stror an error

The picture as lines of text width characters wide (4 to 400), dark parts drawn with dense characters for dark text on a light background. style is "simple" (" .:-=+*#%@"), "detailed" (70 characters), "blocks" (" ░▒▓█") or your own characters from light to dark; invert true for light text on dark.

ascii_art_html(path: str, width: int, style: str) → stror an error

The picture as an HTML <pre> of coloured characters, each the colour of its spot, on a dark background: put it in a page with raw(...).

Get it

$ quillo install ascii_art               # ascii_art.qwer into this folder$ quillo install ascii_art --example     # and its example, ascii_art.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/ascii_art.qwer$ curl -O https://quillolang.org/plugins/files/ascii_art.qwe$ curl -O https://quillolang.org/plugins/files/ascii_art_sample.png

Then, in your program, all of it or only what you call:

main.qwe
use ascii_art.qweruse ascii_art, ascii_art_html from ascii_art      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }

Example

ascii_art.qwe
// A picture as ASCII art, for the server's start-up banner, a plain-text email and a retro page.////   quillo run ascii_art.qwe      then open http://localhost:8080/artuse "ascii_art.qwer"route GET /art = ascii_art("ascii_art_sample.png", 60, "simple", false)route GET /blocks = ascii_art("ascii_art_sample.png", 40, "blocks", false)route GET / = <html><body style="background: #222">{raw(ascii_art_html("ascii_art_sample.png", 80, "detailed")?)}</body></html>
$ quillo run ascii_art.qwe$ curl 'localhost:8080/art'$ curl 'localhost:8080/blocks'$ curl 'localhost:8080/'

Source

ascii_art.qwer
//! Pictures turned into ASCII art: text of characters from light to dark, plain for terminals and emails, or coloured as HTML.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }use image::{DynamicImage, ImageDecoder};fn reader(path: &str) -> Result<image::ImageReader<std::io::BufReader<std::fs::File>>, String> {    let mut r = image::ImageReader::open(path).map_err(|e| format!("{}: {}", path, e))?.with_guessed_format().map_err(|e| format!("{}: {}", path, e))?;    // A small file claiming to be enormous is refused before anything is allocated.    let mut l = image::Limits::default();    l.max_image_width = Some(12_000);    l.max_image_height = Some(12_000);    l.max_alloc = Some(512 * 1024 * 1024);    r.limits(l);    Ok(r)}// The picture turned upright by its EXIF orientation, as phones expect.fn open(path: &str) -> Result<DynamicImage, String> {    let mut dec = reader(path)?.into_decoder().map_err(|e| format!("{}: {}", path, e))?;    let o = dec.orientation().unwrap_or(image::metadata::Orientation::NoTransforms);    let mut img = DynamicImage::from_decoder(dec).map_err(|e| format!("{}: {}", path, e))?;    img.apply_orientation(o);    Ok(img)}// Saved in the format its extension names: .jpg (quality 88), .png, .webp (lossless) or .gif; folders are made.fn save(img: &DynamicImage, path: &str) -> Result<(), String> {    if let Some(dir) = std::path::Path::new(path).parent().filter(|d| !d.as_os_str().is_empty()) {        std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?;    }    let ext = path.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();    let err = |e: image::ImageError| format!("{}: {}", path, e);    match ext.as_str() {        "jpg" | "jpeg" => {            let f = std::fs::File::create(path).map_err(|e| format!("{}: {}", path, e))?;            image::codecs::jpeg::JpegEncoder::new_with_quality(std::io::BufWriter::new(f), 88).encode_image(&img.to_rgb8()).map_err(err)        }        "png" => img.save_with_format(path, image::ImageFormat::Png).map_err(err),        "webp" => DynamicImage::ImageRgba8(img.to_rgba8()).save_with_format(path, image::ImageFormat::WebP).map_err(err),        "gif" => DynamicImage::ImageRgba8(img.to_rgba8()).save_with_format(path, image::ImageFormat::Gif).map_err(err),        _ => Err(format!("{}: save as .jpg, .png, .webp or .gif", path)),    }}fn ramp(style: &str) -> Vec<char> {    match style {        "blocks" => " ░▒▓█".chars().collect(),        "detailed" => " .'`^\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$".chars().collect(),        "simple" | "" => " .:-=+*#%@".chars().collect(),        s => s.chars().collect(),    }}// The picture shrunk to `width` columns; characters are about twice as tall as wide, so rows are halved.fn grid(path: &str, width: i64) -> Result<image::RgbaImage, String> {    let img = open(path)?;    let cols = width.clamp(4, 400) as u32;    let rows = ((img.height() as f64 / img.width().max(1) as f64 * cols as f64 * 0.5).round() as u32).clamp(1, 400);    Ok(img.resize_exact(cols, rows, image::imageops::FilterType::Triangle).to_rgba8())}fn level(p: &image::Rgba<u8>, invert: bool) -> f64 {    // Light from 0 (black) to 1 (white); transparent counts as the background.    let a = p.0[3] as f64 / 255.0;    let l = (0.2126 * p.0[0] as f64 + 0.7152 * p.0[1] as f64 + 0.0722 * p.0[2] as f64) / 255.0 * a + (1.0 - a);    if invert { 1.0 - l } else { l }}/// The picture as lines of text `width` characters wide (4 to 400), dark parts drawn with dense characters for dark text on a light background./// `style` is "simple" (" .:-=+*#%@"), "detailed" (70 characters), "blocks" (" ░▒▓█") or your own characters from light to dark; `invert` true for light text on dark.pub fn ascii_art(path: &str, width: i64, style: &str, invert: bool) -> Result<String, String> {    let g = grid(path, width)?;    let chars = ramp(style);    if chars.len() < 2 {        return Err("give at least two characters, from light to dark".into());    }    let last = (chars.len() - 1) as f64;    let mut out = String::new();    for row in g.rows() {        let line: String = row.map(|p| chars[((1.0 - level(p, invert)) * last).round() as usize]).collect();        out += line.trim_end();        out.push('\n');    }    Ok(out)}/// The picture as an HTML `<pre>` of coloured characters, each the colour of its spot, on a dark background: put it in a page with `raw(...)`.pub fn ascii_art_html(path: &str, width: i64, style: &str) -> Result<String, String> {    let g = grid(path, width.clamp(4, 200))?;    let chars = ramp(style);    if chars.len() < 2 {        return Err("give at least two characters, from light to dark".into());    }    let last = (chars.len() - 1) as f64;    let mut out = String::from(r#"<pre style="background:#111;line-height:1;font-size:10px;font-family:monospace;display:inline-block;padding:8px">"#);    for row in g.rows() {        for p in row {            // Bright spots get dense characters, as the background is dark.            let c = chars[((1.0 - level(p, true)) * last).round() as usize];            let c = match c { '<' => "&lt;".to_string(), '>' => "&gt;".to_string(), '&' => "&amp;".to_string(), c => c.to_string() };            out += &format!(r#"<span style="color:#{:02x}{:02x}{:02x}">{}</span>"#, p.0[0], p.0[1], p.0[2], c);        }        out.push('\n');    }    out += "</pre>";    Ok(out)}
Thread
610Useful? Like it, or add to its thread below.

Thread 0 posts

On ASCII art: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.