crates/veilvoice-video/src/font.rs

what this file is for · veilvoice-video · 418 lines · the same file on GitHub

The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.


// SPDX-License-Identifier: GPL-3.0-or-later
//! A monospace face, five pixels by seven, drawn here.
//!
//! # Why this exists rather than a font file
//!
//! [`crate::frames`] draws the video's pictures as actual pixels, and names are
//! text. Text needs a face and a rasteriser, and the two usual ways to get
//! those are both wrong for this project.
//!
//! Loading one of the system's fonts makes the output depend on which machine
//! drew it: the same recording rendered on two computers would produce
//! different files, and this project publishes reproducible builds and checks
//! them. Pulling in a font rasteriser means a large dependency to draw eight
//! names, in a program whose front page invites the reader to run `cargo tree`
//! and find nothing large.
//!
//! So the face is here, it is ninety-five glyphs, and it is the same everywhere.
//!
//! # What it can and cannot draw
//!
//! Printable ASCII, from space to `~`. **Anything else is drawn as an open
//! box**, which is the honest way to render a character this cannot: a name in
//! Cyrillic or Japanese comes out as boxes rather than as nothing, and
//! [`crate::frames`] says so in its notes rather than letting somebody find out
//! by watching the finished video.
//!
//! The preview page does not have this limit, because it is markup and uses
//! whatever the reader's machine has. That is a real difference between the two
//! outputs and it is written down rather than glossed over.
//!
//! # Five by seven
//!
//! Small enough to write out and check by eye, large enough to stay legible
//! when scaled up by whole numbers, which is the only way it is ever scaled: a
//! bitmap glyph drawn at a fractional size is a blurred glyph, so
//! [`Face::scale_for`] picks a whole-number multiple and the text is crisp at
//! any frame size.
//!
//! # In plain words
//!
//! The letters used in the video, drawn dot by dot inside this program.
//!
//! It is here rather than taken from the computer so that the same recording
//! makes the same video on every machine, and so that this program does not have
//! to carry a large piece of somebody else's code to write eight names.


/// The first character the face has a glyph for.
pub const FIRST: char = ' ';


/// The last character the face has a glyph for.
pub const LAST: char = '~';



/// Width of one glyph, in pixels, before scaling.
pub const WIDTH: usize = 5;


/// Height of one glyph, in pixels, before scaling.
pub const HEIGHT: usize = 7;



/// The gap between two glyphs, in unscaled pixels.
///
/// One column. A monospace face with no gap runs its letters together, and two
/// makes eight names wider than the picture they sit in.
pub const GAP: usize = 1;



/// The glyphs, one row of five bits per line, seven lines per character.
///
/// Indexed by `character as usize - FIRST as usize`. Written out rather than
/// generated, so that what is in this file is what gets drawn.
#[rustfmt::skip]
const GLYPHS: [[u8; HEIGHT]; 95] = [

    // ' '
    [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
    // '!'
    [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100],
    // '"'
    [0b01010, 0b01010, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
    // '#'
    [0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010],
    // '$'
    [0b00100, 0b01111, 0b10100, 0b01110, 0b00101, 0b11110, 0b00100],
    // '%'
    [0b11000, 0b11001, 0b00010, 0b00100, 0b01000, 0b10011, 0b00011],
    // '&'
    [0b01100, 0b10010, 0b10010, 0b01100, 0b10010, 0b10001, 0b01110],
    // "'"
    [0b00100, 0b00100, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
    // '('
    [0b00010, 0b00100, 0b01000, 0b01000, 0b01000, 0b00100, 0b00010],
    // ')'
    [0b01000, 0b00100, 0b00010, 0b00010, 0b00010, 0b00100, 0b01000],
    // '*'
    [0b00000, 0b10101, 0b01110, 0b11111, 0b01110, 0b10101, 0b00000],
    // '+'
    [0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000],
    // ','
    [0b00000, 0b00000, 0b00000, 0b00000, 0b00110, 0b00100, 0b01000],
    // '-'
    [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000],
    // '.'
    [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00110, 0b00110],
    // '/'
    [0b00001, 0b00010, 0b00100, 0b00100, 0b00100, 0b01000, 0b10000],
    // '0'
    [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
    // '1'
    [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
    // '2'
    [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
    // '3'
    [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
    // '4'
    [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
    // '5'
    [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
    // '6'
    [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
    // '7'
    [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
    // '8'
    [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
    // '9'
    [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
    // ':'
    [0b00000, 0b00110, 0b00110, 0b00000, 0b00110, 0b00110, 0b00000],
    // ';'
    [0b00000, 0b00110, 0b00110, 0b00000, 0b00110, 0b00100, 0b01000],
    // '<'
    [0b00010, 0b00100, 0b01000, 0b10000, 0b01000, 0b00100, 0b00010],
    // '='
    [0b00000, 0b00000, 0b11111, 0b00000, 0b11111, 0b00000, 0b00000],
    // '>'
    [0b01000, 0b00100, 0b00010, 0b00001, 0b00010, 0b00100, 0b01000],
    // '?'
    [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b00000, 0b00100],
    // '@'
    [0b01110, 0b10001, 0b10111, 0b10101, 0b10111, 0b10000, 0b01111],
    // 'A'
    [0b00100, 0b01010, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001],
    // 'B'
    [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
    // 'C'
    [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
    // 'D'
    [0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100],
    // 'E'
    [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
    // 'F'
    [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
    // 'G'
    [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111],
    // 'H'
    [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
    // 'I'
    [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
    // 'J'
    [0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100],
    // 'K'
    [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
    // 'L'
    [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
    // 'M'
    [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],
    // 'N'
    [0b10001, 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001],
    // 'O'
    [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
    // 'P'
    [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
    // 'Q'
    [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
    // 'R'
    [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
    // 'S'
    [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],
    // 'T'
    [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
    // 'U'
    [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
    // 'V'
    [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
    // 'W'
    [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
    // 'X'
    [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],
    // 'Y'
    [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],
    // 'Z'
    [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
    // '['
    [0b01110, 0b01000, 0b01000, 0b01000, 0b01000, 0b01000, 0b01110],
    // '\\'
    [0b10000, 0b01000, 0b01000, 0b00100, 0b00100, 0b00010, 0b00001],
    // ']'
    [0b01110, 0b00010, 0b00010, 0b00010, 0b00010, 0b00010, 0b01110],
    // '^'
    [0b00100, 0b01010, 0b10001, 0b00000, 0b00000, 0b00000, 0b00000],
    // '_'
    [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111],
    // '`'
    [0b01000, 0b00100, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
    // 'a'
    [0b00000, 0b00000, 0b01110, 0b00001, 0b01111, 0b10001, 0b01111],
    // 'b'
    [0b10000, 0b10000, 0b11110, 0b10001, 0b10001, 0b10001, 0b11110],
    // 'c'
    [0b00000, 0b00000, 0b01111, 0b10000, 0b10000, 0b10000, 0b01111],
    // 'd'
    [0b00001, 0b00001, 0b01111, 0b10001, 0b10001, 0b10001, 0b01111],
    // 'e'
    [0b00000, 0b00000, 0b01110, 0b10001, 0b11111, 0b10000, 0b01110],
    // 'f'
    [0b00110, 0b01001, 0b01000, 0b11110, 0b01000, 0b01000, 0b01000],
    // 'g'
    [0b00000, 0b01111, 0b10001, 0b10001, 0b01111, 0b00001, 0b01110],
    // 'h'
    [0b10000, 0b10000, 0b11110, 0b10001, 0b10001, 0b10001, 0b10001],
    // 'i'
    [0b00100, 0b00000, 0b01100, 0b00100, 0b00100, 0b00100, 0b01110],
    // 'j'
    [0b00010, 0b00000, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100],
    // 'k'
    [0b10000, 0b10000, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010],
    // 'l'
    [0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
    // 'm'
    [0b00000, 0b00000, 0b11010, 0b10101, 0b10101, 0b10101, 0b10001],
    // 'n'
    [0b00000, 0b00000, 0b11110, 0b10001, 0b10001, 0b10001, 0b10001],
    // 'o'
    [0b00000, 0b00000, 0b01110, 0b10001, 0b10001, 0b10001, 0b01110],
    // 'p'
    [0b00000, 0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000],
    // 'q'
    [0b00000, 0b01111, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001],
    // 'r'
    [0b00000, 0b00000, 0b10111, 0b11000, 0b10000, 0b10000, 0b10000],
    // 's'
    [0b00000, 0b00000, 0b01111, 0b10000, 0b01110, 0b00001, 0b11110],
    // 't'
    [0b00100, 0b00100, 0b01111, 0b00100, 0b00100, 0b00101, 0b00010],
    // 'u'
    [0b00000, 0b00000, 0b10001, 0b10001, 0b10001, 0b10001, 0b01111],
    // 'v'
    [0b00000, 0b00000, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
    // 'w'
    [0b00000, 0b00000, 0b10001, 0b10101, 0b10101, 0b10101, 0b01010],
    // 'x'
    [0b00000, 0b00000, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001],
    // 'y'
    [0b00000, 0b10001, 0b10001, 0b10001, 0b01111, 0b00001, 0b01110],
    // 'z'
    [0b00000, 0b00000, 0b11111, 0b00010, 0b00100, 0b01000, 0b11111],
    // '{'
    [0b00011, 0b00100, 0b00100, 0b01000, 0b00100, 0b00100, 0b00011],
    // '|'
    [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
    // '}'
    [0b11000, 0b00100, 0b00100, 0b00010, 0b00100, 0b00100, 0b11000],
    // '~'
    [0b00000, 0b00000, 0b01001, 0b10110, 0b00000, 0b00000, 0b00000],
];


/// The box drawn for a character this face has no glyph for.
///
/// Open rather than solid: a filled block reads as a redaction, and nothing has
/// been redacted. This is "the video cannot draw this letter", which is a
/// different thing and should not look like the other one.
const UNKNOWN: [u8; HEIGHT] = [

    0b11111, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11111,
];


/// The rows of `character`, and whether the face actually had it.
///
/// The second half of the answer is what lets a caller say "three of these
/// names cannot be drawn" instead of quietly producing boxes.
pub fn glyph(character: char) -> ([u8; HEIGHT], bool) {
    if character < FIRST || character > LAST {
        return (UNKNOWN, false);
    }
    let at = character as usize - FIRST as usize;
    match GLYPHS.get(at) {
        Some(rows) => (*rows, true),
        None => (UNKNOWN, false),
    }
}



/// Whether every character in `text` can be drawn.
pub fn can_draw(text: &str) -> bool {
    text.chars().all(|c| glyph(c).1)
}



/// How wide `text` is at `scale`, in pixels.
pub fn width_of(text: &str, scale: usize) -> usize {
    let count = text.chars().count();
    if count == 0 {
        return 0;
    }
    // Every glyph but the last carries a gap after it.
    (count * WIDTH + (count - 1) * GAP) * scale
}



/// The largest whole-number scale at which `text` fits inside `room` pixels.
///
/// **Whole numbers only.** A bitmap glyph drawn at 2.5 times its size has to
/// put half a pixel somewhere, and every way of doing that is a blurred letter.
/// Rounding down to a whole multiple keeps every edge on a pixel boundary, so
/// the text is as crisp at 4K as it is at 720p, and one size smaller is a much
/// better outcome than one that is soft.
///
/// Never returns zero: a scale of zero draws nothing, and a name too long for
/// the space it was given should be drawn small and overflow rather than vanish.
pub fn scale_for(text: &str, room: usize) -> usize {
    let mut scale = 1;
    while width_of(text, scale + 1) <= room {
        scale += 1;
        // A frame is at most 7680 pixels across and a glyph is five, so nothing
        // legitimate reaches this. It is here because a loop that depends on
        // arithmetic not overflowing should say what stops it.
        if scale > 512 {
            break;
        }
    }
    scale
}


#[cfg(test)]
mod tests {
    use super::*;
    /// Every printable ASCII character has a glyph of its own.
    ///
    /// The face is written out by hand, so a missing row is a real possibility
    /// and would show up as a box in the middle of an ordinary name.
    #[test]
    fn every_printable_ascii_character_can_be_drawn() {
        for code in (FIRST as u32)..=(LAST as u32) {
            let character = char::from_u32(code).expect("ascii");
            let (_, known) = glyph(character);
            assert!(known, "{character:?} has no glyph");
        }
        assert_eq!(GLYPHS.len(), (LAST as usize - FIRST as usize) + 1);
    }

    /// A character the face does not have says so rather than drawing nothing.
    #[test]
    fn anything_else_is_a_box_and_reports_itself() {
        for character in ['é', 'ß', 'д', '日', '👤'] {
            let (rows, known) = glyph(character);
            assert!(!known, "{character:?} was claimed as drawable");
            assert_eq!(rows, UNKNOWN);
        }
        assert!(!can_draw("Zoë"));
        assert!(can_draw("Alex"));
        assert!(can_draw(""));
    }

    /// No two characters share a glyph.
    ///
    /// Two identical rows in a hand-written face means two letters a reader
    /// cannot tell apart, which in a list of speaker names is the same failure
    /// as two voices nobody can separate.
    #[test]
    fn no_two_characters_are_drawn_the_same() {
        let mut seen: Vec<([u8; HEIGHT], char)> = Vec::new();
        for code in (FIRST as u32)..=(LAST as u32) {
            let character = char::from_u32(code).expect("ascii");
            if character == ' ' {
                continue;
            }
            let (rows, _) = glyph(character);
            if let Some((_, other)) = seen.iter().find(|(held, _)| *held == rows) {
                panic!("{character:?} and {other:?} are drawn identically");
            }
            seen.push((rows, character));
        }
    }

    /// A glyph only uses the five columns it claims to.
    #[test]
    fn nothing_is_drawn_outside_the_five_columns() {
        for code in (FIRST as u32)..=(LAST as u32) {
            let character = char::from_u32(code).expect("ascii");
            for (row, bits) in glyph(character).0.iter().enumerate() {
                assert!(
                    bits >> WIDTH == 0,
                    "{character:?} row {row} sets a bit past column {WIDTH}: {bits:#07b}"
                );
            }
        }
    }

    /// The scale is a whole number and the text fits at it.
    #[test]
    fn text_is_scaled_by_whole_numbers_and_fits() {
        for text in ["Alex", "a", "a much longer name than usual"] {
            for room in [10, 60, 200, 1000] {
                let scale = scale_for(text, room);
                assert!(scale >= 1, "{text:?} in {room} got scale {scale}");
                if scale > 1 {
                    assert!(
                        width_of(text, scale) <= room,
                        "{text:?} at {scale} is {} wide in {room}",
                        width_of(text, scale)
                    );
                }
                assert!(
                    width_of(text, scale + 1) > room,
                    "{text:?} could have been drawn one size larger in {room}"
                );
            }
        }
    }

    /// The empty string is nothing wide, at any scale.
    #[test]
    fn nothing_is_no_pixels_wide() {
        for scale in [1, 3, 12] {
            assert_eq!(width_of("", scale), 0);
        }
    }
}