source: packages/fontMath/branches/ufo3/Lib/fontMath/mathKerning.py @ 1076

Revision 1076, 14.2 KB checked in by tal, 16 months ago (diff)
UFO 3 branch.
Line 
1"""
2An object that serves kerning data from a
3class kerning dictionary.
4
5It scans a group dictionary and stores
6a mapping of glyph to group relationships.
7this map is then used to lookup kerning values.
8
9It is important to note that all groups names
10used in class kerning pairs must have the
11standard kerning class prefix (@) at the begining
12of the group name.
13"""
14
15from copy import deepcopy
16from mathFunctions import add, sub, mul, div
17
18
19class MathKerning(object):
20
21    def __init__(self, kerning={}, groups={}):
22        self.update(kerning)
23        self.updateGroups(groups)
24
25    def update(self, kerning):
26        from robofab.objects.objectsBase import BaseKerning
27        if isinstance(kerning, BaseKerning):
28            kerning = kerning.asDict()
29        else:
30            kerning = deepcopy(kerning)
31        self._kerning = kerning
32
33    def updateGroups(self, groups):
34        self._groupMap = {}
35        self._groups = {}
36        groupDict = groups
37        groupMap = self._groupMap
38        for groupName, glyphList in groupDict.items():
39            if not groupName.startswith("@"):
40                continue
41            self._groups[groupName] = list(glyphList)
42            for glyphName in glyphList:
43                if not groupMap.has_key(glyphName):
44                    groupMap[glyphName] = []
45                if groupName not in groupMap[glyphName]:
46                    groupMap[glyphName].append(groupName)
47
48    def keys(self):
49        return self._kerning.keys()
50
51    def values(self):
52        return self._kerning.values()
53
54    def items(self):
55        return self._kerning.items()
56
57    def groups(self):
58        return deepcopy(self._groups)
59
60    def getGroupsForGlyph(self, glyphName):
61        """
62        >>> groups = {
63        ...     "@A1" : ["A", "B"],
64        ...     "@A2" : ["A"],
65        ...     "@A3" : ["A"],
66        ...     "@A4" : ["A"],
67        ... }
68        >>> obj = MathKerning({}, groups)
69        >>> sorted(obj.getGroupsForGlyph("A"))
70        ['@A1', '@A2', '@A3', '@A4']
71        >>> sorted(obj.getGroupsForGlyph("B"))
72        ['@A1']
73        """
74        return list(self._groupMap.get(glyphName, []))
75
76    def getGroupContents(self, groupName):
77        """
78        >>> groups = {
79        ...     "@A1" : ["A", "B"]
80        ... }
81        >>> obj = MathKerning({}, groups)
82        >>> obj.getGroupContents("@A1")
83        ['A', 'B']
84        """
85        return list(self._groups[groupName])
86
87    def __contains__(self, pair):
88        return pair in self._kerning
89
90    def __getitem__(self, pair):
91        """
92        >>> kerning = {
93        ...     ("@A_left", "@A_right") : 1,
94        ...     ("A1", "@A_right") : 2,
95        ...     ("@A_left", "A2") : 3,
96        ...     ("A3", "A3") : 4,
97        ... }
98        >>> groups = {
99        ... "@A_left" : ["A", "A1", "A2", "A3"],
100        ... "@A_right" : ["A", "A1", "A2", "A3"],
101        ... }
102        >>> obj = MathKerning(kerning, groups)
103        >>> obj["A", "A"]
104        1
105        >>> obj["A1", "A"]
106        2
107        >>> obj["A", "A2"]
108        3
109        >>> obj["A3", "A3"]
110        4
111        >>> obj["X", "X"]
112        0
113        """
114        if self._kerning.has_key(pair):
115            return self._kerning[pair]
116
117        left, right = pair
118        potentialLeft = [left]
119        potentialLeft.extend(self._groupMap.get(left, []))
120        potentialRight = [right]
121        potentialRight.extend(self._groupMap.get(right, []))
122
123        notClassed = []
124        halfClassed = []
125        fullClassed = []
126        for l in potentialLeft:
127            for r in potentialRight:
128                if self._kerning.has_key((l, r)):
129                    v = self._kerning[l, r]
130                    if l[0] == "@" and r[0] == "@":
131                        fullClassed.append((l, r, v))
132                    elif l[0] == "@" and r[0] != "@":
133                        halfClassed.append((l, r, v))
134                    elif l[0] != "@" and r[0] == "@":
135                        halfClassed.append((l, r, v))
136                    else:
137                        notClassed.append((l, r, v))
138        if len(notClassed) != 0:
139            return notClassed[0][2]
140        elif len(halfClassed) != 0:
141            halfClassed.sort()
142            return halfClassed[0][2]
143        elif len(fullClassed) != 0:
144            fullClassed.sort()
145            return fullClassed[0][2]
146        # hm, maybe this should raise a key error
147        # instead of returning 0...
148        return 0
149
150    def guessPairType(self, pair):
151        """
152        >>> kerning = {
153        ...     ("@A_left", "@A_right") : 1,
154        ...     ("A1", "@A_right") : 2,
155        ...     ("@A_left", "A2") : 3,
156        ...     ("A3", "A3") : 4,
157        ... }
158        >>> groups = {
159        ... "@A_left" : ["A", "A1", "A2", "A3"],
160        ... "@A_right" : ["A", "A1", "A2", "A3"],
161        ... }
162        >>> obj = MathKerning(kerning, groups)
163        >>> obj.guessPairType(("@A_left", "@A_right"))
164        ('class', 'class')
165        >>> obj.guessPairType(("A1", "@A_right"))
166        ('exception', 'class')
167        >>> obj.guessPairType(("@A_left", "A2"))
168        ('class', 'exception')
169        >>> obj.guessPairType(("A3", "A3"))
170        ('exception', 'exception')
171        >>> obj.guessPairType(("A", "A"))
172        ('single', 'single')
173        """
174        left, right = pair
175        CLASS_TYPE = "class"
176        SINGLE_TYPE = "single"
177        EXCEPTION_TYPE = "exception"
178
179        leftType = SINGLE_TYPE
180        rightType = SINGLE_TYPE
181        # is the left a simple class?
182        if left[0] == "@":
183            leftType = CLASS_TYPE
184        # or is it part of a class?
185        if right[0] == "@":
186            rightType = CLASS_TYPE
187
188        if self._kerning.has_key(pair):
189            potLeft = [left]
190            potRight = [right]
191            if leftType == SINGLE_TYPE and self._groupMap.has_key(left):
192                    for groupName in self._groupMap[left]:
193                        potLeft.append(groupName)
194            if rightType == SINGLE_TYPE and self._groupMap.has_key(right):
195                    for groupName in self._groupMap[right]:
196                        potRight.append(groupName)
197            hits = []
198            for left in potLeft:
199                for right in potRight:
200                    if self._kerning.has_key((left, right)):
201                        hits.append((left, right))
202            for left, right in hits:
203                if leftType != CLASS_TYPE:
204                    if left[0] == "@":
205                        leftType = EXCEPTION_TYPE
206                if rightType != CLASS_TYPE:
207                    if right[0] == "@":
208                        rightType = EXCEPTION_TYPE
209        return (leftType, rightType)
210
211    def get(self, pair, default=0):
212        v = self[pair]
213        if v == 0:
214            v = default
215        return v
216
217    def copy(self):
218        k = MathKerning(self._kerning)
219        k._groupMap = deepcopy(self._groupMap)
220        return k
221
222    def _processMathOne(self, other, funct):
223        comboPairs = set(self._kerning.keys()) | set(other._kerning.keys())
224        kerning = dict.fromkeys(comboPairs, None)
225        for k in comboPairs:
226            v1 = self.get(k, 0)
227            v2 = other.get(k, 0)
228            v = funct(v1, v2)
229            kerning[k] = v
230        g1 = self.groups()
231        g2 = other.groups()
232        if g1 == g2:
233            groups = g1
234        else:
235            comboGroups = set(g1.keys()) | set(g2.keys())
236            groups = dict.fromkeys(comboGroups, None)
237            for groupName in comboGroups:
238                s1 = set(g1.get(groupName, []))
239                s2 = set(g2.get(groupName, []))
240                groups[groupName] = list(s1 | s2)
241        ks = MathKerning(kerning, groups)
242        return ks
243
244    def _processMathTwo(self, factor, funct):
245        kerning = deepcopy(self._kerning)
246        for k, v in self._kerning.items():
247            v = funct(v, factor)
248            kerning[k] = v
249        ks = MathKerning(kerning)
250        ks._groupMap = deepcopy(self._groupMap)
251        return ks
252
253    def __add__(self, other):
254        """
255        >>> kerning1 = {
256        ...     ("A", "A") : 1,
257        ...     ("B", "B") : 1,
258        ...     ("NotIn2", "NotIn2") : 1,
259        ...     ("@NotIn2", "C") : 1,
260        ...     ("@D", "@D") : 1,
261        ... }
262        >>> groups1 = {
263        ...     "@NotIn1" : ["C"],
264        ...     "@D" : ["D", "H"],
265        ... }
266        >>> kerning2 = {
267        ...     ("A", "A") : -1,
268        ...     ("B", "B") : 1,
269        ...     ("NotIn1", "NotIn1") : 1,
270        ...     ("@NotIn1", "C") : 1,
271        ...     ("@D", "@D") : 1,
272        ... }
273        >>> groups2 = {
274        ...     "@NotIn2" : ["C"],
275        ...     "@D" : ["D"],
276        ... }
277        >>> obj = MathKerning(kerning1, groups1) + MathKerning(kerning2, groups2)
278        >>> sorted(obj.items())
279        [(('@D', '@D'), 2), (('@NotIn1', 'C'), 1), (('@NotIn2', 'C'), 1), (('B', 'B'), 2), (('NotIn1', 'NotIn1'), 1), (('NotIn2', 'NotIn2'), 1)]
280        >>> sorted(obj.groups()["@D"])
281        ['D', 'H']
282        """
283        k = self._processMathOne(other, add)
284        k.cleanup()
285        return k
286
287    def __sub__(self, other):
288        """
289        >>> kerning1 = {
290        ...     ("A", "A") : 1,
291        ...     ("B", "B") : 1,
292        ...     ("NotIn2", "NotIn2") : 1,
293        ...     ("@NotIn2", "C") : 1,
294        ...     ("@D", "@D") : 1,
295        ... }
296        >>> groups1 = {
297        ...     "@NotIn1" : ["C"],
298        ...     "@D" : ["D", "H"],
299        ... }
300        >>> kerning2 = {
301        ...     ("A", "A") : -1,
302        ...     ("B", "B") : 1,
303        ...     ("NotIn1", "NotIn1") : 1,
304        ...     ("@NotIn1", "C") : 1,
305        ...     ("@D", "@D") : 1,
306        ... }
307        >>> groups2 = {
308        ...     "@NotIn2" : ["C"],
309        ...     "@D" : ["D"],
310        ... }
311        >>> obj = MathKerning(kerning1, groups1) - MathKerning(kerning2, groups2)
312        >>> sorted(obj.items())
313        [(('@NotIn1', 'C'), -1), (('@NotIn2', 'C'), 1), (('A', 'A'), 2), (('NotIn1', 'NotIn1'), -1), (('NotIn2', 'NotIn2'), 1)]
314        >>> sorted(obj.groups()["@D"])
315        ['D', 'H']
316        """
317        k = self._processMathOne(other, sub)
318        k.cleanup()
319        return k
320
321    def __mul__(self, value):
322        """
323        >>> kerning = {
324        ...     ("A", "A") : 0,
325        ...     ("B", "B") : 1,
326        ...     ("C2", "@C") : 0,
327        ...     ("@C", "@C") : 2,
328        ... }
329        >>> groups = {
330        ...     "@C" : ["C1", "C2"],
331        ...     "@C" : ["C1", "C2"],
332        ... }
333        >>> obj = MathKerning(kerning, groups) * 2
334        >>> sorted(obj.items())
335        [(('@C', '@C'), 4), (('B', 'B'), 2), (('C2', '@C'), 0)]
336        """
337        k = self._processMathTwo(value, mul)
338        k.cleanup()
339        return k
340
341    def __rmul__(self, value):
342        """
343        >>> kerning = {
344        ...     ("A", "A") : 0,
345        ...     ("B", "B") : 1,
346        ...     ("C2", "@C") : 0,
347        ...     ("@C", "@C") : 2,
348        ... }
349        >>> groups = {
350        ...     "@C" : ["C1", "C2"],
351        ...     "@C" : ["C1", "C2"],
352        ... }
353        >>> obj = 2 * MathKerning(kerning, groups)
354        >>> sorted(obj.items())
355        [(('@C', '@C'), 4), (('B', 'B'), 2), (('C2', '@C'), 0)]
356        """
357        k = self._processMathTwo(value, mul)
358        k.cleanup()
359        return k
360
361    def __div__(self, value):
362        """
363        >>> kerning = {
364        ...     ("A", "A") : 0,
365        ...     ("B", "B") : 4,
366        ...     ("C2", "@C") : 0,
367        ...     ("@C", "@C") : 4,
368        ... }
369        >>> groups = {
370        ...     "@C" : ["C1", "C2"],
371        ...     "@C" : ["C1", "C2"],
372        ... }
373        >>> obj = MathKerning(kerning, groups) / 2
374        >>> sorted(obj.items())
375        [(('@C', '@C'), 2), (('B', 'B'), 2), (('C2', '@C'), 0)]
376        """
377        k = self._processMathTwo(value, div)
378        k.cleanup()
379        return k
380
381    def __rdiv__(self, value):
382        """
383        >>> kerning = {
384        ...     ("A", "A") : 0,
385        ...     ("B", "B") : 4,
386        ...     ("C2", "@C") : 0,
387        ...     ("@C", "@C") : 4,
388        ... }
389        >>> groups = {
390        ...     "@C" : ["C1", "C2"],
391        ...     "@C" : ["C1", "C2"],
392        ... }
393        >>> obj = 2 / MathKerning(kerning, groups)
394        >>> sorted(obj.items())
395        [(('@C', '@C'), 2), (('B', 'B'), 2), (('C2', '@C'), 0)]
396        """
397        k = self._processMathTwo(value, div)
398        k.cleanup()
399        return k
400
401    def round(self, multiple=1):
402        """
403        >>> kerning = {
404        ...     ("A", "A") : 2,
405        ...     ("B", "B") : 4,
406        ...     ("C", "C") : 7,
407        ...     ("D", "D") : 9,
408        ... }
409        >>> obj = MathKerning(kerning)
410        >>> obj.round(5)
411        >>> sorted(obj.items())
412        [(('A', 'A'), 0), (('B', 'B'), 5), (('C', 'C'), 5), (('D', 'D'), 10)]
413        """
414        multiple = float(multiple)
415        for k, v in self._kerning.items():
416            self._kerning[k] = int(round(int(round(v / multiple)) * multiple))
417
418    def cleanup(self):
419        """
420        >>> kerning = {
421        ...     ("A", "A") : 0,
422        ...     ("B", "B") : 1,
423        ...     ("C", "@C") : 0,
424        ...     ("@C", "@C") : 1,
425        ...     ("D", "D") : 1.0,
426        ...     ("E", "E") : 1.2,
427        ... }
428        >>> groups = {
429        ...     "@C" : ["C", "C1"]
430        ... }
431        >>> obj = MathKerning(kerning, groups)
432        >>> obj.cleanup()
433        >>> sorted(obj.items())
434        [(('@C', '@C'), 1), (('B', 'B'), 1), (('C', '@C'), 0), (('D', 'D'), 1), (('E', 'E'), 1.2)]
435        """
436        for (left, right), v in self._kerning.items():
437            if int(v) == v:
438                v = int(v)
439                self._kerning[left, right] = v
440            if v == 0:
441                leftType, rightType = self.guessPairType((left, right))
442                if leftType != "exception" and rightType != "exception":
443                    del self._kerning[left, right]
444
445    def addTo(self, value):
446        """
447        >>> kerning = {
448        ...     ("A", "A") : 1,
449        ...     ("B", "B") : -1,
450        ... }
451        >>> obj = MathKerning(kerning)
452        >>> obj.addTo(1)
453        >>> sorted(obj.items())
454        [(('A', 'A'), 2), (('B', 'B'), 0)]
455        """
456        for k, v in self._kerning.items():
457            self._kerning[k] = v + value
458
459    def extractKerning(self, font):
460        font.kerning.clear()
461        font.kerning.update(self._kerning)
462        font.groups.update(self.groups())
463
464
465if __name__ == "__main__":
466    import doctest
467    doctest.testmod()
Note: See TracBrowser for help on using the repository browser.