use crate::{BMFont, Char, CommonBlock, InfoBlock, KerningPair};
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use std::str::FromStr;
impl FromStr for BMFont {
type Err = io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bmfont = BMFont {
info: None,
common: None,
pages: Vec::new(),
chars: HashMap::new(),
kernings: Vec::new(),
};
for line in s.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.splitn(2, ' ').collect();
if parts.len() < 2 {
continue;
}
let tag = parts[0];
let content = parts[1];
match tag {
"info" => bmfont.info = Some(parse_info_block(content)?),
"common" => bmfont.common = Some(parse_common_block(content)?),
"page" => {
let page_name = parse_page(content)?;
bmfont.pages.push(page_name);
}
"char" => {
let char_data = parse_char(content)?;
bmfont.chars.insert(char_data.id, char_data);
}
"kerning" => {
let kerning = parse_kerning(content)?;
bmfont.kernings.push(kerning);
}
_ => {} }
}
Ok(bmfont)
}
}
pub fn parse_bmfont_text<P: AsRef<Path>>(path: P) -> io::Result<BMFont> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
content.parse()
}
fn parse_key_value_pairs(content: &str) -> HashMap<String, String> {
let mut map = HashMap::new();
let mut chars = content.chars().peekable();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
continue;
}
let mut key = String::new();
while let Some(&c) = chars.peek() {
if c == '=' {
chars.next();
break;
}
key.push(chars.next().unwrap());
}
let mut value = String::new();
let mut in_quotes = false;
while let Some(&c) = chars.peek() {
if c == '"' {
in_quotes = !in_quotes;
chars.next();
if !in_quotes && value.is_empty() {
break;
}
continue;
}
if c.is_whitespace() && !in_quotes {
break;
}
value.push(chars.next().unwrap());
}
map.insert(key, value);
}
map
}
fn parse_required<T: FromStr>(pairs: &HashMap<String, String>, key: &str) -> io::Result<T> {
pairs
.get(key)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, String::from("Missing ") + key))?
.parse::<T>()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, String::from("Invalid ") + key))
}
fn parse_optional<T: FromStr>(pairs: &HashMap<String, String>, key: &str, default: T) -> T {
pairs
.get(key)
.and_then(|v| v.parse::<T>().ok())
.unwrap_or(default)
}
fn parse_array<T: FromStr>(text: &str, expected_size: usize) -> io::Result<Vec<T>> {
let mut values = Vec::with_capacity(expected_size);
for part in text.split(',') {
let value = part.trim().parse::<T>().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
String::from("Failed to parse array value"),
)
})?;
values.push(value);
}
if values.len() == expected_size {
Ok(values)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
String::from("Array has wrong size"),
))
}
}
fn parse_info_block(content: &str) -> io::Result<InfoBlock> {
let pairs = parse_key_value_pairs(content);
let font_size = parse_required::<i16>(&pairs, "size")?;
let bit_field = parse_optional::<u8>(&pairs, "bold", 0) & 0x01
| (parse_optional::<u8>(&pairs, "italic", 0) & 0x01) << 1
| (parse_optional::<u8>(&pairs, "unicode", 0) & 0x01) << 2
| (parse_optional::<u8>(&pairs, "smooth", 0) & 0x01) << 3;
let char_set = parse_optional::<u8>(&pairs, "charset", 0);
let stretch_h = parse_optional::<u16>(&pairs, "stretchH", 100);
let aa = parse_optional::<u8>(&pairs, "aa", 1);
let default_padding = String::from("0,0,0,0");
let padding_str = pairs.get("padding").unwrap_or(&default_padding);
let padding = parse_array::<u8>(padding_str, 4)?;
let default_spacing = String::from("0,0");
let spacing_str = pairs.get("spacing").unwrap_or(&default_spacing);
let spacing = parse_array::<u8>(spacing_str, 2)?;
let outline = parse_optional::<u8>(&pairs, "outline", 0);
let font_name = parse_required::<String>(&pairs, "face")?;
Ok(InfoBlock {
font_size,
bit_field,
char_set,
stretch_h,
aa,
padding: [padding[0], padding[1], padding[2], padding[3]],
spacing: [spacing[0], spacing[1]],
outline,
font_name,
})
}
fn parse_common_block(content: &str) -> io::Result<CommonBlock> {
let pairs = parse_key_value_pairs(content);
let line_height = parse_required::<u16>(&pairs, "lineHeight")?;
let base = parse_required::<u16>(&pairs, "base")?;
let scale_w = parse_required::<u16>(&pairs, "scaleW")?;
let scale_h = parse_required::<u16>(&pairs, "scaleH")?;
let pages = parse_required::<u16>(&pairs, "pages")?;
let bit_field = parse_optional::<u8>(&pairs, "packed", 0);
let alpha_chnl = parse_optional::<u8>(&pairs, "alphaChnl", 0);
let red_chnl = parse_optional::<u8>(&pairs, "redChnl", 0);
let green_chnl = parse_optional::<u8>(&pairs, "greenChnl", 0);
let blue_chnl = parse_optional::<u8>(&pairs, "blueChnl", 0);
Ok(CommonBlock {
line_height,
base,
scale_w,
scale_h,
pages,
bit_field,
alpha_chnl,
red_chnl,
green_chnl,
blue_chnl,
})
}
fn parse_page(content: &str) -> io::Result<String> {
let pairs = parse_key_value_pairs(content);
parse_required::<String>(&pairs, "file")
}
fn parse_char(content: &str) -> io::Result<Char> {
let pairs = parse_key_value_pairs(content);
Ok(Char {
id: parse_required::<u32>(&pairs, "id")?,
x: parse_required::<u16>(&pairs, "x")?,
y: parse_required::<u16>(&pairs, "y")?,
width: parse_required::<u16>(&pairs, "width")?,
height: parse_required::<u16>(&pairs, "height")?,
x_offset: parse_required::<i16>(&pairs, "xoffset")?,
y_offset: parse_required::<i16>(&pairs, "yoffset")?,
x_advance: parse_required::<i16>(&pairs, "xadvance")?,
page: parse_optional::<u8>(&pairs, "page", 0),
chnl: parse_optional::<u8>(&pairs, "chnl", 15),
})
}
fn parse_kerning(content: &str) -> io::Result<KerningPair> {
let pairs = parse_key_value_pairs(content);
Ok(KerningPair {
first: parse_required::<u32>(&pairs, "first")?,
second: parse_required::<u32>(&pairs, "second")?,
amount: parse_required::<i16>(&pairs, "amount")?,
})
}