Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 | |
| 7 | import re |
| 8 | import string |
| 9 | import types |
| 10 | |
Just | 6175deb | 2001-06-24 15:11:31 +0000 | [diff] [blame] | 11 | __version__ = "$Id: afmLib.py,v 1.3 2001-06-24 15:11:31 Just Exp $" |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 12 | |
| 13 | |
| 14 | # every single line starts with a "word" |
| 15 | identifierRE = re.compile("^([A-Za-z]+).*") |
| 16 | |
| 17 | # regular expression to parse char lines |
| 18 | charRE = re.compile( |
| 19 | "(-?\d+)" # charnum |
| 20 | "\s*;\s*WX\s+" # ; WX |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 21 | "(\d+)" # width |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 |
| 36 | kernRE = 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 | |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 45 | # regular expressions to parse composite info lines of the form: |
| 46 | # Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; |
| 47 | compositeRE = re.compile( |
| 48 | "([.A-Za-z0-9_]+)" # char name |
| 49 | "\s+" # |
| 50 | "(\d+)" # number of parts |
| 51 | "\s*;\s*" # |
| 52 | ) |
| 53 | componentRE = 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 | |
| 63 | preferredAttributeOrder = [ |
| 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 | |
| 83 | class error(Exception): pass |
| 84 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 85 | |
| 86 | class AFM: |
| 87 | |
| 88 | _keywords = ['StartFontMetrics', |
| 89 | 'EndFontMetrics', |
| 90 | 'StartCharMetrics', |
| 91 | 'EndCharMetrics', |
| 92 | 'StartKernData', |
| 93 | 'StartKernPairs', |
| 94 | 'EndKernPairs', |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 95 | 'EndKernData', |
| 96 | 'StartComposites', |
| 97 | 'EndComposites', |
| 98 | ] |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 99 | |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 100 | def __init__(self, path=None): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 101 | self._attrs = {} |
| 102 | self._chars = {} |
| 103 | self._kerning = {} |
| 104 | self._index = {} |
| 105 | self._comments = [] |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 106 | self._composites = {} |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 124 | if word == "C": |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 125 | self.parsechar(rest) |
| 126 | elif word == "KPX": |
| 127 | self.parsekernpair(rest) |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 128 | elif word == "CC": |
| 129 | self.parsecomposite(rest) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 | |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 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 | |
Just | 6175deb | 2001-06-24 15:11:31 +0000 | [diff] [blame] | 192 | def write(self, path, sep='\r'): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 | |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 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() |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 218 | for attr, value in items: |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 219 | if attr in preferredAttributeOrder: |
| 220 | continue |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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)) |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 251 | lines.append("EndKernPairs") |
| 252 | lines.append("EndKernData") |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 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 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 | |
Just | 6175deb | 2001-06-24 15:11:31 +0000 | [diff] [blame] | 284 | def addComment(self, comment): |
| 285 | self._comments.append(comment) |
| 286 | |
| 287 | def addComposite(self, glyphName, components): |
| 288 | self._composites[glyphName] = components |
| 289 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 | |
Just | 6175deb | 2001-06-24 15:11:31 +0000 | [diff] [blame] | 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 | |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 316 | def __getitem__(self, key): |
| 317 | if type(key) == types.TupleType: |
| 318 | # key is a tuple, return the kernpair |
Just | 6175deb | 2001-06-24 15:11:31 +0000 | [diff] [blame] | 319 | return self._kerning[key] |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 320 | else: |
| 321 | # return the metrics instead |
Just | 6175deb | 2001-06-24 15:11:31 +0000 | [diff] [blame] | 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] |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 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 | |
| 347 | def 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 | |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 359 | def writelines(path, lines, sep='\r'): |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 360 | f = open(path, 'wb') |
| 361 | for line in lines: |
| 362 | f.write(line + sep) |
| 363 | f.close() |
| 364 | |
| 365 | |
| 366 | |
| 367 | if __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 |
Just | 32f8684 | 2001-04-30 14:40:17 +0000 | [diff] [blame] | 386 | afm.write(path + ".muck") |
Just | 7842e56 | 1999-12-16 21:34:53 +0000 | [diff] [blame] | 387 | |