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): pass 81 82 83class AFM(object): 84 85 _attrs = None 86 87 _keywords = ['StartFontMetrics', 88 'EndFontMetrics', 89 'StartCharMetrics', 90 'EndCharMetrics', 91 'StartKernData', 92 'StartKernPairs', 93 'EndKernPairs', 94 'EndKernData', 95 'StartComposites', 96 'EndComposites', 97 ] 98 99 def __init__(self, path=None): 100 self._attrs = {} 101 self._chars = {} 102 self._kerning = {} 103 self._index = {} 104 self._comments = [] 105 self._composites = {} 106 if path is not None: 107 self.read(path) 108 109 def read(self, path): 110 lines = readlines(path) 111 for line in lines: 112 if not line.strip(): 113 continue 114 m = identifierRE.match(line) 115 if m is None: 116 raise error("syntax error in AFM file: " + repr(line)) 117 118 pos = m.regs[1][1] 119 word = line[:pos] 120 rest = line[pos:].strip() 121 if word in self._keywords: 122 continue 123 if word == "C": 124 self.parsechar(rest) 125 elif word == "KPX": 126 self.parsekernpair(rest) 127 elif word == "CC": 128 self.parsecomposite(rest) 129 else: 130 self.parseattr(word, rest) 131 132 def parsechar(self, rest): 133 m = charRE.match(rest) 134 if m is None: 135 raise error("syntax error in AFM file: " + repr(rest)) 136 things = [] 137 for fr, to in m.regs[1:]: 138 things.append(rest[fr:to]) 139 charname = things[2] 140 del things[2] 141 charnum, width, l, b, r, t = (int(thing) for thing in things) 142 self._chars[charname] = charnum, width, (l, b, r, t) 143 144 def parsekernpair(self, rest): 145 m = kernRE.match(rest) 146 if m is None: 147 raise error("syntax error in AFM file: " + repr(rest)) 148 things = [] 149 for fr, to in m.regs[1:]: 150 things.append(rest[fr:to]) 151 leftchar, rightchar, value = things 152 value = int(value) 153 self._kerning[(leftchar, rightchar)] = value 154 155 def parseattr(self, word, rest): 156 if word == "FontBBox": 157 l, b, r, t = [int(thing) for thing in rest.split()] 158 self._attrs[word] = l, b, r, t 159 elif word == "Comment": 160 self._comments.append(rest) 161 else: 162 try: 163 value = int(rest) 164 except (ValueError, OverflowError): 165 self._attrs[word] = rest 166 else: 167 self._attrs[word] = value 168 169 def parsecomposite(self, rest): 170 m = compositeRE.match(rest) 171 if m is None: 172 raise error("syntax error in AFM file: " + repr(rest)) 173 charname = m.group(1) 174 ncomponents = int(m.group(2)) 175 rest = rest[m.regs[0][1]:] 176 components = [] 177 while True: 178 m = componentRE.match(rest) 179 if m is None: 180 raise error("syntax error in AFM file: " + repr(rest)) 181 basechar = m.group(1) 182 xoffset = int(m.group(2)) 183 yoffset = int(m.group(3)) 184 components.append((basechar, xoffset, yoffset)) 185 rest = rest[m.regs[0][1]:] 186 if not rest: 187 break 188 assert len(components) == ncomponents 189 self._composites[charname] = components 190 191 def write(self, path, sep='\r'): 192 import time 193 lines = [ "StartFontMetrics 2.0", 194 "Comment Generated by afmLib; at %s" % ( 195 time.strftime("%m/%d/%Y %H:%M:%S", 196 time.localtime(time.time())))] 197 198 # write comments, assuming (possibly wrongly!) they should 199 # all appear at the top 200 for comment in self._comments: 201 lines.append("Comment " + comment) 202 203 # write attributes, first the ones we know about, in 204 # a preferred order 205 attrs = self._attrs 206 for attr in preferredAttributeOrder: 207 if attr in attrs: 208 value = attrs[attr] 209 if attr == "FontBBox": 210 value = "%s %s %s %s" % value 211 lines.append(attr + " " + str(value)) 212 # then write the attributes we don't know about, 213 # in alphabetical order 214 items = sorted(attrs.items()) 215 for attr, value in items: 216 if attr in preferredAttributeOrder: 217 continue 218 lines.append(attr + " " + str(value)) 219 220 # write char metrics 221 lines.append("StartCharMetrics " + repr(len(self._chars))) 222 items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()] 223 224 def myKey(a): 225 """Custom key function to make sure unencoded chars (-1) 226 end up at the end of the list after sorting.""" 227 if a[0] == -1: 228 a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number 229 return a 230 items.sort(key=myKey) 231 232 for charnum, (charname, width, (l, b, r, t)) in items: 233 lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" % 234 (charnum, width, charname, l, b, r, t)) 235 lines.append("EndCharMetrics") 236 237 # write kerning info 238 lines.append("StartKernData") 239 lines.append("StartKernPairs " + repr(len(self._kerning))) 240 items = sorted(self._kerning.items()) 241 for (leftchar, rightchar), value in items: 242 lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) 243 lines.append("EndKernPairs") 244 lines.append("EndKernData") 245 246 if self._composites: 247 composites = sorted(self._composites.items()) 248 lines.append("StartComposites %s" % len(self._composites)) 249 for charname, components in composites: 250 line = "CC %s %s ;" % (charname, len(components)) 251 for basechar, xoffset, yoffset in components: 252 line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) 253 lines.append(line) 254 lines.append("EndComposites") 255 256 lines.append("EndFontMetrics") 257 258 writelines(path, lines, sep) 259 260 def has_kernpair(self, pair): 261 return pair in self._kerning 262 263 def kernpairs(self): 264 return list(self._kerning.keys()) 265 266 def has_char(self, char): 267 return char in self._chars 268 269 def chars(self): 270 return list(self._chars.keys()) 271 272 def comments(self): 273 return self._comments 274 275 def addComment(self, comment): 276 self._comments.append(comment) 277 278 def addComposite(self, glyphName, components): 279 self._composites[glyphName] = components 280 281 def __getattr__(self, attr): 282 if attr in self._attrs: 283 return self._attrs[attr] 284 else: 285 raise AttributeError(attr) 286 287 def __setattr__(self, attr, value): 288 # all attrs *not* starting with "_" are consider to be AFM keywords 289 if attr[:1] == "_": 290 self.__dict__[attr] = value 291 else: 292 self._attrs[attr] = value 293 294 def __delattr__(self, attr): 295 # all attrs *not* starting with "_" are consider to be AFM keywords 296 if attr[:1] == "_": 297 try: 298 del self.__dict__[attr] 299 except KeyError: 300 raise AttributeError(attr) 301 else: 302 try: 303 del self._attrs[attr] 304 except KeyError: 305 raise AttributeError(attr) 306 307 def __getitem__(self, key): 308 if isinstance(key, tuple): 309 # key is a tuple, return the kernpair 310 return self._kerning[key] 311 else: 312 # return the metrics instead 313 return self._chars[key] 314 315 def __setitem__(self, key, value): 316 if isinstance(key, tuple): 317 # key is a tuple, set kernpair 318 self._kerning[key] = value 319 else: 320 # set char metrics 321 self._chars[key] = value 322 323 def __delitem__(self, key): 324 if isinstance(key, tuple): 325 # key is a tuple, del kernpair 326 del self._kerning[key] 327 else: 328 # del char metrics 329 del self._chars[key] 330 331 def __repr__(self): 332 if hasattr(self, "FullName"): 333 return '<AFM object for %s>' % self.FullName 334 else: 335 return '<AFM object at %x>' % id(self) 336 337 338def readlines(path): 339 f = open(path, 'rb') 340 data = f.read() 341 f.close() 342 # read any text file, regardless whether it's formatted for Mac, Unix or Dos 343 sep = "" 344 if '\r' in data: 345 sep = sep + '\r' # mac or dos 346 if '\n' in data: 347 sep = sep + '\n' # unix or dos 348 return data.split(sep) 349 350def writelines(path, lines, sep='\r'): 351 f = open(path, 'wb') 352 for line in lines: 353 f.write(line + sep) 354 f.close() 355 356 357 358if __name__ == "__main__": 359 import EasyDialogs 360 path = EasyDialogs.AskFileForOpen() 361 if path: 362 afm = AFM(path) 363 char = 'A' 364 if afm.has_char(char): 365 print(afm[char]) # print charnum, width and boundingbox 366 pair = ('A', 'V') 367 if afm.has_kernpair(pair): 368 print(afm[pair]) # print kerning value for pair 369 print(afm.Version) # various other afm entries have become attributes 370 print(afm.Weight) 371 # afm.comments() returns a list of all Comment lines found in the AFM 372 print(afm.comments()) 373 #print afm.chars() 374 #print afm.kernpairs() 375 print(afm) 376 afm.write(path + ".muck") 377 378