ParseFont.py 12 KB

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