spritebox 0.6.0

Create 2D pixel art and game assets from text: sprites, backgrounds with parallax, tilemaps, procedural textures, UI and pixel fonts. A zero-dependency Rust library and CLI with PNG, atlas and TTF export.
Documentation
//! A built-in 3×5 pixel font for the labels of preview sheets. Lowercase letters are
//! drawn as uppercase; characters it lacks are drawn as `?`.

use crate::{Color, Image};

/// Width of a glyph, and the advance with the space after it.
pub(crate) const GLYPH_WIDTH: u32 = 3;
pub(crate) const ADVANCE: u32 = 4;
pub(crate) const HEIGHT: u32 = 5;

#[rustfmt::skip]
const GLYPHS: &[(char, [&str; 5])] = &[
    ('A', [".#.", "#.#", "###", "#.#", "#.#"]),
    ('B', ["##.", "#.#", "##.", "#.#", "##."]),
    ('C', [".##", "#..", "#..", "#..", ".##"]),
    ('D', ["##.", "#.#", "#.#", "#.#", "##."]),
    ('E', ["###", "#..", "##.", "#..", "###"]),
    ('F', ["###", "#..", "##.", "#..", "#.."]),
    ('G', [".##", "#..", "#.#", "#.#", ".##"]),
    ('H', ["#.#", "#.#", "###", "#.#", "#.#"]),
    ('I', ["###", ".#.", ".#.", ".#.", "###"]),
    ('J', ["..#", "..#", "..#", "#.#", ".#."]),
    ('K', ["#.#", "#.#", "##.", "#.#", "#.#"]),
    ('L', ["#..", "#..", "#..", "#..", "###"]),
    ('M', ["#.#", "###", "###", "#.#", "#.#"]),
    ('N', ["##.", "#.#", "#.#", "#.#", "#.#"]),
    ('O', [".#.", "#.#", "#.#", "#.#", ".#."]),
    ('P', ["##.", "#.#", "##.", "#..", "#.."]),
    ('Q', [".#.", "#.#", "#.#", "##.", ".##"]),
    ('R', ["##.", "#.#", "##.", "#.#", "#.#"]),
    ('S', [".##", "#..", ".#.", "..#", "##."]),
    ('T', ["###", ".#.", ".#.", ".#.", ".#."]),
    ('U', ["#.#", "#.#", "#.#", "#.#", "###"]),
    ('V', ["#.#", "#.#", "#.#", "#.#", ".#."]),
    ('W', ["#.#", "#.#", "###", "###", "#.#"]),
    ('X', ["#.#", "#.#", ".#.", "#.#", "#.#"]),
    ('Y', ["#.#", "#.#", ".#.", ".#.", ".#."]),
    ('Z', ["###", "..#", ".#.", "#..", "###"]),
    ('0', ["###", "#.#", "#.#", "#.#", "###"]),
    ('1', [".#.", "##.", ".#.", ".#.", "###"]),
    ('2', ["##.", "..#", ".#.", "#..", "###"]),
    ('3', ["##.", "..#", ".#.", "..#", "##."]),
    ('4', ["#.#", "#.#", "###", "..#", "..#"]),
    ('5', ["###", "#..", "##.", "..#", "##."]),
    ('6', [".##", "#..", "###", "#.#", "###"]),
    ('7', ["###", "..#", ".#.", ".#.", ".#."]),
    ('8', ["###", "#.#", "###", "#.#", "###"]),
    ('9', ["###", "#.#", "###", "..#", "##."]),
    (' ', ["...", "...", "...", "...", "..."]),
    ('_', ["...", "...", "...", "...", "###"]),
    ('-', ["...", "...", "###", "...", "..."]),
    (':', ["...", ".#.", "...", ".#.", "..."]),
    ('.', ["...", "...", "...", "...", ".#."]),
    (',', ["...", "...", "...", ".#.", "#.."]),
    ('(', [".#.", "#..", "#..", "#..", ".#."]),
    (')', [".#.", "..#", "..#", "..#", ".#."]),
    ('[', ["##.", "#..", "#..", "#..", "##."]),
    (']', [".##", "..#", "..#", "..#", ".##"]),
    ('<', ["..#", ".#.", "#..", ".#.", "..#"]),
    ('>', ["#..", ".#.", "..#", ".#.", "#.."]),
    ('=', ["...", "###", "...", "###", "..."]),
    ('+', ["...", ".#.", "###", ".#.", "..."]),
    ('/', ["..#", "..#", ".#.", "#..", "#.."]),
    ('#', ["#.#", "###", "#.#", "###", "#.#"]),
    ('*', ["#.#", ".#.", "#.#", "...", "..."]),
    ('\'', [".#.", ".#.", "...", "...", "..."]),
    ('!', [".#.", ".#.", ".#.", "...", ".#."]),
    ('?', ["##.", "..#", ".#.", "...", ".#."]),
];

fn glyph(c: char) -> &'static [&'static str; 5] {
    let c = c.to_ascii_uppercase();
    GLYPHS
        .iter()
        .find(|(g, _)| *g == c)
        .or_else(|| GLYPHS.iter().find(|(g, _)| *g == '?'))
        .map(|(_, rows)| rows)
        .unwrap_or(&GLYPHS[0].1)
}

/// The width of `text` drawn at `scale`, without the space after the last glyph.
pub(crate) fn width(text: &str, scale: u32) -> u32 {
    let n = text.chars().count() as u32;
    if n == 0 {
        0
    } else {
        (n * ADVANCE - (ADVANCE - GLYPH_WIDTH)) * scale
    }
}

/// Draws `text` with its top-left corner at `(x, y)`, each font pixel `scale` wide.
pub(crate) fn draw(image: &mut Image, text: &str, x: i64, y: i64, scale: u32, color: Color) {
    for (i, c) in text.chars().enumerate() {
        let left = x + (i as u32 * ADVANCE * scale) as i64;
        for (row, line) in glyph(c).iter().enumerate() {
            for (col, pixel) in line.bytes().enumerate() {
                if pixel == b'#' {
                    image.fill_rect(
                        left + (col as u32 * scale) as i64,
                        y + (row as u32 * scale) as i64,
                        scale,
                        scale,
                        color,
                    );
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn glyphs_are_well_formed_and_distinct() {
        for (c, rows) in GLYPHS {
            assert!(
                rows.iter()
                    .all(|r| r.len() == 3 && r.bytes().all(|b| b == b'#' || b == b'.')),
                "{c}"
            );
        }
        for (i, (a, ra)) in GLYPHS.iter().enumerate() {
            for (b, rb) in &GLYPHS[i + 1..] {
                assert!(a != b && ra != rb, "{a} and {b}");
            }
        }
        assert_eq!(glyph('a'), glyph('A'));
        assert_eq!(glyph('é'), glyph('?'));
        assert_eq!(width("ab", 2), 14);
    }
}