use std::collections::BTreeMap;
use crate::{Color, Error, Image, Limits, Result};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Glyph {
pub(crate) ch: char,
pub(crate) width: u32,
pub(crate) advance: u32,
pub(crate) pixels: Vec<bool>,
}
impl Glyph {
pub fn char(&self) -> char {
self.ch
}
pub fn width(&self) -> u32 {
self.width
}
pub fn advance(&self) -> u32 {
self.advance
}
pub fn pixel(&self, x: u32, y: u32) -> bool {
x < self.width
&& self
.pixels
.get((y * self.width + x) as usize)
.copied()
.unwrap_or(false)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Font {
pub(crate) name: String,
pub(crate) height: u32,
pub(crate) baseline: u32,
pub(crate) line_height: u32,
pub(crate) glyphs: Vec<Glyph>,
}
impl Font {
pub fn name(&self) -> &str {
&self.name
}
pub fn height(&self) -> u32 {
self.height
}
pub fn baseline(&self) -> u32 {
self.baseline
}
pub fn line_height(&self) -> u32 {
self.line_height
}
pub fn glyphs(&self) -> &[Glyph] {
&self.glyphs
}
pub fn glyph(&self, ch: char) -> Option<&Glyph> {
self.glyphs.iter().find(|g| g.ch == ch)
}
pub fn measure(&self, text: &str) -> (u32, u32) {
let mut width = 0u64;
let mut lines = 0u64;
for line in text.split('\n') {
lines += 1;
let w: u64 = line
.chars()
.map(|c| {
self.glyph(c)
.or_else(|| self.glyph('?'))
.map_or(0, |g| g.advance as u64)
})
.sum();
width = width.max(w);
}
let height = (lines - 1) * self.line_height as u64 + self.height as u64;
(
width.min(u32::MAX as u64) as u32,
height.min(u32::MAX as u64) as u32,
)
}
pub fn render(&self, text: &str, color: Color) -> Result<Image> {
self.render_with(text, color, &Limits::default())
}
pub fn render_with(&self, text: &str, color: Color, limits: &Limits) -> Result<Image> {
let (w, h) = self.measure(text);
let mut image = Image::new_with(w.max(1), h.max(1), limits)?;
for (row, line) in text.split('\n').enumerate() {
let top = row as u32 * self.line_height;
let mut pen = 0;
for c in line.chars() {
let Some(glyph) = self.glyph(c).or_else(|| self.glyph('?')) else {
continue;
};
for y in 0..self.height {
for x in 0..glyph.width {
if glyph.pixel(x, y) {
image.set_pixel(pen + x, top + y, color);
}
}
}
pen += glyph.advance;
}
}
Ok(image)
}
pub fn to_ttf(&self, options: &TtfOptions) -> Result<Vec<u8>> {
crate::ttf::write(self, options)
}
pub fn outline(&self, ch: char) -> Option<Vec<Vec<(i32, i32)>>> {
self.glyph(ch)
.map(|g| outline(g, self.height, self.baseline))
}
pub fn units_per_pixel(&self) -> u32 {
let widest = self
.glyphs
.iter()
.map(|g| g.width.max(g.advance))
.max()
.unwrap_or(1)
.max(1);
(16384 / self.height.max(1))
.min(32000 / widest)
.clamp(1, 128)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TtfOptions {
pub family: Option<String>,
pub version: (u16, u16),
}
impl Default for TtfOptions {
fn default() -> Self {
Self {
family: None,
version: (1, 0),
}
}
}
pub(crate) fn outline(glyph: &Glyph, font_height: u32, baseline: u32) -> Vec<Vec<(i32, i32)>> {
let filled = |x: i32, row: i32| -> bool {
x >= 0 && row >= 0 && (row as u32) < font_height && glyph.pixel(x as u32, row as u32)
};
let bottom = |row: i32| baseline as i32 - row - 1;
let mut edges: BTreeMap<(i32, i32), Vec<(i32, i32)>> = BTreeMap::new();
let mut add = |from: (i32, i32), to: (i32, i32)| edges.entry(from).or_default().push(to);
for row in 0..font_height as i32 {
for x in 0..glyph.width as i32 {
if !filled(x, row) {
continue;
}
let (y0, y1) = (bottom(row), bottom(row) + 1);
if !filled(x, row - 1) {
add((x, y1), (x + 1, y1));
}
if !filled(x + 1, row) {
add((x + 1, y1), (x + 1, y0));
}
if !filled(x, row + 1) {
add((x + 1, y0), (x, y0));
}
if !filled(x - 1, row) {
add((x, y0), (x, y1));
}
}
}
let mut contours = Vec::new();
while let Some(start) = edges
.iter()
.filter(|(_, ends)| !ends.is_empty())
.map(|(&p, _)| p)
.min_by_key(|&(x, y)| (-y, x))
{
let mut points = vec![start];
let mut at = start;
let mut direction = (0, 0);
loop {
let ends = edges.get_mut(&at).filter(|e| !e.is_empty());
let Some(ends) = ends else { break };
let pick = |to: &(i32, i32)| {
let d = (to.0 - at.0, to.1 - at.1);
let right = (direction.1, -direction.0);
if d == right {
0
} else if d == direction {
1
} else {
2
}
};
let index = (0..ends.len()).min_by_key(|&i| pick(&ends[i])).unwrap_or(0);
let to = ends.swap_remove(index);
direction = (to.0 - at.0, to.1 - at.1);
at = to;
if at == start {
break;
}
points.push(at);
}
contours.push(simplify(points));
}
contours
}
fn simplify(points: Vec<(i32, i32)>) -> Vec<(i32, i32)> {
let n = points.len();
(0..n)
.filter(|&i| {
let (a, b, c) = (points[(i + n - 1) % n], points[i], points[(i + 1) % n]);
(b.0 - a.0) * (c.1 - b.1) != (b.1 - a.1) * (c.0 - b.0)
})
.map(|i| points[i])
.collect()
}
impl Font {
pub(crate) fn validate(&self) -> Result<()> {
if self.height == 0 || self.baseline > self.height || self.line_height == 0 {
return Err(Error::invalid(
"A font needs a height, a baseline and a line height",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn glyph(rows: &[&str]) -> Glyph {
Glyph {
ch: 'x',
width: rows[0].len() as u32,
advance: rows[0].len() as u32 + 1,
pixels: rows
.iter()
.flat_map(|r| r.chars().map(|c| c == '#'))
.collect(),
}
}
#[test]
fn outlines() {
let square = glyph(&["##", "##"]);
assert_eq!(
outline(&square, 2, 2),
vec![vec![(0, 2), (2, 2), (2, 0), (0, 0)]]
);
let ring = glyph(&["###", "#.#", "###"]);
let c = outline(&ring, 3, 3);
assert_eq!(c[0], [(0, 3), (3, 3), (3, 0), (0, 0)]);
assert_eq!(c[1], [(1, 2), (1, 1), (2, 1), (2, 2)]);
let diagonal = glyph(&["#.", ".#"]);
let c = outline(&diagonal, 2, 1);
assert_eq!(c.len(), 2);
assert_eq!(c[0], [(0, 1), (1, 1), (1, 0), (0, 0)]);
assert_eq!(c[1], [(1, 0), (2, 0), (2, -1), (1, -1)]);
let l = glyph(&["#.", "##"]);
assert_eq!(
outline(&l, 2, 1),
vec![vec![(0, 1), (1, 1), (1, 0), (2, 0), (2, -1), (0, -1)]]
);
assert!(outline(&glyph(&[".."]), 1, 1).is_empty());
}
}