# Copyright (C) 2002-2006 Alexei Gilchrist and Paul Cochrane # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # $Id: afm.py,v 1.14 2006/04/24 14:20:20 paultcochrane Exp $ """ Module for reading and writing AFM files. """ __revision__ = '$Revision: 1.14 $' # It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics # File Format Specification). Still, it should read most "common" AFM files. # Taken and adapted from afmLib.py in fonttools by Just van Rossum import re, os, types, cPickle, sys from pyscript.base import FontError import pyscript FONTDIR = os.path.join(pyscript.__path__[0], "fonts") # every single line starts with a "word" identifierRE = re.compile("^([A-Za-z]+).*") # regular expression to parse char lines charRE = re.compile( "(-?\d+)" # charnum "\s*;\s*WX\s+" # ; WX "(\d+)" # width "\s*;\s*N\s+" # ; N "([.A-Za-z0-9_]+)" # charname "\s*;\s*B\s+" # ; B "(-?\d+)" # left "\s+" # "(-?\d+)" # bottom "\s+" # "(-?\d+)" # right "\s+" # "(-?\d+)" # top "\s*;\s*" # ; ) # regular expression to parse kerning lines kernRE = re.compile( "([.A-Za-z0-9_]+)" # leftchar "\s+" # "([.A-Za-z0-9_]+)" # rightchar "\s+" # "(-?\d+)" # value "\s*" # ) # regular expressions to parse composite info lines of the form: # Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; compositeRE = re.compile( "([.A-Za-z0-9_]+)" # char name "\s+" # "(\d+)" # number of parts "\s*;\s*" # ) componentRE = re.compile( "PCC\s+" # PPC "([.A-Za-z0-9_]+)" # base char name "\s+" # "(-?\d+)" # x offset "\s+" # "(-?\d+)" # y offset "\s*;\s*" # ) class AFMError(Exception): """ Class for handling errors """ pass class ConvertAFM: """ Convert Adobe Font Metrics """ _keywords = ['StartFontMetrics', 'EndFontMetrics', 'StartCharMetrics', 'EndCharMetrics', 'StartKernData', 'StartKernPairs', 'EndKernPairs', 'EndKernData', 'StartComposites', 'EndComposites', ] def __init__(self, filename): """ Initialisation of object @param filename: the name of the font file name @type filename: string """ self._attrs = {} self._chars = {} self._kerning = {} self._comments = [] self._composites = {} self.parse(filename) def parse(self, path): """ Parse the afm file @param path: path to the afm file @type path: string """ f = open(path, 'rb') data = f.read() f.close() # read any text file, regardless whether it's # formatted for Mac, Unix or Dos sep = "" if '\r' in data: sep = sep + '\r' # mac or dos if '\n' in data: sep = sep + '\n' # unix or dos #lines = string.split(data, sep) lines = data.split(sep) for line in lines: if not line.strip(): continue m = identifierRE.match(line) if m is None: raise AFMError, "syntax error in AFM file: " + `line` pos = m.regs[1][1] word = line[:pos] rest = line[pos:].strip() if word in self._keywords: continue if word == "C": self.parsechar(rest) elif word == "KPX": self.parsekernpair(rest) elif word == "CC": self.parsecomposite(rest) else: self.parseattr(word, rest) def write(self, filename): """ Write the font file @param filename: the name of the font file to write @type filename: string """ out = open(filename, "w") # the new ugly way, that produces nice files # docstring out.write('"""\n') out.write("PyScript font module for %s\n" % self._attrs['FullName']) out.write('"""\n') # this is the revision attribute useful for python # with extra code due to cvs keyword expansion issues out.write("__revision__ = '$") out.write("Revision$'\n") # write out the attributes out.write("attrs = {\n") for key in self._attrs.keys(): value = self._attrs[key] if str(value.__class__) == "": out.write("'%s' : '%s',\n" % \ (key, value)) else: out.write("'%s' : %s,\n" % \ (key, value)) out.write("}\n") # write out the characters out.write("chars = {\n") for key in self._chars.keys(): value = self._chars[key] out.write("%s : %s,\n" % \ (key, value)) out.write("}\n"); # write out the kerning out.write("kerning = {\n") for key in self._kerning.keys(): value = self._kerning[key] out.write("%s : %s,\n" % \ (key, value)) out.write("}\n") # write out the comments out.write("comments = [\n") for elem in self._comments: out.write("'%s',\n" % elem) out.write("]\n") # write out the composites out.write("composites = {\n") for key in self._composites.keys(): value = self._composites[key] out.write("%s : %s,\n" % \ (key, value)) out.write("}\n") # the old, simple way, that produces ugly files # out.write("attrs = %s"%repr(self._attrs)) # out.write("\n") # out.write("chars = %s"%repr(self._chars)) # out.write("\n") # out.write("kerning = %s"%repr(self._kerning)) # out.write("\n") # out.write("comments = %s"%repr(self._comments)) # out.write("\n") # out.write("composites = %s"%repr(self._composites)) # out.write("\n") out.close() def write2(self, filename): """ Another version of writing the font file, uses the CPickle module @param filename: the name of the font file to write @type filename: string """ afm = AFM() afm._attrs = self._attrs afm._chars = self._chars afm._kerning = self._kerning afm._comments = self._comments afm._composites = self._composites fp = open(filename, "w") cPickle.dump(afm, fp) fp.close() def parsechar(self, rest): """ Parse a character @param rest: the character to parse @type rest: string """ m = charRE.match(rest) if m is None: raise AFMError, "syntax error in AFM file: " + `rest` things = [] for fr, to in m.regs[1:]: things.append(rest[fr:to]) #charname = things[2] del things[2] #charnum, width, l, b, r, t = map(string.atoi, things) charnum, width, l, b, r, t = map(int, things) # width l b r t self._chars[charnum] = width, l, b, r, t def parsekernpair(self, rest): """ Parse a kerning pair @param rest: the kerning pair to parse @type rest: string """ m = kernRE.match(rest) if m is None: raise AFMError, "syntax error in AFM file: " + `rest` things = [] for fr, to in m.regs[1:]: things.append(rest[fr:to]) leftchar, rightchar, value = things value = int(value) #self._kerning[(leftchar, rightchar)] = value # fix for all kernings if len(leftchar) == len(rightchar) == 1: self._kerning[(ord(leftchar), ord(rightchar))]=value def parseattr(self, word, rest): """ Parse an attribute @param word: the kind of attribute to be parsed (?) @type word: string @param rest: the attribute to parse @type rest: string """ if word == "FontBBox": #l, b, r, t = map(string.atoi, string.split(rest)) l, b, r, t = map(int, rest.split()) self._attrs[word] = l, b, r, t elif word == "Comment": self._comments.append(rest) else: try: value = int(rest) except (ValueError, OverflowError): self._attrs[word] = rest else: self._attrs[word] = value def parsecomposite(self, rest): """ Parse a composite string/expression/thing @param rest: the string to parse @type rest: string """ m = compositeRE.match(rest) if m is None: raise AFMError, "syntax error in AFM file: " + `rest` charname = m.group(1) ncomponents = int(m.group(2)) rest = rest[m.regs[0][1]:] components = [] while 1: m = componentRE.match(rest) if m is None: raise AFMError, "syntax error in AFM file: " + `rest` basechar = m.group(1) xoffset = int(m.group(2)) yoffset = int(m.group(3)) components.append((basechar, xoffset, yoffset)) rest = rest[m.regs[0][1]:] if not rest: break assert len(components) == ncomponents self._composites[charname] = components # ------------------------------------------------------------------- class AFM: """ Class for handling Adobe Font Metric objects """ def __init__(self, fontname): """ Initialisation of the AFM object @param fontname: the name of the font @type fontname: string """ # this should be a better name, but will stop the possible error # from occurring self.FullName = fontname fontname = fontname.lower() fontname = fontname.replace("-", "_") # the import statement seem a little bit of a hack # but this will work for now. try: f = __import__('pyscript.fonts.%s'%fontname, None, None, [fontname]) except ImportError, x: # rename the exception raise FontError, x self.f = f def has_kernpair(self, pair): """ Determines if the kerning pair exists in the font @param pair: the kering pair to look for @type pair: ? """ return self.f.kerning.has_key(pair) def kernpairs(self): """ Returns the kerning pairs in the font """ return self.f.kerning.keys() def has_char(self, char): """ Determins if the character exists in the font @param char: the character to look for @type char: ? """ return self.f.chars.has_key(char) def chars(self): """ Returns the characters in the font """ return self.f.chars.keys() def comments(self): """ Returns the comments in the font """ return self.f.comments def __getattr__(self, attr): """ Gets an attribute of the font @param attr: the attribute to get @type attr: ? """ if self.f.attrs.has_key(attr): return self.f.attrs[attr] else: raise AttributeError, attr def __getitem__(self, key): """ Gets an item within the font @param key: the item to get @type key: ? """ if type(key) == types.TupleType: # key is a tuple, return the kernpair return self.f.kerning.get(key, 0) else: # return the metrics instead return self.f.chars[key] def __repr__(self): """ Returns the representation of the font object """ if hasattr(self, "FullName"): return '' % self.FullName else: return '' % id(self) def bbox(self, string, size=1, kerning=0): ''' Return a strings boundingbox in this font at the scale provided (relative to 1 point?) @param string: the string to measure @type string: string @param size: the point size of the font (sort of) @type size: int @param kerning: whether to subtract off the kerning @type kerning: boolean @return: xl, yb, xr, yt ''' chars = map(ord, list(string)) # order: width l b r t # use 'reduce' and 'map' as they're written in C # add up all the widths width = reduce(lambda x, y : x+self[y][0], chars, 0) # subtract the kerning if kerning == 1: if len(chars)>1: kk = map(lambda x, y:self[(x, y)], chars[:-1], chars[1:]) kern = reduce(lambda x, y:x+y, kk) width += kern kk = map(lambda x, y:self[(x, y)], chars[:-1], chars[1:]) print kk # get rid of the end bits start = self[chars[0]][1] f = self[chars[-1]] width = width-start-(f[0]-f[3]) # accumulate maximum height top = reduce(lambda x, y: max(x, self[y][4]), chars, 0) # accumulate lowest point bottom = reduce(lambda x, y: min(x, self[y][2]), chars, self[chars[0]][2]) sc = size/1000. xl = start*sc yb = bottom*sc xr = xl + width*sc yt = top*sc return xl, yb, xr, yt def load(fontname): """ Loads the font of the given font name @param fontname: the name of the font to load @type fontname: string """ fontpath = os.path.join(FONTDIR, fontname) fp = open(fontpath+".font") font = cPickle.load(fp) fp.close() return font if __name__ == "__main__": # utility for converting afm files to pyscripts # font modules for afmFname in sys.argv[1:]: afmObj = ConvertAFM(afmFname) dirname, fname = os.path.split(afmFname) base, ext = os.path.splitext(fname) base = base.lower() base = base.replace("-", "_") outfile = os.path.join(dirname, base+".py") afmObj.write(outfile) # vim: expandtab shiftwidth=4: