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