1*e1fe3e4aSElliott Hughes"""Module for reading and writing AFM (Adobe Font Metrics) files. 2*e1fe3e4aSElliott Hughes 3*e1fe3e4aSElliott HughesNote that this has been designed to read in AFM files generated by Fontographer 4*e1fe3e4aSElliott Hughesand has not been tested on many other files. In particular, it does not 5*e1fe3e4aSElliott Hughesimplement the whole Adobe AFM specification [#f1]_ but, it should read most 6*e1fe3e4aSElliott Hughes"common" AFM files. 7*e1fe3e4aSElliott Hughes 8*e1fe3e4aSElliott HughesHere is an example of using `afmLib` to read, modify and write an AFM file: 9*e1fe3e4aSElliott Hughes 10*e1fe3e4aSElliott Hughes >>> from fontTools.afmLib import AFM 11*e1fe3e4aSElliott Hughes >>> f = AFM("Tests/afmLib/data/TestAFM.afm") 12*e1fe3e4aSElliott Hughes >>> 13*e1fe3e4aSElliott Hughes >>> # Accessing a pair gets you the kern value 14*e1fe3e4aSElliott Hughes >>> f[("V","A")] 15*e1fe3e4aSElliott Hughes -60 16*e1fe3e4aSElliott Hughes >>> 17*e1fe3e4aSElliott Hughes >>> # Accessing a glyph name gets you metrics 18*e1fe3e4aSElliott Hughes >>> f["A"] 19*e1fe3e4aSElliott Hughes (65, 668, (8, -25, 660, 666)) 20*e1fe3e4aSElliott Hughes >>> # (charnum, width, bounding box) 21*e1fe3e4aSElliott Hughes >>> 22*e1fe3e4aSElliott Hughes >>> # Accessing an attribute gets you metadata 23*e1fe3e4aSElliott Hughes >>> f.FontName 24*e1fe3e4aSElliott Hughes 'TestFont-Regular' 25*e1fe3e4aSElliott Hughes >>> f.FamilyName 26*e1fe3e4aSElliott Hughes 'TestFont' 27*e1fe3e4aSElliott Hughes >>> f.Weight 28*e1fe3e4aSElliott Hughes 'Regular' 29*e1fe3e4aSElliott Hughes >>> f.XHeight 30*e1fe3e4aSElliott Hughes 500 31*e1fe3e4aSElliott Hughes >>> f.Ascender 32*e1fe3e4aSElliott Hughes 750 33*e1fe3e4aSElliott Hughes >>> 34*e1fe3e4aSElliott Hughes >>> # Attributes and items can also be set 35*e1fe3e4aSElliott Hughes >>> f[("A","V")] = -150 # Tighten kerning 36*e1fe3e4aSElliott Hughes >>> f.FontName = "TestFont Squished" 37*e1fe3e4aSElliott Hughes >>> 38*e1fe3e4aSElliott Hughes >>> # And the font written out again (remove the # in front) 39*e1fe3e4aSElliott Hughes >>> #f.write("testfont-squished.afm") 40*e1fe3e4aSElliott Hughes 41*e1fe3e4aSElliott Hughes.. rubric:: Footnotes 42*e1fe3e4aSElliott Hughes 43*e1fe3e4aSElliott Hughes.. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_, 44*e1fe3e4aSElliott Hughes Adobe Font Metrics File Format Specification. 45*e1fe3e4aSElliott Hughes 46*e1fe3e4aSElliott Hughes""" 47*e1fe3e4aSElliott Hughes 48*e1fe3e4aSElliott Hughesimport re 49*e1fe3e4aSElliott Hughes 50*e1fe3e4aSElliott Hughes# every single line starts with a "word" 51*e1fe3e4aSElliott HughesidentifierRE = re.compile(r"^([A-Za-z]+).*") 52*e1fe3e4aSElliott Hughes 53*e1fe3e4aSElliott Hughes# regular expression to parse char lines 54*e1fe3e4aSElliott HughescharRE = re.compile( 55*e1fe3e4aSElliott Hughes r"(-?\d+)" # charnum 56*e1fe3e4aSElliott Hughes r"\s*;\s*WX\s+" # ; WX 57*e1fe3e4aSElliott Hughes r"(-?\d+)" # width 58*e1fe3e4aSElliott Hughes r"\s*;\s*N\s+" # ; N 59*e1fe3e4aSElliott Hughes r"([.A-Za-z0-9_]+)" # charname 60*e1fe3e4aSElliott Hughes r"\s*;\s*B\s+" # ; B 61*e1fe3e4aSElliott Hughes r"(-?\d+)" # left 62*e1fe3e4aSElliott Hughes r"\s+" 63*e1fe3e4aSElliott Hughes r"(-?\d+)" # bottom 64*e1fe3e4aSElliott Hughes r"\s+" 65*e1fe3e4aSElliott Hughes r"(-?\d+)" # right 66*e1fe3e4aSElliott Hughes r"\s+" 67*e1fe3e4aSElliott Hughes r"(-?\d+)" # top 68*e1fe3e4aSElliott Hughes r"\s*;\s*" # ; 69*e1fe3e4aSElliott Hughes) 70*e1fe3e4aSElliott Hughes 71*e1fe3e4aSElliott Hughes# regular expression to parse kerning lines 72*e1fe3e4aSElliott HugheskernRE = re.compile( 73*e1fe3e4aSElliott Hughes r"([.A-Za-z0-9_]+)" # leftchar 74*e1fe3e4aSElliott Hughes r"\s+" 75*e1fe3e4aSElliott Hughes r"([.A-Za-z0-9_]+)" # rightchar 76*e1fe3e4aSElliott Hughes r"\s+" 77*e1fe3e4aSElliott Hughes r"(-?\d+)" # value 78*e1fe3e4aSElliott Hughes r"\s*" 79*e1fe3e4aSElliott Hughes) 80*e1fe3e4aSElliott Hughes 81*e1fe3e4aSElliott Hughes# regular expressions to parse composite info lines of the form: 82*e1fe3e4aSElliott Hughes# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; 83*e1fe3e4aSElliott HughescompositeRE = re.compile( 84*e1fe3e4aSElliott Hughes r"([.A-Za-z0-9_]+)" # char name 85*e1fe3e4aSElliott Hughes r"\s+" 86*e1fe3e4aSElliott Hughes r"(\d+)" # number of parts 87*e1fe3e4aSElliott Hughes r"\s*;\s*" 88*e1fe3e4aSElliott Hughes) 89*e1fe3e4aSElliott HughescomponentRE = re.compile( 90*e1fe3e4aSElliott Hughes r"PCC\s+" # PPC 91*e1fe3e4aSElliott Hughes r"([.A-Za-z0-9_]+)" # base char name 92*e1fe3e4aSElliott Hughes r"\s+" 93*e1fe3e4aSElliott Hughes r"(-?\d+)" # x offset 94*e1fe3e4aSElliott Hughes r"\s+" 95*e1fe3e4aSElliott Hughes r"(-?\d+)" # y offset 96*e1fe3e4aSElliott Hughes r"\s*;\s*" 97*e1fe3e4aSElliott Hughes) 98*e1fe3e4aSElliott Hughes 99*e1fe3e4aSElliott HughespreferredAttributeOrder = [ 100*e1fe3e4aSElliott Hughes "FontName", 101*e1fe3e4aSElliott Hughes "FullName", 102*e1fe3e4aSElliott Hughes "FamilyName", 103*e1fe3e4aSElliott Hughes "Weight", 104*e1fe3e4aSElliott Hughes "ItalicAngle", 105*e1fe3e4aSElliott Hughes "IsFixedPitch", 106*e1fe3e4aSElliott Hughes "FontBBox", 107*e1fe3e4aSElliott Hughes "UnderlinePosition", 108*e1fe3e4aSElliott Hughes "UnderlineThickness", 109*e1fe3e4aSElliott Hughes "Version", 110*e1fe3e4aSElliott Hughes "Notice", 111*e1fe3e4aSElliott Hughes "EncodingScheme", 112*e1fe3e4aSElliott Hughes "CapHeight", 113*e1fe3e4aSElliott Hughes "XHeight", 114*e1fe3e4aSElliott Hughes "Ascender", 115*e1fe3e4aSElliott Hughes "Descender", 116*e1fe3e4aSElliott Hughes] 117*e1fe3e4aSElliott Hughes 118*e1fe3e4aSElliott Hughes 119*e1fe3e4aSElliott Hughesclass error(Exception): 120*e1fe3e4aSElliott Hughes pass 121*e1fe3e4aSElliott Hughes 122*e1fe3e4aSElliott Hughes 123*e1fe3e4aSElliott Hughesclass AFM(object): 124*e1fe3e4aSElliott Hughes _attrs = None 125*e1fe3e4aSElliott Hughes 126*e1fe3e4aSElliott Hughes _keywords = [ 127*e1fe3e4aSElliott Hughes "StartFontMetrics", 128*e1fe3e4aSElliott Hughes "EndFontMetrics", 129*e1fe3e4aSElliott Hughes "StartCharMetrics", 130*e1fe3e4aSElliott Hughes "EndCharMetrics", 131*e1fe3e4aSElliott Hughes "StartKernData", 132*e1fe3e4aSElliott Hughes "StartKernPairs", 133*e1fe3e4aSElliott Hughes "EndKernPairs", 134*e1fe3e4aSElliott Hughes "EndKernData", 135*e1fe3e4aSElliott Hughes "StartComposites", 136*e1fe3e4aSElliott Hughes "EndComposites", 137*e1fe3e4aSElliott Hughes ] 138*e1fe3e4aSElliott Hughes 139*e1fe3e4aSElliott Hughes def __init__(self, path=None): 140*e1fe3e4aSElliott Hughes """AFM file reader. 141*e1fe3e4aSElliott Hughes 142*e1fe3e4aSElliott Hughes Instantiating an object with a path name will cause the file to be opened, 143*e1fe3e4aSElliott Hughes read, and parsed. Alternatively the path can be left unspecified, and a 144*e1fe3e4aSElliott Hughes file can be parsed later with the :meth:`read` method.""" 145*e1fe3e4aSElliott Hughes self._attrs = {} 146*e1fe3e4aSElliott Hughes self._chars = {} 147*e1fe3e4aSElliott Hughes self._kerning = {} 148*e1fe3e4aSElliott Hughes self._index = {} 149*e1fe3e4aSElliott Hughes self._comments = [] 150*e1fe3e4aSElliott Hughes self._composites = {} 151*e1fe3e4aSElliott Hughes if path is not None: 152*e1fe3e4aSElliott Hughes self.read(path) 153*e1fe3e4aSElliott Hughes 154*e1fe3e4aSElliott Hughes def read(self, path): 155*e1fe3e4aSElliott Hughes """Opens, reads and parses a file.""" 156*e1fe3e4aSElliott Hughes lines = readlines(path) 157*e1fe3e4aSElliott Hughes for line in lines: 158*e1fe3e4aSElliott Hughes if not line.strip(): 159*e1fe3e4aSElliott Hughes continue 160*e1fe3e4aSElliott Hughes m = identifierRE.match(line) 161*e1fe3e4aSElliott Hughes if m is None: 162*e1fe3e4aSElliott Hughes raise error("syntax error in AFM file: " + repr(line)) 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes pos = m.regs[1][1] 165*e1fe3e4aSElliott Hughes word = line[:pos] 166*e1fe3e4aSElliott Hughes rest = line[pos:].strip() 167*e1fe3e4aSElliott Hughes if word in self._keywords: 168*e1fe3e4aSElliott Hughes continue 169*e1fe3e4aSElliott Hughes if word == "C": 170*e1fe3e4aSElliott Hughes self.parsechar(rest) 171*e1fe3e4aSElliott Hughes elif word == "KPX": 172*e1fe3e4aSElliott Hughes self.parsekernpair(rest) 173*e1fe3e4aSElliott Hughes elif word == "CC": 174*e1fe3e4aSElliott Hughes self.parsecomposite(rest) 175*e1fe3e4aSElliott Hughes else: 176*e1fe3e4aSElliott Hughes self.parseattr(word, rest) 177*e1fe3e4aSElliott Hughes 178*e1fe3e4aSElliott Hughes def parsechar(self, rest): 179*e1fe3e4aSElliott Hughes m = charRE.match(rest) 180*e1fe3e4aSElliott Hughes if m is None: 181*e1fe3e4aSElliott Hughes raise error("syntax error in AFM file: " + repr(rest)) 182*e1fe3e4aSElliott Hughes things = [] 183*e1fe3e4aSElliott Hughes for fr, to in m.regs[1:]: 184*e1fe3e4aSElliott Hughes things.append(rest[fr:to]) 185*e1fe3e4aSElliott Hughes charname = things[2] 186*e1fe3e4aSElliott Hughes del things[2] 187*e1fe3e4aSElliott Hughes charnum, width, l, b, r, t = (int(thing) for thing in things) 188*e1fe3e4aSElliott Hughes self._chars[charname] = charnum, width, (l, b, r, t) 189*e1fe3e4aSElliott Hughes 190*e1fe3e4aSElliott Hughes def parsekernpair(self, rest): 191*e1fe3e4aSElliott Hughes m = kernRE.match(rest) 192*e1fe3e4aSElliott Hughes if m is None: 193*e1fe3e4aSElliott Hughes raise error("syntax error in AFM file: " + repr(rest)) 194*e1fe3e4aSElliott Hughes things = [] 195*e1fe3e4aSElliott Hughes for fr, to in m.regs[1:]: 196*e1fe3e4aSElliott Hughes things.append(rest[fr:to]) 197*e1fe3e4aSElliott Hughes leftchar, rightchar, value = things 198*e1fe3e4aSElliott Hughes value = int(value) 199*e1fe3e4aSElliott Hughes self._kerning[(leftchar, rightchar)] = value 200*e1fe3e4aSElliott Hughes 201*e1fe3e4aSElliott Hughes def parseattr(self, word, rest): 202*e1fe3e4aSElliott Hughes if word == "FontBBox": 203*e1fe3e4aSElliott Hughes l, b, r, t = [int(thing) for thing in rest.split()] 204*e1fe3e4aSElliott Hughes self._attrs[word] = l, b, r, t 205*e1fe3e4aSElliott Hughes elif word == "Comment": 206*e1fe3e4aSElliott Hughes self._comments.append(rest) 207*e1fe3e4aSElliott Hughes else: 208*e1fe3e4aSElliott Hughes try: 209*e1fe3e4aSElliott Hughes value = int(rest) 210*e1fe3e4aSElliott Hughes except (ValueError, OverflowError): 211*e1fe3e4aSElliott Hughes self._attrs[word] = rest 212*e1fe3e4aSElliott Hughes else: 213*e1fe3e4aSElliott Hughes self._attrs[word] = value 214*e1fe3e4aSElliott Hughes 215*e1fe3e4aSElliott Hughes def parsecomposite(self, rest): 216*e1fe3e4aSElliott Hughes m = compositeRE.match(rest) 217*e1fe3e4aSElliott Hughes if m is None: 218*e1fe3e4aSElliott Hughes raise error("syntax error in AFM file: " + repr(rest)) 219*e1fe3e4aSElliott Hughes charname = m.group(1) 220*e1fe3e4aSElliott Hughes ncomponents = int(m.group(2)) 221*e1fe3e4aSElliott Hughes rest = rest[m.regs[0][1] :] 222*e1fe3e4aSElliott Hughes components = [] 223*e1fe3e4aSElliott Hughes while True: 224*e1fe3e4aSElliott Hughes m = componentRE.match(rest) 225*e1fe3e4aSElliott Hughes if m is None: 226*e1fe3e4aSElliott Hughes raise error("syntax error in AFM file: " + repr(rest)) 227*e1fe3e4aSElliott Hughes basechar = m.group(1) 228*e1fe3e4aSElliott Hughes xoffset = int(m.group(2)) 229*e1fe3e4aSElliott Hughes yoffset = int(m.group(3)) 230*e1fe3e4aSElliott Hughes components.append((basechar, xoffset, yoffset)) 231*e1fe3e4aSElliott Hughes rest = rest[m.regs[0][1] :] 232*e1fe3e4aSElliott Hughes if not rest: 233*e1fe3e4aSElliott Hughes break 234*e1fe3e4aSElliott Hughes assert len(components) == ncomponents 235*e1fe3e4aSElliott Hughes self._composites[charname] = components 236*e1fe3e4aSElliott Hughes 237*e1fe3e4aSElliott Hughes def write(self, path, sep="\r"): 238*e1fe3e4aSElliott Hughes """Writes out an AFM font to the given path.""" 239*e1fe3e4aSElliott Hughes import time 240*e1fe3e4aSElliott Hughes 241*e1fe3e4aSElliott Hughes lines = [ 242*e1fe3e4aSElliott Hughes "StartFontMetrics 2.0", 243*e1fe3e4aSElliott Hughes "Comment Generated by afmLib; at %s" 244*e1fe3e4aSElliott Hughes % (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))), 245*e1fe3e4aSElliott Hughes ] 246*e1fe3e4aSElliott Hughes 247*e1fe3e4aSElliott Hughes # write comments, assuming (possibly wrongly!) they should 248*e1fe3e4aSElliott Hughes # all appear at the top 249*e1fe3e4aSElliott Hughes for comment in self._comments: 250*e1fe3e4aSElliott Hughes lines.append("Comment " + comment) 251*e1fe3e4aSElliott Hughes 252*e1fe3e4aSElliott Hughes # write attributes, first the ones we know about, in 253*e1fe3e4aSElliott Hughes # a preferred order 254*e1fe3e4aSElliott Hughes attrs = self._attrs 255*e1fe3e4aSElliott Hughes for attr in preferredAttributeOrder: 256*e1fe3e4aSElliott Hughes if attr in attrs: 257*e1fe3e4aSElliott Hughes value = attrs[attr] 258*e1fe3e4aSElliott Hughes if attr == "FontBBox": 259*e1fe3e4aSElliott Hughes value = "%s %s %s %s" % value 260*e1fe3e4aSElliott Hughes lines.append(attr + " " + str(value)) 261*e1fe3e4aSElliott Hughes # then write the attributes we don't know about, 262*e1fe3e4aSElliott Hughes # in alphabetical order 263*e1fe3e4aSElliott Hughes items = sorted(attrs.items()) 264*e1fe3e4aSElliott Hughes for attr, value in items: 265*e1fe3e4aSElliott Hughes if attr in preferredAttributeOrder: 266*e1fe3e4aSElliott Hughes continue 267*e1fe3e4aSElliott Hughes lines.append(attr + " " + str(value)) 268*e1fe3e4aSElliott Hughes 269*e1fe3e4aSElliott Hughes # write char metrics 270*e1fe3e4aSElliott Hughes lines.append("StartCharMetrics " + repr(len(self._chars))) 271*e1fe3e4aSElliott Hughes items = [ 272*e1fe3e4aSElliott Hughes (charnum, (charname, width, box)) 273*e1fe3e4aSElliott Hughes for charname, (charnum, width, box) in self._chars.items() 274*e1fe3e4aSElliott Hughes ] 275*e1fe3e4aSElliott Hughes 276*e1fe3e4aSElliott Hughes def myKey(a): 277*e1fe3e4aSElliott Hughes """Custom key function to make sure unencoded chars (-1) 278*e1fe3e4aSElliott Hughes end up at the end of the list after sorting.""" 279*e1fe3e4aSElliott Hughes if a[0] == -1: 280*e1fe3e4aSElliott Hughes a = (0xFFFF,) + a[1:] # 0xffff is an arbitrary large number 281*e1fe3e4aSElliott Hughes return a 282*e1fe3e4aSElliott Hughes 283*e1fe3e4aSElliott Hughes items.sort(key=myKey) 284*e1fe3e4aSElliott Hughes 285*e1fe3e4aSElliott Hughes for charnum, (charname, width, (l, b, r, t)) in items: 286*e1fe3e4aSElliott Hughes lines.append( 287*e1fe3e4aSElliott Hughes "C %d ; WX %d ; N %s ; B %d %d %d %d ;" 288*e1fe3e4aSElliott Hughes % (charnum, width, charname, l, b, r, t) 289*e1fe3e4aSElliott Hughes ) 290*e1fe3e4aSElliott Hughes lines.append("EndCharMetrics") 291*e1fe3e4aSElliott Hughes 292*e1fe3e4aSElliott Hughes # write kerning info 293*e1fe3e4aSElliott Hughes lines.append("StartKernData") 294*e1fe3e4aSElliott Hughes lines.append("StartKernPairs " + repr(len(self._kerning))) 295*e1fe3e4aSElliott Hughes items = sorted(self._kerning.items()) 296*e1fe3e4aSElliott Hughes for (leftchar, rightchar), value in items: 297*e1fe3e4aSElliott Hughes lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) 298*e1fe3e4aSElliott Hughes lines.append("EndKernPairs") 299*e1fe3e4aSElliott Hughes lines.append("EndKernData") 300*e1fe3e4aSElliott Hughes 301*e1fe3e4aSElliott Hughes if self._composites: 302*e1fe3e4aSElliott Hughes composites = sorted(self._composites.items()) 303*e1fe3e4aSElliott Hughes lines.append("StartComposites %s" % len(self._composites)) 304*e1fe3e4aSElliott Hughes for charname, components in composites: 305*e1fe3e4aSElliott Hughes line = "CC %s %s ;" % (charname, len(components)) 306*e1fe3e4aSElliott Hughes for basechar, xoffset, yoffset in components: 307*e1fe3e4aSElliott Hughes line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) 308*e1fe3e4aSElliott Hughes lines.append(line) 309*e1fe3e4aSElliott Hughes lines.append("EndComposites") 310*e1fe3e4aSElliott Hughes 311*e1fe3e4aSElliott Hughes lines.append("EndFontMetrics") 312*e1fe3e4aSElliott Hughes 313*e1fe3e4aSElliott Hughes writelines(path, lines, sep) 314*e1fe3e4aSElliott Hughes 315*e1fe3e4aSElliott Hughes def has_kernpair(self, pair): 316*e1fe3e4aSElliott Hughes """Returns `True` if the given glyph pair (specified as a tuple) exists 317*e1fe3e4aSElliott Hughes in the kerning dictionary.""" 318*e1fe3e4aSElliott Hughes return pair in self._kerning 319*e1fe3e4aSElliott Hughes 320*e1fe3e4aSElliott Hughes def kernpairs(self): 321*e1fe3e4aSElliott Hughes """Returns a list of all kern pairs in the kerning dictionary.""" 322*e1fe3e4aSElliott Hughes return list(self._kerning.keys()) 323*e1fe3e4aSElliott Hughes 324*e1fe3e4aSElliott Hughes def has_char(self, char): 325*e1fe3e4aSElliott Hughes """Returns `True` if the given glyph exists in the font.""" 326*e1fe3e4aSElliott Hughes return char in self._chars 327*e1fe3e4aSElliott Hughes 328*e1fe3e4aSElliott Hughes def chars(self): 329*e1fe3e4aSElliott Hughes """Returns a list of all glyph names in the font.""" 330*e1fe3e4aSElliott Hughes return list(self._chars.keys()) 331*e1fe3e4aSElliott Hughes 332*e1fe3e4aSElliott Hughes def comments(self): 333*e1fe3e4aSElliott Hughes """Returns all comments from the file.""" 334*e1fe3e4aSElliott Hughes return self._comments 335*e1fe3e4aSElliott Hughes 336*e1fe3e4aSElliott Hughes def addComment(self, comment): 337*e1fe3e4aSElliott Hughes """Adds a new comment to the file.""" 338*e1fe3e4aSElliott Hughes self._comments.append(comment) 339*e1fe3e4aSElliott Hughes 340*e1fe3e4aSElliott Hughes def addComposite(self, glyphName, components): 341*e1fe3e4aSElliott Hughes """Specifies that the glyph `glyphName` is made up of the given components. 342*e1fe3e4aSElliott Hughes The components list should be of the following form:: 343*e1fe3e4aSElliott Hughes 344*e1fe3e4aSElliott Hughes [ 345*e1fe3e4aSElliott Hughes (glyphname, xOffset, yOffset), 346*e1fe3e4aSElliott Hughes ... 347*e1fe3e4aSElliott Hughes ] 348*e1fe3e4aSElliott Hughes 349*e1fe3e4aSElliott Hughes """ 350*e1fe3e4aSElliott Hughes self._composites[glyphName] = components 351*e1fe3e4aSElliott Hughes 352*e1fe3e4aSElliott Hughes def __getattr__(self, attr): 353*e1fe3e4aSElliott Hughes if attr in self._attrs: 354*e1fe3e4aSElliott Hughes return self._attrs[attr] 355*e1fe3e4aSElliott Hughes else: 356*e1fe3e4aSElliott Hughes raise AttributeError(attr) 357*e1fe3e4aSElliott Hughes 358*e1fe3e4aSElliott Hughes def __setattr__(self, attr, value): 359*e1fe3e4aSElliott Hughes # all attrs *not* starting with "_" are consider to be AFM keywords 360*e1fe3e4aSElliott Hughes if attr[:1] == "_": 361*e1fe3e4aSElliott Hughes self.__dict__[attr] = value 362*e1fe3e4aSElliott Hughes else: 363*e1fe3e4aSElliott Hughes self._attrs[attr] = value 364*e1fe3e4aSElliott Hughes 365*e1fe3e4aSElliott Hughes def __delattr__(self, attr): 366*e1fe3e4aSElliott Hughes # all attrs *not* starting with "_" are consider to be AFM keywords 367*e1fe3e4aSElliott Hughes if attr[:1] == "_": 368*e1fe3e4aSElliott Hughes try: 369*e1fe3e4aSElliott Hughes del self.__dict__[attr] 370*e1fe3e4aSElliott Hughes except KeyError: 371*e1fe3e4aSElliott Hughes raise AttributeError(attr) 372*e1fe3e4aSElliott Hughes else: 373*e1fe3e4aSElliott Hughes try: 374*e1fe3e4aSElliott Hughes del self._attrs[attr] 375*e1fe3e4aSElliott Hughes except KeyError: 376*e1fe3e4aSElliott Hughes raise AttributeError(attr) 377*e1fe3e4aSElliott Hughes 378*e1fe3e4aSElliott Hughes def __getitem__(self, key): 379*e1fe3e4aSElliott Hughes if isinstance(key, tuple): 380*e1fe3e4aSElliott Hughes # key is a tuple, return the kernpair 381*e1fe3e4aSElliott Hughes return self._kerning[key] 382*e1fe3e4aSElliott Hughes else: 383*e1fe3e4aSElliott Hughes # return the metrics instead 384*e1fe3e4aSElliott Hughes return self._chars[key] 385*e1fe3e4aSElliott Hughes 386*e1fe3e4aSElliott Hughes def __setitem__(self, key, value): 387*e1fe3e4aSElliott Hughes if isinstance(key, tuple): 388*e1fe3e4aSElliott Hughes # key is a tuple, set kernpair 389*e1fe3e4aSElliott Hughes self._kerning[key] = value 390*e1fe3e4aSElliott Hughes else: 391*e1fe3e4aSElliott Hughes # set char metrics 392*e1fe3e4aSElliott Hughes self._chars[key] = value 393*e1fe3e4aSElliott Hughes 394*e1fe3e4aSElliott Hughes def __delitem__(self, key): 395*e1fe3e4aSElliott Hughes if isinstance(key, tuple): 396*e1fe3e4aSElliott Hughes # key is a tuple, del kernpair 397*e1fe3e4aSElliott Hughes del self._kerning[key] 398*e1fe3e4aSElliott Hughes else: 399*e1fe3e4aSElliott Hughes # del char metrics 400*e1fe3e4aSElliott Hughes del self._chars[key] 401*e1fe3e4aSElliott Hughes 402*e1fe3e4aSElliott Hughes def __repr__(self): 403*e1fe3e4aSElliott Hughes if hasattr(self, "FullName"): 404*e1fe3e4aSElliott Hughes return "<AFM object for %s>" % self.FullName 405*e1fe3e4aSElliott Hughes else: 406*e1fe3e4aSElliott Hughes return "<AFM object at %x>" % id(self) 407*e1fe3e4aSElliott Hughes 408*e1fe3e4aSElliott Hughes 409*e1fe3e4aSElliott Hughesdef readlines(path): 410*e1fe3e4aSElliott Hughes with open(path, "r", encoding="ascii") as f: 411*e1fe3e4aSElliott Hughes data = f.read() 412*e1fe3e4aSElliott Hughes return data.splitlines() 413*e1fe3e4aSElliott Hughes 414*e1fe3e4aSElliott Hughes 415*e1fe3e4aSElliott Hughesdef writelines(path, lines, sep="\r"): 416*e1fe3e4aSElliott Hughes with open(path, "w", encoding="ascii", newline=sep) as f: 417*e1fe3e4aSElliott Hughes f.write("\n".join(lines) + "\n") 418*e1fe3e4aSElliott Hughes 419*e1fe3e4aSElliott Hughes 420*e1fe3e4aSElliott Hughesif __name__ == "__main__": 421*e1fe3e4aSElliott Hughes import EasyDialogs 422*e1fe3e4aSElliott Hughes 423*e1fe3e4aSElliott Hughes path = EasyDialogs.AskFileForOpen() 424*e1fe3e4aSElliott Hughes if path: 425*e1fe3e4aSElliott Hughes afm = AFM(path) 426*e1fe3e4aSElliott Hughes char = "A" 427*e1fe3e4aSElliott Hughes if afm.has_char(char): 428*e1fe3e4aSElliott Hughes print(afm[char]) # print charnum, width and boundingbox 429*e1fe3e4aSElliott Hughes pair = ("A", "V") 430*e1fe3e4aSElliott Hughes if afm.has_kernpair(pair): 431*e1fe3e4aSElliott Hughes print(afm[pair]) # print kerning value for pair 432*e1fe3e4aSElliott Hughes print(afm.Version) # various other afm entries have become attributes 433*e1fe3e4aSElliott Hughes print(afm.Weight) 434*e1fe3e4aSElliott Hughes # afm.comments() returns a list of all Comment lines found in the AFM 435*e1fe3e4aSElliott Hughes print(afm.comments()) 436*e1fe3e4aSElliott Hughes # print afm.chars() 437*e1fe3e4aSElliott Hughes # print afm.kernpairs() 438*e1fe3e4aSElliott Hughes print(afm) 439*e1fe3e4aSElliott Hughes afm.write(path + ".muck") 440