use fontdue::{Font, FontSettings};
#[derive(Clone)]
pub struct TrueTypeFont {
font: Font,
}
impl std::fmt::Debug for TrueTypeFont {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrueTypeFont").finish()
}
}
#[derive(Debug, Clone)]
pub struct RasterizedGlyph {
pub character: char,
pub width: usize,
pub height: usize,
pub bitmap: Vec<u8>,
pub advance_width: f32,
pub x_offset: f32,
pub y_offset: f32,
}
#[derive(Debug, Clone)]
pub struct TextLayout {
pub glyphs: Vec<PositionedGlyph>,
pub total_width: f32,
pub line_height: f32,
}
#[derive(Debug, Clone)]
pub struct PositionedGlyph {
pub glyph: RasterizedGlyph,
pub x: f32,
pub y: f32,
}
impl TrueTypeFont {
pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
let font = Font::from_bytes(data, FontSettings::default())
.map_err(|e| format!("failed to load font: {}", e))?;
Ok(Self { font })
}
pub fn builtin() -> Option<Self> {
None
}
pub fn rasterize(&self, ch: char, px_size: f32) -> RasterizedGlyph {
let (metrics, bitmap) = self.font.rasterize(ch, px_size);
RasterizedGlyph {
character: ch,
width: metrics.width,
height: metrics.height,
bitmap,
advance_width: metrics.advance_width,
x_offset: metrics.xmin as f32,
y_offset: metrics.ymin as f32,
}
}
pub fn layout(&self, text: &str, px_size: f32) -> TextLayout {
let mut glyphs = Vec::new();
let mut cursor_x: f32 = 0.0;
let metrics_a = self.font.rasterize('A', px_size).0;
let line_height = px_size;
for ch in text.chars() {
if ch == ' ' {
let (m, _) = self.font.rasterize(' ', px_size);
cursor_x += m.advance_width.max(px_size * 0.25);
continue;
}
if ch == '\n' {
continue;
}
let glyph = self.rasterize(ch, px_size);
let x = cursor_x + glyph.x_offset;
let y = glyph.y_offset;
cursor_x += glyph.advance_width;
glyphs.push(PositionedGlyph { glyph, x, y });
}
TextLayout {
glyphs,
total_width: cursor_x,
line_height,
}
}
pub fn render_to_rgba(
&self,
text: &str,
px_size: f32,
color: [u8; 3],
) -> (Vec<u8>, u32, u32) {
let layout = self.layout(text, px_size);
let img_w = (layout.total_width.ceil() as u32).max(1);
let img_h = (layout.line_height.ceil() as u32).max(1);
let mut pixels = vec![0u8; (img_w * img_h * 4) as usize];
for pg in &layout.glyphs {
let gx = pg.x.round() as i32;
let gy = (img_h as f32 - layout.line_height + pg.y).round() as i32;
for row in 0..pg.glyph.height {
for col in 0..pg.glyph.width {
let coverage = pg.glyph.bitmap[row * pg.glyph.width + col];
if coverage == 0 { continue; }
let px = gx + col as i32;
let py = gy + row as i32;
if px < 0 || py < 0 || px >= img_w as i32 || py >= img_h as i32 {
continue;
}
let idx = ((py as u32 * img_w + px as u32) * 4) as usize;
if idx + 3 < pixels.len() {
let alpha = coverage as f32 / 255.0;
let inv = 1.0 - alpha;
pixels[idx] = (color[0] as f32 * alpha + pixels[idx] as f32 * inv) as u8;
pixels[idx + 1] = (color[1] as f32 * alpha + pixels[idx + 1] as f32 * inv) as u8;
pixels[idx + 2] = (color[2] as f32 * alpha + pixels[idx + 2] as f32 * inv) as u8;
pixels[idx + 3] = ((alpha * 255.0) as u8).max(pixels[idx + 3]);
}
}
}
}
(pixels, img_w, img_h)
}
pub fn render_to_texture(
&self,
text: &str,
px_size: f32,
color: [u8; 3],
) -> crate::render::Texture {
let (pixels, w, h) = self.render_to_rgba(text, px_size, color);
crate::render::Texture::from_rgba(pixels, w, h)
}
pub fn render_to_overlay_quads(
&self,
text: &str,
px_size: f32,
screen_x: f32,
screen_y: f32,
screen_height: f32,
color: [f32; 4],
screen_width_px: f32,
screen_height_px: f32,
) -> (Vec<[f32; 2]>, Vec<[f32; 4]>, Vec<u32>) {
let layout = self.layout(text, px_size);
let mut positions = Vec::new();
let mut colors = Vec::new();
let mut indices = Vec::new();
let scale_x = screen_height / screen_width_px;
let scale_y = screen_height / screen_height_px;
for pg in &layout.glyphs {
for row in 0..pg.glyph.height {
for col in 0..pg.glyph.width {
let coverage = pg.glyph.bitmap[row * pg.glyph.width + col];
if coverage < 32 { continue; }
let px = pg.x + col as f32;
let py = pg.y + row as f32;
let nx = screen_x + px * scale_x;
let ny = screen_y - py * scale_y;
let qw = scale_x;
let qh = scale_y;
let alpha = coverage as f32 / 255.0 * color[3];
let c = [color[0], color[1], color[2], alpha];
let base = positions.len() as u32;
positions.push([nx, ny]);
positions.push([nx + qw, ny]);
positions.push([nx + qw, ny + qh]);
positions.push([nx, ny + qh]);
colors.push(c);
colors.push(c);
colors.push(c);
colors.push(c);
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
}
}
}
(positions, colors, indices)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rasterized_glyph_struct() {
let g = RasterizedGlyph {
character: 'A',
width: 10,
height: 12,
bitmap: vec![128; 120],
advance_width: 11.0,
x_offset: 0.0,
y_offset: -10.0,
};
assert_eq!(g.character, 'A');
assert_eq!(g.bitmap.len(), 120);
}
#[test]
fn text_layout_struct() {
let layout = TextLayout {
glyphs: vec![],
total_width: 100.0,
line_height: 16.0,
};
assert_eq!(layout.total_width, 100.0);
}
#[test]
fn builtin_not_available() {
assert!(TrueTypeFont::builtin().is_none());
}
#[test]
fn load_system_font_and_rasterize() {
let paths = [
"/usr/share/fonts/truetype/freefont/FreeMono.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
"C:\\Windows\\Fonts\\arial.ttf",
];
let font = paths.iter().find_map(|p| {
std::fs::read(p).ok().and_then(|data| TrueTypeFont::from_bytes(&data).ok())
});
let Some(font) = font else {
return;
};
let glyph = font.rasterize('A', 24.0);
assert!(glyph.width > 0, "glyph should have nonzero width");
assert!(glyph.height > 0, "glyph should have nonzero height");
assert!(!glyph.bitmap.is_empty());
let layout = font.layout("Hello", 24.0);
assert!(layout.total_width > 0.0);
assert!(!layout.glyphs.is_empty());
let (pixels, w, h) = font.render_to_rgba("Hi", 32.0, [255, 255, 255]);
assert!(w > 0);
assert!(h > 0);
let has_content = pixels.chunks(4).any(|p| p[3] > 0);
assert!(has_content, "rendered text should have visible pixels");
let tex = font.render_to_texture("VTK", 16.0, [200, 200, 200]);
assert!(tex.width > 0);
assert!(tex.height > 0);
}
}