1#Copyright ReportLab Europe Ltd. 2000-2019 2#see license.txt for license details 3__version__='3.4.22' 4 5#modification of users/robin/ttflist.py. 6__doc__="""This provides some general-purpose tools for finding fonts. 7 8The FontFinder object can search for font files. It aims to build 9a catalogue of fonts which our framework can work with. It may be useful 10if you are building GUIs or design-time interfaces and want to present users 11with a choice of fonts. 12 13There are 3 steps to using it 141. create FontFinder and set options and directories 152. search 163. query 17 18>>> import fontfinder 19>>> ff = fontfinder.FontFinder() 20>>> ff.addDirectories([dir1, dir2, dir3]) 21>>> ff.search() 22>>> ff.getFamilyNames() #or whichever queries you want... 23 24Because the disk search takes some time to find and parse hundreds of fonts, 25it can use a cache to store a file with all fonts found. The cache file name 26 27For each font found, it creates a structure with 28- the short font name 29- the long font name 30- the principal file (.pfb for type 1 fonts), and the metrics file if appropriate 31- the time modified (unix time stamp) 32- a type code ('ttf') 33- the family name 34- bold and italic attributes 35 36One common use is to display families in a dialog for end users; 37then select regular, bold and italic variants of the font. To get 38the initial list, use getFamilyNames; these will be in alpha order. 39 40>>> ff.getFamilyNames() 41['Bitstream Vera Sans', 'Century Schoolbook L', 'Dingbats', 'LettErrorRobot', 42'MS Gothic', 'MS Mincho', 'Nimbus Mono L', 'Nimbus Roman No9 L', 43'Nimbus Sans L', 'Vera', 'Standard Symbols L', 44'URW Bookman L', 'URW Chancery L', 'URW Gothic L', 'URW Palladio L'] 45 46One can then obtain a specific font as follows 47 48>>> f = ff.getFont('Bitstream Vera Sans', bold=False, italic=True) 49>>> f.fullName 50'Bitstream Vera Sans' 51>>> f.fileName 52'C:\\code\\reportlab\\fonts\\Vera.ttf' 53>>> 54 55It can also produce an XML report of fonts found by family, for the benefit 56of non-Python applications. 57 58Future plans might include using this to auto-register fonts; and making it 59update itself smartly on repeated instantiation. 60""" 61import sys, os, tempfile 62from reportlab.lib.utils import pickle, asNative as _asNative 63from xml.sax.saxutils import quoteattr 64from reportlab.lib.utils import asBytes 65try: 66 from time import process_time as clock 67except ImportError: 68 from time import clock 69try: 70 from hashlib import md5 71except ImportError: 72 from md5 import md5 73 74def asNative(s): 75 try: 76 return _asNative(s) 77 except: 78 return _asNative(s,enc='latin-1') 79 80EXTENSIONS = ['.ttf','.ttc','.otf','.pfb','.pfa'] 81 82# PDF font flags (see PDF Reference Guide table 5.19) 83FF_FIXED = 1 << 1-1 84FF_SERIF = 1 << 2-1 85FF_SYMBOLIC = 1 << 3-1 86FF_SCRIPT = 1 << 4-1 87FF_NONSYMBOLIC = 1 << 6-1 88FF_ITALIC = 1 << 7-1 89FF_ALLCAP = 1 << 17-1 90FF_SMALLCAP = 1 << 18-1 91FF_FORCEBOLD = 1 << 19-1 92 93class FontDescriptor: 94 """This is a short descriptive record about a font. 95 96 typeCode should be a file extension e.g. ['ttf','ttc','otf','pfb','pfa'] 97 """ 98 def __init__(self): 99 self.name = None 100 self.fullName = None 101 self.familyName = None 102 self.styleName = None 103 self.isBold = False #true if it's somehow bold 104 self.isItalic = False #true if it's italic or oblique or somehow slanty 105 self.isFixedPitch = False 106 self.isSymbolic = False #false for Dingbats, Symbols etc. 107 108 self.typeCode = None #normally the extension minus the dot 109 self.fileName = None #full path to where we found it. 110 self.metricsFileName = None #defined only for type='type1pc', or 'type1mac' 111 112 self.timeModified = 0 113 114 def __repr__(self): 115 return "FontDescriptor(%s)" % self.name 116 117 def getTag(self): 118 "Return an XML tag representation" 119 attrs = [] 120 for k, v in self.__dict__.items(): 121 if k not in ['timeModified']: 122 if v: 123 attrs.append('%s=%s' % (k, quoteattr(str(v)))) 124 return '<font ' + ' '.join(attrs) + '/>' 125 126from reportlab.lib.utils import rl_isdir, rl_isfile, rl_listdir, rl_getmtime 127class FontFinder: 128 def __init__(self, dirs=[], useCache=True, validate=False, recur=False, fsEncoding=None, verbose=0): 129 self.useCache = useCache 130 self.validate = validate 131 if fsEncoding is None: 132 fsEncoding = sys.getfilesystemencoding() 133 self._fsEncoding = fsEncoding or 'utf8' 134 135 self._dirs = set() 136 self._recur = recur 137 self.addDirectories(dirs) 138 self._fonts = [] 139 140 self._skippedFiles = [] #list of filenames we did not handle 141 self._badFiles = [] #list of filenames we rejected 142 143 self._fontsByName = {} 144 self._fontsByFamily = {} 145 self._fontsByFamilyBoldItalic = {} #indexed by bold, italic 146 self.verbose = verbose 147 148 def addDirectory(self, dirName, recur=None): 149 #aesthetics - if there are 2 copies of a font, should the first or last 150 #be picked up? might need reversing 151 if rl_isdir(dirName): 152 self._dirs.add(dirName) 153 if recur if recur is not None else self._recur: 154 for r,D,F in os.walk(dirName): 155 for d in D: 156 self._dirs.add(os.path.join(r,d)) 157 158 def addDirectories(self, dirNames,recur=None): 159 for dirName in dirNames: 160 self.addDirectory(dirName,recur=recur) 161 162 def getFamilyNames(self): 163 "Returns a list of the distinct font families found" 164 if not self._fontsByFamily: 165 fonts = self._fonts 166 for font in fonts: 167 fam = font.familyName 168 if fam is None: continue 169 if fam in self._fontsByFamily: 170 self._fontsByFamily[fam].append(font) 171 else: 172 self._fontsByFamily[fam] = [font] 173 fsEncoding = self._fsEncoding 174 names = list(asBytes(_,enc=fsEncoding) for _ in self._fontsByFamily.keys()) 175 names.sort() 176 return names 177 178 def getFontsInFamily(self, familyName): 179 "Return list of all font objects with this family name" 180 return self._fontsByFamily.get(familyName,[]) 181 182 def getFamilyXmlReport(self): 183 """Reports on all families found as XML. 184 """ 185 lines = [] 186 lines.append('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>') 187 lines.append("<font_families>") 188 for dirName in self._dirs: 189 lines.append(" <directory name=%s/>" % quoteattr(asNative(dirName))) 190 for familyName in self.getFamilyNames(): 191 if familyName: #skip null case 192 lines.append(' <family name=%s>' % quoteattr(asNative(familyName))) 193 for font in self.getFontsInFamily(familyName): 194 lines.append(' ' + font.getTag()) 195 lines.append(' </family>') 196 lines.append("</font_families>") 197 return '\n'.join(lines) 198 199 def getFontsWithAttributes(self, **kwds): 200 """This is a general lightweight search.""" 201 selected = [] 202 for font in self._fonts: 203 OK = True 204 for k, v in kwds.items(): 205 if getattr(font, k, None) != v: 206 OK = False 207 if OK: 208 selected.append(font) 209 return selected 210 211 def getFont(self, familyName, bold=False, italic=False): 212 """Try to find a font matching the spec""" 213 214 for font in self._fonts: 215 if font.familyName == familyName: 216 if font.isBold == bold: 217 if font.isItalic == italic: 218 return font 219 220 raise KeyError("Cannot find font %s with bold=%s, italic=%s" % (familyName, bold, italic)) 221 222 def _getCacheFileName(self): 223 """Base this on the directories...same set of directories 224 should give same cache""" 225 fsEncoding = self._fsEncoding 226 hash = md5(b''.join(asBytes(_,enc=fsEncoding) for _ in sorted(self._dirs))).hexdigest() 227 from reportlab.lib.utils import get_rl_tempfile 228 fn = get_rl_tempfile('fonts_%s.dat' % hash) 229 return fn 230 231 def save(self, fileName): 232 f = open(fileName, 'wb') 233 pickle.dump(self, f) 234 f.close() 235 236 def load(self, fileName): 237 f = open(fileName, 'rb') 238 finder2 = pickle.load(f) 239 f.close() 240 self.__dict__.update(finder2.__dict__) 241 242 def search(self): 243 if self.verbose: 244 started = clock() 245 if not self._dirs: 246 raise ValueError("Font search path is empty! Please specify search directories using addDirectory or addDirectories") 247 248 if self.useCache: 249 cfn = self._getCacheFileName() 250 if rl_isfile(cfn): 251 try: 252 self.load(cfn) 253 if self.verbose>=3: 254 print("loaded cached file with %d fonts (%s)" % (len(self._fonts), cfn)) 255 return 256 except: 257 pass #pickle load failed. Ho hum, maybe it's an old pickle. Better rebuild it. 258 259 from stat import ST_MTIME 260 for dirName in self._dirs: 261 try: 262 fileNames = rl_listdir(dirName) 263 except: 264 continue 265 for fileName in fileNames: 266 root, ext = os.path.splitext(fileName) 267 if ext.lower() in EXTENSIONS: 268 #it's a font 269 f = FontDescriptor() 270 f.fileName = fileName = os.path.normpath(os.path.join(dirName, fileName)) 271 try: 272 f.timeModified = rl_getmtime(fileName) 273 except: 274 self._skippedFiles.append(fileName) 275 continue 276 277 ext = ext.lower() 278 if ext[0] == '.': 279 ext = ext[1:] 280 f.typeCode = ext #strip the dot 281 282 #what to do depends on type. We only accept .pfb if we 283 #have .afm to go with it, and don't handle .otf now. 284 285 if ext in ('otf', 'pfa'): 286 self._skippedFiles.append(fileName) 287 288 elif ext in ('ttf','ttc'): 289 #parsing should check it for us 290 from reportlab.pdfbase.ttfonts import TTFontFile, TTFError 291 try: 292 font = TTFontFile(fileName,validate=self.validate) 293 except TTFError: 294 self._badFiles.append(fileName) 295 continue 296 f.name = font.name 297 f.fullName = font.fullName 298 f.styleName = font.styleName 299 f.familyName = font.familyName 300 f.isBold = (FF_FORCEBOLD == FF_FORCEBOLD & font.flags) 301 f.isItalic = (FF_ITALIC == FF_ITALIC & font.flags) 302 303 elif ext == 'pfb': 304 305 # type 1; we need an AFM file or have to skip. 306 if rl_isfile(os.path.join(dirName, root + '.afm')): 307 f.metricsFileName = os.path.normpath(os.path.join(dirName, root + '.afm')) 308 elif rl_isfile(os.path.join(dirName, root + '.AFM')): 309 f.metricsFileName = os.path.normpath(os.path.join(dirName, root + '.AFM')) 310 else: 311 self._skippedFiles.append(fileName) 312 continue 313 from reportlab.pdfbase.pdfmetrics import parseAFMFile 314 315 (info, glyphs) = parseAFMFile(f.metricsFileName) 316 f.name = info['FontName'] 317 f.fullName = info.get('FullName', f.name) 318 f.familyName = info.get('FamilyName', None) 319 f.isItalic = (float(info.get('ItalicAngle', 0)) > 0.0) 320 #if the weight has the word bold, deem it bold 321 f.isBold = ('bold' in info.get('Weight','').lower()) 322 323 self._fonts.append(f) 324 if self.useCache: 325 self.save(cfn) 326 327 if self.verbose: 328 finished = clock() 329 print("found %d fonts; skipped %d; bad %d. Took %0.2f seconds" % ( 330 len(self._fonts), len(self._skippedFiles), len(self._badFiles), 331 finished - started 332 )) 333 334def test(): 335 #windows-centric test maybe 336 from reportlab import rl_config 337 ff = FontFinder(verbose=rl_config.verbose) 338 ff.useCache = True 339 ff.validate = True 340 341 import reportlab 342 ff.addDirectory('C:\\windows\\fonts') 343 rlFontDir = os.path.join(os.path.dirname(reportlab.__file__), 'fonts') 344 ff.addDirectory(rlFontDir) 345 ff.search() 346 347 print('cache file name...') 348 print(ff._getCacheFileName()) 349 350 print('families...') 351 for familyName in ff.getFamilyNames(): 352 print('\t%s' % familyName) 353 354 print() 355 outw = sys.stdout.write 356 outw('fonts called Vera:') 357 for font in ff.getFontsInFamily('Bitstream Vera Sans'): 358 outw(' %s' % font.name) 359 print() 360 outw('Bold fonts\n\t') 361 for font in ff.getFontsWithAttributes(isBold=True, isItalic=False): 362 outw(font.fullName+' ') 363 print() 364 print('family report') 365 print(ff.getFamilyXmlReport()) 366 367if __name__=='__main__': 368 test() 369