use rusttype::{Font, Point, Rect, Scale}; use std::fs::{self, File}; use std::io::{self, Write}; // TODO: Build a Rust Macro that does what this does but without a build.rs #[derive(Clone)] struct FontMetadata { pub charset: &'static str, pub name: &'static str, pub scale: Scale, pub font: Font<'static>, } impl FontMetadata { pub fn render_character(&self, scale: Scale, character: char) -> Vec { let glyph = self.font.glyph(character).scaled(scale); let positioned_glyph = glyph.clone().positioned(Point { x: 0.0, y: 0.0 }); let pixel_bounding_box = { let bounding_box = positioned_glyph.pixel_bounding_box(); if bounding_box.is_none() { Rect { min: Point { x: 0, y: 0 }, max: Point { x: glyph.h_metrics().advance_width as i32, y: 0, }, } } else { bounding_box.unwrap() } }; let bounding_box = { let bounding_box = glyph.exact_bounding_box(); if bounding_box.is_none() { Rect { min: Point { x: 0.0, y: 0.0 }, max: Point { x: pixel_bounding_box.width() as f32, y: 0.0, }, } } else { bounding_box.unwrap() } }; let width = bounding_box.width() as u32; let height = bounding_box.height() as u32; let mut image: Vec = Vec::new(); fn define_item(a: &mut Vec, i: usize, v: u8) { if a.len() <= i { while a.len() < i { a.push(0x00); } a.push(v) } else { a[i] = v; } } define_item( &mut image, 10 + ((width as usize) * (height as usize)), 0x00, ); image[0] = (width & 0b0000_0000_1111_1111) as u8; image[1] = (width & 0b1111_1111_0000_0000 << 8) as u8; if (image[0] as u16) | (image[1] as u16 >> 8) != width as u16 { panic!("Width missmatch!"); } // TODO: do we *really* need i32 values here? wouldn't i16 be sufficient? image[2..6].copy_from_slice(&pixel_bounding_box.min.x.to_le_bytes()); image[6..10].copy_from_slice(&(pixel_bounding_box.min.y + (scale.y / 2.0) as i32).to_le_bytes()); positioned_glyph.draw(|gx: u32, gy: u32, v| { let bit = (v * 255.0) as u8; if gx < width { define_item(&mut image, (10 + (gy * width) + gx) as usize, bit); } }); image } pub fn img_to_hex(image: Vec) -> Vec { let mut image2: Vec = Vec::new(); let width = (image[0] as u16) | (image[1] as u16 >> 8); let mut i: i32 = -3; for bit in image { i += 1; if i >= 0 { if i as u16 == width { i = 0; image2.push('\n' as u8); } let v = format!("{:x?}", bit); let v = if v.len() == 1 { format!("0{}", v) } else { v }; for char in v.chars() { image2.push(char as u8); } // image2.push(if bit < 80 {' ' as u8} else if bit < 150 {'-' as u8} else {'#' as u8}) } } image2 } fn unique_chars(&self) -> Vec { let mut v: Vec = Vec::new(); for char in self.charset.chars() { if !v.contains(&char) { v.push(char); } } v } } fn exec(font: FontMetadata) -> io::Result<()> { for c in font.unique_chars() { let image = font.render_character(font.scale, c); save_bits_to_file(&font, c as u8, &image)?; } Ok(()) } fn save_bits_to_file(font: &FontMetadata, char: u8, bits: &[u8]) -> io::Result<()> { fs::create_dir_all(format!("assets/computed-fonts/{}/", font.name))?; let filename = format!("assets/computed-fonts/{}/{}.bin", font.name, char); let mut file = File::create(&filename)?; file.write_all(bits)?; let filename = format!("assets/computed-fonts/{}/{}.txt", font.name, char); let mut file = File::create(&filename)?; file.write_all(&FontMetadata::img_to_hex(bits.to_vec()))?; Ok(()) } fn to_upper_snake_with_first_upper_preceeded_by_underscore(str: String) -> String { let mut str = str; for char in "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars() { str = str.replace(char, format!("_{char}").as_str()); } str.to_uppercase() } fn generate_struct(font: &FontMetadata) -> io::Result { let name = font.name; let mut contents = format!( "pub struct {name}Struct {{}} impl BakedFont for {name}Struct {{ fn has_char(&self, c: char) -> bool {{ match c as u8 {{" ); for char in font.unique_chars() { contents = format!( "{contents} | {}", char as u8, ); } contents = format!( "{contents} => true, _ => false }} }} fn get_char_bytes(&self, c: char) -> &'static [u8] {{ match c as u8 {{ " ); for char in font.unique_chars() { contents = format!( "{contents} {} => include_bytes!(\"../../assets/computed-fonts/{}/{}.bin\"), ", char as u8, font.name, char as u8 ); } contents = format!( "{contents} _ => panic!(\"Glyph {{}} not included in precomputed fonts\", c) }} }} }} pub const FONT{}: {name}Struct = {name}Struct{{ }}; ", to_upper_snake_with_first_upper_preceeded_by_underscore(name.to_string()) ); Ok(contents) } fn main() -> Result<(), Box> { fs::create_dir_all("src/generated")?; let mut modrs = format!( "// Copyright is a sham this is a @generated file. use crate::font::BakedFont; " ); let fonts = [ FontMetadata { name: "Galmuri", font: { Font::try_from_vec(fs::read("assets/fonts/Galmuri11.ttf")?).unwrap() }, charset: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz2053: ", scale: Scale::uniform(20.0), }, FontMetadata { name: "CherryBombOne", font: { Font::try_from_vec(fs::read("assets/fonts/CherryBombOne.ttf")?).unwrap() }, charset: "UwUSpace ", scale: Scale::uniform(20.0), }, ]; for font in fonts { modrs = format!( "{modrs}{} ", generate_struct(&font)? ); exec(font)?; } File::create(&"src/generated/fonts.rs")?.write_all(modrs.as_bytes())?; Ok(()) }