ParseFont.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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, parent=None):
  158. super(ParseFont, self).__init__()
  159. # regular fonts
  160. self.regular_f = {}
  161. # bold fonts
  162. self.bold_f = {}
  163. # italic fonts
  164. self.italic_f = {}
  165. # bold and italic fonts
  166. self.bold_italic_f = {}
  167. def get_fonts(self, paths=None):
  168. """
  169. Find fonts in paths, or the system paths if not given
  170. """
  171. files = {}
  172. if paths is None:
  173. if sys.platform == 'win32':
  174. font_directory = ParseFont.get_win32_font_path()
  175. paths = [font_directory,]
  176. # now get all installed fonts directly...
  177. for f in self.get_win32_fonts(font_directory):
  178. files[f] = 1
  179. elif sys.platform == 'linux':
  180. paths = ParseFont.get_linux_font_paths()
  181. else:
  182. paths = ParseFont.get_mac_font_paths()
  183. elif isinstance(paths, str):
  184. paths = [paths]
  185. for path in paths:
  186. for file in glob.glob(os.path.join(path, '*.ttf')):
  187. files[os.path.abspath(file)] = 1
  188. return list(files.keys())
  189. def get_fonts_by_types(self):
  190. system_fonts = self.get_fonts()
  191. # split the installed fonts by type: regular, bold, italic (oblique), bold-italic and
  192. # store them in separate dictionaries {name: file_path/filename.ttf}
  193. for font in system_fonts:
  194. name, family = ParseFont.get_font_name(font)
  195. if 'Bold' in name and 'Italic' in name:
  196. name = name.replace(" Bold Italic", '')
  197. self.bold_italic_f.update({name: font})
  198. elif 'Bold' in name and 'Oblique' in name:
  199. name = name.replace(" Bold Oblique", '')
  200. self.bold_italic_f.update({name: font})
  201. elif 'Bold' in name:
  202. name = name.replace(" Bold", '')
  203. self.bold_f.update({name: font})
  204. elif 'SemiBold' in name:
  205. name = name.replace(" SemiBold", '')
  206. self.bold_f.update({name: font})
  207. elif 'DemiBold' in name:
  208. name = name.replace(" DemiBold", '')
  209. self.bold_f.update({name: font})
  210. elif 'Demi' in name:
  211. name = name.replace(" Demi", '')
  212. self.bold_f.update({name: font})
  213. elif 'Italic' in name:
  214. name = name.replace(" Italic", '')
  215. self.italic_f.update({name: font})
  216. elif 'Oblique' in name:
  217. name = name.replace(" Italic", '')
  218. self.italic_f.update({name: font})
  219. else:
  220. try:
  221. name = name.replace(" Regular", '')
  222. except:
  223. pass
  224. self.regular_f.update({name: font})
  225. log.debug("Font parsing is finished.")
  226. def font_to_geometry(self, char_string, font_name, font_type, font_size, units='MM', coordx=0, coordy=0):
  227. path = []
  228. scaled_path = []
  229. path_filename = ""
  230. regular_dict = self.regular_f
  231. bold_dict = self.bold_f
  232. italic_dict = self.italic_f
  233. bold_italic_dict = self.bold_italic_f
  234. try:
  235. if font_type == 'bi':
  236. path_filename = bold_italic_dict[font_name]
  237. elif font_type == 'bold':
  238. path_filename = bold_dict[font_name]
  239. elif font_type == 'italic':
  240. path_filename = italic_dict[font_name]
  241. elif font_type == 'regular':
  242. path_filename = regular_dict[font_name]
  243. except Exception as e:
  244. log.debug("[error_notcl] Font Loading: %s" % str(e))
  245. return"[ERROR] Font Loading: %s" % str(e)
  246. face = ft.Face(path_filename)
  247. face.set_char_size(int(font_size) * 64)
  248. pen_x = coordx
  249. previous = 0
  250. # done as here: https://www.freetype.org/freetype2/docs/tutorial/step2.html
  251. for char in char_string:
  252. glyph_index = face.get_char_index(char)
  253. try:
  254. if previous > 0 and glyph_index > 0:
  255. delta = face.get_kerning(previous, glyph_index)
  256. pen_x += delta.x
  257. except:
  258. pass
  259. face.load_glyph(glyph_index)
  260. # face.load_char(char, flags=8)
  261. slot = face.glyph
  262. outline = slot.outline
  263. start, end = 0, 0
  264. for i in range(len(outline.contours)):
  265. end = outline.contours[i]
  266. points = outline.points[start:end + 1]
  267. points.append(points[0])
  268. char_geo = Polygon(points)
  269. char_geo = translate(char_geo, xoff=pen_x, yoff=coordy)
  270. path.append(char_geo)
  271. start = end + 1
  272. pen_x += slot.advance.x
  273. previous = glyph_index
  274. for item in path:
  275. if units == 'MM':
  276. scaled_path.append(scale(item, 0.0080187969924812, 0.0080187969924812, origin=(coordx, coordy)))
  277. else:
  278. scaled_path.append(scale(item, 0.00031570066, 0.00031570066, origin=(coordx, coordy)))
  279. return MultiPolygon(scaled_path)