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