aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/afmLib.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/afmLib.py')
-rw-r--r--Lib/fontTools/afmLib.py709
1 files changed, 358 insertions, 351 deletions
diff --git a/Lib/fontTools/afmLib.py b/Lib/fontTools/afmLib.py
index 49d99512..935a1e8e 100644
--- a/Lib/fontTools/afmLib.py
+++ b/Lib/fontTools/afmLib.py
@@ -53,378 +53,385 @@ identifierRE = re.compile(r"^([A-Za-z]+).*")
# regular expression to parse char lines
charRE = re.compile(
- r"(-?\d+)" # charnum
- r"\s*;\s*WX\s+" # ; WX
- r"(-?\d+)" # width
- r"\s*;\s*N\s+" # ; N
- r"([.A-Za-z0-9_]+)" # charname
- r"\s*;\s*B\s+" # ; B
- r"(-?\d+)" # left
- r"\s+"
- r"(-?\d+)" # bottom
- r"\s+"
- r"(-?\d+)" # right
- r"\s+"
- r"(-?\d+)" # top
- r"\s*;\s*" # ;
- )
+ r"(-?\d+)" # charnum
+ r"\s*;\s*WX\s+" # ; WX
+ r"(-?\d+)" # width
+ r"\s*;\s*N\s+" # ; N
+ r"([.A-Za-z0-9_]+)" # charname
+ r"\s*;\s*B\s+" # ; B
+ r"(-?\d+)" # left
+ r"\s+"
+ r"(-?\d+)" # bottom
+ r"\s+"
+ r"(-?\d+)" # right
+ r"\s+"
+ r"(-?\d+)" # top
+ r"\s*;\s*" # ;
+)
# regular expression to parse kerning lines
kernRE = re.compile(
- r"([.A-Za-z0-9_]+)" # leftchar
- r"\s+"
- r"([.A-Za-z0-9_]+)" # rightchar
- r"\s+"
- r"(-?\d+)" # value
- r"\s*"
- )
+ r"([.A-Za-z0-9_]+)" # leftchar
+ r"\s+"
+ r"([.A-Za-z0-9_]+)" # rightchar
+ r"\s+"
+ r"(-?\d+)" # value
+ r"\s*"
+)
# regular expressions to parse composite info lines of the form:
# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
compositeRE = re.compile(
- r"([.A-Za-z0-9_]+)" # char name
- r"\s+"
- r"(\d+)" # number of parts
- r"\s*;\s*"
- )
+ r"([.A-Za-z0-9_]+)" r"\s+" r"(\d+)" r"\s*;\s*" # char name # number of parts
+)
componentRE = re.compile(
- r"PCC\s+" # PPC
- r"([.A-Za-z0-9_]+)" # base char name
- r"\s+"
- r"(-?\d+)" # x offset
- r"\s+"
- r"(-?\d+)" # y offset
- r"\s*;\s*"
- )
+ r"PCC\s+" # PPC
+ r"([.A-Za-z0-9_]+)" # base char name
+ r"\s+"
+ r"(-?\d+)" # x offset
+ r"\s+"
+ r"(-?\d+)" # y offset
+ r"\s*;\s*"
+)
preferredAttributeOrder = [
- "FontName",
- "FullName",
- "FamilyName",
- "Weight",
- "ItalicAngle",
- "IsFixedPitch",
- "FontBBox",
- "UnderlinePosition",
- "UnderlineThickness",
- "Version",
- "Notice",
- "EncodingScheme",
- "CapHeight",
- "XHeight",
- "Ascender",
- "Descender",
+ "FontName",
+ "FullName",
+ "FamilyName",
+ "Weight",
+ "ItalicAngle",
+ "IsFixedPitch",
+ "FontBBox",
+ "UnderlinePosition",
+ "UnderlineThickness",
+ "Version",
+ "Notice",
+ "EncodingScheme",
+ "CapHeight",
+ "XHeight",
+ "Ascender",
+ "Descender",
]
class error(Exception):
- pass
+ pass
class AFM(object):
-
- _attrs = None
-
- _keywords = ['StartFontMetrics',
- 'EndFontMetrics',
- 'StartCharMetrics',
- 'EndCharMetrics',
- 'StartKernData',
- 'StartKernPairs',
- 'EndKernPairs',
- 'EndKernData',
- 'StartComposites',
- 'EndComposites',
- ]
-
- def __init__(self, path=None):
- """AFM file reader.
-
- Instantiating an object with a path name will cause the file to be opened,
- read, and parsed. Alternatively the path can be left unspecified, and a
- file can be parsed later with the :meth:`read` method."""
- self._attrs = {}
- self._chars = {}
- self._kerning = {}
- self._index = {}
- self._comments = []
- self._composites = {}
- if path is not None:
- self.read(path)
-
- def read(self, path):
- """Opens, reads and parses a file."""
- 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)
-
- 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)
-
- 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
-
- 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
-
- 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
-
- def write(self, path, sep='\r'):
- """Writes out an AFM font to the given path."""
- 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)
-
- def has_kernpair(self, pair):
- """Returns `True` if the given glyph pair (specified as a tuple) exists
- in the kerning dictionary."""
- return pair in self._kerning
-
- def kernpairs(self):
- """Returns a list of all kern pairs in the kerning dictionary."""
- return list(self._kerning.keys())
-
- def has_char(self, char):
- """Returns `True` if the given glyph exists in the font."""
- return char in self._chars
-
- def chars(self):
- """Returns a list of all glyph names in the font."""
- return list(self._chars.keys())
-
- def comments(self):
- """Returns all comments from the file."""
- return self._comments
-
- def addComment(self, comment):
- """Adds a new comment to the file."""
- self._comments.append(comment)
-
- def addComposite(self, glyphName, components):
- """Specifies that the glyph `glyphName` is made up of the given components.
- The components list should be of the following form::
-
- [
- (glyphname, xOffset, yOffset),
- ...
- ]
-
- """
- 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)
+ _attrs = None
+
+ _keywords = [
+ "StartFontMetrics",
+ "EndFontMetrics",
+ "StartCharMetrics",
+ "EndCharMetrics",
+ "StartKernData",
+ "StartKernPairs",
+ "EndKernPairs",
+ "EndKernData",
+ "StartComposites",
+ "EndComposites",
+ ]
+
+ def __init__(self, path=None):
+ """AFM file reader.
+
+ Instantiating an object with a path name will cause the file to be opened,
+ read, and parsed. Alternatively the path can be left unspecified, and a
+ file can be parsed later with the :meth:`read` method."""
+ self._attrs = {}
+ self._chars = {}
+ self._kerning = {}
+ self._index = {}
+ self._comments = []
+ self._composites = {}
+ if path is not None:
+ self.read(path)
+
+ def read(self, path):
+ """Opens, reads and parses a file."""
+ 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)
+
+ 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)
+
+ 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
+
+ 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
+
+ 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
+
+ def write(self, path, sep="\r"):
+ """Writes out an AFM font to the given path."""
+ 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)
+
+ def has_kernpair(self, pair):
+ """Returns `True` if the given glyph pair (specified as a tuple) exists
+ in the kerning dictionary."""
+ return pair in self._kerning
+
+ def kernpairs(self):
+ """Returns a list of all kern pairs in the kerning dictionary."""
+ return list(self._kerning.keys())
+
+ def has_char(self, char):
+ """Returns `True` if the given glyph exists in the font."""
+ return char in self._chars
+
+ def chars(self):
+ """Returns a list of all glyph names in the font."""
+ return list(self._chars.keys())
+
+ def comments(self):
+ """Returns all comments from the file."""
+ return self._comments
+
+ def addComment(self, comment):
+ """Adds a new comment to the file."""
+ self._comments.append(comment)
+
+ def addComposite(self, glyphName, components):
+ """Specifies that the glyph `glyphName` is made up of the given components.
+ The components list should be of the following form::
+
+ [
+ (glyphname, xOffset, yOffset),
+ ...
+ ]
+
+ """
+ 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)
def readlines(path):
- with open(path, "r", encoding="ascii") as f:
- data = f.read()
- return data.splitlines()
+ with open(path, "r", encoding="ascii") as f:
+ data = f.read()
+ 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")
+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
- 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")
+ 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")