107 lines
3.7 KiB
Python
107 lines
3.7 KiB
Python
import itertools
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
from funcy import lmap, complement, keep, first, lzip, omit, project
|
|
from loguru import logger
|
|
|
|
from synthesis.random import rnd
|
|
|
|
|
|
class RandomFontPicker:
|
|
def __init__(self, font_dir=None, return_default_font=False):
|
|
fonts = get_fonts(font_dir)
|
|
fonts_lower = [font.lower() for font in fonts]
|
|
domestic_fonts_mask = lmap(complement(self.looks_foreign), fonts_lower)
|
|
self.fonts = list(itertools.compress(fonts, domestic_fonts_mask))
|
|
self.fonts_lower = list(itertools.compress(fonts_lower, domestic_fonts_mask))
|
|
|
|
self.test_image = Image.new("RGB", (200, 200), (255, 255, 255))
|
|
self.draw = ImageDraw.Draw(self.test_image)
|
|
self.return_default_font = return_default_font
|
|
|
|
def looks_foreign(self, font):
|
|
# This filters out foreign fonts (e.g. 'Noto Serif Malayalam')
|
|
return len(font.split("-")[0]) > 10
|
|
|
|
def pick_random_font_available_on_system(self, includes=None, excludes=None) -> ImageFont: # FIXME: Slow!
|
|
|
|
if self.return_default_font:
|
|
return ImageFont.load_default()
|
|
|
|
includes = [i.lower() for i in includes] if includes else []
|
|
excludes = [i.lower() for i in excludes] if excludes else []
|
|
|
|
logger.debug(f"Picking font by includes={includes} and excludes={excludes}.")
|
|
|
|
def includes_pattern(font):
|
|
return not includes or any(include in font for include in includes)
|
|
|
|
def excludes_pattern(font):
|
|
return not excludes or not any(exclude in font for exclude in excludes)
|
|
|
|
self.shuffle_fonts()
|
|
|
|
mask = lmap(lambda f: includes_pattern(f) and excludes_pattern(f), self.fonts_lower)
|
|
fonts = itertools.compress(self.fonts, mask)
|
|
fonts = keep(map(self.load_font, fonts))
|
|
# fonts = filter(self.font_is_renderable, fonts) # FIXME: this does not work
|
|
|
|
font = first(fonts)
|
|
logger.info(f"Using font: {font.getname()}")
|
|
return font
|
|
|
|
def shuffle_fonts(self):
|
|
l = lzip(self.fonts, self.fonts_lower)
|
|
rnd.shuffle(l)
|
|
self.fonts, self.fonts_lower = lzip(*l)
|
|
|
|
def pick_random_mono_space_font_available_on_system(self) -> ImageFont:
|
|
return self.pick_random_font_available_on_system(includes=["mono"], excludes=["oblique"])
|
|
|
|
@lru_cache(maxsize=None)
|
|
def load_font(self, font: str):
|
|
logger.trace(f"Loading font: {font}")
|
|
try:
|
|
return ImageFont.truetype(font, size=11)
|
|
except OSError:
|
|
return None
|
|
|
|
@lru_cache(maxsize=None)
|
|
def font_is_renderable(self, font):
|
|
text_size = self.draw.textsize("Test String", font=font)
|
|
return text_size[0] > 0 and text_size[1]
|
|
|
|
|
|
def get_fonts(path: Path = None) -> List[str]:
|
|
path = path or Path("/usr/share/fonts")
|
|
fonts = list(path.rglob("*.ttf"))
|
|
fonts = [font.name for font in fonts]
|
|
return fonts
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def get_font_picker(**kwargs):
|
|
return RandomFontPicker(**kwargs, return_default_font=True)
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def pick_random_mono_space_font_available_on_system(**kwargs):
|
|
font_picker = get_font_picker(**omit(kwargs, ["includes", "excludes"]))
|
|
return font_picker.pick_random_mono_space_font_available_on_system()
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def pick_random_font_available_on_system(**kwargs):
|
|
kwargs["excludes"] = (
|
|
*kwargs.get(
|
|
"excludes",
|
|
),
|
|
"Kinnari",
|
|
"KacstOne",
|
|
)
|
|
font_picker = get_font_picker(**omit(kwargs, ["includes", "excludes"]))
|
|
return font_picker.pick_random_font_available_on_system(**project(kwargs, ["includes", "excludes"]))
|