blob: e679770c9e1b2559be92a649feb8c9be4ca2d226 [file] [log] [blame]
Just7842e561999-12-16 21:34:53 +00001"""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
Behdad Esfahbod1ae29592014-01-14 15:07:50 +08007from __future__ import print_function, division, absolute_import
Behdad Esfahbod7ed91ec2013-11-27 15:16:28 -05008from fontTools.misc.py23 import *
Behdad Esfahbod30e691e2013-11-27 17:27:45 -05009import re
Just7842e561999-12-16 21:34:53 +000010
Just7842e561999-12-16 21:34:53 +000011# every single line starts with a "word"
12identifierRE = re.compile("^([A-Za-z]+).*")
13
14# regular expression to parse char lines
15charRE = re.compile(
16 "(-?\d+)" # charnum
17 "\s*;\s*WX\s+" # ; WX
jvr31ad3512002-11-26 14:09:52 +000018 "(-?\d+)" # width
Just7842e561999-12-16 21:34:53 +000019 "\s*;\s*N\s+" # ; N
jvr3c3c32c2002-03-12 14:34:43 +000020 "([.A-Za-z0-9_]+)" # charname
Just7842e561999-12-16 21:34:53 +000021 "\s*;\s*B\s+" # ; B
22 "(-?\d+)" # left
23 "\s+" #
24 "(-?\d+)" # bottom
25 "\s+" #
26 "(-?\d+)" # right
27 "\s+" #
28 "(-?\d+)" # top
29 "\s*;\s*" # ;
30 )
31
32# regular expression to parse kerning lines
33kernRE = re.compile(
34 "([.A-Za-z0-9_]+)" # leftchar
35 "\s+" #
36 "([.A-Za-z0-9_]+)" # rightchar
37 "\s+" #
38 "(-?\d+)" # value
39 "\s*" #
40 )
41
Just32f86842001-04-30 14:40:17 +000042# regular expressions to parse composite info lines of the form:
43# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
44compositeRE = re.compile(
45 "([.A-Za-z0-9_]+)" # char name
46 "\s+" #
47 "(\d+)" # number of parts
48 "\s*;\s*" #
49 )
50componentRE = re.compile(
51 "PCC\s+" # PPC
52 "([.A-Za-z0-9_]+)" # base char name
53 "\s+" #
54 "(-?\d+)" # x offset
55 "\s+" #
56 "(-?\d+)" # y offset
57 "\s*;\s*" #
58 )
59
60preferredAttributeOrder = [
61 "FontName",
62 "FullName",
63 "FamilyName",
64 "Weight",
65 "ItalicAngle",
66 "IsFixedPitch",
67 "FontBBox",
68 "UnderlinePosition",
69 "UnderlineThickness",
70 "Version",
71 "Notice",
72 "EncodingScheme",
73 "CapHeight",
74 "XHeight",
75 "Ascender",
76 "Descender",
77]
78
79
80class error(Exception): pass
81
Just7842e561999-12-16 21:34:53 +000082
Behdad Esfahbode388db52013-11-28 14:26:58 -050083class AFM(object):
Just7842e561999-12-16 21:34:53 +000084
jvr5910f972003-05-24 12:50:47 +000085 _attrs = None
86
Just7842e561999-12-16 21:34:53 +000087 _keywords = ['StartFontMetrics',
88 'EndFontMetrics',
89 'StartCharMetrics',
90 'EndCharMetrics',
91 'StartKernData',
92 'StartKernPairs',
93 'EndKernPairs',
Just32f86842001-04-30 14:40:17 +000094 'EndKernData',
95 'StartComposites',
96 'EndComposites',
97 ]
Just7842e561999-12-16 21:34:53 +000098
Just32f86842001-04-30 14:40:17 +000099 def __init__(self, path=None):
Just7842e561999-12-16 21:34:53 +0000100 self._attrs = {}
101 self._chars = {}
102 self._kerning = {}
103 self._index = {}
104 self._comments = []
Just32f86842001-04-30 14:40:17 +0000105 self._composites = {}
Just7842e561999-12-16 21:34:53 +0000106 if path is not None:
107 self.read(path)
108
109 def read(self, path):
110 lines = readlines(path)
111 for line in lines:
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500112 if not line.strip():
Just7842e561999-12-16 21:34:53 +0000113 continue
114 m = identifierRE.match(line)
115 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500116 raise error("syntax error in AFM file: " + repr(line))
Just7842e561999-12-16 21:34:53 +0000117
118 pos = m.regs[1][1]
119 word = line[:pos]
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500120 rest = line[pos:].strip()
Just7842e561999-12-16 21:34:53 +0000121 if word in self._keywords:
122 continue
Just32f86842001-04-30 14:40:17 +0000123 if word == "C":
Just7842e561999-12-16 21:34:53 +0000124 self.parsechar(rest)
125 elif word == "KPX":
126 self.parsekernpair(rest)
Just32f86842001-04-30 14:40:17 +0000127 elif word == "CC":
128 self.parsecomposite(rest)
Just7842e561999-12-16 21:34:53 +0000129 else:
130 self.parseattr(word, rest)
131
132 def parsechar(self, rest):
133 m = charRE.match(rest)
134 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500135 raise error("syntax error in AFM file: " + repr(rest))
Just7842e561999-12-16 21:34:53 +0000136 things = []
137 for fr, to in m.regs[1:]:
138 things.append(rest[fr:to])
139 charname = things[2]
140 del things[2]
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500141 charnum, width, l, b, r, t = (int(thing) for thing in things)
Just7842e561999-12-16 21:34:53 +0000142 self._chars[charname] = charnum, width, (l, b, r, t)
143
144 def parsekernpair(self, rest):
145 m = kernRE.match(rest)
146 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500147 raise error("syntax error in AFM file: " + repr(rest))
Just7842e561999-12-16 21:34:53 +0000148 things = []
149 for fr, to in m.regs[1:]:
150 things.append(rest[fr:to])
151 leftchar, rightchar, value = things
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500152 value = int(value)
Just7842e561999-12-16 21:34:53 +0000153 self._kerning[(leftchar, rightchar)] = value
154
155 def parseattr(self, word, rest):
156 if word == "FontBBox":
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500157 l, b, r, t = [int(thing) for thing in rest.split()]
Just7842e561999-12-16 21:34:53 +0000158 self._attrs[word] = l, b, r, t
159 elif word == "Comment":
160 self._comments.append(rest)
161 else:
162 try:
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500163 value = int(rest)
Just7842e561999-12-16 21:34:53 +0000164 except (ValueError, OverflowError):
165 self._attrs[word] = rest
166 else:
167 self._attrs[word] = value
168
Just32f86842001-04-30 14:40:17 +0000169 def parsecomposite(self, rest):
170 m = compositeRE.match(rest)
171 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500172 raise error("syntax error in AFM file: " + repr(rest))
Just32f86842001-04-30 14:40:17 +0000173 charname = m.group(1)
174 ncomponents = int(m.group(2))
175 rest = rest[m.regs[0][1]:]
176 components = []
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500177 while True:
Just32f86842001-04-30 14:40:17 +0000178 m = componentRE.match(rest)
179 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500180 raise error("syntax error in AFM file: " + repr(rest))
Just32f86842001-04-30 14:40:17 +0000181 basechar = m.group(1)
182 xoffset = int(m.group(2))
183 yoffset = int(m.group(3))
184 components.append((basechar, xoffset, yoffset))
185 rest = rest[m.regs[0][1]:]
186 if not rest:
187 break
188 assert len(components) == ncomponents
189 self._composites[charname] = components
190
Just6175deb2001-06-24 15:11:31 +0000191 def write(self, path, sep='\r'):
Just7842e561999-12-16 21:34:53 +0000192 import time
193 lines = [ "StartFontMetrics 2.0",
Khaled Hosnyea48dba2013-12-04 08:46:43 +0200194 "Comment Generated by afmLib; at %s" % (
Just7842e561999-12-16 21:34:53 +0000195 time.strftime("%m/%d/%Y %H:%M:%S",
196 time.localtime(time.time())))]
197
Just32f86842001-04-30 14:40:17 +0000198 # write comments, assuming (possibly wrongly!) they should
199 # all appear at the top
200 for comment in self._comments:
201 lines.append("Comment " + comment)
202
203 # write attributes, first the ones we know about, in
204 # a preferred order
205 attrs = self._attrs
206 for attr in preferredAttributeOrder:
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500207 if attr in attrs:
Just32f86842001-04-30 14:40:17 +0000208 value = attrs[attr]
209 if attr == "FontBBox":
210 value = "%s %s %s %s" % value
211 lines.append(attr + " " + str(value))
212 # then write the attributes we don't know about,
213 # in alphabetical order
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500214 items = sorted(attrs.items())
Just7842e561999-12-16 21:34:53 +0000215 for attr, value in items:
Just32f86842001-04-30 14:40:17 +0000216 if attr in preferredAttributeOrder:
217 continue
Just7842e561999-12-16 21:34:53 +0000218 lines.append(attr + " " + str(value))
219
220 # write char metrics
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500221 lines.append("StartCharMetrics " + repr(len(self._chars)))
Behdad Esfahbode5ca7962013-11-27 04:38:16 -0500222 items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()]
Just7842e561999-12-16 21:34:53 +0000223
Behdad Esfahbodb7fd2e12013-11-27 18:58:45 -0500224 def myKey(a):
225 """Custom key function to make sure unencoded chars (-1)
Just7842e561999-12-16 21:34:53 +0000226 end up at the end of the list after sorting."""
227 if a[0] == -1:
228 a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number
Behdad Esfahbodb7fd2e12013-11-27 18:58:45 -0500229 return a
230 items.sort(key=myKey)
Just7842e561999-12-16 21:34:53 +0000231
232 for charnum, (charname, width, (l, b, r, t)) in items:
233 lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" %
234 (charnum, width, charname, l, b, r, t))
235 lines.append("EndCharMetrics")
236
237 # write kerning info
238 lines.append("StartKernData")
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500239 lines.append("StartKernPairs " + repr(len(self._kerning)))
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500240 items = sorted(self._kerning.items())
Just7842e561999-12-16 21:34:53 +0000241 for (leftchar, rightchar), value in items:
242 lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
Just7842e561999-12-16 21:34:53 +0000243 lines.append("EndKernPairs")
244 lines.append("EndKernData")
Just32f86842001-04-30 14:40:17 +0000245
246 if self._composites:
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500247 composites = sorted(self._composites.items())
Just32f86842001-04-30 14:40:17 +0000248 lines.append("StartComposites %s" % len(self._composites))
249 for charname, components in composites:
250 line = "CC %s %s ;" % (charname, len(components))
251 for basechar, xoffset, yoffset in components:
252 line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
253 lines.append(line)
254 lines.append("EndComposites")
255
Just7842e561999-12-16 21:34:53 +0000256 lines.append("EndFontMetrics")
257
258 writelines(path, lines, sep)
259
260 def has_kernpair(self, pair):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500261 return pair in self._kerning
Just7842e561999-12-16 21:34:53 +0000262
263 def kernpairs(self):
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500264 return list(self._kerning.keys())
Just7842e561999-12-16 21:34:53 +0000265
266 def has_char(self, char):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500267 return char in self._chars
Just7842e561999-12-16 21:34:53 +0000268
269 def chars(self):
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500270 return list(self._chars.keys())
Just7842e561999-12-16 21:34:53 +0000271
272 def comments(self):
273 return self._comments
274
Just6175deb2001-06-24 15:11:31 +0000275 def addComment(self, comment):
276 self._comments.append(comment)
277
278 def addComposite(self, glyphName, components):
279 self._composites[glyphName] = components
280
Just7842e561999-12-16 21:34:53 +0000281 def __getattr__(self, attr):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500282 if attr in self._attrs:
Just7842e561999-12-16 21:34:53 +0000283 return self._attrs[attr]
284 else:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500285 raise AttributeError(attr)
Just7842e561999-12-16 21:34:53 +0000286
287 def __setattr__(self, attr, value):
288 # all attrs *not* starting with "_" are consider to be AFM keywords
289 if attr[:1] == "_":
290 self.__dict__[attr] = value
291 else:
292 self._attrs[attr] = value
293
Just6175deb2001-06-24 15:11:31 +0000294 def __delattr__(self, attr):
295 # all attrs *not* starting with "_" are consider to be AFM keywords
296 if attr[:1] == "_":
297 try:
298 del self.__dict__[attr]
299 except KeyError:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500300 raise AttributeError(attr)
Just6175deb2001-06-24 15:11:31 +0000301 else:
302 try:
303 del self._attrs[attr]
304 except KeyError:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500305 raise AttributeError(attr)
Just6175deb2001-06-24 15:11:31 +0000306
Just7842e561999-12-16 21:34:53 +0000307 def __getitem__(self, key):
Behdad Esfahbod002c32f2013-11-27 04:48:20 -0500308 if isinstance(key, tuple):
Just7842e561999-12-16 21:34:53 +0000309 # key is a tuple, return the kernpair
Just6175deb2001-06-24 15:11:31 +0000310 return self._kerning[key]
Just7842e561999-12-16 21:34:53 +0000311 else:
312 # return the metrics instead
Just6175deb2001-06-24 15:11:31 +0000313 return self._chars[key]
314
315 def __setitem__(self, key, value):
Behdad Esfahbod002c32f2013-11-27 04:48:20 -0500316 if isinstance(key, tuple):
Just6175deb2001-06-24 15:11:31 +0000317 # key is a tuple, set kernpair
318 self._kerning[key] = value
319 else:
320 # set char metrics
321 self._chars[key] = value
322
323 def __delitem__(self, key):
Behdad Esfahbod002c32f2013-11-27 04:48:20 -0500324 if isinstance(key, tuple):
Just6175deb2001-06-24 15:11:31 +0000325 # key is a tuple, del kernpair
326 del self._kerning[key]
327 else:
328 # del char metrics
329 del self._chars[key]
Just7842e561999-12-16 21:34:53 +0000330
331 def __repr__(self):
332 if hasattr(self, "FullName"):
333 return '<AFM object for %s>' % self.FullName
334 else:
335 return '<AFM object at %x>' % id(self)
336
337
338def readlines(path):
339 f = open(path, 'rb')
340 data = f.read()
341 f.close()
342 # read any text file, regardless whether it's formatted for Mac, Unix or Dos
343 sep = ""
344 if '\r' in data:
345 sep = sep + '\r' # mac or dos
346 if '\n' in data:
347 sep = sep + '\n' # unix or dos
Behdad Esfahbod14fb0312013-11-27 05:47:34 -0500348 return data.split(sep)
Just7842e561999-12-16 21:34:53 +0000349
Just32f86842001-04-30 14:40:17 +0000350def writelines(path, lines, sep='\r'):
Just7842e561999-12-16 21:34:53 +0000351 f = open(path, 'wb')
352 for line in lines:
353 f.write(line + sep)
354 f.close()
355
356
357
358if __name__ == "__main__":
jvr91bca422012-10-18 12:49:22 +0000359 import EasyDialogs
360 path = EasyDialogs.AskFileForOpen()
361 if path:
Just7842e561999-12-16 21:34:53 +0000362 afm = AFM(path)
363 char = 'A'
364 if afm.has_char(char):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500365 print(afm[char]) # print charnum, width and boundingbox
Just7842e561999-12-16 21:34:53 +0000366 pair = ('A', 'V')
367 if afm.has_kernpair(pair):
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500368 print(afm[pair]) # print kerning value for pair
369 print(afm.Version) # various other afm entries have become attributes
370 print(afm.Weight)
Just7842e561999-12-16 21:34:53 +0000371 # afm.comments() returns a list of all Comment lines found in the AFM
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500372 print(afm.comments())
Just7842e561999-12-16 21:34:53 +0000373 #print afm.chars()
374 #print afm.kernpairs()
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -0500375 print(afm)
Just32f86842001-04-30 14:40:17 +0000376 afm.write(path + ".muck")
Just7842e561999-12-16 21:34:53 +0000377