xref: /aosp_15_r20/external/fonttools/Lib/fontTools/afmLib.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Module for reading and writing AFM (Adobe Font Metrics) files.
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott HughesNote that this has been designed to read in AFM files generated by Fontographer
4*e1fe3e4aSElliott Hughesand has not been tested on many other files. In particular, it does not
5*e1fe3e4aSElliott Hughesimplement the whole Adobe AFM specification [#f1]_ but, it should read most
6*e1fe3e4aSElliott Hughes"common" AFM files.
7*e1fe3e4aSElliott Hughes
8*e1fe3e4aSElliott HughesHere is an example of using `afmLib` to read, modify and write an AFM file:
9*e1fe3e4aSElliott Hughes
10*e1fe3e4aSElliott Hughes	>>> from fontTools.afmLib import AFM
11*e1fe3e4aSElliott Hughes	>>> f = AFM("Tests/afmLib/data/TestAFM.afm")
12*e1fe3e4aSElliott Hughes	>>>
13*e1fe3e4aSElliott Hughes	>>> # Accessing a pair gets you the kern value
14*e1fe3e4aSElliott Hughes	>>> f[("V","A")]
15*e1fe3e4aSElliott Hughes	-60
16*e1fe3e4aSElliott Hughes	>>>
17*e1fe3e4aSElliott Hughes	>>> # Accessing a glyph name gets you metrics
18*e1fe3e4aSElliott Hughes	>>> f["A"]
19*e1fe3e4aSElliott Hughes	(65, 668, (8, -25, 660, 666))
20*e1fe3e4aSElliott Hughes	>>> # (charnum, width, bounding box)
21*e1fe3e4aSElliott Hughes	>>>
22*e1fe3e4aSElliott Hughes	>>> # Accessing an attribute gets you metadata
23*e1fe3e4aSElliott Hughes	>>> f.FontName
24*e1fe3e4aSElliott Hughes	'TestFont-Regular'
25*e1fe3e4aSElliott Hughes	>>> f.FamilyName
26*e1fe3e4aSElliott Hughes	'TestFont'
27*e1fe3e4aSElliott Hughes	>>> f.Weight
28*e1fe3e4aSElliott Hughes	'Regular'
29*e1fe3e4aSElliott Hughes	>>> f.XHeight
30*e1fe3e4aSElliott Hughes	500
31*e1fe3e4aSElliott Hughes	>>> f.Ascender
32*e1fe3e4aSElliott Hughes	750
33*e1fe3e4aSElliott Hughes	>>>
34*e1fe3e4aSElliott Hughes	>>> # Attributes and items can also be set
35*e1fe3e4aSElliott Hughes	>>> f[("A","V")] = -150 # Tighten kerning
36*e1fe3e4aSElliott Hughes	>>> f.FontName = "TestFont Squished"
37*e1fe3e4aSElliott Hughes	>>>
38*e1fe3e4aSElliott Hughes	>>> # And the font written out again (remove the # in front)
39*e1fe3e4aSElliott Hughes	>>> #f.write("testfont-squished.afm")
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughes.. rubric:: Footnotes
42*e1fe3e4aSElliott Hughes
43*e1fe3e4aSElliott Hughes.. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_,
44*e1fe3e4aSElliott Hughes   Adobe Font Metrics File Format Specification.
45*e1fe3e4aSElliott Hughes
46*e1fe3e4aSElliott Hughes"""
47*e1fe3e4aSElliott Hughes
48*e1fe3e4aSElliott Hughesimport re
49*e1fe3e4aSElliott Hughes
50*e1fe3e4aSElliott Hughes# every single line starts with a "word"
51*e1fe3e4aSElliott HughesidentifierRE = re.compile(r"^([A-Za-z]+).*")
52*e1fe3e4aSElliott Hughes
53*e1fe3e4aSElliott Hughes# regular expression to parse char lines
54*e1fe3e4aSElliott HughescharRE = re.compile(
55*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # charnum
56*e1fe3e4aSElliott Hughes    r"\s*;\s*WX\s+"  # ; WX
57*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # width
58*e1fe3e4aSElliott Hughes    r"\s*;\s*N\s+"  # ; N
59*e1fe3e4aSElliott Hughes    r"([.A-Za-z0-9_]+)"  # charname
60*e1fe3e4aSElliott Hughes    r"\s*;\s*B\s+"  # ; B
61*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # left
62*e1fe3e4aSElliott Hughes    r"\s+"
63*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # bottom
64*e1fe3e4aSElliott Hughes    r"\s+"
65*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # right
66*e1fe3e4aSElliott Hughes    r"\s+"
67*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # top
68*e1fe3e4aSElliott Hughes    r"\s*;\s*"  # ;
69*e1fe3e4aSElliott Hughes)
70*e1fe3e4aSElliott Hughes
71*e1fe3e4aSElliott Hughes# regular expression to parse kerning lines
72*e1fe3e4aSElliott HugheskernRE = re.compile(
73*e1fe3e4aSElliott Hughes    r"([.A-Za-z0-9_]+)"  # leftchar
74*e1fe3e4aSElliott Hughes    r"\s+"
75*e1fe3e4aSElliott Hughes    r"([.A-Za-z0-9_]+)"  # rightchar
76*e1fe3e4aSElliott Hughes    r"\s+"
77*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # value
78*e1fe3e4aSElliott Hughes    r"\s*"
79*e1fe3e4aSElliott Hughes)
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughes# regular expressions to parse composite info lines of the form:
82*e1fe3e4aSElliott Hughes# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
83*e1fe3e4aSElliott HughescompositeRE = re.compile(
84*e1fe3e4aSElliott Hughes    r"([.A-Za-z0-9_]+)"  # char name
85*e1fe3e4aSElliott Hughes    r"\s+"
86*e1fe3e4aSElliott Hughes    r"(\d+)"  # number of parts
87*e1fe3e4aSElliott Hughes    r"\s*;\s*"
88*e1fe3e4aSElliott Hughes)
89*e1fe3e4aSElliott HughescomponentRE = re.compile(
90*e1fe3e4aSElliott Hughes    r"PCC\s+"  # PPC
91*e1fe3e4aSElliott Hughes    r"([.A-Za-z0-9_]+)"  # base char name
92*e1fe3e4aSElliott Hughes    r"\s+"
93*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # x offset
94*e1fe3e4aSElliott Hughes    r"\s+"
95*e1fe3e4aSElliott Hughes    r"(-?\d+)"  # y offset
96*e1fe3e4aSElliott Hughes    r"\s*;\s*"
97*e1fe3e4aSElliott Hughes)
98*e1fe3e4aSElliott Hughes
99*e1fe3e4aSElliott HughespreferredAttributeOrder = [
100*e1fe3e4aSElliott Hughes    "FontName",
101*e1fe3e4aSElliott Hughes    "FullName",
102*e1fe3e4aSElliott Hughes    "FamilyName",
103*e1fe3e4aSElliott Hughes    "Weight",
104*e1fe3e4aSElliott Hughes    "ItalicAngle",
105*e1fe3e4aSElliott Hughes    "IsFixedPitch",
106*e1fe3e4aSElliott Hughes    "FontBBox",
107*e1fe3e4aSElliott Hughes    "UnderlinePosition",
108*e1fe3e4aSElliott Hughes    "UnderlineThickness",
109*e1fe3e4aSElliott Hughes    "Version",
110*e1fe3e4aSElliott Hughes    "Notice",
111*e1fe3e4aSElliott Hughes    "EncodingScheme",
112*e1fe3e4aSElliott Hughes    "CapHeight",
113*e1fe3e4aSElliott Hughes    "XHeight",
114*e1fe3e4aSElliott Hughes    "Ascender",
115*e1fe3e4aSElliott Hughes    "Descender",
116*e1fe3e4aSElliott Hughes]
117*e1fe3e4aSElliott Hughes
118*e1fe3e4aSElliott Hughes
119*e1fe3e4aSElliott Hughesclass error(Exception):
120*e1fe3e4aSElliott Hughes    pass
121*e1fe3e4aSElliott Hughes
122*e1fe3e4aSElliott Hughes
123*e1fe3e4aSElliott Hughesclass AFM(object):
124*e1fe3e4aSElliott Hughes    _attrs = None
125*e1fe3e4aSElliott Hughes
126*e1fe3e4aSElliott Hughes    _keywords = [
127*e1fe3e4aSElliott Hughes        "StartFontMetrics",
128*e1fe3e4aSElliott Hughes        "EndFontMetrics",
129*e1fe3e4aSElliott Hughes        "StartCharMetrics",
130*e1fe3e4aSElliott Hughes        "EndCharMetrics",
131*e1fe3e4aSElliott Hughes        "StartKernData",
132*e1fe3e4aSElliott Hughes        "StartKernPairs",
133*e1fe3e4aSElliott Hughes        "EndKernPairs",
134*e1fe3e4aSElliott Hughes        "EndKernData",
135*e1fe3e4aSElliott Hughes        "StartComposites",
136*e1fe3e4aSElliott Hughes        "EndComposites",
137*e1fe3e4aSElliott Hughes    ]
138*e1fe3e4aSElliott Hughes
139*e1fe3e4aSElliott Hughes    def __init__(self, path=None):
140*e1fe3e4aSElliott Hughes        """AFM file reader.
141*e1fe3e4aSElliott Hughes
142*e1fe3e4aSElliott Hughes        Instantiating an object with a path name will cause the file to be opened,
143*e1fe3e4aSElliott Hughes        read, and parsed. Alternatively the path can be left unspecified, and a
144*e1fe3e4aSElliott Hughes        file can be parsed later with the :meth:`read` method."""
145*e1fe3e4aSElliott Hughes        self._attrs = {}
146*e1fe3e4aSElliott Hughes        self._chars = {}
147*e1fe3e4aSElliott Hughes        self._kerning = {}
148*e1fe3e4aSElliott Hughes        self._index = {}
149*e1fe3e4aSElliott Hughes        self._comments = []
150*e1fe3e4aSElliott Hughes        self._composites = {}
151*e1fe3e4aSElliott Hughes        if path is not None:
152*e1fe3e4aSElliott Hughes            self.read(path)
153*e1fe3e4aSElliott Hughes
154*e1fe3e4aSElliott Hughes    def read(self, path):
155*e1fe3e4aSElliott Hughes        """Opens, reads and parses a file."""
156*e1fe3e4aSElliott Hughes        lines = readlines(path)
157*e1fe3e4aSElliott Hughes        for line in lines:
158*e1fe3e4aSElliott Hughes            if not line.strip():
159*e1fe3e4aSElliott Hughes                continue
160*e1fe3e4aSElliott Hughes            m = identifierRE.match(line)
161*e1fe3e4aSElliott Hughes            if m is None:
162*e1fe3e4aSElliott Hughes                raise error("syntax error in AFM file: " + repr(line))
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughes            pos = m.regs[1][1]
165*e1fe3e4aSElliott Hughes            word = line[:pos]
166*e1fe3e4aSElliott Hughes            rest = line[pos:].strip()
167*e1fe3e4aSElliott Hughes            if word in self._keywords:
168*e1fe3e4aSElliott Hughes                continue
169*e1fe3e4aSElliott Hughes            if word == "C":
170*e1fe3e4aSElliott Hughes                self.parsechar(rest)
171*e1fe3e4aSElliott Hughes            elif word == "KPX":
172*e1fe3e4aSElliott Hughes                self.parsekernpair(rest)
173*e1fe3e4aSElliott Hughes            elif word == "CC":
174*e1fe3e4aSElliott Hughes                self.parsecomposite(rest)
175*e1fe3e4aSElliott Hughes            else:
176*e1fe3e4aSElliott Hughes                self.parseattr(word, rest)
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughes    def parsechar(self, rest):
179*e1fe3e4aSElliott Hughes        m = charRE.match(rest)
180*e1fe3e4aSElliott Hughes        if m is None:
181*e1fe3e4aSElliott Hughes            raise error("syntax error in AFM file: " + repr(rest))
182*e1fe3e4aSElliott Hughes        things = []
183*e1fe3e4aSElliott Hughes        for fr, to in m.regs[1:]:
184*e1fe3e4aSElliott Hughes            things.append(rest[fr:to])
185*e1fe3e4aSElliott Hughes        charname = things[2]
186*e1fe3e4aSElliott Hughes        del things[2]
187*e1fe3e4aSElliott Hughes        charnum, width, l, b, r, t = (int(thing) for thing in things)
188*e1fe3e4aSElliott Hughes        self._chars[charname] = charnum, width, (l, b, r, t)
189*e1fe3e4aSElliott Hughes
190*e1fe3e4aSElliott Hughes    def parsekernpair(self, rest):
191*e1fe3e4aSElliott Hughes        m = kernRE.match(rest)
192*e1fe3e4aSElliott Hughes        if m is None:
193*e1fe3e4aSElliott Hughes            raise error("syntax error in AFM file: " + repr(rest))
194*e1fe3e4aSElliott Hughes        things = []
195*e1fe3e4aSElliott Hughes        for fr, to in m.regs[1:]:
196*e1fe3e4aSElliott Hughes            things.append(rest[fr:to])
197*e1fe3e4aSElliott Hughes        leftchar, rightchar, value = things
198*e1fe3e4aSElliott Hughes        value = int(value)
199*e1fe3e4aSElliott Hughes        self._kerning[(leftchar, rightchar)] = value
200*e1fe3e4aSElliott Hughes
201*e1fe3e4aSElliott Hughes    def parseattr(self, word, rest):
202*e1fe3e4aSElliott Hughes        if word == "FontBBox":
203*e1fe3e4aSElliott Hughes            l, b, r, t = [int(thing) for thing in rest.split()]
204*e1fe3e4aSElliott Hughes            self._attrs[word] = l, b, r, t
205*e1fe3e4aSElliott Hughes        elif word == "Comment":
206*e1fe3e4aSElliott Hughes            self._comments.append(rest)
207*e1fe3e4aSElliott Hughes        else:
208*e1fe3e4aSElliott Hughes            try:
209*e1fe3e4aSElliott Hughes                value = int(rest)
210*e1fe3e4aSElliott Hughes            except (ValueError, OverflowError):
211*e1fe3e4aSElliott Hughes                self._attrs[word] = rest
212*e1fe3e4aSElliott Hughes            else:
213*e1fe3e4aSElliott Hughes                self._attrs[word] = value
214*e1fe3e4aSElliott Hughes
215*e1fe3e4aSElliott Hughes    def parsecomposite(self, rest):
216*e1fe3e4aSElliott Hughes        m = compositeRE.match(rest)
217*e1fe3e4aSElliott Hughes        if m is None:
218*e1fe3e4aSElliott Hughes            raise error("syntax error in AFM file: " + repr(rest))
219*e1fe3e4aSElliott Hughes        charname = m.group(1)
220*e1fe3e4aSElliott Hughes        ncomponents = int(m.group(2))
221*e1fe3e4aSElliott Hughes        rest = rest[m.regs[0][1] :]
222*e1fe3e4aSElliott Hughes        components = []
223*e1fe3e4aSElliott Hughes        while True:
224*e1fe3e4aSElliott Hughes            m = componentRE.match(rest)
225*e1fe3e4aSElliott Hughes            if m is None:
226*e1fe3e4aSElliott Hughes                raise error("syntax error in AFM file: " + repr(rest))
227*e1fe3e4aSElliott Hughes            basechar = m.group(1)
228*e1fe3e4aSElliott Hughes            xoffset = int(m.group(2))
229*e1fe3e4aSElliott Hughes            yoffset = int(m.group(3))
230*e1fe3e4aSElliott Hughes            components.append((basechar, xoffset, yoffset))
231*e1fe3e4aSElliott Hughes            rest = rest[m.regs[0][1] :]
232*e1fe3e4aSElliott Hughes            if not rest:
233*e1fe3e4aSElliott Hughes                break
234*e1fe3e4aSElliott Hughes        assert len(components) == ncomponents
235*e1fe3e4aSElliott Hughes        self._composites[charname] = components
236*e1fe3e4aSElliott Hughes
237*e1fe3e4aSElliott Hughes    def write(self, path, sep="\r"):
238*e1fe3e4aSElliott Hughes        """Writes out an AFM font to the given path."""
239*e1fe3e4aSElliott Hughes        import time
240*e1fe3e4aSElliott Hughes
241*e1fe3e4aSElliott Hughes        lines = [
242*e1fe3e4aSElliott Hughes            "StartFontMetrics 2.0",
243*e1fe3e4aSElliott Hughes            "Comment Generated by afmLib; at %s"
244*e1fe3e4aSElliott Hughes            % (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))),
245*e1fe3e4aSElliott Hughes        ]
246*e1fe3e4aSElliott Hughes
247*e1fe3e4aSElliott Hughes        # write comments, assuming (possibly wrongly!) they should
248*e1fe3e4aSElliott Hughes        # all appear at the top
249*e1fe3e4aSElliott Hughes        for comment in self._comments:
250*e1fe3e4aSElliott Hughes            lines.append("Comment " + comment)
251*e1fe3e4aSElliott Hughes
252*e1fe3e4aSElliott Hughes        # write attributes, first the ones we know about, in
253*e1fe3e4aSElliott Hughes        # a preferred order
254*e1fe3e4aSElliott Hughes        attrs = self._attrs
255*e1fe3e4aSElliott Hughes        for attr in preferredAttributeOrder:
256*e1fe3e4aSElliott Hughes            if attr in attrs:
257*e1fe3e4aSElliott Hughes                value = attrs[attr]
258*e1fe3e4aSElliott Hughes                if attr == "FontBBox":
259*e1fe3e4aSElliott Hughes                    value = "%s %s %s %s" % value
260*e1fe3e4aSElliott Hughes                lines.append(attr + " " + str(value))
261*e1fe3e4aSElliott Hughes        # then write the attributes we don't know about,
262*e1fe3e4aSElliott Hughes        # in alphabetical order
263*e1fe3e4aSElliott Hughes        items = sorted(attrs.items())
264*e1fe3e4aSElliott Hughes        for attr, value in items:
265*e1fe3e4aSElliott Hughes            if attr in preferredAttributeOrder:
266*e1fe3e4aSElliott Hughes                continue
267*e1fe3e4aSElliott Hughes            lines.append(attr + " " + str(value))
268*e1fe3e4aSElliott Hughes
269*e1fe3e4aSElliott Hughes        # write char metrics
270*e1fe3e4aSElliott Hughes        lines.append("StartCharMetrics " + repr(len(self._chars)))
271*e1fe3e4aSElliott Hughes        items = [
272*e1fe3e4aSElliott Hughes            (charnum, (charname, width, box))
273*e1fe3e4aSElliott Hughes            for charname, (charnum, width, box) in self._chars.items()
274*e1fe3e4aSElliott Hughes        ]
275*e1fe3e4aSElliott Hughes
276*e1fe3e4aSElliott Hughes        def myKey(a):
277*e1fe3e4aSElliott Hughes            """Custom key function to make sure unencoded chars (-1)
278*e1fe3e4aSElliott Hughes            end up at the end of the list after sorting."""
279*e1fe3e4aSElliott Hughes            if a[0] == -1:
280*e1fe3e4aSElliott Hughes                a = (0xFFFF,) + a[1:]  # 0xffff is an arbitrary large number
281*e1fe3e4aSElliott Hughes            return a
282*e1fe3e4aSElliott Hughes
283*e1fe3e4aSElliott Hughes        items.sort(key=myKey)
284*e1fe3e4aSElliott Hughes
285*e1fe3e4aSElliott Hughes        for charnum, (charname, width, (l, b, r, t)) in items:
286*e1fe3e4aSElliott Hughes            lines.append(
287*e1fe3e4aSElliott Hughes                "C %d ; WX %d ; N %s ; B %d %d %d %d ;"
288*e1fe3e4aSElliott Hughes                % (charnum, width, charname, l, b, r, t)
289*e1fe3e4aSElliott Hughes            )
290*e1fe3e4aSElliott Hughes        lines.append("EndCharMetrics")
291*e1fe3e4aSElliott Hughes
292*e1fe3e4aSElliott Hughes        # write kerning info
293*e1fe3e4aSElliott Hughes        lines.append("StartKernData")
294*e1fe3e4aSElliott Hughes        lines.append("StartKernPairs " + repr(len(self._kerning)))
295*e1fe3e4aSElliott Hughes        items = sorted(self._kerning.items())
296*e1fe3e4aSElliott Hughes        for (leftchar, rightchar), value in items:
297*e1fe3e4aSElliott Hughes            lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
298*e1fe3e4aSElliott Hughes        lines.append("EndKernPairs")
299*e1fe3e4aSElliott Hughes        lines.append("EndKernData")
300*e1fe3e4aSElliott Hughes
301*e1fe3e4aSElliott Hughes        if self._composites:
302*e1fe3e4aSElliott Hughes            composites = sorted(self._composites.items())
303*e1fe3e4aSElliott Hughes            lines.append("StartComposites %s" % len(self._composites))
304*e1fe3e4aSElliott Hughes            for charname, components in composites:
305*e1fe3e4aSElliott Hughes                line = "CC %s %s ;" % (charname, len(components))
306*e1fe3e4aSElliott Hughes                for basechar, xoffset, yoffset in components:
307*e1fe3e4aSElliott Hughes                    line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
308*e1fe3e4aSElliott Hughes                lines.append(line)
309*e1fe3e4aSElliott Hughes            lines.append("EndComposites")
310*e1fe3e4aSElliott Hughes
311*e1fe3e4aSElliott Hughes        lines.append("EndFontMetrics")
312*e1fe3e4aSElliott Hughes
313*e1fe3e4aSElliott Hughes        writelines(path, lines, sep)
314*e1fe3e4aSElliott Hughes
315*e1fe3e4aSElliott Hughes    def has_kernpair(self, pair):
316*e1fe3e4aSElliott Hughes        """Returns `True` if the given glyph pair (specified as a tuple) exists
317*e1fe3e4aSElliott Hughes        in the kerning dictionary."""
318*e1fe3e4aSElliott Hughes        return pair in self._kerning
319*e1fe3e4aSElliott Hughes
320*e1fe3e4aSElliott Hughes    def kernpairs(self):
321*e1fe3e4aSElliott Hughes        """Returns a list of all kern pairs in the kerning dictionary."""
322*e1fe3e4aSElliott Hughes        return list(self._kerning.keys())
323*e1fe3e4aSElliott Hughes
324*e1fe3e4aSElliott Hughes    def has_char(self, char):
325*e1fe3e4aSElliott Hughes        """Returns `True` if the given glyph exists in the font."""
326*e1fe3e4aSElliott Hughes        return char in self._chars
327*e1fe3e4aSElliott Hughes
328*e1fe3e4aSElliott Hughes    def chars(self):
329*e1fe3e4aSElliott Hughes        """Returns a list of all glyph names in the font."""
330*e1fe3e4aSElliott Hughes        return list(self._chars.keys())
331*e1fe3e4aSElliott Hughes
332*e1fe3e4aSElliott Hughes    def comments(self):
333*e1fe3e4aSElliott Hughes        """Returns all comments from the file."""
334*e1fe3e4aSElliott Hughes        return self._comments
335*e1fe3e4aSElliott Hughes
336*e1fe3e4aSElliott Hughes    def addComment(self, comment):
337*e1fe3e4aSElliott Hughes        """Adds a new comment to the file."""
338*e1fe3e4aSElliott Hughes        self._comments.append(comment)
339*e1fe3e4aSElliott Hughes
340*e1fe3e4aSElliott Hughes    def addComposite(self, glyphName, components):
341*e1fe3e4aSElliott Hughes        """Specifies that the glyph `glyphName` is made up of the given components.
342*e1fe3e4aSElliott Hughes        The components list should be of the following form::
343*e1fe3e4aSElliott Hughes
344*e1fe3e4aSElliott Hughes                [
345*e1fe3e4aSElliott Hughes                        (glyphname, xOffset, yOffset),
346*e1fe3e4aSElliott Hughes                        ...
347*e1fe3e4aSElliott Hughes                ]
348*e1fe3e4aSElliott Hughes
349*e1fe3e4aSElliott Hughes        """
350*e1fe3e4aSElliott Hughes        self._composites[glyphName] = components
351*e1fe3e4aSElliott Hughes
352*e1fe3e4aSElliott Hughes    def __getattr__(self, attr):
353*e1fe3e4aSElliott Hughes        if attr in self._attrs:
354*e1fe3e4aSElliott Hughes            return self._attrs[attr]
355*e1fe3e4aSElliott Hughes        else:
356*e1fe3e4aSElliott Hughes            raise AttributeError(attr)
357*e1fe3e4aSElliott Hughes
358*e1fe3e4aSElliott Hughes    def __setattr__(self, attr, value):
359*e1fe3e4aSElliott Hughes        # all attrs *not* starting with "_" are consider to be AFM keywords
360*e1fe3e4aSElliott Hughes        if attr[:1] == "_":
361*e1fe3e4aSElliott Hughes            self.__dict__[attr] = value
362*e1fe3e4aSElliott Hughes        else:
363*e1fe3e4aSElliott Hughes            self._attrs[attr] = value
364*e1fe3e4aSElliott Hughes
365*e1fe3e4aSElliott Hughes    def __delattr__(self, attr):
366*e1fe3e4aSElliott Hughes        # all attrs *not* starting with "_" are consider to be AFM keywords
367*e1fe3e4aSElliott Hughes        if attr[:1] == "_":
368*e1fe3e4aSElliott Hughes            try:
369*e1fe3e4aSElliott Hughes                del self.__dict__[attr]
370*e1fe3e4aSElliott Hughes            except KeyError:
371*e1fe3e4aSElliott Hughes                raise AttributeError(attr)
372*e1fe3e4aSElliott Hughes        else:
373*e1fe3e4aSElliott Hughes            try:
374*e1fe3e4aSElliott Hughes                del self._attrs[attr]
375*e1fe3e4aSElliott Hughes            except KeyError:
376*e1fe3e4aSElliott Hughes                raise AttributeError(attr)
377*e1fe3e4aSElliott Hughes
378*e1fe3e4aSElliott Hughes    def __getitem__(self, key):
379*e1fe3e4aSElliott Hughes        if isinstance(key, tuple):
380*e1fe3e4aSElliott Hughes            # key is a tuple, return the kernpair
381*e1fe3e4aSElliott Hughes            return self._kerning[key]
382*e1fe3e4aSElliott Hughes        else:
383*e1fe3e4aSElliott Hughes            # return the metrics instead
384*e1fe3e4aSElliott Hughes            return self._chars[key]
385*e1fe3e4aSElliott Hughes
386*e1fe3e4aSElliott Hughes    def __setitem__(self, key, value):
387*e1fe3e4aSElliott Hughes        if isinstance(key, tuple):
388*e1fe3e4aSElliott Hughes            # key is a tuple, set kernpair
389*e1fe3e4aSElliott Hughes            self._kerning[key] = value
390*e1fe3e4aSElliott Hughes        else:
391*e1fe3e4aSElliott Hughes            # set char metrics
392*e1fe3e4aSElliott Hughes            self._chars[key] = value
393*e1fe3e4aSElliott Hughes
394*e1fe3e4aSElliott Hughes    def __delitem__(self, key):
395*e1fe3e4aSElliott Hughes        if isinstance(key, tuple):
396*e1fe3e4aSElliott Hughes            # key is a tuple, del kernpair
397*e1fe3e4aSElliott Hughes            del self._kerning[key]
398*e1fe3e4aSElliott Hughes        else:
399*e1fe3e4aSElliott Hughes            # del char metrics
400*e1fe3e4aSElliott Hughes            del self._chars[key]
401*e1fe3e4aSElliott Hughes
402*e1fe3e4aSElliott Hughes    def __repr__(self):
403*e1fe3e4aSElliott Hughes        if hasattr(self, "FullName"):
404*e1fe3e4aSElliott Hughes            return "<AFM object for %s>" % self.FullName
405*e1fe3e4aSElliott Hughes        else:
406*e1fe3e4aSElliott Hughes            return "<AFM object at %x>" % id(self)
407*e1fe3e4aSElliott Hughes
408*e1fe3e4aSElliott Hughes
409*e1fe3e4aSElliott Hughesdef readlines(path):
410*e1fe3e4aSElliott Hughes    with open(path, "r", encoding="ascii") as f:
411*e1fe3e4aSElliott Hughes        data = f.read()
412*e1fe3e4aSElliott Hughes    return data.splitlines()
413*e1fe3e4aSElliott Hughes
414*e1fe3e4aSElliott Hughes
415*e1fe3e4aSElliott Hughesdef writelines(path, lines, sep="\r"):
416*e1fe3e4aSElliott Hughes    with open(path, "w", encoding="ascii", newline=sep) as f:
417*e1fe3e4aSElliott Hughes        f.write("\n".join(lines) + "\n")
418*e1fe3e4aSElliott Hughes
419*e1fe3e4aSElliott Hughes
420*e1fe3e4aSElliott Hughesif __name__ == "__main__":
421*e1fe3e4aSElliott Hughes    import EasyDialogs
422*e1fe3e4aSElliott Hughes
423*e1fe3e4aSElliott Hughes    path = EasyDialogs.AskFileForOpen()
424*e1fe3e4aSElliott Hughes    if path:
425*e1fe3e4aSElliott Hughes        afm = AFM(path)
426*e1fe3e4aSElliott Hughes        char = "A"
427*e1fe3e4aSElliott Hughes        if afm.has_char(char):
428*e1fe3e4aSElliott Hughes            print(afm[char])  # print charnum, width and boundingbox
429*e1fe3e4aSElliott Hughes        pair = ("A", "V")
430*e1fe3e4aSElliott Hughes        if afm.has_kernpair(pair):
431*e1fe3e4aSElliott Hughes            print(afm[pair])  # print kerning value for pair
432*e1fe3e4aSElliott Hughes        print(afm.Version)  # various other afm entries have become attributes
433*e1fe3e4aSElliott Hughes        print(afm.Weight)
434*e1fe3e4aSElliott Hughes        # afm.comments() returns a list of all Comment lines found in the AFM
435*e1fe3e4aSElliott Hughes        print(afm.comments())
436*e1fe3e4aSElliott Hughes        # print afm.chars()
437*e1fe3e4aSElliott Hughes        # print afm.kernpairs()
438*e1fe3e4aSElliott Hughes        print(afm)
439*e1fe3e4aSElliott Hughes        afm.write(path + ".muck")
440