font3d_blender_addon/common/Font.py
themancalledjakob 335ab1face stabilizing, user experience
use class for glyph availability
use isinstance instead of type
better user experience when export directory does not exist
2025-05-29 15:27:24 +02:00

357 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Dict
from pathlib import Path
from typing import NamedTuple
# convenience dictionary for translating names to glyph ids
# note: overwritten/extended by the content of "glypNamesToUnicode.txt"
# when addon is registered in __init__.py
name_to_glyph_d = {
"zero": "0",
"one": "1",
"two": "2",
"three": "3",
"four": "4",
"five": "5",
"six": "6",
"seven": "7",
"eight": "8",
"nine": "9",
"ampersand": "&",
"backslash": "\\",
"colon": ":",
"comma": ",",
"equal": "=",
"exclam": "!",
"hyphen": "-",
"minus": "",
"parenleft": "(",
"parenright": "(",
"period": ".",
"plus": "+",
"question": "?",
"quotedblleft": "",
"quotedblright": "",
"semicolon": ";",
"slash": "/",
"space": " ",
}
space_d = {}
known_misspellings = {
# simple misspelling
"excent": "accent",
"overdot": "dotaccent",
"diaresis": "dieresis",
"diaeresis": "dieresis",
# different conventions
"doubleacute": "hungarumlaut",
# character does not exist.. maybe something else
"Wcaron": "Wcircumflex",
"Neng": "Nlongrightleg",
"Lgrave": "Lacute",
# currency stuff
"doller": "dollar",
"euro": "Euro",
"yuan": "yen", # https://en.wikipedia.org/wiki/Yen_and_yuan_sign
"pound": "sterling",
# whoopsie
"__": "_",
}
def fix_glyph_name_misspellings(name):
for misspelling in known_misspellings:
if misspelling in name:
return name.replace(misspelling, known_misspellings[misspelling])
return name
def name_to_glyph(name):
if len(name) == 1:
return name
if name in name_to_glyph_d:
return name_to_glyph_d[name]
else:
return None
def glyph_to_name(glyph_id):
for k in name_to_glyph_d:
if glyph_id == name_to_glyph_d[k]:
return k
return glyph_id
def is_space(character):
for name in space_d:
if character == space_d[name][0]:
return space_d[name][1]
return False
def generate_from_file_d(filepath):
d = {}
with open(filepath) as f:
for line in f:
if line[0] == "#":
continue
split = line.split(" ")
if len(split) == 2:
(name, hexstr) = line.split(" ")
val = chr(int(hexstr, base=16))
d[name] = val
if len(split) == 3:
# we might have a parameter, like for the spaces
(name, hexstr, parameter) = line.split(" ")
parameter_value = float(parameter)
val = chr(int(hexstr, base=16))
d[name] = [val, parameter_value]
return d
def generate_name_to_glyph_d():
return generate_from_file_d(f"{Path(__file__).parent}/glyphNamesToUnicode.txt")
def generate_space_d():
return generate_from_file_d(f"{Path(__file__).parent}/spacesUnicode.txt")
def init():
global name_to_glyph_d
global space_d
name_to_glyph_d = generate_name_to_glyph_d()
space_d = generate_space_d()
class FontFace:
"""FontFace is a class holding glyphs
:param glyphs: dictionary of glyphs, defaults to ``{}``
:type glyphs: dict, optional
:param loaded_glyphs: glyphs currently loaded
:type loaded_glyphs: List[str], optional
:param missing_glyphs: glyphs not present in the fontfile
:type missing_glyphs: List[str], optional
:param filenames: from which file is this face
:type filenames: List[str]
"""
def __init__(self, glyphs={}):
self.glyphs = glyphs
# lists have to be initialized in __init__
# to be attributes per instance.
# otherwise they are static class attributes
self.loaded_glyphs = []
self.missing_glyphs = []
self.glyphs_in_fontfile = []
self.filepaths = []
self.unit_factor = 1.0
class Font:
"""Font holds the faces and various metadata for a font
:param faces: dictionary of faces, defaults to ``Dict[str, FontFace]``
:type faces: Dict[str, FontFace]
"""
def __init__(self, faces=Dict[str, FontFace]):
self.faces = faces
def register_font(font_name, face_name, glyphs_in_fontfile, filepath):
if not fonts.keys().__contains__(font_name):
fonts[font_name] = Font({})
if fonts[font_name].faces.get(face_name) is None:
fonts[font_name].faces[face_name] = FontFace({})
fonts[font_name].faces[face_name].glyphs_in_fontfile = glyphs_in_fontfile
else:
fonts[font_name].faces[face_name].glyphs_in_fontfile = list(
set(
fonts[font_name].faces[face_name].glyphs_in_fontfile
+ glyphs_in_fontfile
)
)
if filepath not in fonts[font_name].faces[face_name].filepaths:
fonts[font_name].faces[face_name].filepaths.append(filepath)
def get_font(font_name):
if not fonts.keys().__contains__(font_name):
print(f"ABC3D::get_font: font name({font_name}) not found")
print(fonts.keys())
return None
return fonts[font_name]
def get_font_face(font_name, face_name):
font = get_font(font_name)
if font is None:
return None
if not font.faces.keys().__contains__(face_name):
print(
f"ABC3D::get_font_face (font: {font_name}): face name({face_name}) not found"
)
print(font.faces.keys())
return None
return font.faces[face_name]
def get_font_face_filepaths(font_name, face_name):
face = get_font_face(font_name, face_name)
if not face:
return None
return face.filepaths
def add_glyph(font_name, face_name, glyph_id, glyph_object):
"""add_glyph adds a glyph to a FontFace
it creates the :class:`Font` and :class:`FontFace` if it does not exist yet
:param font_name: The Font you want to add the glyph to
:type font_name: str
:param face_name: The FontFace you want to add the glyph to
:type face_name: str
:param glyph_id: The glyph_id you want this glyph to be stored under
:type glyph_id: str
:param glyph_object: The object containing the glyph
:type glyph_object: `Object`
"""
if not fonts.keys().__contains__(font_name):
fonts[font_name] = Font({})
if fonts[font_name].faces.get(face_name) is None:
fonts[font_name].faces[face_name] = FontFace({})
if fonts[font_name].faces[face_name].glyphs.get(glyph_id) is None:
fonts[font_name].faces[face_name].glyphs[glyph_id] = []
fonts[font_name].faces[face_name].glyphs.get(glyph_id).append(glyph_object)
if glyph_id not in fonts[font_name].faces[face_name].loaded_glyphs:
fonts[font_name].faces[face_name].loaded_glyphs.append(glyph_id)
def get_glyphs(font_name, face_name, glyph_id):
"""get_glyphs returns an array of glyphs of a FontFace
:param font_name: The :class:`Font` you want to get the glyph from
:type font_name: str
:param face_name: The :class:`FontFace` you want to get the glyph from
:type face_name: str
:param glyph_id: The ``glyph_id`` from the glyph you want
:type glyph_id: str
...
:return: returns a list of the glyph objects, or an empty list if none exists
:rtype: `List`
"""
face = get_font_face(font_name, face_name)
if face is None:
print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found")
print(fonts[font_name].faces.keys())
return []
glyphs_for_id = face.glyphs.get(glyph_id)
if glyphs_for_id is None:
print(
f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id}) not found"
)
if glyph_id not in fonts[font_name].faces[face_name].missing_glyphs:
fonts[font_name].faces[face_name].missing_glyphs.append(glyph_id)
return []
return glyphs_for_id
def get_glyph(font_name, face_name, glyph_id, alternate=0):
"""add_glyph adds a glyph to a FontFace
it creates the :class:`Font` and :class:`FontFace` if it does not exist yet
:param font_name: The :class:`Font` you want to get the glyph from
:type font_name: str
:param face_name: The :class:`FontFace` you want to get the glyph from
:type face_name: str
:param glyph_id: The ``glyph_id`` from the glyph you want
:type glyph_id: str
...
:return: returns the glyph object, or ``None`` if it does not exist
:rtype: `Object`
"""
glyphs = get_glyphs(font_name, face_name, glyph_id)
if len(glyphs) <= alternate or len(glyphs) == 0:
print(
f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] not found"
)
return None
return glyphs[alternate]
class GlyphsAvailability(NamedTuple):
loaded: str
missing: str
unloaded: str
filepaths: list[str]
def test_glyphs_availability(font_name, face_name, text):
# maybe there is NOTHING yet
if (
not fonts.keys().__contains__(font_name)
or fonts[font_name].faces.get(face_name) is None
):
return GlyphsAvailability("", "", "", [])
loaded = []
missing = []
unloaded = []
for c in text:
if c in fonts[font_name].faces[face_name].loaded_glyphs:
loaded.append(c)
elif c in fonts[font_name].faces[face_name].glyphs_in_fontfile:
unloaded.append(c)
else:
if c not in fonts[font_name].faces[face_name].missing_glyphs:
fonts[font_name].faces[face_name].missing_glyphs.append(c)
missing.append(c)
return GlyphsAvailability(
"".join(loaded),
"".join(missing),
"".join(unloaded),
fonts[font_name].faces[face_name].filepaths,
)
def get_loaded_fonts():
return fonts.keys()
def get_loaded_fonts_and_faces():
out = []
for f in fonts.keys():
for ff in fonts[f].faces.keys():
out.append([f, ff])
return out
MISSING_FONT = 0
MISSING_FACE = 1
def test_availability(font_name, face_name, text):
if not fonts.keys().__contains__(font_name):
return MISSING_FONT
if fonts[font_name].faces.get(face_name) is None:
return MISSING_FACE
availability: GlyphsAvailability = test_glyphs_availability(
font_name, face_name, text
)
return availability
# holds all fonts
fonts = {}