source: packages/ufo2fdk/trunk/Lib/ufo2fdk/kernFeatureWriter.py @ 448

Revision 448, 14.1 KB checked in by tal, 4 years ago (diff)
- Be meticulous about whitespace. - User could have passed a note with non-ascii.
Line 
1inlineGroupInstance = (list, tuple, set)
2
3class KernFeatureWriter(object):
4
5    """
6    This object will create a kerning feature in FDK
7    syntax using the kerning in the given font. The
8    only external method is :meth:`ufo2fdk.tools.kernFeatureWriter.write`.
9    """
10
11    def __init__(self, font):
12        self.font = font
13        self.leftGroups, self.rightGroups = self.getReferencedGroups()
14        self.unreferencedGroups = self.getUnreferencedGroups()
15        self.pairs = self.getPairs()
16        self.flatLeftGroups, self.flatRightGroups, self.flatUnreferencedGroups = self.getFlatGroups()
17
18    def write(self, headerText=None):
19        """
20        Write the feature text. If *headerText* is provided
21        it will inserted after the ``feature kern {`` line.
22        """
23        glyphGlyph, glyphGroupDecomposed, groupGlyphDecomposed, glyphGroup, groupGlyph, groupGroup = self.getSeparatedPairs(self.pairs)
24        # write the classes
25        groups = dict(self.leftGroups)
26        groups.update(self.rightGroups)
27        for groupName, glyphList in groups.items():
28            if not glyphList:
29                del groups[groupName]
30        classes = self.getClassDefinitionsForGroups(groups)
31        # write the kerning rules
32        rules = []
33        order = [
34            ("# glyph, glyph", glyphGlyph),
35            ("# glyph, group exceptions", glyphGroupDecomposed),
36            ("# group exceptions, glyph", groupGlyphDecomposed),
37            ("# glyph, group", glyphGroup),
38            ("# group, glyph", groupGlyph),
39            ("# group, group", groupGroup),
40        ]
41        for note, pairs in order:
42            if pairs:
43                rules.append("")
44                rules.append(note)
45                rules += self.getFeatureRulesForPairs(pairs)
46        # compile
47        feature = ["feature kern {"]
48        if headerText:
49            for line in headerText.splitlines():
50                line = line.strip()
51                if not line.startswith("#"):
52                    line = "# " + line
53                line = "    " + line
54                feature.append(line)
55        for line in classes + rules:
56            if line:
57                line = "    " + line
58            feature.append(line)
59        feature.append("} kern;")
60        # done
61        return u"\n".join(feature)
62
63    # -------------
64    # Initial Setup
65    # -------------
66
67    def getReferencedGroups(self):
68        """
69        Get two dictionaries representing groups
70        referenced on the left and right of pairs.
71        You should not call this method directly.
72        """
73        leftReferencedGroups = set()
74        rightReferencedGroups = set()
75        groups = self.font.groups
76        for left, right in self.font.kerning.keys():
77            if left in groups:
78                leftReferencedGroups.add(left)
79            if right in groups:
80                rightReferencedGroups.add(right)
81        leftGroups = {}
82        for groupName in leftReferencedGroups:
83            glyphList = [glyphName for glyphName in groups[groupName] if glyphName in self.font]
84            glyphList = set(glyphList)
85            if not groupName.startswith("@"):
86                groupName = "@" + groupName
87            leftGroups[groupName] = glyphList
88        rightGroups = {}
89        for groupName in rightReferencedGroups:
90            glyphList = [glyphName for glyphName in groups[groupName] if glyphName in self.font]
91            glyphList = set(glyphList)
92            if not groupName.startswith("@"):
93                groupName = "@" + groupName
94            rightGroups[groupName] = glyphList
95        return leftGroups, rightGroups
96
97    def getUnreferencedGroups(self):
98        """
99        Get a dictionary representing kerning groups
100        that are not referenced in any kerning pairs.
101        You should not call this method directly.
102        """
103        unreferencedGroups = {}
104        for groupName, glyphList in self.font.groups.items():
105            if not groupName.startswith("@"):
106                continue
107            if groupName in self.leftGroups:
108                continue
109            if groupName in self.rightGroups:
110                continue
111            unreferencedGroups[groupName] = set(glyphList)
112        return unreferencedGroups
113
114    def getPairs(self):
115        """
116        Get a dictionary containing all kerning pairs.
117        This should filter out pairs containing empty groups
118        and groups/glyphs that are not in the font.
119        You should not call this method directly.
120        """
121        pairs = {}
122        for (left, right), value in self.font.kerning.items():
123            # skip missing glyphs
124            if left not in self.font.groups and left not in self.font:
125                continue
126            if right not in self.font.groups and right not in self.font:
127                continue
128            # skip empty groups
129            if left in self.font.groups and not self.font.groups[left]:
130                continue
131            if right in self.font.groups and not self.font.groups[right]:
132                continue
133            # store pair
134            if left in self.font.groups:
135                if not left.startswith("@"):
136                    left = "@" + left
137            if right in self.font.groups:
138                if not right.startswith("@"):
139                    right = "@" + right
140            pairs[left, right] = value
141        return pairs
142
143    def getFlatGroups(self):
144        """
145        Get three dictionaries keyed by glyph names with
146        group names as values for left, right and
147        unreferenced groups. You should not call this
148        method directly.
149        """
150        flatLeftGroups = {}
151        flatRightGroups = {}
152        for groupName, glyphList in self.leftGroups.items():
153            for glyphName in glyphList:
154                # user has glyph in more than one group.
155                # this is not allowed.
156                if glyphName in flatLeftGroups:
157                    continue
158                flatLeftGroups[glyphName] = groupName
159        for groupName, glyphList in self.rightGroups.items():
160            for glyphName in glyphList:
161                # user has glyph in more than one group.
162                # this is not allowed.
163                if glyphName in flatRightGroups:
164                    continue
165                flatRightGroups[glyphName] = groupName
166        flatUnreferencedGroups = {}
167        for groupName, glyphList in self.unreferencedGroups.items():
168            for glyphName in glyphList:
169                flatUnreferencedGroups[glyphName] = groupName
170        return flatLeftGroups, flatRightGroups, flatUnreferencedGroups
171
172    # ------------
173    # Pair Support
174    # ------------
175
176    def isHigherLevelPairPossible(self, (left, right)):
177        """
178        Determine if there is a higher level pair possible.
179        This doesn't indicate that the pair exists, it simply
180        indicates that something higher than (left, right)
181        can exist.
182        You should not call this method directly.
183        """
184        leftInUnreferenced = False
185        rightInUnreferenced = False
186        if left.startswith("@"):
187            leftGroup = left
188            leftGlyph = None
189        else:
190            leftGroup = self.flatLeftGroups.get(left)
191            leftGlyph = left
192            if leftGroup is None and left in self.flatUnreferencedGroups:
193                leftInUnreferenced= True
194        if right.startswith("@"):
195            rightGroup = right
196            rightGlyph = None
197        else:
198            rightGroup = self.flatRightGroups.get(right)
199            rightGlyph = right
200            if rightGroup is None and right in self.flatUnreferencedGroups:
201                rightInUnreferenced = True
202
203        havePotentialHigherLevelPair = False
204        if left.startswith("@") and right.startswith("@"):
205            pass
206        elif left.startswith("@"):
207            if rightGroup is not None or rightInUnreferenced:
208                if (left, right) in self.pairs:
209                    havePotentialHigherLevelPair = True
210        elif right.startswith("@"):
211            if leftGroup is not None or leftInUnreferenced:
212                if (left, right) in self.pairs:
213                    havePotentialHigherLevelPair = True
214        else:
215            if leftGroup is not None and rightGroup is not None:
216                if (leftGlyph, rightGlyph) in self.pairs:
217                    havePotentialHigherLevelPair = True
218                elif (leftGroup, rightGlyph) in self.pairs:
219                    havePotentialHigherLevelPair = True
220                elif (leftGlyph, rightGroup) in self.pairs:
221                    havePotentialHigherLevelPair = True
222            elif leftGroup is not None:
223                if (leftGlyph, rightGlyph) in self.pairs:
224                    havePotentialHigherLevelPair = True
225            elif rightGroup is not None:
226                if (leftGlyph, rightGlyph) in self.pairs:
227                    havePotentialHigherLevelPair = True
228        return havePotentialHigherLevelPair
229
230    def getSeparatedPairs(self, pairs):
231        """
232        Organize *pair* into the following groups:
233
234        * glyph, glyph
235        * glyph, group (decomposed)
236        * group, glyph (decomposed)
237        * glyph, group
238        * group, glyph
239        * group, group
240
241        You should not call this method directly.
242        """
243        ## seperate pairs
244        glyphGlyph = {}
245        glyphGroup = {}
246        glyphGroupDecomposed = {}
247        groupGlyph = {}
248        groupGlyphDecomposed = {}
249        groupGroup = {}
250        for (left, right), value in pairs.items():
251            if left.startswith("@") and right.startswith("@"):
252                groupGroup[left, right] = value
253            elif left.startswith("@"):
254                groupGlyph[left, right] = value
255            elif right.startswith("@"):
256                glyphGroup[left, right] = value
257            else:
258                glyphGlyph[left, right] = value
259        ## handle decomposition
260        allGlyphGlyph = set(glyphGlyph.keys())
261        # glyph to group
262        for (left, right), value in glyphGroup.items():
263            if self.isHigherLevelPairPossible((left, right)):
264                finalRight = tuple([r for r in sorted(self.rightGroups[right]) if (left, r) not in allGlyphGlyph])
265                for r in finalRight:
266                    allGlyphGlyph.add((left, r))
267                glyphGroupDecomposed[left, finalRight] = value
268                del glyphGroup[left, right]
269        # group to glyph
270        for (left, right), value in groupGlyph.items():
271            if self.isHigherLevelPairPossible((left, right)):
272                finalLeft = tuple([l for l in sorted(self.leftGroups[left]) if (l, right) not in glyphGlyph and (l, right) not in allGlyphGlyph])
273                for l in finalLeft:
274                    allGlyphGlyph.add((l, right))
275                groupGlyphDecomposed[finalLeft, right] = value
276                del groupGlyph[left, right]
277        ## return the result
278        return glyphGlyph, glyphGroupDecomposed, groupGlyphDecomposed, glyphGroup, groupGlyph, groupGroup
279
280    # -------------
281    # Write Support
282    # -------------
283
284    def getClassDefinitionsForGroups(self, groups):
285        """
286        Write class definitions to a list of strings.
287        You should not call this method directly.
288        """
289        classes = []
290        for groupName in sorted(groups.keys()):
291            group = groups[groupName]
292            l = "%s = [%s];" % (groupName, " ".join(sorted(group)))
293            classes.append(l)
294        return classes
295
296    def getFeatureRulesForPairs(self, pairs):
297        """
298        Write pair rules to a list of strings.
299        You should not call this method directly.
300        """
301        rules = []
302        for (left, right), value in sorted(pairs.items()):
303            if not left or not right:
304                continue
305            if isinstance(left, inlineGroupInstance) or isinstance(right, inlineGroupInstance):
306                line = "enum pos %s %s %d;"
307            else:
308                line = "pos %s %s %d;"
309            if isinstance(left, inlineGroupInstance):
310                left = "[%s]" % " ".join(sorted(left))
311            if isinstance(right, inlineGroupInstance):
312                right = "[%s]" % " ".join(sorted(right))
313            rules.append(line % (left, right, value))
314        return rules
315
316
317# ----
318# Test
319# ----
320
321
322def _test():
323    """
324    >>> from fontTools.agl import AGL2UV
325    >>> from defcon import Font
326    >>> font = Font()
327    >>> for glyphName in AGL2UV:
328    ...     font.newGlyph(glyphName)
329    >>> kerning = {
330    ...     # various pair types
331    ...     ("Agrave", "Agrave") : -100,
332    ...     ("@LEFT_A", "Agrave") : -75,
333    ...     ("@LEFT_A", "Aacute") : -74,
334    ...     ("eight", "@RIGHT_B") : -49,
335    ...     ("@LEFT_A", "@RIGHT_A") : -25,
336    ...     ("@LEFT_D", "X") : -25,
337    ...     ("X", "@RIGHT_D") : -25,
338    ...     # empty groups
339    ...     ("@LEFT_C", "@RIGHT_C") : 25,
340    ...     ("C", "@RIGHT_C") : 25,
341    ...     ("@LEFT_C", "C") : 25,
342    ...     # nonexistant glyphs
343    ...     ("NotInFont", "NotInFont") : 25,
344    ...     # nonexistant groups
345    ...     ("@LEFT_NotInFont", "@RIGHT_NotInFont") : 25,
346    ... }
347    >>> groups = {
348    ...     "@LEFT_A" : ["A", "Aacute", "Agrave"],
349    ...     "@RIGHT_A" : ["A", "Aacute", "Agrave"],
350    ...     "@LEFT_B" : ["B", "eight"],
351    ...     "@RIGHT_B" : ["B", "eight"],
352    ...     "@LEFT_C" : [],
353    ...     "@RIGHT_C" : [],
354    ...     "@LEFT_D" : ["D"],
355    ...     "@RIGHT_D" : ["D"],
356    ... }
357    >>> font.groups.update(groups)
358    >>> font.kerning.update(kerning)
359
360    >>> writer = KernFeatureWriter(font)
361    >>> text = writer.write()
362    >>> t1 = [line.strip() for line in text.strip().splitlines()]
363    >>> t2 = [line.strip() for line in _expectedFeatureText.strip().splitlines()]
364    >>> t1 == t2
365    True
366    """
367
368_expectedFeatureText = """
369feature kern {
370    @LEFT_A = [A Aacute Agrave];
371    @LEFT_D = [D];
372    @RIGHT_A = [A Aacute Agrave];
373    @RIGHT_B = [B eight];
374    @RIGHT_D = [D];
375
376    # glyph, glyph
377    pos Agrave Agrave -100;
378
379    # glyph, group exceptions
380    enum pos eight [B eight] -49;
381
382    # group exceptions, glyph
383    enum pos [A Aacute] Agrave -75;
384    enum pos [A Aacute Agrave] Aacute -74;
385
386    # glyph, group
387    pos X @RIGHT_D -25;
388
389    # group, glyph
390    pos @LEFT_D X -25;
391
392    # group, group
393    pos @LEFT_A @RIGHT_A -25;
394} kern;
395"""
396
397if __name__ == "__main__":
398    import doctest
399    doctest.testmod()
Note: See TracBrowser for help on using the repository browser.