|
|
|
|
|
|
|
|
import logging |
|
|
import pickle |
|
|
from typing import Any, Dict, List, Optional |
|
|
|
|
|
import fsspec |
|
|
import huggingface_hub |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
FontDict = Dict[str, List[Dict[str, Any]]] |
|
|
|
|
|
|
|
|
FONT_WEIGHTS = { |
|
|
"thin", |
|
|
"light", |
|
|
"extralight", |
|
|
"regular", |
|
|
"medium", |
|
|
"semibold", |
|
|
"bold", |
|
|
"extrabold", |
|
|
"black", |
|
|
} |
|
|
|
|
|
FONT_STYLES = {"regular", "bold", "italic", "bolditalic"} |
|
|
|
|
|
_OPENCOLE_REPOSITORY = "cyberagent/opencole" |
|
|
_OPENCOLE_FONTS_PATH = "resources/fonts.pickle" |
|
|
|
|
|
|
|
|
class FontManager(object): |
|
|
"""Font face manager. |
|
|
|
|
|
Example:: |
|
|
|
|
|
# Use the font manager to lookup a font face. |
|
|
fm = FontManager("/path/to/fonts.pickle") |
|
|
typeface = skia.Typeface.MakeFromData(fm.lookup("Montserrat")) |
|
|
""" |
|
|
|
|
|
def __init__(self, input_path: Optional[str] = None) -> None: |
|
|
self._fonts: Optional[FontDict] = None |
|
|
|
|
|
if input_path is None: |
|
|
input_path = huggingface_hub.hf_hub_download( |
|
|
repo_id=_OPENCOLE_REPOSITORY, |
|
|
filename=_OPENCOLE_FONTS_PATH, |
|
|
repo_type="dataset", |
|
|
) |
|
|
|
|
|
self.load(input_path) |
|
|
|
|
|
def save(self, output_path: str) -> None: |
|
|
"""Save fonts to a pickle file.""" |
|
|
assert self._fonts is not None, "Fonts not loaded yet." |
|
|
logger.info("Saving fonts to %s", output_path) |
|
|
with fsspec.open(output_path, "wb") as f: |
|
|
pickle.dump(self._fonts, f) |
|
|
|
|
|
def load(self, input_path: str) -> None: |
|
|
"""Load fonts from a pickle file.""" |
|
|
logger.info("Loading fonts from %s", input_path) |
|
|
with fsspec.open(input_path, "rb") as f: |
|
|
self._fonts = pickle.load(f) |
|
|
assert self._fonts is not None, "No font loaded." |
|
|
logger.info("Loaded %d font families", len(self._fonts)) |
|
|
|
|
|
def lookup( |
|
|
self, |
|
|
font_family: str, |
|
|
font_weight: str = "regular", |
|
|
font_style: str = "regular", |
|
|
) -> bytes: |
|
|
"""Lookup the specified font face.""" |
|
|
assert self._fonts is not None, "Fonts not loaded yet." |
|
|
assert font_weight in FONT_WEIGHTS, f"Invalid font weight: {font_weight}" |
|
|
assert font_style in FONT_STYLES, f"Invalid font style: {font_style}" |
|
|
|
|
|
family = [] |
|
|
for i, family_name in enumerate([font_family, "Montserrat"]): |
|
|
try: |
|
|
family = self._fonts[normalize_family(family_name)] |
|
|
if i > 0: |
|
|
logger.warning( |
|
|
f"Font family fallback to {family[0]['fontFamily']}." |
|
|
) |
|
|
break |
|
|
except KeyError: |
|
|
logger.warning(f"Font family not found: {font_family}") |
|
|
|
|
|
if not family: |
|
|
family = next(iter(self._fonts.values())) |
|
|
logger.warning(f"Font family fallback to {family[0]['fontFamily']}.") |
|
|
|
|
|
font_weight = get_font_weight(font_family, font_weight) |
|
|
font_style = get_font_style(font_family, font_style) |
|
|
try: |
|
|
font = next( |
|
|
font |
|
|
for font in family |
|
|
if font.get("fontWeight", "regular") == font_weight |
|
|
and font.get("fontStyle", "regular") == font_style |
|
|
) |
|
|
except StopIteration: |
|
|
font = family[0] |
|
|
logger.warning( |
|
|
f"Font style for {font['fontFamily']} not found: {font_weight} " |
|
|
f"{font_style}, fallback to {font.get('fontWeight', 'regular')} " |
|
|
f"{font.get('fontStyle', 'regular')}" |
|
|
) |
|
|
return font["bytes"] |
|
|
|
|
|
|
|
|
def normalize_family(name: str) -> str: |
|
|
"""Normalize font name.""" |
|
|
name = name.replace("_", " ").title() |
|
|
name = name.replace(" Bold", "") |
|
|
name = name.replace(" Regular", "") |
|
|
name = name.replace(" Light", "") |
|
|
name = name.replace(" Italic", "") |
|
|
name = name.replace(" Medium", "") |
|
|
|
|
|
FONT_MAP = { |
|
|
"Arkana Script": "Arkana", |
|
|
"Blogger": "Blogger Sans", |
|
|
"Delius Swash": "Delius Swash Caps", |
|
|
"Elsie Swash": "Elsie Swash Caps", |
|
|
"Gluk Glametrix": "Gluk Foglihtenno06", |
|
|
"Gluk Znikomitno25": "Gluk Foglihtenno06", |
|
|
"Im Fell": "Im Fell Dw Pica Sc", |
|
|
"Medieval Sharp": "Medievalsharp", |
|
|
"Playlist Caps": "Playlist", |
|
|
"Rissa Typeface": "Rissatypeface", |
|
|
"Selima": "Selima Script", |
|
|
"Six": "Six Caps", |
|
|
"V T323": "Vt323", |
|
|
|
|
|
"Different Summer": "Montserrat", |
|
|
"Dukomdesign Constantine": "Montserrat", |
|
|
"Sunday": "Montserrat", |
|
|
} |
|
|
return FONT_MAP.get(name, name) |
|
|
|
|
|
|
|
|
def get_font_weight(name: str, default: str) -> str: |
|
|
"""Get font weight from the font name.""" |
|
|
key = name.replace("_", " ").lower().split(" ")[-1] |
|
|
return key if key in FONT_WEIGHTS else default |
|
|
|
|
|
|
|
|
def get_font_style(name: str, default: str) -> str: |
|
|
"""Get font style from the font name.""" |
|
|
key = name.replace("_", " ").lower().split(" ")[-1] |
|
|
return key if key in FONT_STYLES else default |
|
|
|