Source code for fontTools.afmLib

"""Module for reading and writing AFM files."""

# XXX reads AFM's generated by Fog, not tested with much else.
# It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics
# File Format Specification). Still, it should read most "common" AFM files.

from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
import re

# 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*"
		)

preferredAttributeOrder = [
		"FontName",
		"FullName",
		"FamilyName",
		"Weight",
		"ItalicAngle",
		"IsFixedPitch",
		"FontBBox",
		"UnderlinePosition",
		"UnderlineThickness",
		"Version",
		"Notice",
		"EncodingScheme",
		"CapHeight",
		"XHeight",
		"Ascender",
		"Descender",
]


[docs]class error(Exception): pass
[docs]class AFM(object): _attrs = None _keywords = ['StartFontMetrics', 'EndFontMetrics', 'StartCharMetrics', 'EndCharMetrics', 'StartKernData', 'StartKernPairs', 'EndKernPairs', 'EndKernData', 'StartComposites', 'EndComposites', ] def __init__(self, path=None): self._attrs = {} self._chars = {} self._kerning = {} self._index = {} self._comments = [] self._composites = {} if path is not None: self.read(path)
[docs] def read(self, path): lines = readlines(path) for line in lines: if not line.strip(): continue m = identifierRE.match(line) if m is None: raise error("syntax error in AFM file: " + repr(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)
[docs] def parsechar(self, rest): m = charRE.match(rest) if m is None: raise error("syntax error in AFM file: " + repr(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 = (int(thing) for thing in things) self._chars[charname] = charnum, width, (l, b, r, t)
[docs] def parsekernpair(self, rest): m = kernRE.match(rest) if m is None: raise error("syntax error in AFM file: " + repr(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
[docs] def parseattr(self, word, rest): if word == "FontBBox": l, b, r, t = [int(thing) for thing in 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
[docs] def parsecomposite(self, rest): m = compositeRE.match(rest) if m is None: raise error("syntax error in AFM file: " + repr(rest)) charname = m.group(1) ncomponents = int(m.group(2)) rest = rest[m.regs[0][1]:] components = [] while True: m = componentRE.match(rest) if m is None: raise error("syntax error in AFM file: " + repr(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
[docs] def write(self, path, sep='\r'): import time lines = [ "StartFontMetrics 2.0", "Comment Generated by afmLib; at %s" % ( time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time())))] # write comments, assuming (possibly wrongly!) they should # all appear at the top for comment in self._comments: lines.append("Comment " + comment) # write attributes, first the ones we know about, in # a preferred order attrs = self._attrs for attr in preferredAttributeOrder: if attr in attrs: value = attrs[attr] if attr == "FontBBox": value = "%s %s %s %s" % value lines.append(attr + " " + str(value)) # then write the attributes we don't know about, # in alphabetical order items = sorted(attrs.items()) for attr, value in items: if attr in preferredAttributeOrder: continue lines.append(attr + " " + str(value)) # write char metrics lines.append("StartCharMetrics " + repr(len(self._chars))) items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()] def myKey(a): """Custom key function to make sure unencoded chars (-1) end up at the end of the list after sorting.""" if a[0] == -1: a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number return a items.sort(key=myKey) for charnum, (charname, width, (l, b, r, t)) in items: lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" % (charnum, width, charname, l, b, r, t)) lines.append("EndCharMetrics") # write kerning info lines.append("StartKernData") lines.append("StartKernPairs " + repr(len(self._kerning))) items = sorted(self._kerning.items()) for (leftchar, rightchar), value in items: lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) lines.append("EndKernPairs") lines.append("EndKernData") if self._composites: composites = sorted(self._composites.items()) lines.append("StartComposites %s" % len(self._composites)) for charname, components in composites: line = "CC %s %s ;" % (charname, len(components)) for basechar, xoffset, yoffset in components: line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) lines.append(line) lines.append("EndComposites") lines.append("EndFontMetrics") writelines(path, lines, sep)
[docs] def has_kernpair(self, pair): return pair in self._kerning
[docs] def kernpairs(self): return list(self._kerning.keys())
[docs] def has_char(self, char): return char in self._chars
[docs] def chars(self): return list(self._chars.keys())
[docs] def comments(self): return self._comments
[docs] def addComment(self, comment): self._comments.append(comment)
[docs] def addComposite(self, glyphName, components): self._composites[glyphName] = components
def __getattr__(self, attr): if attr in self._attrs: return self._attrs[attr] else: raise AttributeError(attr) def __setattr__(self, attr, value): # all attrs *not* starting with "_" are consider to be AFM keywords if attr[:1] == "_": self.__dict__[attr] = value else: self._attrs[attr] = value def __delattr__(self, attr): # all attrs *not* starting with "_" are consider to be AFM keywords if attr[:1] == "_": try: del self.__dict__[attr] except KeyError: raise AttributeError(attr) else: try: del self._attrs[attr] except KeyError: raise AttributeError(attr) def __getitem__(self, key): if isinstance(key, tuple): # key is a tuple, return the kernpair return self._kerning[key] else: # return the metrics instead return self._chars[key] def __setitem__(self, key, value): if isinstance(key, tuple): # key is a tuple, set kernpair self._kerning[key] = value else: # set char metrics self._chars[key] = value def __delitem__(self, key): if isinstance(key, tuple): # key is a tuple, del kernpair del self._kerning[key] else: # del char metrics del self._chars[key] def __repr__(self): if hasattr(self, "FullName"): return '<AFM object for %s>' % self.FullName else: return '<AFM object at %x>' % id(self)
[docs]def readlines(path): 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 return data.split(sep)
[docs]def writelines(path, lines, sep='\r'): f = open(path, 'wb') for line in lines: f.write(line + sep) f.close()
if __name__ == "__main__": import EasyDialogs path = EasyDialogs.AskFileForOpen() if path: afm = AFM(path) char = 'A' if afm.has_char(char): print(afm[char]) # print charnum, width and boundingbox pair = ('A', 'V') if afm.has_kernpair(pair): print(afm[pair]) # print kerning value for pair print(afm.Version) # various other afm entries have become attributes print(afm.Weight) # afm.comments() returns a list of all Comment lines found in the AFM print(afm.comments()) #print afm.chars() #print afm.kernpairs() print(afm) afm.write(path + ".muck")