ParseFont.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 3/10/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. # ######################################################################
  8. # ## Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ #
  9. # ## and made it work with Python 3 #
  10. # ######################################################################
  11. import re
  12. import os
  13. import sys
  14. import glob
  15. from shapely.geometry import Polygon
  16. from shapely.affinity import translate, scale
  17. from shapely.geometry import MultiPolygon
  18. import freetype as ft
  19. from fontTools import ttLib
  20. import logging
  21. import gettext
  22. import appTranslation as fcTranslate
  23. import builtins
  24. fcTranslate.apply_language('strings')
  25. if '_' not in builtins.__dict__:
  26. _ = gettext.gettext
  27. log = logging.getLogger('base2')
  28. class ParseFont:
  29. FONT_SPECIFIER_NAME_ID = 4
  30. FONT_SPECIFIER_FAMILY_ID = 1
  31. @staticmethod
  32. def get_win32_font_path():
  33. """Get User-specific font directory on Win32"""
  34. try:
  35. import winreg
  36. except ImportError:
  37. return os.path.join(os.environ['WINDIR'], 'Fonts')
  38. else:
  39. k = winreg.OpenKey(
  40. winreg.HKEY_CURRENT_USER,
  41. r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
  42. try:
  43. # should check that k is valid? How?
  44. return winreg.QueryValueEx(k, "Fonts")[0]
  45. finally:
  46. winreg.CloseKey(k)
  47. @staticmethod
  48. def get_linux_font_paths():
  49. """Get system font directories on Linux/Unix
  50. Uses /usr/sbin/chkfontpath to get the list
  51. of system-font directories, note that many
  52. of these will *not* be truetype font directories.
  53. If /usr/sbin/chkfontpath isn't available, uses
  54. returns a set of common Linux/Unix paths
  55. """
  56. executable = '/usr/sbin/chkfontpath'
  57. if os.path.isfile(executable):
  58. data = os.popen(executable).readlines()
  59. match = re.compile('\d+: (.+)')
  60. set_lst = []
  61. for line in data:
  62. result = match.match(line)
  63. if result:
  64. set_lst.append(result.group(1))
  65. return set_lst
  66. else:
  67. directories = [
  68. # what seems to be the standard installation point
  69. "/usr/X11R6/lib/X11/fonts/TTF/",
  70. # common application, not really useful
  71. "/usr/lib/openoffice/share/fonts/truetype/",
  72. # documented as a good place to install new fonts...
  73. "/usr/share/fonts",
  74. "/usr/local/share/fonts",
  75. # seems to be where fonts are installed for an individual user?
  76. "~/.fonts",
  77. ]
  78. dir_set = []
  79. for directory in directories:
  80. directory = os.path.expanduser(os.path.expandvars(directory))
  81. try:
  82. if os.path.isdir(directory):
  83. for path, children, files in os.walk(directory):
  84. dir_set.append(path)
  85. except (IOError, OSError, TypeError, ValueError):
  86. pass
  87. return dir_set
  88. @staticmethod
  89. def get_mac_font_paths():
  90. """Get system font directories on MacOS
  91. """
  92. directories = [
  93. # okay, now the OS X variants...
  94. "~/Library/Fonts/",
  95. "/Library/Fonts/",
  96. "/Network/Library/Fonts/",
  97. "/System/Library/Fonts/",
  98. "System Folder:Fonts:",
  99. ]
  100. dir_set = []
  101. for directory in directories:
  102. directory = os.path.expanduser(os.path.expandvars(directory))
  103. try:
  104. if os.path.isdir(directory):
  105. for path, children, files in os.walk(directory):
  106. dir_set.append(path)
  107. except (IOError, OSError, TypeError, ValueError):
  108. pass
  109. return dir_set
  110. @staticmethod
  111. def get_win32_fonts(font_directory=None):
  112. """Get list of explicitly *installed* font names"""
  113. import winreg
  114. if font_directory is None:
  115. font_directory = ParseFont.get_win32_font_path()
  116. k = None
  117. items = {}
  118. for keyName in (
  119. r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
  120. r"SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts",
  121. ):
  122. try:
  123. k = winreg.OpenKey(
  124. winreg.HKEY_LOCAL_MACHINE,
  125. keyName
  126. )
  127. except OSError:
  128. pass
  129. if not k:
  130. # couldn't open either WinNT or Win98 key???
  131. return glob.glob(os.path.join(font_directory, '*.ttf'))
  132. try:
  133. # should check that k is valid? How?
  134. for index in range(winreg.QueryInfoKey(k)[1]):
  135. key, value, _ = winreg.EnumValue(k, index)
  136. if not os.path.dirname(value):
  137. value = os.path.join(font_directory, value)
  138. value = os.path.abspath(value).lower()
  139. if value[-4:] == '.ttf':
  140. items[value] = 1
  141. return list(items.keys())
  142. finally:
  143. winreg.CloseKey(k)
  144. @staticmethod
  145. def get_font_name(font_path):
  146. """
  147. Get the short name from the font's names table
  148. From 'https://github.com/gddc/ttfquery/blob/master/ttfquery/describe.py'
  149. and
  150. http://www.starrhorne.com/2012/01/18/
  151. how-to-extract-font-names-from-ttf-files-using-python-and-our-old-friend-the-command-line.html
  152. ported to Python 3 here: https://gist.github.com/pklaus/dce37521579513c574d0
  153. """
  154. name = ""
  155. family = ""
  156. font = ttLib.TTFont(font_path)
  157. for record in font['name'].names:
  158. if b'\x00' in record.string:
  159. name_str = record.string.decode('utf-16-be')
  160. else:
  161. # name_str = record.string.decode('utf-8')
  162. name_str = record.string.decode('latin-1')
  163. if record.nameID == ParseFont.FONT_SPECIFIER_NAME_ID and not name:
  164. name = name_str
  165. elif record.nameID == ParseFont.FONT_SPECIFIER_FAMILY_ID and not family:
  166. family = name_str
  167. if name and family:
  168. break
  169. return name, family
  170. def __init__(self, app):
  171. super(ParseFont, self).__init__()
  172. self.app = app
  173. # regular fonts
  174. self.regular_f = {}
  175. # bold fonts
  176. self.bold_f = {}
  177. # italic fonts
  178. self.italic_f = {}
  179. # bold and italic fonts
  180. self.bold_italic_f = {}
  181. def get_fonts(self, paths=None):
  182. """
  183. Find fonts in paths, or the system paths if not given
  184. """
  185. files = {}
  186. if paths is None:
  187. if sys.platform == 'win32':
  188. font_directory = ParseFont.get_win32_font_path()
  189. paths = [font_directory, ]
  190. # now get all installed fonts directly...
  191. for f in self.get_win32_fonts(font_directory):
  192. files[f] = 1
  193. elif sys.platform == 'linux':
  194. paths = ParseFont.get_linux_font_paths()
  195. else:
  196. paths = ParseFont.get_mac_font_paths()
  197. elif isinstance(paths, str):
  198. paths = [paths]
  199. for path in paths:
  200. for file in glob.glob(os.path.join(path, '*.ttf')):
  201. files[os.path.abspath(file)] = 1
  202. return list(files.keys())
  203. def get_fonts_by_types(self):
  204. system_fonts = self.get_fonts()
  205. # split the installed fonts by type: regular, bold, italic (oblique), bold-italic and
  206. # store them in separate dictionaries {name: file_path/filename.ttf}
  207. for font in system_fonts:
  208. try:
  209. name, family = ParseFont.get_font_name(font)
  210. except Exception as e:
  211. log.debug("ParseFont.get_fonts_by_types() --> Could not get the font name. %s" % str(e))
  212. continue
  213. if 'Bold' in name and 'Italic' in name:
  214. name = name.replace(" Bold Italic", '')
  215. self.bold_italic_f.update({name: font})
  216. elif 'Bold' in name and 'Oblique' in name:
  217. name = name.replace(" Bold Oblique", '')
  218. self.bold_italic_f.update({name: font})
  219. elif 'Bold' in name:
  220. name = name.replace(" Bold", '')
  221. self.bold_f.update({name: font})
  222. elif 'SemiBold' in name:
  223. name = name.replace(" SemiBold", '')
  224. self.bold_f.update({name: font})
  225. elif 'DemiBold' in name:
  226. name = name.replace(" DemiBold", '')
  227. self.bold_f.update({name: font})
  228. elif 'Demi' in name:
  229. name = name.replace(" Demi", '')
  230. self.bold_f.update({name: font})
  231. elif 'Italic' in name:
  232. name = name.replace(" Italic", '')
  233. self.italic_f.update({name: font})
  234. elif 'Oblique' in name:
  235. name = name.replace(" Italic", '')
  236. self.italic_f.update({name: font})
  237. else:
  238. try:
  239. name = name.replace(" Regular", '')
  240. except Exception:
  241. pass
  242. self.regular_f.update({name: font})
  243. log.debug("Font parsing is finished.")
  244. def font_to_geometry(self, char_string, font_name, font_type, font_size, units='MM', coordx=0, coordy=0):
  245. path = []
  246. scaled_path = []
  247. path_filename = ""
  248. regular_dict = self.regular_f
  249. bold_dict = self.bold_f
  250. italic_dict = self.italic_f
  251. bold_italic_dict = self.bold_italic_f
  252. try:
  253. if font_type == 'bi':
  254. path_filename = bold_italic_dict[font_name]
  255. elif font_type == 'bold':
  256. path_filename = bold_dict[font_name]
  257. elif font_type == 'italic':
  258. path_filename = italic_dict[font_name]
  259. elif font_type == 'regular':
  260. path_filename = regular_dict[font_name]
  261. except Exception as e:
  262. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Font not supported, try another one."))
  263. log.debug("[ERROR_NOTCL] Font Loading: %s" % str(e))
  264. return "flatcam font parse failed"
  265. face = ft.Face(path_filename)
  266. face.set_char_size(int(font_size) * 64)
  267. pen_x = coordx
  268. previous = 0
  269. # done as here: https://www.freetype.org/freetype2/docs/tutorial/step2.html
  270. for char in char_string:
  271. glyph_index = face.get_char_index(char)
  272. try:
  273. if previous > 0 and glyph_index > 0:
  274. delta = face.get_kerning(previous, glyph_index)
  275. pen_x += delta.x
  276. except Exception:
  277. pass
  278. face.load_glyph(glyph_index)
  279. # face.load_char(char, flags=8)
  280. slot = face.glyph
  281. outline = slot.outline
  282. start, end = 0, 0
  283. for i in range(len(outline.contours)):
  284. end = outline.contours[i]
  285. points = outline.points[start:end + 1]
  286. points.append(points[0])
  287. char_geo = Polygon(points)
  288. char_geo = translate(char_geo, xoff=pen_x, yoff=coordy)
  289. path.append(char_geo)
  290. start = end + 1
  291. pen_x += slot.advance.x
  292. previous = glyph_index
  293. for item in path:
  294. if units == 'MM':
  295. scaled_path.append(scale(item, 0.0080187969924812, 0.0080187969924812, origin=(coordx, coordy)))
  296. else:
  297. scaled_path.append(scale(item, 0.00031570066, 0.00031570066, origin=(coordx, coordy)))
  298. return MultiPolygon(scaled_path)