import os
import tempfile
from .font_manager import findSystemFonts
from pathlib import Path
import warnings
warnings.simplefilter("ignore")
from fuzzywuzzy import process, fuzz
from opentypesvg import fonts2svg
from .draw_svg import drawSVG
# make it possible to at runtime process text
class Fonts2SVGFakeOptions(): # this just makes it easier
def __init__(self, fontpath, outfolder):
self.colors_list = ['#ffffff']
self.output_folder_path = outfolder
self.gnames_to_generate = []
self.gnames_to_add = []
self.gnames_to_exclude = []
self.glyphsets_union = False
self.adjust_view_box_to_glyph = False
self.input_paths = [fontpath]
self.font_paths_list = fonts2svg.validate_font_paths(self.input_paths)
[docs]
class LetterDrawer():
letter_gap = -0.1
line_spacing = 1.15
def __init__(self, turtle, load_common_fonts:bool=False):
self.turtle = turtle
self.loaded_fonts = {}
self.created_tmpdirs = []
# equivalent to self.clear_fonts()
if load_common_fonts:
try:
self.load_font('Arial')
except:
pass
try:
self.load_font("Helvetica")
except:
pass
try:
self.load_font("Comic") # comic sans
except:
pass
self.prev_fontsize = None
# context manager functions
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.clear_fonts() # this will delete the temp directories with the .svg files
return False # don't suppress the errors
font_search_score_threshold = 90
# if we want to use .otf or whatever font files
def load_font(self, fontname:str, fontpath:str=None, search_threshold=None, force_reload:bool=False):
# Loads a font file from a fontpath, and gives it a name to be referred to.
# if no fontpath specified, will try to find it in system files
# check cache
if os.path.isdir('.fontcache/'+fontname) and not force_reload:
self._add_font_paths(fontname, '.fontcache/'+fontname)
return fontname
if fontpath is None:
# set search threshold
if search_threshold is None:
search_threshold = LetterDrawer.font_search_score_threshold
# get system fonts
fontpaths = findSystemFonts(fontpaths=None, fontext='otf')
fontnames = [Path(fp).name.lower() for fp in fontpaths]
res_ttf = process.extract(fontname.lower()+".ttf", fontnames, scorer=fuzz.ratio)
res_otf = process.extract(fontname.lower()+".otf", fontnames, scorer=fuzz.ratio)
for filename, score in res_ttf + res_otf:
if score >= search_threshold:
# THIS IS A GOOD ENOUGH MATCH!
fontpath = fontpaths[fontnames.index(filename)]
break
if fontpath is None: # means it couldn't be found from previously
raise ValueError("Could not find font in system files: "+str(res_ttf+res_otf))
#td = tempfile.TemporaryDirectory()
#self.created_tmpdirs.append(td)
#tmpdirname = td.name
tmpdirname = '.fontcache/'+fontname
if not os.path.isdir(tmpdirname):
os.makedirs(tmpdirname)
# processs text format: convert to svg first
opts = Fonts2SVGFakeOptions(fontpath=fontpath, outfolder=tmpdirname)
font_paths_list = opts.font_paths_list
hex_colors_list = opts.colors_list
output_folder_path = fonts2svg.get_output_folder_path(opts.output_folder_path,
font_paths_list[0])
fonts2svg.processFonts(font_paths_list, hex_colors_list, output_folder_path, opts)
# font SVGs are now in tmpdirname
self._add_font_paths(fontname, tmpdirname)
return fontpath
def _add_font_paths(self, fontname, savedir):
# get the paths to the font SVGs
paths = {}
for rt, _, files in os.walk(savedir):
for i in range(len(files)):
try:
paths[files[i][:files[i].index('.svg')]] = os.path.join(rt, files[i])
except ValueError as ve:
print("WARNING (GETTING SVGS):", ve)
self.loaded_fonts[fontname] = paths
def get_loaded_fontnames(self):
# get a list of names of fonts that are already loaded
return list(self.loaded_fonts.keys())
[docs]
def draw_one_letter(self, fontname, lettername, fontsize=120, colour='#000000', thickness=1, fill=False, outline=True, fill_min_y_dist:int=10, fill_min_x_dist=10, full_fill=True, outline_satin_thickness=None, turtle=None, flip_y=False):
"""This function draws a single letter.
Parameters
----------
fontname : str
Specify a font name (from the loaded fonts)
lettername : str
Specify the name of a letter. Spaces should be called 'space' and not ' '.
fontsize : int (optional, default=20)
Specify the font size used to draw the text.
fill : bool (optional, default=False)
Specify whether or not the text should be filled.
full_fill : bool (optional, default=True)
Specify whether the text should be filled with ``full_fill`` or ``partial_fill`` (only used if ``fill=True``).
Note that ``full_fill`` can have bugs at small font sizes (120 is considered small), while ``partial_fill`` might cross over the boundaries of the text.
outline : bool (optional, default=True)
Specify whether the text should be outlined (it is recommended to outline when not filling or using ```partial_fill```, but not ```full_fill```).
outline_satin_thickness : int, optional, can be None
If not None, the SVG's lines will use satin stitch rather than direct stitch
flip_y : bool (optional, default=False)
Allow you to vertically flip the text if desired. Defaults to ``False``.
fill_min_x_dist : int (optional, default=10)
Advanced parameter deciding the resolution of ``partial_fill``.
If ``partial_fill`` is taking too long, consider increasing this value to make it run faster, at the cost of a lower resolution of fill.
Default value is 10, and this should never be set below 5.
fill_min_y_dist : int (optional, default=10)
Advanced parameter deciding the resolution of ``partial_fill``.
If ``partial_fill`` is taking too long, consider increasing this value to make it run faster, at the cost of a lower resolution of fill.
Default value is 10, and this should never be set below 5.
"""
self.prev_fontsize = fontsize
# draws one letter with the turtles, with the specified fields.
# turtle defaults to self.turtle
if turtle is None:
if self.turtle is None:
raise ValueError("MUST DECLARE turtle TO USE IN LetterDrawer.draw_one_letter in either draw_one_letter() or LetterDrawer() init")
turtle = self.turtle
#print("DRAWING LETTER", lettername)
if lettername == 'space':
currpos = list(turtle.position())
# move right a bit
with turtle.jump_stitch():
turtle.goto(currpos[0]+fontsize, currpos[1])
return
# DRAW ONE LETTER OF A FONT WITH A LOADED NAME, GIVEN A COLOUR
if fontname in self.loaded_fonts.keys():
try:
drawSVG(turtle, self.loaded_fonts[fontname][lettername], fontsize, fontsize, colour, thickness, fill, outline, fill_min_y_dist, fill_min_x_dist, full_fill, outline_satin_thickness, flip_y)
#print("DREW SVG")
except Exception as e:
print("OR, it might be some other error({})".format(e))
raise ValueError("font '{}' does not have the letter '{}'".format(fontname, lettername))
else:
raise ValueError("font '{}' not loaded".format(fontname))
return
[docs]
def draw_letter_gap(self, fontsize=None, letter_gap=None):
'''
Draws a gap between two letters.
Parameters
----------
fontsize : int (optional)
The font size to take this letter gap to represent. Defaults to the previous font size in draw_one_letter or draw_string.
letter_gap : float (optional)
The letter gap scale factor. defaults to LetterDrawer.letter_gap (which defaults to -0.1)
'''
if fontsize is None:
assert ( not (self.prev_fontsize is None) ) , "Cannot draw letter gap before drawing letters!"
fontsize = self.prev_fontsize
if letter_gap is None:
letter_gap = LetterDrawer.letter_gap
#print("DRAWING LETTER GAP")
# this draws the gap between two letters (not whitespace)
with self.turtle.jump_stitch():
currpos = list(self.turtle.position())
self.turtle.goto(currpos[0] + letter_gap*fontsize, currpos[1])
#print("DRAEW")
[docs]
def draw_string(self, fontname, string, fontsize, colours='#000000', thicknesses = 1, fills=False, outlines=True, fill_min_y_dist=10, fill_min_x_dist=10, full_fill=True, outline_satin_thickness=None, letter_gaps=None, turtle=None, flip_y=False):
"""This function draws a string of letters.
Parameters
----------
fontname : str
Specify a font name (from the loaded fonts)
lettername : str
Specify the name of a letter. Spaces should be called 'space' and not ' '.
fontsize : int (optional, default=20)
Specify the font size used to draw the text.
fills : bool/list[bool] (optional, default=False)
Specify whether or not the text should be filled. Also accepts a list with one element per letter in the string.
full_fill : bool (optional, default=True)
Specify whether the text should be filled with ``full_fill`` or ``partial_fill`` (only used if ``fill=True``).
Note that ``full_fill`` can have bugs at small font sizes (120 is considered small), while ``partial_fill`` might cross over the boundaries of the text.
outlines : bool/list[bool] (optional, default=True)
Specify whether the text should be outlined (it is recommended to outline when not filling or using ```partial_fill```, but not ```full_fill```).
Also accepts a list with one element per letter in the string.
outline_satin_thickness : int, optional, can be None
If not None, the SVG's lines will use satin stitch rather than direct stitch
flip_y : bool (optional, default=False)
Allow you to vertically flip the text if desired. Defaults to ``False``.
fill_min_x_dist : int (optional, default=10)
Advanced parameter deciding the resolution of ``partial_fill``.
If ``partial_fill`` is taking too long, consider increasing this value to make it run faster, at the cost of a lower resolution of fill.
Default value is 10, and this should never be set below 5.
fill_min_y_dist : int (optional, default=10)
Advanced parameter deciding the resolution of ``partial_fill``.
If ``partial_fill`` is taking too long, consider increasing this value to make it run faster, at the cost of a lower resolution of fill.
Default value is 10, and this should never be set below 5.
"""
self.prev_fontsize = fontsize
# this draws a multiline string, automatically drawing letter gaps as desired
# if fills is True, will fill the text with satin stitch. else, will draw the text outline
if turtle is None:
if self.turtle is None:
raise ValueError("MUST DECLARE turtle TO USE IN LetterDrawer.draw_one_letter in either draw_one_letter() or LetterDrawer() init")
turtle = self.turtle
startx, starty = turtle.position()
#print("DRAWING STRING", string)
if isinstance(colours, list):
assert len(colours) >= len(string), "'colours' in LetterDrawer.draw_string is a list; it's length must be at least length of the string! (characters like '\\n' and ' ' will not use the colour, but will still have an item on the colours list assigned to them)"
if isinstance(thicknesses, list):
assert len(thicknesses) >= len(string), "'thicknesses' in LetterDrawer.draw_string is a list; it's length must be at least length of the string! (characters like '\\n' and ' ' will not use the colour, but will still have an item on the colours list assigned to them)"
if isinstance(fills, list):
assert len(fills) >= len(string), "'fills' in LetterDrawer.draw_string is a list; it's length must be at least length of the string! (characters like '\\n' and ' ' will not consider the fill variable, but will still have an item on the fills list assigned to them)"
if isinstance(outlines, list):
assert len(outlines) >= len(string), "'outlines' in LetterDrawer.draw_string is a list; it's length must be at least length of the string! (characters like '\\n' and ' ' will not consider the outline variable, but will still have an item on theoutlines list assigned to them)"
if isinstance(letter_gaps, list):
assert len(letter_gaps) >= len(string)-1, "'letter_gaps' in LetterDrawer.draw_string is a list; it's length must be at least (length of the string - 1)! (characters like '\\n' and ' ' will not consider the outline variable, but will still have an item on theoutlines list assigned to them)"
#print("HELLO>..")
for cidx in range(len(string)-1):
if string[cidx] in ['\n', '\r']:
# newline
with turtle.jump_stitch():
starty += fontsize*LetterDrawer.line_spacing
turtle.goto(startx, starty)
continue
if isinstance(colours, str):
col = colours
else:
col = colours[cidx]
if isinstance(thicknesses, int):
thickness = thicknesses
else:
thickness = thicknesses[cidx]
if isinstance(fills, bool):
fill = fills
else:
fill = fills[cidx]
if isinstance(outlines, bool):
outline = outlines
else:
outline = outlines[cidx]
#print("DRAWING LETTER", string[cidx], "FILL", fill)
self.draw_one_letter(fontname, LetterDrawer.char_to_name(string[cidx]), fontsize, col, thickness, fill, outline, fill_min_y_dist, fill_min_x_dist, full_fill, outline_satin_thickness, turtle, flip_y)
if isinstance(letter_gaps, list):
letter_gap = letter_gaps[cidx]
else:
letter_gap = letter_gaps
self.draw_letter_gap(fontsize, letter_gap)
cidx = len(string) - 1
# draw last letter
if string[cidx] in ['\n', '\r']:
# newline
with turtle.jump_stitch():
starty += fontsize*LetterDrawer.line_spacing
turtle.goto(startx, starty)
else:
if isinstance(colours, str):
col = colours
else:
col = colours[cidx]
if isinstance(thicknesses, int):
thickness = thicknesses
else:
thickness = thicknesses[cidx]
if isinstance(fills, bool):
fill = fills
else:
fill = fills[cidx]
if isinstance(outlines, bool):
outline = outlines
else:
outline = outlines[cidx]
#print("DRAWING LETTER", string[-1], "FILL", fill)
self.draw_one_letter(fontname, LetterDrawer.char_to_name(string[cidx]), fontsize, col, thickness, fill, outline, fill_min_y_dist, fill_min_x_dist, full_fill, outline_satin_thickness, turtle, flip_y)
if isinstance(letter_gaps, list) and len(letter_gaps) > cidx: # if we have another letter gap, include it.
letter_gap = letter_gaps[cidx]
self.draw_letter_gap(fontsize, letter_gap)
punctuation_to_name = {'!': 'exclam',
'@': 'at',
'#': 'numbersign',
'$': 'dollar',
'%': 'percent',
'^': 'circumflex',
'&': 'ampersand',
'*': 'asterisk',
'(': 'parenleft',
')': 'parenright',
'{': 'braceleft',
'}': 'braceright',
'.': 'period',
',': 'comma',
'"': "quotedbl",
"'": 'quotesingle',
'?': 'question',
'<': 'guilsinglleft',
'>': 'guilsinglright',
'[': 'bracketleft',
']': 'bracketright',
'_': 'underscore',
'-': 'hyphen',
':': 'colon',
';': 'semicolon',
'/': 'slash',
'\\': 'backslash',
'+': 'plus',
'=': 'equal',
'|': 'bar',
'~': 'tilde',
'`': 'quotereversed',
'©': 'copyright',
}
@classmethod
def char_to_name(cls, char:str):
# converts a 1-length string (character) to its identifier (in the unpacked .otf/.ttf svgs)
if char == ' ':
return 'space'
if char.isdigit():
digit_to_name = {'0': 'zero',
'1': 'one',
'2': 'two',
'3': 'three',
'4': 'four',
'5': 'five',
'6': 'six',
'7': 'seven',
'8': 'eight',
'9': 'nine', }
return digit_to_name[char]
if char.isalpha():
return char # normal character
try:
return LetterDrawer.punctuation_to_name[char]
except:
raise ValueError("CANNOT RECOGNIZE CHARACTER '{}'".format(char))
def clear_fonts(self):
# clears loaded fonts, freeing up memory
for td in self.created_tmpdirs:
del td
self.created_tmpdirs = []
self.loaded_fonts = {}