Source code for pySSV.ssv_fonts

#  Copyright (c) 2024 Thomas Mathieson.
#  Distributed under the terms of the MIT license.
import logging
import os.path
import sys
if sys.version_info >= (3, 9):
    from importlib.resources import files
else:
    from importlib.resources import open_binary, read_text
import xml.etree.ElementTree as et
from dataclasses import dataclass
from PIL import Image
from typing import Optional, Dict

from .ssv_logging import log


def _find_font(font_path: str) -> str:
    """
    Attempts to load a font file from the file system or from pySSV's built in fonts directory.

    :param font_path: the path to the font file.
    :return: the contents of the file as a string.
    """
    if os.path.isfile(font_path):
        with open(font_path, "r") as f:
            return f.read()

    try:
        if sys.version_info >= (3, 9):
            template_traversable = files("pySSV.fonts").joinpath(font_path)
            return template_traversable.read_text()
        else:
            return read_text("pySSV.fonts", font_path)
    except Exception as e:
        raise FileNotFoundError(f"Couldn't find/read the font: '{font_path}'. \n"
                                f"Inner exception: {e}")


def _load_bitmap(bitmap_path: str, font_path: str) -> Image.Image:
    """
    Attempts to load a font bitmap from the file system or from pySSV's built in fonts directory.

    :param bitmap_path: the path to the font bitmap.
    :param font_path: the path to the font file.
    :return: the contents of the font bitmap as an Image.
    """
    if os.path.isfile(bitmap_path):
        return Image.open(bitmap_path)

    font_dir = os.path.dirname(font_path)
    if os.path.isdir(font_dir):
        path = os.path.join(font_dir, font_path)
        if os.path.isfile(path):
            return Image.open(path)

    try:
        if sys.version_info >= (3, 9):
            template_traversable = files("pySSV.fonts").joinpath(bitmap_path)
            f = template_traversable.open("rb")
        else:
            f = open_binary("pySSV.fonts", bitmap_path)
        return Image.open(f)
    except Exception as e:
        raise FileNotFoundError(f"Couldn't find/read the font bitmap: '{bitmap_path}'. \n"
                                f"Inner exception: {e}")


[docs] @dataclass class SSVCharacterDefinition: id: int """The id of the character. (Usually the ascii character code)""" char: str """The character being represented.""" x: int """The x coordinate of the character in the bitmap from the left in pixels.""" y: int """The y coordinate of the character in the bitmap from the top in pixels.""" width: int """The width of the character in the bitmap in pixels.""" height: int """The height of the character in the bitmap in pixels.""" x_offset: int """How much to offset the character by in the x axis when rendering in pixels.""" y_offset: int """How much to offset the character by in the y axis when rendering in pixels.""" x_advance: int """How far to advance before drawing the next character."""
[docs] class SSVFont: def __init__(self, font_path: str): """ Constructs a new SSVFont instance from an existing ``.fnt`` file. A ``.fnt`` file is a Bitmap Font file which is an xml file following the schema defined here: https://www.angelcode.com/products/bmfont/doc/file_format.html Font files can be generated using: https://github.com/soimy/msdf-bmfont-xml :param font_path: the path to the font file to load. """ bm_font = et.fromstring(_find_font(font_path)) try: info = bm_font.find("info") common = bm_font.find("common") self.font_name: str = info.get("face") # type: ignore self.is_bold: bool = info.get("bold") == "1" # type: ignore self.is_italic: bool = info.get("italic") == "1" # type: ignore self.size: int = int(info.get("size")) # type: ignore self.line_height: int = int(common.get("lineHeight")) # type: ignore self.base_height: int = int(common.get("base")) # type: ignore self.width: int = int(common.get("scaleW")) # type: ignore self.height: int = int(common.get("scaleH")) # type: ignore self.pages: int = int(common.get("pages")) # type: ignore if self.pages != 1: raise ValueError(f"Font {self.font_name} has {self.pages} font pages, currently only 1 page is " f"supported.") distance_field = bm_font.find("distanceField") self.sdf_distance: Optional[int] = None if distance_field is not None: self.sdf_distance = int(distance_field.get("distanceRange")) # type: ignore self.bitmap_path: str = bm_font.find("pages").find("page").get("file") # type: ignore if self.bitmap_path is None: raise ValueError("Font bitmap path is not defined.") except Exception as e: raise ValueError(f"Error while parsing font file '{font_path}'. \n" f"Inner exception: {e}") self.bitmap = _load_bitmap(self.bitmap_path, font_path) self.chars: Dict[str, SSVCharacterDefinition] = {} chars_el = bm_font.find("chars") if chars_el is not None: self._parse_chars(chars_el) def _parse_chars(self, chars: et.Element): for char in chars.iter("char"): char_id_str = char.get("id") if char_id_str is None: continue char_id = int(char_id_str) char_def = SSVCharacterDefinition(char_id, char.get("char", chr(char_id)), # type: ignore int(char.get("x")), # type: ignore int(char.get("y")), # type: ignore int(char.get("width")), # type: ignore int(char.get("height")), # type: ignore int(char.get("xoffset")), # type: ignore int(char.get("yoffset")), # type: ignore int(char.get("xadvance"))) # type: ignore self.chars[char_def.char] = char_def
ssv_font_noto_sans_sb = SSVFont("NotoSans-SemiBold.fnt")