2019-05-30 18:05:12 +00:00
|
|
|
# ########################################################## ##
|
2019-03-10 13:22:16 +00:00
|
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
|
|
# http://flatcam.org #
|
|
|
|
# File Author: Marius Adrian Stanciu (c) #
|
|
|
|
# Date: 3/10/2019 #
|
|
|
|
# MIT Licence #
|
2019-05-30 18:05:12 +00:00
|
|
|
# ########################################################## ##
|
2019-03-10 13:22:16 +00:00
|
|
|
|
2019-05-30 18:05:12 +00:00
|
|
|
# ####################################################################### ##
|
|
|
|
# ## Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ # ##
|
|
|
|
# ## and made it work with Python 3 ########### ##
|
|
|
|
# ####################################################################### ##
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
import re, os, sys, glob
|
|
|
|
import itertools
|
|
|
|
|
|
|
|
from shapely.geometry import Point, Polygon
|
|
|
|
from shapely.affinity import translate, scale, rotate
|
|
|
|
from shapely.geometry import MultiPolygon
|
|
|
|
from shapely.geometry.base import BaseGeometry
|
|
|
|
|
|
|
|
import freetype as ft
|
|
|
|
from fontTools import ttLib
|
|
|
|
|
|
|
|
import logging
|
2019-09-10 00:20:14 +00:00
|
|
|
import gettext
|
|
|
|
import FlatCAMTranslation as fcTranslate
|
|
|
|
import builtins
|
|
|
|
|
|
|
|
fcTranslate.apply_language('strings')
|
|
|
|
if '_' not in builtins.__dict__:
|
|
|
|
_ = gettext.gettext
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
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 = []
|
|
|
|
for line in data:
|
|
|
|
result = match.match(line)
|
|
|
|
if result:
|
|
|
|
set.append(result.group(1))
|
|
|
|
return set
|
|
|
|
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 = 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 = 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 as err:
|
|
|
|
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
|
|
|
|
|
2019-01-11 02:02:02 +00:00
|
|
|
def __init__(self, app, parent=None):
|
2019-01-03 19:25:08 +00:00
|
|
|
super(ParseFont, self).__init__()
|
|
|
|
|
2019-01-11 02:02:02 +00:00
|
|
|
self.app = app
|
|
|
|
|
2019-01-03 19:25:08 +00:00
|
|
|
# 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:
|
2019-01-20 23:17:04 +00:00
|
|
|
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
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
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", '')
|
2019-07-19 18:46:11 +00:00
|
|
|
except Exception as e:
|
2019-01-03 19:25:08 +00:00
|
|
|
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:
|
2019-09-10 00:20:14 +00:00
|
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Font not supported, try another one."))
|
2019-02-03 13:13:09 +00:00
|
|
|
log.debug("[ERROR_NOTCL] Font Loading: %s" % str(e))
|
2019-01-11 02:02:02 +00:00
|
|
|
return "flatcam font parse failed"
|
2019-01-03 19:25:08 +00:00
|
|
|
|
|
|
|
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
|
2019-07-19 18:46:11 +00:00
|
|
|
except Exception as e:
|
2019-01-03 19:25:08 +00:00
|
|
|
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)
|