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 | |
| 11 | __version__ = "$Id: afmLib.py,v 1.1 1999-12-16 21:34:51 Just Exp $" |
| 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 |
| 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 |
| 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 | |
| 45 | error = "AFM.error" |
| 46 | |
| 47 | class AFM: |
| 48 | |
| 49 | _keywords = ['StartFontMetrics', |
| 50 | 'EndFontMetrics', |
| 51 | 'StartCharMetrics', |
| 52 | 'EndCharMetrics', |
| 53 | 'StartKernData', |
| 54 | 'StartKernPairs', |
| 55 | 'EndKernPairs', |
| 56 | 'EndKernData', ] |
| 57 | |
| 58 | def __init__(self, path = None): |
| 59 | self._attrs = {} |
| 60 | self._chars = {} |
| 61 | self._kerning = {} |
| 62 | self._index = {} |
| 63 | self._comments = [] |
| 64 | if path is not None: |
| 65 | self.read(path) |
| 66 | |
| 67 | def read(self, path): |
| 68 | lines = readlines(path) |
| 69 | for line in lines: |
| 70 | if not string.strip(line): |
| 71 | continue |
| 72 | m = identifierRE.match(line) |
| 73 | if m is None: |
| 74 | raise error, "syntax error in AFM file: " + `line` |
| 75 | |
| 76 | pos = m.regs[1][1] |
| 77 | word = line[:pos] |
| 78 | rest = string.strip(line[pos:]) |
| 79 | if word in self._keywords: |
| 80 | continue |
| 81 | if word == 'C': |
| 82 | self.parsechar(rest) |
| 83 | elif word == "KPX": |
| 84 | self.parsekernpair(rest) |
| 85 | else: |
| 86 | self.parseattr(word, rest) |
| 87 | |
| 88 | def parsechar(self, rest): |
| 89 | m = charRE.match(rest) |
| 90 | if m is None: |
| 91 | raise error, "syntax error in AFM file: " + `rest` |
| 92 | things = [] |
| 93 | for fr, to in m.regs[1:]: |
| 94 | things.append(rest[fr:to]) |
| 95 | charname = things[2] |
| 96 | del things[2] |
| 97 | charnum, width, l, b, r, t = map(string.atoi, things) |
| 98 | self._chars[charname] = charnum, width, (l, b, r, t) |
| 99 | |
| 100 | def parsekernpair(self, rest): |
| 101 | m = kernRE.match(rest) |
| 102 | if m is None: |
| 103 | raise error, "syntax error in AFM file: " + `rest` |
| 104 | things = [] |
| 105 | for fr, to in m.regs[1:]: |
| 106 | things.append(rest[fr:to]) |
| 107 | leftchar, rightchar, value = things |
| 108 | value = string.atoi(value) |
| 109 | self._kerning[(leftchar, rightchar)] = value |
| 110 | |
| 111 | def parseattr(self, word, rest): |
| 112 | if word == "FontBBox": |
| 113 | l, b, r, t = map(string.atoi, string.split(rest)) |
| 114 | self._attrs[word] = l, b, r, t |
| 115 | elif word == "Comment": |
| 116 | self._comments.append(rest) |
| 117 | else: |
| 118 | try: |
| 119 | value = string.atoi(rest) |
| 120 | except (ValueError, OverflowError): |
| 121 | self._attrs[word] = rest |
| 122 | else: |
| 123 | self._attrs[word] = value |
| 124 | |
| 125 | def write(self, path, sep = '\r'): |
| 126 | import time |
| 127 | lines = [ "StartFontMetrics 2.0", |
| 128 | "Comment Generated by afmLib, version %s; at %s" % |
| 129 | (string.split(__version__)[2], |
| 130 | time.strftime("%m/%d/%Y %H:%M:%S", |
| 131 | time.localtime(time.time())))] |
| 132 | |
| 133 | # write attributes |
| 134 | items = self._attrs.items() |
| 135 | items.sort() # XXX proper ordering??? |
| 136 | for attr, value in items: |
| 137 | if attr == "FontBBox": |
| 138 | value = string.join(map(str, value), " ") |
| 139 | lines.append(attr + " " + str(value)) |
| 140 | |
| 141 | # write char metrics |
| 142 | lines.append("StartCharMetrics " + `len(self._chars)`) |
| 143 | items = map(lambda (charname, (charnum, width, box)): |
| 144 | (charnum, (charname, width, box)), |
| 145 | self._chars.items()) |
| 146 | |
| 147 | def myCmp(a, b): |
| 148 | """Custom compare function to make sure unencoded chars (-1) |
| 149 | end up at the end of the list after sorting.""" |
| 150 | if a[0] == -1: |
| 151 | a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number |
| 152 | if b[0] == -1: |
| 153 | b = (0xffff,) + b[1:] |
| 154 | return cmp(a, b) |
| 155 | items.sort(myCmp) |
| 156 | |
| 157 | for charnum, (charname, width, (l, b, r, t)) in items: |
| 158 | lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" % |
| 159 | (charnum, width, charname, l, b, r, t)) |
| 160 | lines.append("EndCharMetrics") |
| 161 | |
| 162 | # write kerning info |
| 163 | lines.append("StartKernData") |
| 164 | lines.append("StartKernPairs " + `len(self._kerning)`) |
| 165 | items = self._kerning.items() |
| 166 | items.sort() # XXX is order important? |
| 167 | for (leftchar, rightchar), value in items: |
| 168 | lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) |
| 169 | |
| 170 | lines.append("EndKernPairs") |
| 171 | lines.append("EndKernData") |
| 172 | lines.append("EndFontMetrics") |
| 173 | |
| 174 | writelines(path, lines, sep) |
| 175 | |
| 176 | def has_kernpair(self, pair): |
| 177 | return self._kerning.has_key(pair) |
| 178 | |
| 179 | def kernpairs(self): |
| 180 | return self._kerning.keys() |
| 181 | |
| 182 | def has_char(self, char): |
| 183 | return self._chars.has_key(char) |
| 184 | |
| 185 | def chars(self): |
| 186 | return self._chars.keys() |
| 187 | |
| 188 | def comments(self): |
| 189 | return self._comments |
| 190 | |
| 191 | def __getattr__(self, attr): |
| 192 | if self._attrs.has_key(attr): |
| 193 | return self._attrs[attr] |
| 194 | else: |
| 195 | raise AttributeError, attr |
| 196 | |
| 197 | def __setattr__(self, attr, value): |
| 198 | # all attrs *not* starting with "_" are consider to be AFM keywords |
| 199 | if attr[:1] == "_": |
| 200 | self.__dict__[attr] = value |
| 201 | else: |
| 202 | self._attrs[attr] = value |
| 203 | |
| 204 | def __getitem__(self, key): |
| 205 | if type(key) == types.TupleType: |
| 206 | # key is a tuple, return the kernpair |
| 207 | if self._kerning.has_key(key): |
| 208 | return self._kerning[key] |
| 209 | else: |
| 210 | raise KeyError, "no kerning pair: " + str(key) |
| 211 | else: |
| 212 | # return the metrics instead |
| 213 | if self._chars.has_key(key): |
| 214 | return self._chars[key] |
| 215 | else: |
| 216 | raise KeyError, "metrics index " + str(key) + " out of range" |
| 217 | |
| 218 | def __repr__(self): |
| 219 | if hasattr(self, "FullName"): |
| 220 | return '<AFM object for %s>' % self.FullName |
| 221 | else: |
| 222 | return '<AFM object at %x>' % id(self) |
| 223 | |
| 224 | |
| 225 | def readlines(path): |
| 226 | f = open(path, 'rb') |
| 227 | data = f.read() |
| 228 | f.close() |
| 229 | # read any text file, regardless whether it's formatted for Mac, Unix or Dos |
| 230 | sep = "" |
| 231 | if '\r' in data: |
| 232 | sep = sep + '\r' # mac or dos |
| 233 | if '\n' in data: |
| 234 | sep = sep + '\n' # unix or dos |
| 235 | return string.split(data, sep) |
| 236 | |
| 237 | def writelines(path, lines, sep = '\r'): |
| 238 | f = open(path, 'wb') |
| 239 | for line in lines: |
| 240 | f.write(line + sep) |
| 241 | f.close() |
| 242 | |
| 243 | |
| 244 | |
| 245 | if __name__ == "__main__": |
| 246 | import macfs |
| 247 | fss, ok = macfs.StandardGetFile('TEXT') |
| 248 | if ok: |
| 249 | path = fss.as_pathname() |
| 250 | afm = AFM(path) |
| 251 | char = 'A' |
| 252 | if afm.has_char(char): |
| 253 | print afm[char] # print charnum, width and boundingbox |
| 254 | pair = ('A', 'V') |
| 255 | if afm.has_kernpair(pair): |
| 256 | print afm[pair] # print kerning value for pair |
| 257 | print afm.Version # various other afm entries have become attributes |
| 258 | print afm.Weight |
| 259 | # afm.comments() returns a list of all Comment lines found in the AFM |
| 260 | print afm.comments() |
| 261 | #print afm.chars() |
| 262 | #print afm.kernpairs() |
| 263 | print afm |
| 264 | afm.write(path + ".xxx") |
| 265 | |