use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::str::FromStr;
use hashbrown::HashMap;
use crate::graphics::text::cache::{RasterizedGlyph, Rasterizer};
use crate::graphics::{ImageData, Rectangle};
use crate::math::Vec2;
use crate::{fs, Context};
use crate::{Result, TetraError};
use super::cache::FontCache;
use super::Font;
struct BmFontGlyph {
x: u32,
y: u32,
width: u32,
height: u32,
x_offset: i32,
y_offset: i32,
x_advance: i32,
page: u32,
}
#[derive(Debug, Clone)]
pub struct BmFontBuilder {
font: String,
image_dir: Option<PathBuf>,
pages: HashMap<u32, ImageData>,
}
impl BmFontBuilder {
pub fn new<P>(path: P) -> Result<BmFontBuilder>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let font = fs::read_to_string(path)?;
let image_dir = path.parent().unwrap().to_owned();
Ok(BmFontBuilder {
font,
image_dir: Some(image_dir),
pages: HashMap::new(),
})
}
pub fn from_file_data<D>(data: D) -> BmFontBuilder
where
D: Into<String>,
{
BmFontBuilder {
font: data.into(),
image_dir: None,
pages: HashMap::new(),
}
}
pub fn with_image_dir<P>(mut self, path: P) -> BmFontBuilder
where
P: Into<PathBuf>,
{
self.image_dir = Some(path.into());
self
}
pub fn with_page<P>(mut self, id: u32, path: P) -> Result<BmFontBuilder>
where
P: AsRef<Path>,
{
self.pages.insert(id, ImageData::from_file(path)?);
Ok(self)
}
pub fn with_page_file_data(mut self, id: u32, data: &[u8]) -> Result<BmFontBuilder> {
self.pages.insert(id, ImageData::from_file_data(data)?);
Ok(self)
}
pub fn with_page_image_data(mut self, id: u32, data: ImageData) -> BmFontBuilder {
self.pages.insert(id, data);
self
}
pub fn with_page_rgba<D>(
mut self,
id: u32,
width: i32,
height: i32,
data: D,
) -> Result<BmFontBuilder>
where
D: Into<Vec<u8>>,
{
self.pages
.insert(id, ImageData::from_rgba(width, height, data)?);
Ok(self)
}
pub fn build(self, ctx: &mut Context) -> Result<Font> {
let rasterizer: Box<dyn Rasterizer> = Box::new(BmFontRasterizer::new(
&self.font,
self.image_dir,
self.pages,
)?);
let cache = FontCache::new(
&mut ctx.device,
rasterizer,
ctx.graphics.default_filter_mode,
)?;
Ok(Font {
data: Rc::new(RefCell::new(cache)),
})
}
}
pub struct BmFontRasterizer {
line_height: u32,
base: u32,
pages: HashMap<u32, ImageData>,
glyphs: HashMap<u32, BmFontGlyph>,
kerning: HashMap<(u32, u32), i32>,
}
impl BmFontRasterizer {
fn new(
font: &str,
image_path: Option<PathBuf>,
mut pages: HashMap<u32, ImageData>,
) -> Result<BmFontRasterizer> {
let mut line_height = None;
let mut base = None;
let mut glyphs = HashMap::new();
let mut kerning = HashMap::new();
for line in font.lines() {
let (tag, attributes) = parse_tag(line);
match tag {
"common" => {
let attributes = parse_attributes(attributes)?;
line_height = Some(attributes.parse("lineHeight")?);
base = Some(attributes.parse("base")?);
}
"page" => {
let attributes = parse_attributes(attributes)?;
let id = attributes.parse("id")?;
if !pages.contains_key(&id) {
let file = attributes.get("file")?;
let file_path = image_path
.as_ref()
.ok_or(TetraError::InvalidFont)?
.join(file);
pages.insert(id, ImageData::from_file(file_path)?);
}
}
"char" => {
let attributes = parse_attributes(attributes)?;
let id = attributes.parse("id")?;
let glyph = BmFontGlyph {
x: attributes.parse("x")?,
y: attributes.parse("y")?,
width: attributes.parse("width")?,
height: attributes.parse("height")?,
x_offset: attributes.parse("xoffset")?,
y_offset: attributes.parse("yoffset")?,
x_advance: attributes.parse("xadvance")?,
page: attributes.parse("page")?,
};
glyphs.insert(id, glyph);
}
"kerning" => {
let attributes = parse_attributes(attributes)?;
let first = attributes.parse("first")?;
let second = attributes.parse("second")?;
let amount = attributes.parse("amount")?;
kerning.insert((first, second), amount);
}
_ => {}
}
}
Ok(BmFontRasterizer {
line_height: line_height.ok_or(TetraError::InvalidFont)?,
base: base.ok_or(TetraError::InvalidFont)?,
pages,
glyphs,
kerning,
})
}
}
impl Rasterizer for BmFontRasterizer {
fn rasterize(&self, glyph: char, _: Vec2<f32>) -> Option<RasterizedGlyph> {
if let Some(bmglyph) = self.glyphs.get(&(glyph as u32)) {
let page = self.pages.get(&bmglyph.page)?;
let region = page.region(Rectangle::new(
bmglyph.x as i32,
bmglyph.y as i32,
bmglyph.width as i32,
bmglyph.height as i32,
));
Some(RasterizedGlyph {
data: region.as_bytes().into(),
bounds: Rectangle::new(
bmglyph.x_offset as f32,
-(self.base as i32 - bmglyph.y_offset) as f32,
bmglyph.width as f32,
bmglyph.height as f32,
),
})
} else {
None
}
}
fn advance(&self, glyph: char) -> f32 {
self.glyphs
.get(&(glyph as u32))
.map(|bmchar| bmchar.x_advance as f32)
.unwrap_or(0.0)
}
fn line_height(&self) -> f32 {
self.line_height as f32
}
fn ascent(&self) -> f32 {
self.base as f32
}
fn kerning(&self, previous: char, current: char) -> f32 {
self.kerning
.get(&(previous as u32, current as u32))
.copied()
.unwrap_or(0) as f32
}
}
struct BmFontAttributes<'a> {
attributes: HashMap<&'a str, &'a str>,
}
impl BmFontAttributes<'_> {
fn get(&self, key: &str) -> Result<&str> {
self.attributes
.get(key)
.copied()
.ok_or(TetraError::InvalidFont)
}
fn parse<T>(&self, key: &str) -> Result<T>
where
T: FromStr,
{
let value = self.get(key)?;
value.parse().map_err(|_| TetraError::InvalidFont)
}
}
fn parse_tag(input: &str) -> (&str, &str) {
let trimmed = input.trim_start();
let tag_end = trimmed.find(' ').unwrap_or_else(|| trimmed.len());
trimmed.split_at(tag_end)
}
fn parse_attributes(input: &str) -> Result<BmFontAttributes<'_>> {
let mut remaining = input.trim_start();
let mut attributes = HashMap::new();
while !remaining.is_empty() {
let key_end = remaining.find('=').ok_or(TetraError::InvalidFont)?;
let (key, next) = remaining.split_at(key_end);
remaining = &next[1..];
if remaining.starts_with('"') {
remaining = &remaining[1..];
let value_end = remaining.find('"').ok_or(TetraError::InvalidFont)?;
let (value, next) = remaining.split_at(value_end);
attributes.insert(key, value);
remaining = &next[1..].trim_start();
} else {
let value_end = remaining.find(' ').unwrap_or_else(|| remaining.len());
let (value, next) = remaining.split_at(value_end);
attributes.insert(key, value);
remaining = next.trim_start();
}
}
Ok(BmFontAttributes { attributes })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_line() {
let (tag, rest) = parse_tag("tag keyA=123 keyB=\"string\" keyC=1,2,3,4");
assert_eq!("tag", tag);
assert_eq!(" keyA=123 keyB=\"string\" keyC=1,2,3,4", rest);
let attributes = parse_attributes(rest).unwrap();
assert_eq!(attributes.get("keyA").unwrap(), "123");
assert_eq!(attributes.get("keyB").unwrap(), "string");
assert_eq!(attributes.get("keyC").unwrap(), "1,2,3,4");
}
#[test]
fn parse_valid_line_with_whitespace() {
let (tag, rest) = parse_tag(" tag keyA=123 ");
assert_eq!("tag", tag);
assert_eq!(" keyA=123 ", rest);
let attributes = parse_attributes(rest).unwrap();
assert_eq!(attributes.get("keyA").unwrap(), "123");
}
#[test]
fn parse_valid_line_with_no_data() {
let (tag, rest) = parse_tag(" tag");
assert_eq!("tag", tag);
assert_eq!("", rest);
let attributes = parse_attributes(rest).unwrap();
assert!(attributes.attributes.is_empty());
}
#[test]
#[should_panic]
fn parse_invalid_line_with_missing_equals() {
let (tag, rest) = parse_tag(" tag keyA");
assert_eq!("tag", tag);
assert_eq!(" keyA", rest);
parse_attributes(rest).unwrap();
}
#[test]
#[should_panic]
fn parse_invalid_line_with_missing_string_terminator() {
let (tag, rest) = parse_tag(" tag keyA=\"string");
assert_eq!("tag", tag);
assert_eq!(" keyA=\"string", rest);
parse_attributes(rest).unwrap();
}
}