"""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", ] class error(Exception): pass 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) 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) 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'): 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): 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): self._comments.append(comment) 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 '' % self.FullName else: return '' % id(self) def readlines(path): 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") 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")