blob: e0ccafee0672f74d1d492e6b4eb39606194a6563 [file] [log] [blame]
"""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
"(-?\d+)" # bottom
"(-?\d+)" # right
"(-?\d+)" # top
"\s*;\s*" # ;
# regular expression to parse kerning lines
kernRE = re.compile(
"([.A-Za-z0-9_]+)" # leftchar
"([.A-Za-z0-9_]+)" # rightchar
"(-?\d+)" # value
# 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
"(\d+)" # number of parts
componentRE = re.compile(
"PCC\s+" # PPC
"([.A-Za-z0-9_]+)" # base char name
"(-?\d+)" # x offset
"(-?\d+)" # y offset
preferredAttributeOrder = [
class error(Exception):
class AFM(object):
_attrs = None
_keywords = ['StartFontMetrics',
def __init__(self, path=None):
self._attrs = {}
self._chars = {}
self._kerning = {}
self._index = {}
self._comments = []
self._composites = {}
if path is not None:
def read(self, path):
lines = readlines(path)
for line in lines:
if not line.strip():
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:
if word == "C":
elif word == "KPX":
elif word == "CC":
self.parseattr(word, rest)
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:]:
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)
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:]:
leftchar, rightchar, value = things
value = int(value)
self._kerning[(leftchar, rightchar)] = value
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":
value = int(rest)
except (ValueError, OverflowError):
self._attrs[word] = rest
self._attrs[word] = value
def parsecomposite(self, rest):
m = compositeRE.match(rest)
if m is None:
raise error("syntax error in AFM file: " + repr(rest))
charname =
ncomponents = int(
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 =
xoffset = int(
yoffset = int(
components.append((basechar, xoffset, yoffset))
rest = rest[m.regs[0][1]:]
if not rest:
assert len(components) == ncomponents
self._composites[charname] = components
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",
# 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:
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
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))
# write kerning info
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))
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)
writelines(path, lines, sep)
def has_kernpair(self, pair):
return pair in self._kerning
def kernpairs(self):
return list(self._kerning.keys())
def has_char(self, char):
return char in self._chars
def chars(self):
return list(self._chars.keys())
def comments(self):
return self._comments
def addComment(self, comment):
def addComposite(self, glyphName, components):
self._composites[glyphName] = components
def __getattr__(self, attr):
if attr in self._attrs:
return self._attrs[attr]
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
self._attrs[attr] = value
def __delattr__(self, attr):
# all attrs *not* starting with "_" are consider to be AFM keywords
if attr[:1] == "_":
del self.__dict__[attr]
except KeyError:
raise AttributeError(attr)
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]
# 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
# 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]
# del char metrics
del self._chars[key]
def __repr__(self):
if hasattr(self, "FullName"):
return '<AFM object for %s>' % self.FullName
return '<AFM object at %x>' % id(self)
def readlines(path):
with open(path, "r", encoding="ascii") as f:
data =
return data.splitlines()
def writelines(path, lines, sep='\r'):
with open(path, "w", encoding="ascii", newline=sep) as f:
f.write("\n".join(lines) + "\n")
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
# afm.comments() returns a list of all Comment lines found in the AFM
#print afm.chars()
#print afm.kernpairs()
afm.write(path + ".muck")