ParseFont.py 11 KB

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