flatcam/appParsers/ParseFont.py

355 lines
12 KiB
Python

# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
# ######################################################################
# ## Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ #
# ## and made it work with Python 3 #
# ######################################################################
import re
import os
import sys
import glob
from shapely.geometry import Polygon
from shapely.affinity import translate, scale
from shapely.geometry import MultiPolygon
import freetype as ft
from fontTools import ttLib
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base2')
class ParseFont:
FONT_SPECIFIER_NAME_ID = 4
FONT_SPECIFIER_FAMILY_ID = 1
@staticmethod
def get_win32_font_path():
"""Get User-specific font directory on Win32"""
try:
import winreg
except ImportError:
return os.path.join(os.environ['WINDIR'], 'Fonts')
else:
k = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
try:
# should check that k is valid? How?
return winreg.QueryValueEx(k, "Fonts")[0]
finally:
winreg.CloseKey(k)
@staticmethod
def get_linux_font_paths():
"""Get system font directories on Linux/Unix
Uses /usr/sbin/chkfontpath to get the list
of system-font directories, note that many
of these will *not* be truetype font directories.
If /usr/sbin/chkfontpath isn't available, uses
returns a set of common Linux/Unix paths
"""
executable = '/usr/sbin/chkfontpath'
if os.path.isfile(executable):
data = os.popen(executable).readlines()
match = re.compile('\d+: (.+)')
set_lst = []
for line in data:
result = match.match(line)
if result:
set_lst.append(result.group(1))
return set_lst
else:
directories = [
# what seems to be the standard installation point
"/usr/X11R6/lib/X11/fonts/TTF/",
# common application, not really useful
"/usr/lib/openoffice/share/fonts/truetype/",
# documented as a good place to install new fonts...
"/usr/share/fonts",
"/usr/local/share/fonts",
# seems to be where fonts are installed for an individual user?
"~/.fonts",
]
dir_set = []
for directory in directories:
directory = os.path.expanduser(os.path.expandvars(directory))
try:
if os.path.isdir(directory):
for path, children, files in os.walk(directory):
dir_set.append(path)
except (IOError, OSError, TypeError, ValueError):
pass
return dir_set
@staticmethod
def get_mac_font_paths():
"""Get system font directories on MacOS
"""
directories = [
# okay, now the OS X variants...
"~/Library/Fonts/",
"/Library/Fonts/",
"/Network/Library/Fonts/",
"/System/Library/Fonts/",
"System Folder:Fonts:",
]
dir_set = []
for directory in directories:
directory = os.path.expanduser(os.path.expandvars(directory))
try:
if os.path.isdir(directory):
for path, children, files in os.walk(directory):
dir_set.append(path)
except (IOError, OSError, TypeError, ValueError):
pass
return dir_set
@staticmethod
def get_win32_fonts(font_directory=None):
"""Get list of explicitly *installed* font names"""
import winreg
if font_directory is None:
font_directory = ParseFont.get_win32_font_path()
k = None
items = {}
for keyName in (
r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts",
):
try:
k = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
keyName
)
except OSError:
pass
if not k:
# couldn't open either WinNT or Win98 key???
return glob.glob(os.path.join(font_directory, '*.ttf'))
try:
# should check that k is valid? How?
for index in range(winreg.QueryInfoKey(k)[1]):
key, value, _ = winreg.EnumValue(k, index)
if not os.path.dirname(value):
value = os.path.join(font_directory, value)
value = os.path.abspath(value).lower()
if value[-4:] == '.ttf':
items[value] = 1
return list(items.keys())
finally:
winreg.CloseKey(k)
@staticmethod
def get_font_name(font_path):
"""
Get the short name from the font's names table
From 'https://github.com/gddc/ttfquery/blob/master/ttfquery/describe.py'
and
http://www.starrhorne.com/2012/01/18/
how-to-extract-font-names-from-ttf-files-using-python-and-our-old-friend-the-command-line.html
ported to Python 3 here: https://gist.github.com/pklaus/dce37521579513c574d0
"""
name = ""
family = ""
font = ttLib.TTFont(font_path)
for record in font['name'].names:
if b'\x00' in record.string:
name_str = record.string.decode('utf-16-be')
else:
# name_str = record.string.decode('utf-8')
name_str = record.string.decode('latin-1')
if record.nameID == ParseFont.FONT_SPECIFIER_NAME_ID and not name:
name = name_str
elif record.nameID == ParseFont.FONT_SPECIFIER_FAMILY_ID and not family:
family = name_str
if name and family:
break
return name, family
def __init__(self, app):
super(ParseFont, self).__init__()
self.app = app
# regular fonts
self.regular_f = {}
# bold fonts
self.bold_f = {}
# italic fonts
self.italic_f = {}
# bold and italic fonts
self.bold_italic_f = {}
def get_fonts(self, paths=None):
"""
Find fonts in paths, or the system paths if not given
"""
files = {}
if paths is None:
if sys.platform == 'win32':
font_directory = ParseFont.get_win32_font_path()
paths = [font_directory, ]
# now get all installed fonts directly...
for f in self.get_win32_fonts(font_directory):
files[f] = 1
elif sys.platform == 'linux':
paths = ParseFont.get_linux_font_paths()
else:
paths = ParseFont.get_mac_font_paths()
elif isinstance(paths, str):
paths = [paths]
for path in paths:
for file in glob.glob(os.path.join(path, '*.ttf')):
files[os.path.abspath(file)] = 1
return list(files.keys())
def get_fonts_by_types(self):
system_fonts = self.get_fonts()
# split the installed fonts by type: regular, bold, italic (oblique), bold-italic and
# store them in separate dictionaries {name: file_path/filename.ttf}
for font in system_fonts:
try:
name, family = ParseFont.get_font_name(font)
except Exception as e:
log.debug("ParseFont.get_fonts_by_types() --> Could not get the font name. %s" % str(e))
continue
if 'Bold' in name and 'Italic' in name:
name = name.replace(" Bold Italic", '')
self.bold_italic_f.update({name: font})
elif 'Bold' in name and 'Oblique' in name:
name = name.replace(" Bold Oblique", '')
self.bold_italic_f.update({name: font})
elif 'Bold' in name:
name = name.replace(" Bold", '')
self.bold_f.update({name: font})
elif 'SemiBold' in name:
name = name.replace(" SemiBold", '')
self.bold_f.update({name: font})
elif 'DemiBold' in name:
name = name.replace(" DemiBold", '')
self.bold_f.update({name: font})
elif 'Demi' in name:
name = name.replace(" Demi", '')
self.bold_f.update({name: font})
elif 'Italic' in name:
name = name.replace(" Italic", '')
self.italic_f.update({name: font})
elif 'Oblique' in name:
name = name.replace(" Italic", '')
self.italic_f.update({name: font})
else:
try:
name = name.replace(" Regular", '')
except Exception:
pass
self.regular_f.update({name: font})
log.debug("Font parsing is finished.")
def font_to_geometry(self, char_string, font_name, font_type, font_size, units='MM', coordx=0, coordy=0):
path = []
scaled_path = []
path_filename = ""
regular_dict = self.regular_f
bold_dict = self.bold_f
italic_dict = self.italic_f
bold_italic_dict = self.bold_italic_f
try:
if font_type == 'bi':
path_filename = bold_italic_dict[font_name]
elif font_type == 'bold':
path_filename = bold_dict[font_name]
elif font_type == 'italic':
path_filename = italic_dict[font_name]
elif font_type == 'regular':
path_filename = regular_dict[font_name]
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Font not supported, try another one."))
log.debug("[ERROR_NOTCL] Font Loading: %s" % str(e))
return "flatcam font parse failed"
face = ft.Face(path_filename)
face.set_char_size(int(font_size) * 64)
pen_x = coordx
previous = 0
# done as here: https://www.freetype.org/freetype2/docs/tutorial/step2.html
for char in char_string:
glyph_index = face.get_char_index(char)
try:
if previous > 0 and glyph_index > 0:
delta = face.get_kerning(previous, glyph_index)
pen_x += delta.x
except Exception:
pass
face.load_glyph(glyph_index)
# face.load_char(char, flags=8)
slot = face.glyph
outline = slot.outline
start, end = 0, 0
for i in range(len(outline.contours)):
end = outline.contours[i]
points = outline.points[start:end + 1]
points.append(points[0])
char_geo = Polygon(points)
char_geo = translate(char_geo, xoff=pen_x, yoff=coordy)
path.append(char_geo)
start = end + 1
pen_x += slot.advance.x
previous = glyph_index
for item in path:
if units == 'MM':
scaled_path.append(scale(item, 0.0080187969924812, 0.0080187969924812, origin=(coordx, coordy)))
else:
scaled_path.append(scale(item, 0.00031570066, 0.00031570066, origin=(coordx, coordy)))
return MultiPolygon(scaled_path)