From de9b3bad93d91b2d1820b59403fc357e243238e6 Mon Sep 17 00:00:00 2001 From: Matthias Bisping Date: Wed, 1 Feb 2023 17:05:47 +0100 Subject: [PATCH] Refactoring: Move Move font picker into new font module --- synthesis/text/font.py | 82 +++++++++++++++++++++++++++ test/fixtures/page_generation/page.py | 77 +------------------------ 2 files changed, 83 insertions(+), 76 deletions(-) create mode 100644 synthesis/text/font.py diff --git a/synthesis/text/font.py b/synthesis/text/font.py new file mode 100644 index 0000000..48368e4 --- /dev/null +++ b/synthesis/text/font.py @@ -0,0 +1,82 @@ +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 +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 diff --git a/test/fixtures/page_generation/page.py b/test/fixtures/page_generation/page.py index ecb7ebb..7d84451 100644 --- a/test/fixtures/page_generation/page.py +++ b/test/fixtures/page_generation/page.py @@ -8,7 +8,6 @@ from copy import deepcopy from enum import Enum from functools import lru_cache, partial from math import sqrt -from pathlib import Path from typing import Tuple, Iterable, List import blend_modes @@ -31,6 +30,7 @@ from synthesis.partitioner.page_partitioner import PagePartitioner from synthesis.random import rnd from synthesis.segment.content_rectangle import ContentRectangle from synthesis.segment.random_content_rectangle import RandomContentRectangle +from synthesis.text.font import RandomFontPicker from synthesis.text.line_formatter.identity import IdentityLineFormatter from synthesis.text.line_formatter.paragraph import ParagraphLineFormatter @@ -50,13 +50,10 @@ from funcy import ( rest, lsplit, lfilter, - lzip, - keep, repeatedly, mapcat, omit, project, - complement, chunks, ) @@ -674,78 +671,6 @@ def dump_plt_to_image(rectangle): return image -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)