afmLib.py revision 6175debd674004b3e88a4fea1338a090fc4b9da8
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 7import re 8import string 9import types 10 11__version__ = "$Id: afmLib.py,v 1.3 2001-06-24 15:11:31 Just Exp $" 12 13 14# every single line starts with a "word" 15identifierRE = re.compile("^([A-Za-z]+).*") 16 17# regular expression to parse char lines 18charRE = re.compile( 19 "(-?\d+)" # charnum 20 "\s*;\s*WX\s+" # ; WX 21 "(\d+)" # width 22 "\s*;\s*N\s+" # ; N 23 "(\.?[A-Za-z0-9_]+)" # charname 24 "\s*;\s*B\s+" # ; B 25 "(-?\d+)" # left 26 "\s+" # 27 "(-?\d+)" # bottom 28 "\s+" # 29 "(-?\d+)" # right 30 "\s+" # 31 "(-?\d+)" # top 32 "\s*;\s*" # ; 33 ) 34 35# regular expression to parse kerning lines 36kernRE = re.compile( 37 "([.A-Za-z0-9_]+)" # leftchar 38 "\s+" # 39 "([.A-Za-z0-9_]+)" # rightchar 40 "\s+" # 41 "(-?\d+)" # value 42 "\s*" # 43 ) 44 45# regular expressions to parse composite info lines of the form: 46# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; 47compositeRE = re.compile( 48 "([.A-Za-z0-9_]+)" # char name 49 "\s+" # 50 "(\d+)" # number of parts 51 "\s*;\s*" # 52 ) 53componentRE = re.compile( 54 "PCC\s+" # PPC 55 "([.A-Za-z0-9_]+)" # base char name 56 "\s+" # 57 "(-?\d+)" # x offset 58 "\s+" # 59 "(-?\d+)" # y offset 60 "\s*;\s*" # 61 ) 62 63preferredAttributeOrder = [ 64 "FontName", 65 "FullName", 66 "FamilyName", 67 "Weight", 68 "ItalicAngle", 69 "IsFixedPitch", 70 "FontBBox", 71 "UnderlinePosition", 72 "UnderlineThickness", 73 "Version", 74 "Notice", 75 "EncodingScheme", 76 "CapHeight", 77 "XHeight", 78 "Ascender", 79 "Descender", 80] 81 82 83class error(Exception): pass 84 85 86class AFM: 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 string.strip(line): 114 continue 115 m = identifierRE.match(line) 116 if m is None: 117 raise error, "syntax error in AFM file: " + `line` 118 119 pos = m.regs[1][1] 120 word = line[:pos] 121 rest = string.strip(line[pos:]) 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: " + `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 = map(string.atoi, 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: " + `rest` 149 things = [] 150 for fr, to in m.regs[1:]: 151 things.append(rest[fr:to]) 152 leftchar, rightchar, value = things 153 value = string.atoi(value) 154 self._kerning[(leftchar, rightchar)] = value 155 156 def parseattr(self, word, rest): 157 if word == "FontBBox": 158 l, b, r, t = map(string.atoi, string.split(rest)) 159 self._attrs[word] = l, b, r, t 160 elif word == "Comment": 161 self._comments.append(rest) 162 else: 163 try: 164 value = string.atoi(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: " + `rest` 174 charname = m.group(1) 175 ncomponents = int(m.group(2)) 176 rest = rest[m.regs[0][1]:] 177 components = [] 178 while 1: 179 m = componentRE.match(rest) 180 if m is None: 181 raise error, "syntax error in AFM file: " + `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, version %s; at %s" % 196 (string.split(__version__)[2], 197 time.strftime("%m/%d/%Y %H:%M:%S", 198 time.localtime(time.time())))] 199 200 # write comments, assuming (possibly wrongly!) they should 201 # all appear at the top 202 for comment in self._comments: 203 lines.append("Comment " + comment) 204 205 # write attributes, first the ones we know about, in 206 # a preferred order 207 attrs = self._attrs 208 for attr in preferredAttributeOrder: 209 if attrs.has_key(attr): 210 value = attrs[attr] 211 if attr == "FontBBox": 212 value = "%s %s %s %s" % value 213 lines.append(attr + " " + str(value)) 214 # then write the attributes we don't know about, 215 # in alphabetical order 216 items = attrs.items() 217 items.sort() 218 for attr, value in items: 219 if attr in preferredAttributeOrder: 220 continue 221 lines.append(attr + " " + str(value)) 222 223 # write char metrics 224 lines.append("StartCharMetrics " + `len(self._chars)`) 225 items = map(lambda (charname, (charnum, width, box)): 226 (charnum, (charname, width, box)), 227 self._chars.items()) 228 229 def myCmp(a, b): 230 """Custom compare function to make sure unencoded chars (-1) 231 end up at the end of the list after sorting.""" 232 if a[0] == -1: 233 a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number 234 if b[0] == -1: 235 b = (0xffff,) + b[1:] 236 return cmp(a, b) 237 items.sort(myCmp) 238 239 for charnum, (charname, width, (l, b, r, t)) in items: 240 lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" % 241 (charnum, width, charname, l, b, r, t)) 242 lines.append("EndCharMetrics") 243 244 # write kerning info 245 lines.append("StartKernData") 246 lines.append("StartKernPairs " + `len(self._kerning)`) 247 items = self._kerning.items() 248 items.sort() # XXX is order important? 249 for (leftchar, rightchar), value in items: 250 lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) 251 lines.append("EndKernPairs") 252 lines.append("EndKernData") 253 254 if self._composites: 255 composites = self._composites.items() 256 composites.sort() 257 lines.append("StartComposites %s" % len(self._composites)) 258 for charname, components in composites: 259 line = "CC %s %s ;" % (charname, len(components)) 260 for basechar, xoffset, yoffset in components: 261 line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) 262 lines.append(line) 263 lines.append("EndComposites") 264 265 lines.append("EndFontMetrics") 266 267 writelines(path, lines, sep) 268 269 def has_kernpair(self, pair): 270 return self._kerning.has_key(pair) 271 272 def kernpairs(self): 273 return self._kerning.keys() 274 275 def has_char(self, char): 276 return self._chars.has_key(char) 277 278 def chars(self): 279 return self._chars.keys() 280 281 def comments(self): 282 return self._comments 283 284 def addComment(self, comment): 285 self._comments.append(comment) 286 287 def addComposite(self, glyphName, components): 288 self._composites[glyphName] = components 289 290 def __getattr__(self, attr): 291 if self._attrs.has_key(attr): 292 return self._attrs[attr] 293 else: 294 raise AttributeError, attr 295 296 def __setattr__(self, attr, value): 297 # all attrs *not* starting with "_" are consider to be AFM keywords 298 if attr[:1] == "_": 299 self.__dict__[attr] = value 300 else: 301 self._attrs[attr] = value 302 303 def __delattr__(self, attr): 304 # all attrs *not* starting with "_" are consider to be AFM keywords 305 if attr[:1] == "_": 306 try: 307 del self.__dict__[attr] 308 except KeyError: 309 raise AttributeError, attr 310 else: 311 try: 312 del self._attrs[attr] 313 except KeyError: 314 raise AttributeError, attr 315 316 def __getitem__(self, key): 317 if type(key) == types.TupleType: 318 # key is a tuple, return the kernpair 319 return self._kerning[key] 320 else: 321 # return the metrics instead 322 return self._chars[key] 323 324 def __setitem__(self, key, value): 325 if type(key) == types.TupleType: 326 # key is a tuple, set kernpair 327 self._kerning[key] = value 328 else: 329 # set char metrics 330 self._chars[key] = value 331 332 def __delitem__(self, key): 333 if type(key) == types.TupleType: 334 # key is a tuple, del kernpair 335 del self._kerning[key] 336 else: 337 # del char metrics 338 del self._chars[key] 339 340 def __repr__(self): 341 if hasattr(self, "FullName"): 342 return '<AFM object for %s>' % self.FullName 343 else: 344 return '<AFM object at %x>' % id(self) 345 346 347def readlines(path): 348 f = open(path, 'rb') 349 data = f.read() 350 f.close() 351 # read any text file, regardless whether it's formatted for Mac, Unix or Dos 352 sep = "" 353 if '\r' in data: 354 sep = sep + '\r' # mac or dos 355 if '\n' in data: 356 sep = sep + '\n' # unix or dos 357 return string.split(data, sep) 358 359def writelines(path, lines, sep='\r'): 360 f = open(path, 'wb') 361 for line in lines: 362 f.write(line + sep) 363 f.close() 364 365 366 367if __name__ == "__main__": 368 import macfs 369 fss, ok = macfs.StandardGetFile('TEXT') 370 if ok: 371 path = fss.as_pathname() 372 afm = AFM(path) 373 char = 'A' 374 if afm.has_char(char): 375 print afm[char] # print charnum, width and boundingbox 376 pair = ('A', 'V') 377 if afm.has_kernpair(pair): 378 print afm[pair] # print kerning value for pair 379 print afm.Version # various other afm entries have become attributes 380 print afm.Weight 381 # afm.comments() returns a list of all Comment lines found in the AFM 382 print afm.comments() 383 #print afm.chars() 384 #print afm.kernpairs() 385 print afm 386 afm.write(path + ".muck") 387 388