# $Id$ # font source for aggdraw # based on the ImageFont cache from the RasterCanvas package ## # This is a helper module that maintains a (family, style) to font # file mapping for the TrueType fonts on your machine. # #
# source = FontSource(cachefile="fontcache.dat")
# font = source.load("black", "times", 10, "bold")
# 
## # -------------------------------------------------------------------- import marshal, os, stat, sys, time # determine default font path for this platform PATH = None if sys.platform == "win32": # check the windows font repository windir = os.environ.get("WINDIR") if windir: PATH = [os.path.join(windir, "fonts")] else: # assume unix PATH = ( "/usr/share/fonts/default/TrueType", # FIXME: add more locations here ) PATH = filter(os.path.isdir, PATH) ## # (Helper) Gets the file signature (size, timestamp) for a given file. def _getsignature(filename): try: info = os.stat(filename) except OSError: return None, None return ( info[stat.ST_SIZE], time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(info[stat.ST_MTIME])), ) ## # Font source base class. # # @keyparam path Font search path. The default is platform dependent, # and is probably most reliable on Windows. # @keyparam cachefile The name of the cache file. If omitted, font # file data is only cached in memory. # @keyparam logger If given, a function that takes a string and logs # it somewhere. If omitted, logging is disabled. class FontSourceBase: # always change this line if you change the cache layout CACHEMAGIC = "fontsource-fileinfo-20060318\n" def __init__(self, path=None, cachefile=None, logger=None): self.cachefile = cachefile if not path: path = PATH self.path = path self.logger = logger self.load_fileinfo() if not self.fileinfo: self.scan() self.make_fontinfo() self.fontcache = {} ## # (Internal) Loads the font cache from disk. def load_fileinfo(self): if not self.cachefile: return try: file = open(self.cachefile, "rb") magic = file.readline() if magic != self.CACHEMAGIC: if self.logger: self.logger( "Reload cache (old version was '%s')" % magic.strip() ) raise IOError self.fileinfo = marshal.load(file) file.close() except: self.fileinfo = {} ## # (Internal) Saves the font cache to disk. def save_fileinfo(self): if not self.cachefile: return file = open(self.cachefile, "wb") file.write(self.CACHEMAGIC) marshal.dump(self.fileinfo, file) file.close() ## # (Internal) Updates the fontinfo attribute. def make_fontinfo(self): self.fontinfo = {} for file, (signature, family, style) in self.fileinfo.items(): self.fontinfo[family, style] = file ## # (Internal) Scans the font directories, looking for new or changed # font files. def scan(self, purge=0): if purge: self.fileinfo = {} elif not self.fileinfo: self.load_fileinfo() dirty = update = 0 for fontdir in self.path: try: files = os.listdir(fontdir) except OSError: continue for file in files: if file.endswith(".fon"): continue # skip old-style font files (windows) filename = os.path.normpath(os.path.join(fontdir, file)) signature = _getsignature(filename) old_signature = self.fileinfo.get(filename) if old_signature: old_signature = old_signature[0] if signature is None or signature == old_signature: continue # no information; update the cache try: family, style = self._getfontstyle(filename) except IOError: self.fileinfo[filename] = signature, None, None dirty = 1 else: family = family.lower() style = style.lower() if not update and self.logger: self.logger("Updating font cache...") self.fileinfo[filename] = signature, family, style dirty = 1 update = update + 1 # update persistent cache if dirty: if update and self.logger: self.logger( "Update ok. Added %d new fonts." % update, ) self.save_fileinfo() self.make_fontinfo() ## # Finds a font file based on font family name and style. # # @param family The font family. # @param style The font style; most fonts support "regular", "bold", # "italic", or "bold italic". # @return The name of the most closely corresponding font file. # @exception KeyError If the font could not be located. def find(self, family, style="regular"): if not self.fileinfo: self.load_fileinfo() family = family.lower() style = style.lower() try: return self.fontinfo[family, style] except KeyError: pass if sys.platform == "win32": # check windows font aliases if family == "times" or family == "serif": family = "times new roman" if family == "helvetica" or family == "sans-serif": family = "arial" try: return self.fontinfo[family, style] except KeyError: pass # update cache and check again self.scan() return self.fontinfo[family, style] ## # Returns a list of available font family names and corresponding # styles. # # @return A list of (family name, list of styles) tuples. The font # list is not sorted. def listfonts(self): self.scan() fonts = {} for signature, family, style in self.fileinfo.values(): if family and style: try: fonts[family].append(style) except KeyError: fonts[family] = [style] return fonts.items() ## # (Internal) Parse a Tkinter-style font descriptor into family, # size, and style parameters. def _parsetkfont(self, font): DEFAULT_SIZE = 10 if font[:1] == "{": font = font[1:].split("}", 1) font[0] = font[0].strip() if len(font) == 2: font[1:] = font[1].split() else: font = font.split() family = font[0] if len(font) == 1: size = DEFAULT_SIZE else: try: size = int(font[1]) del font[1] except ValueError: size = DEFAULT_SIZE style = " ".join([s.lower() for s in font[1:]]) or None return family, size, style ## # (Hook) Get family and style for a given font file. # # @param filename Font file. # @return A 2-tuple containing the font family and the style. # @exception IOError If the file could not be opened. def _getfontstyle(self, filename): raise NotImplementedError # -------------------------------------------------------------------- ## # Font source for aggdraw. See {@link FontSourceBase} for standard # methods. class FontSource(FontSourceBase): def _getfontstyle(self, filename): import aggdraw font = aggdraw.Font("black", filename) return font.family, font.style ## # Loads a font object based on font family name and style. # # @param color The font color. # @param family The font family. # @param size The font size, in pixels. # @param style The font style; one of "regular", "bold", "italic", # or "bold italic". # @return An aggdraw Font object. # @exception KeyError If the font could not be located. def load(self, color, family, size, style=None): import aggdraw if not style: style = "regular" filename = self.find(family, style) key = color, filename, size font = self.fontcache.get(key) if font is None: self.fontcache[key] = font = aggdraw.Font(color, filename, size) return font ## # Same as {@link load}, but uses a Tkinter-style font specifier # instead of separate values. # # @param color The font color. # @param font A font specifier string. def loadfont(self, color, font): return self.load(color, *self._parsetkfont(font)) ## # Creates an application-global caching font source. def getsource(**options): global _source if _source is None: _source = FontSource(**options) return _source _source = None # -------------------------------------------------------------------- if __name__ == "__main__": def test(): def logger(text): print "===", "SOURCE", text source = FontSource(cachefile="cache.dat", logger=logger) for family, styles in source.listfonts(): print "*", family, styles if 1: for style in styles: font = source.load("black", family, 10, style) tkfont = "{%s} %s %s" % (family, 10, style) font = source.loadfont("black", tkfont) test()