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"]))