ParseFont.py 12 KB

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