font3d_blender_addon/common/Font.py
2024-08-28 17:45:27 +02:00

249 lines
8.2 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 TypedDict
from typing import Dict
from dataclasses import dataclass
from pathlib import Path
# 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": " ",
}
known_misspellings = {
# simple misspelling
"excent" : "accent",
"overdot" : "dotaccent",
"diaresis": "dieresis",
"diaeresis": "dieresis",
# character does not exist.. maybe something else
"Odoubleacute": "Ohungarumlaut",
"Udoubleacute": "Uhungarumlaut",
"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 generate_name_to_glyph_d():
d = {}
with open(f"{Path(__file__).parent}/glyphNamesToUnicode.txt") as f:
for line in f:
if line[0] == '#':
continue
(name, hexstr) = line.split(' ')
val = chr(int(hexstr, base=16))
d[name] = val
return 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
# TODO: better class structure?
# TODO: get fonts and faces directly
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) == 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 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) == None:
fonts[font_name].faces[face_name] = FontFace({})
if fonts[font_name].faces[face_name].glyphs.get(glyph_id) == 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_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`
"""
if not fonts.keys().__contains__(font_name):
print(f"ABC3D::get_glyph: font name({font_name}) not found")
print(fonts.keys())
return None
face = fonts[font_name].faces.get(face_name)
if face == None:
print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) not found")
print(fonts[font_name].faces.keys())
return None
glyphs_for_id = face.glyphs.get(glyph_id)
if glyphs_for_id == None or len(glyphs_for_id) <= alternate:
print(f"ABC3D::get_glyph: font({font_name}) face({face_name}) glyph({glyph_id})[{alternate}] 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 None
return fonts[font_name].faces[face_name].glyphs.get(glyph_id)[alternate]
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) == None:
return "", "", text # <loaded>, <missing>, <maybe>
loaded = []
missing = []
maybe = []
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:
maybe.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 ''.join(loaded), ''.join(missing), ''.join(maybe), 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) == None:
return MISSING_FACE
loaded, missing, maybe, filepaths = test_glyphs_availability(font_name,
face_name,
text)
return {
"loaded": loaded,
"missing": missing,
"maybe": maybe,
"filepaths": filepaths,
}
# holds all fonts
fonts = {}