geodesic-wallpaper 1.6.1

Animated Windows desktop wallpaper that draws glowing geodesics flowing over curved 3D surfaces (torus, Klein bottle, pseudosphere and more), rendered with wgpu. Also renders PNGs and GIFs headless.
//! Bitmap font renderer for overlaying text on images.
//!
//! Embeds a 5x7 pixel font covering ASCII 32-126 (95 glyphs).
//! Each entry is 7 bytes; each byte is a 5-bit row bitmask (bit 4 = leftmost).

/// 5x7 bitmap font, indexed by `(char as usize) - 32`.
/// Each `[u8; 7]` is 7 rows; each u8 has bits [4..0] = columns left-to-right.
pub const FONT_5X7: [[u8; 7]; 95] = [
    // 32 space
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
    // 33 !
    [0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x04],
    // 34 "
    [0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00],
    // 35 #
    [0x0A, 0x1F, 0x0A, 0x0A, 0x1F, 0x0A, 0x00],
    // 36 $
    [0x04, 0x0F, 0x14, 0x0E, 0x05, 0x1E, 0x04],
    // 37 %
    [0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03],
    // 38 &
    [0x0C, 0x12, 0x14, 0x08, 0x15, 0x12, 0x0D],
    // 39 '
    [0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00],
    // 40 (
    [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
    // 41 )
    [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
    // 42 *
    [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00],
    // 43 +
    [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
    // 44 ,
    [0x00, 0x00, 0x00, 0x00, 0x06, 0x04, 0x08],
    // 45 -
    [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
    // 46 .
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06],
    // 47 /
    [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10],
    // 48 0
    [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
    // 49 1
    [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
    // 50 2
    [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
    // 51 3
    [0x1F, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0E],
    // 52 4
    [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
    // 53 5
    [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E],
    // 54 6
    [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E],
    // 55 7
    [0x1F, 0x01, 0x02, 0x04, 0x04, 0x04, 0x04],
    // 56 8
    [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
    // 57 9
    [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C],
    // 58 :
    [0x00, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00],
    // 59 ;
    [0x00, 0x06, 0x06, 0x00, 0x06, 0x04, 0x08],
    // 60 <
    [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02],
    // 61 =
    [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00],
    // 62 >
    [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08],
    // 63 ?
    [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
    // 64 @
    [0x0E, 0x11, 0x01, 0x0D, 0x15, 0x15, 0x0E],
    // 65 A
    [0x04, 0x0A, 0x11, 0x11, 0x1F, 0x11, 0x11],
    // 66 B
    [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
    // 67 C
    [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E],
    // 68 D
    [0x1C, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1C],
    // 69 E
    [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
    // 70 F
    [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
    // 71 G
    [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F],
    // 72 H
    [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
    // 73 I
    [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
    // 74 J
    [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C],
    // 75 K
    [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
    // 76 L
    [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
    // 77 M
    [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
    // 78 N
    [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11],
    // 79 O
    [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
    // 80 P
    [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
    // 81 Q
    [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
    // 82 R
    [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
    // 83 S
    [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E],
    // 84 T
    [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
    // 85 U
    [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
    // 86 V
    [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
    // 87 W
    [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11],
    // 88 X
    [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
    // 89 Y
    [0x11, 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04],
    // 90 Z
    [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
    // 91 [
    [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E],
    // 92 backslash
    [0x10, 0x08, 0x08, 0x04, 0x02, 0x02, 0x01],
    // 93 ]
    [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E],
    // 94 ^
    [0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00],
    // 95 _
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
    // 96 `
    [0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00],
    // 97 a
    [0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F],
    // 98 b
    [0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E],
    // 99 c
    [0x00, 0x00, 0x0E, 0x10, 0x10, 0x11, 0x0E],
    // 100 d
    [0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F],
    // 101 e
    [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E],
    // 102 f
    [0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08],
    // 103 g
    [0x00, 0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01],
    // 104 h
    [0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x11],
    // 105 i
    [0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E],
    // 106 j
    [0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x12],
    // 107 k
    [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12],
    // 108 l
    [0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
    // 109 m
    [0x00, 0x00, 0x1A, 0x15, 0x15, 0x15, 0x11],
    // 110 n
    [0x00, 0x00, 0x1E, 0x11, 0x11, 0x11, 0x11],
    // 111 o
    [0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E],
    // 112 p
    [0x00, 0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10],
    // 113 q
    [0x00, 0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01],
    // 114 r
    [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10],
    // 115 s
    [0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E],
    // 116 t
    [0x08, 0x08, 0x1E, 0x08, 0x08, 0x09, 0x06],
    // 117 u
    [0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0F],
    // 118 v
    [0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04],
    // 119 w
    [0x00, 0x00, 0x11, 0x15, 0x15, 0x15, 0x0A],
    // 120 x
    [0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11],
    // 121 y
    [0x00, 0x00, 0x11, 0x11, 0x0F, 0x01, 0x0E],
    // 122 z
    [0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F],
    // 123 {
    [0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02],
    // 124 |
    [0x04, 0x04, 0x04, 0x00, 0x04, 0x04, 0x04],
    // 125 }
    [0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08],
    // 126 ~
    [0x00, 0x08, 0x15, 0x02, 0x00, 0x00, 0x00],
];

/// A rendered glyph as a row-column bit matrix.
#[derive(Debug, Clone)]
pub struct GlyphBitmap {
    /// Glyph width in pixels (at most 8).
    pub width: u8,
    /// Glyph height in pixels.
    pub height: u8,
    /// One byte per row; the low `width` bits hold the pixels, most significant first.
    pub rows: Vec<u8>,
}

impl GlyphBitmap {
    /// Return true if pixel at (row, col) is set.
    pub fn pixel(&self, row: u8, col: u8) -> bool {
        if row >= self.height || col >= self.width {
            return false;
        }
        let shift = self.width - 1 - col;
        (self.rows[row as usize] >> shift) & 1 == 1
    }
}

/// Renders text with the built-in bitmap font.
pub struct FontRenderer;

impl FontRenderer {
    fn glyph_data(c: char) -> &'static [u8; 7] {
        let idx = c as usize;
        if idx >= 32 && idx <= 126 {
            &FONT_5X7[idx - 32]
        } else {
            &FONT_5X7[0] // space fallback
        }
    }

    /// Render a single character to a GlyphBitmap (5w x 7h).
    pub fn render_char(c: char, _color: [u8; 3]) -> GlyphBitmap {
        GlyphBitmap {
            width: 5,
            height: 7,
            rows: Self::glyph_data(c).to_vec(),
        }
    }

    /// Render a full string to a 2D RGB pixel grid.
    pub fn render_text(text: &str, color: [u8; 3], bg: Option<[u8; 3]>) -> Vec<Vec<[u8; 3]>> {
        let bg_color = bg.unwrap_or([0, 0, 0]);
        let (w, h) = Self::text_size(text);
        if w == 0 {
            return vec![];
        }
        let mut img = vec![vec![bg_color; w]; h];

        let mut x_off = 0usize;
        for c in text.chars() {
            let glyph = Self::render_char(c, color);
            for row in 0..7usize {
                for col in 0..5usize {
                    if glyph.pixel(row as u8, col as u8) {
                        if x_off + col < w {
                            img[row][x_off + col] = color;
                        }
                    }
                }
            }
            x_off += 6; // 5px glyph + 1px kerning gap
        }
        img
    }

    /// Blit `text_bitmap` onto `image` at position (x, y), clipping at bounds.
    pub fn blit(
        image: &mut Vec<Vec<[u8; 3]>>,
        text_bitmap: &Vec<Vec<[u8; 3]>>,
        x: usize,
        y: usize,
    ) {
        let img_h = image.len();
        let img_w = if img_h > 0 { image[0].len() } else { return };
        for (row_idx, row) in text_bitmap.iter().enumerate() {
            let dest_row = y + row_idx;
            if dest_row >= img_h {
                break;
            }
            for (col_idx, &px) in row.iter().enumerate() {
                let dest_col = x + col_idx;
                if dest_col >= img_w {
                    break;
                }
                image[dest_row][dest_col] = px;
            }
        }
    }

    /// Return (width, height) in pixels for the given text string.
    pub fn text_size(text: &str) -> (usize, usize) {
        let len = text.chars().count();
        if len == 0 {
            return (0, 0);
        }
        // Each char is 5 wide + 1 gap, last char has no trailing gap.
        let w = len * 5 + (len - 1);
        (w, 7)
    }
}

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

    #[test]
    fn test_font_array_size() {
        assert_eq!(FONT_5X7.len(), 95);
    }

    #[test]
    fn test_glyph_bitmap_pixel_a() {
        let glyph = FontRenderer::render_char('A', [255, 255, 255]);
        assert_eq!(glyph.width, 5);
        assert_eq!(glyph.height, 7);
        // Row 0 of 'A' is 0x04 = 00100 — centre pixel set
        assert!(glyph.pixel(0, 2));
        // Column 0 should be off in row 0
        assert!(!glyph.pixel(0, 0));
    }

    #[test]
    fn test_glyph_pixel_out_of_bounds() {
        let glyph = FontRenderer::render_char('A', [255, 0, 0]);
        assert!(!glyph.pixel(10, 0));
        assert!(!glyph.pixel(0, 10));
    }

    #[test]
    fn test_text_size_single() {
        let (w, h) = FontRenderer::text_size("A");
        assert_eq!(w, 5);
        assert_eq!(h, 7);
    }

    #[test]
    fn test_text_size_multi() {
        let (w, h) = FontRenderer::text_size("AB");
        assert_eq!(w, 11); // 5 + 1 + 5
        assert_eq!(h, 7);
    }

    #[test]
    fn test_text_size_empty() {
        assert_eq!(FontRenderer::text_size(""), (0, 0));
    }

    #[test]
    fn test_render_text_dimensions() {
        let bmp = FontRenderer::render_text("Hi", [255, 0, 0], None);
        assert_eq!(bmp.len(), 7);
        assert_eq!(bmp[0].len(), 11);
    }

    #[test]
    fn test_blit_basic() {
        let mut img = vec![vec![[0u8; 3]; 20]; 20];
        let text = FontRenderer::render_text("A", [255, 255, 255], None);
        FontRenderer::blit(&mut img, &text, 0, 0);
        // Centre pixel of row 0 of 'A' should be white
        assert_eq!(img[0][2], [255, 255, 255]);
    }

    #[test]
    fn test_blit_clipped_at_boundary() {
        let mut img = vec![vec![[0u8; 3]; 4]; 4];
        let text = FontRenderer::render_text("HELLO", [255, 0, 0], None);
        // Should not panic even though text is wider than image
        FontRenderer::blit(&mut img, &text, 0, 0);
    }

    #[test]
    fn test_blit_offset() {
        let mut img = vec![vec![[0u8; 3]; 30]; 20];
        let text = FontRenderer::render_text("Z", [0, 255, 0], Some([0, 0, 0]));
        FontRenderer::blit(&mut img, &text, 10, 5);
        // Spot check: no panic and dimensions unchanged
        assert_eq!(img.len(), 20);
        assert_eq!(img[0].len(), 30);
    }

    #[test]
    fn test_render_char_space() {
        let glyph = FontRenderer::render_char(' ', [255, 255, 255]);
        // Space is all zeros
        for r in 0..7 {
            for c in 0..5 {
                assert!(!glyph.pixel(r, c));
            }
        }
    }
}