# $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()