ParseFont.py 12 KB

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