bmf-parser 0.0.2

read BMFont binary files
Documentation
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Peter Bjorklund. All rights reserved.
 *  Licensed under the MIT License. See LICENSE in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

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);
                }
                _ => {} // TODO: Report error on unknown tags
            }
        }

        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;
        }

        // Parse the key
        let mut key = String::new();
        while let Some(&c) = chars.peek() {
            if c == '=' {
                chars.next();
                break;
            }
            key.push(chars.next().unwrap());
        }

        // Parse value
        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")?,
    })
}