1"""Module for reading and writing AFM files.""" 2 3# XXX reads AFM's generated by Fog, not tested with much else. 4# It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics 5# File Format Specification). Still, it should read most "common" AFM files. 6 7from __future__ import print_function, division, absolute_import 8from fontTools.misc.py23 import * 9import re 10 11# every single line starts with a "word" 12identifierRE = re.compile("^([A-Za-z]+).*") 13 14# regular expression to parse char lines 15charRE = re.compile( 16 "(-?\d+)" # charnum 17 "\s*;\s*WX\s+" # ; WX 18 "(-?\d+)" # width 19 "\s*;\s*N\s+" # ; N 20 "([.A-Za-z0-9_]+)" # charname 21 "\s*;\s*B\s+" # ; B 22 "(-?\d+)" # left 23 "\s+" 24 "(-?\d+)" # bottom 25 "\s+" 26 "(-?\d+)" # right 27 "\s+" 28 "(-?\d+)" # top 29 "\s*;\s*" # ; 30 ) 31 32# regular expression to parse kerning lines 33kernRE = re.compile( 34 "([.A-Za-z0-9_]+)" # leftchar 35 "\s+" 36 "([.A-Za-z0-9_]+)" # rightchar 37 "\s+" 38 "(-?\d+)" # value 39 "\s*" 40 ) 41 42# regular expressions to parse composite info lines of the form: 43# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; 44compositeRE = re.compile( 45 "([.A-Za-z0-9_]+)" # char name 46 "\s+" 47 "(\d+)" # number of parts 48 "\s*;\s*" 49 ) 50componentRE = re.compile( 51 "PCC\s+" # PPC 52 "([.A-Za-z0-9_]+)" # base char name 53 "\s+" 54 "(-?\d+)" # x offset 55 "\s+" 56 "(-?\d+)" # y offset 57 "\s*;\s*" 58 ) 59 60preferredAttributeOrder = [ 61 "FontName", 62 "FullName", 63 "FamilyName", 64 "Weight", 65 "ItalicAngle", 66 "IsFixedPitch", 67 "FontBBox", 68 "UnderlinePosition", 69 "UnderlineThickness", 70 "Version", 71 "Notice", 72 "EncodingScheme", 73 "CapHeight", 74 "XHeight", 75 "Ascender", 76 "Descender", 77] 78 79 80class error(Exception): 81 pass 82 83 84class AFM(object): 85 86 _attrs = None 87 88 _keywords = ['StartFontMetrics', 89 'EndFontMetrics', 90 'StartCharMetrics', 91 'EndCharMetrics', 92 'StartKernData', 93 'StartKernPairs', 94 'EndKernPairs', 95 'EndKernData', 96 'StartComposites', 97 'EndComposites', 98 ] 99 100 def __init__(self, path=None): 101 self._attrs = {} 102 self._chars = {} 103 self._kerning = {} 104 self._index = {} 105 self._comments = [] 106 self._composites = {} 107 if path is not None: 108 self.read(path) 109 110 def read(self, path): 111 lines = readlines(path) 112 for line in lines: 113 if not line.strip(): 114 continue 115 m = identifierRE.match(line) 116 if m is None: 117 raise error("syntax error in AFM file: " + repr(line)) 118 119 pos = m.regs[1][1] 120 word = line[:pos] 121 rest = line[pos:].strip() 122 if word in self._keywords: 123 continue 124 if word == "C": 125 self.parsechar(rest) 126 elif word == "KPX": 127 self.parsekernpair(rest) 128 elif word == "CC": 129 self.parsecomposite(rest) 130 else: 131 self.parseattr(word, rest) 132 133 def parsechar(self, rest): 134 m = charRE.match(rest) 135 if m is None: 136 raise error("syntax error in AFM file: " + repr(rest)) 137 things = [] 138 for fr, to in m.regs[1:]: 139 things.append(rest[fr:to]) 140 charname = things[2] 141 del things[2] 142 charnum, width, l, b, r, t = (int(thing) for thing in things) 143 self._chars[charname] = charnum, width, (l, b, r, t) 144 145 def parsekernpair(self, rest): 146 m = kernRE.match(rest) 147 if m is None: 148 raise error("syntax error in AFM file: " + repr(rest)) 149 things = [] 150 for fr, to in m.regs[1:]: 151 things.append(rest[fr:to]) 152 leftchar, rightchar, value = things 153 value = int(value) 154 self._kerning[(leftchar, rightchar)] = value 155 156 def parseattr(self, word, rest): 157 if word == "FontBBox": 158 l, b, r, t = [int(thing) for thing in rest.split()] 159 self._attrs[word] = l, b, r, t 160 elif word == "Comment": 161 self._comments.append(rest) 162 else: 163 try: 164 value = int(rest) 165 except (ValueError, OverflowError): 166 self._attrs[word] = rest 167 else: 168 self._attrs[word] = value 169 170 def parsecomposite(self, rest): 171 m = compositeRE.match(rest) 172 if m is None: 173 raise error("syntax error in AFM file: " + repr(rest)) 174 charname = m.group(1) 175 ncomponents = int(m.group(2)) 176 rest = rest[m.regs[0][1]:] 177 components = [] 178 while True: 179 m = componentRE.match(rest) 180 if m is None: 181 raise error("syntax error in AFM file: " + repr(rest)) 182 basechar = m.group(1) 183 xoffset = int(m.group(2)) 184 yoffset = int(m.group(3)) 185 components.append((basechar, xoffset, yoffset)) 186 rest = rest[m.regs[0][1]:] 187 if not rest: 188 break 189 assert len(components) == ncomponents 190 self._composites[charname] = components 191 192 def write(self, path, sep='\r'): 193 import time 194 lines = [ "StartFontMetrics 2.0", 195 "Comment Generated by afmLib; at %s" % ( 196 time.strftime("%m/%d/%Y %H:%M:%S", 197 time.localtime(time.time())))] 198 199 # write comments, assuming (possibly wrongly!) they should 200 # all appear at the top 201 for comment in self._comments: 202 lines.append("Comment " + comment) 203 204 # write attributes, first the ones we know about, in 205 # a preferred order 206 attrs = self._attrs 207 for attr in preferredAttributeOrder: 208 if attr in attrs: 209 value = attrs[attr] 210 if attr == "FontBBox": 211 value = "%s %s %s %s" % value 212 lines.append(attr + " " + str(value)) 213 # then write the attributes we don't know about, 214 # in alphabetical order 215 items = sorted(attrs.items()) 216 for attr, value in items: 217 if attr in preferredAttributeOrder: 218 continue 219 lines.append(attr + " " + str(value)) 220 221 # write char metrics 222 lines.append("StartCharMetrics " + repr(len(self._chars))) 223 items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()] 224 225 def myKey(a): 226 """Custom key function to make sure unencoded chars (-1) 227 end up at the end of the list after sorting.""" 228 if a[0] == -1: 229 a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number 230 return a 231 items.sort(key=myKey) 232 233 for charnum, (charname, width, (l, b, r, t)) in items: 234 lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" % 235 (charnum, width, charname, l, b, r, t)) 236 lines.append("EndCharMetrics") 237 238 # write kerning info 239 lines.append("StartKernData") 240 lines.append("StartKernPairs " + repr(len(self._kerning))) 241 items = sorted(self._kerning.items()) 242 for (leftchar, rightchar), value in items: 243 lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) 244 lines.append("EndKernPairs") 245 lines.append("EndKernData") 246 247 if self._composites: 248 composites = sorted(self._composites.items()) 249 lines.append("StartComposites %s" % len(self._composites)) 250 for charname, components in composites: 251 line = "CC %s %s ;" % (charname, len(components)) 252 for basechar, xoffset, yoffset in components: 253 line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) 254 lines.append(line) 255 lines.append("EndComposites") 256 257 lines.append("EndFontMetrics") 258 259 writelines(path, lines, sep) 260 261 def has_kernpair(self, pair): 262 return pair in self._kerning 263 264 def kernpairs(self): 265 return list(self._kerning.keys()) 266 267 def has_char(self, char): 268 return char in self._chars 269 270 def chars(self): 271 return list(self._chars.keys()) 272 273 def comments(self): 274 return self._comments 275 276 def addComment(self, comment): 277 self._comments.append(comment) 278 279 def addComposite(self, glyphName, components): 280 self._composites[glyphName] = components 281 282 def __getattr__(self, attr): 283 if attr in self._attrs: 284 return self._attrs[attr] 285 else: 286 raise AttributeError(attr) 287 288 def __setattr__(self, attr, value): 289 # all attrs *not* starting with "_" are consider to be AFM keywords 290 if attr[:1] == "_": 291 self.__dict__[attr] = value 292 else: 293 self._attrs[attr] = value 294 295 def __delattr__(self, attr): 296 # all attrs *not* starting with "_" are consider to be AFM keywords 297 if attr[:1] == "_": 298 try: 299 del self.__dict__[attr] 300 except KeyError: 301 raise AttributeError(attr) 302 else: 303 try: 304 del self._attrs[attr] 305 except KeyError: 306 raise AttributeError(attr) 307 308 def __getitem__(self, key): 309 if isinstance(key, tuple): 310 # key is a tuple, return the kernpair 311 return self._kerning[key] 312 else: 313 # return the metrics instead 314 return self._chars[key] 315 316 def __setitem__(self, key, value): 317 if isinstance(key, tuple): 318 # key is a tuple, set kernpair 319 self._kerning[key] = value 320 else: 321 # set char metrics 322 self._chars[key] = value 323 324 def __delitem__(self, key): 325 if isinstance(key, tuple): 326 # key is a tuple, del kernpair 327 del self._kerning[key] 328 else: 329 # del char metrics 330 del self._chars[key] 331 332 def __repr__(self): 333 if hasattr(self, "FullName"): 334 return '<AFM object for %s>' % self.FullName 335 else: 336 return '<AFM object at %x>' % id(self) 337 338 339def readlines(path): 340 with open(path, "r", encoding="ascii") as f: 341 data = f.read() 342 return data.splitlines() 343 344def writelines(path, lines, sep='\r'): 345 with open(path, "w", encoding="ascii", newline=sep) as f: 346 f.write("\n".join(lines) + "\n") 347 348 349if __name__ == "__main__": 350 import EasyDialogs 351 path = EasyDialogs.AskFileForOpen() 352 if path: 353 afm = AFM(path) 354 char = 'A' 355 if afm.has_char(char): 356 print(afm[char]) # print charnum, width and boundingbox 357 pair = ('A', 'V') 358 if afm.has_kernpair(pair): 359 print(afm[pair]) # print kerning value for pair 360 print(afm.Version) # various other afm entries have become attributes 361 print(afm.Weight) 362 # afm.comments() returns a list of all Comment lines found in the AFM 363 print(afm.comments()) 364 #print afm.chars() 365 #print afm.kernpairs() 366 print(afm) 367 afm.write(path + ".muck") 368