File: Routine.py

package info (click to toggle)
python-fontfeatures 1.9.0%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,096 kB
  • sloc: python: 9,112; makefile: 22
file content (159 lines) | stat: -rw-r--r-- 6,083 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
from fontTools.otlLib.builder import ClassDefBuilder
from fontFeatures import Substitution, Positioning, Chaining
import logging
import copy


class MoveLongCoverageToClassDefinition:
    level = 1

    def gensym(self, ff):
        if "index" not in ff.scratch:
            ff.scratch["index"] = 0
        ff.scratch["index"] = ff.scratch["index"] + 1
        return str(ff.scratch["index"])

    def replaceLongWithClasses(self, i, ff):
        for ix, gc in enumerate(i):
            if len(gc) > 5:
                classname = ff.getNamedClassFor(gc, "class" + self.gensym(ff))
                i[ix] = ["@" + classname]

    def apply(self, routine, ff):
        for rule in routine.rules:
            if isinstance(rule, Substitution):
                self.replaceLongWithClasses(rule.input, ff)
                self.replaceLongWithClasses(rule.precontext, ff)
                self.replaceLongWithClasses(rule.postcontext, ff)
                self.replaceLongWithClasses(rule.replacement, ff)
            if isinstance(rule, Positioning):
                self.replaceLongWithClasses(rule.glyphs, ff)
                self.replaceLongWithClasses(rule.precontext, ff)
                self.replaceLongWithClasses(rule.postcontext, ff)

        return []


class MergeMultipleSingleSubstitutions:
    level = 1

    def apply(self, routine, ff):
        def _is_single_sub(rule):
            (
                isinstance(rule, Substitution)
                and len(rule.input) == 1
                and len(rule.replacement) == 1
            )

        if len(routine.rules) < 2:
            return
        new_rules = [routine.rules[0]]
        for r in routine.rules[1:]:
            previous = new_rules[-1]
            if (
                _is_single_sub(r)
                and _is_single_sub(previous)
                and r.precontext == previous.precontext
                and r.postcontext == previous.postcontext
            ):
                new_rules.pop()
                new_rules.append(self.merge_two(previous, r))
            else:
                new_rules.append(r)
        routine.rules = new_rules
        # It's possible to get clever later with non-adjacent substitutions
        # if there's nothing in the middle that could affect the output

    def merge_two(self, first, second):
        assert len(first.input) == 1 and len(second.input) == 1
        assert len(first.replacement) == 1 and len(second.replacement) == 1
        assert first.precontext == second.precontext
        assert first.postcontext == second.postcontext
        firstmapping = {l: r for l, r in zip(first.input[0], first.replacement[0])}
        secondmapping = {l: r for l, r in zip(second.input[0], second.replacement[0])}
        for l, r in firstmapping.items():
            if r in secondmapping:
                firstmapping[l] = secondmapping[r]
        for l, r in secondmapping.items():
            if l not in firstmapping:
                firstmapping[l] = r
        logger = logging.getLogger("fontFeatures")
        logger.info("Merging two adjacent single subs")
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(first.asFea())
            logger.debug(second.asFea())
        address = first.address or second.address
        return Substitution(
            [list(firstmapping.keys())],
            [list(firstmapping.values())],
            address=address,
            precontext=first.precontext,
            postcontext=first.postcontext,
        )


class EnsureFormat2Chaining:
    level = 1

    def apply(self, routine, ff):
        rules = routine.rules
        if not all(isinstance(rule, Chaining) for rule in routine.rules):
            return

        while True:
            if self._apply(rules):
                break

    def _apply(self, rules):
        logger = logging.getLogger("fontFeatures")
        logger.warn("Applying format 2 optimization round")

        for which in ["input", "precontext", "postcontext"]:
            classdefbuilder = ClassDefBuilder(useClass0=False)
            for rule in rules:
                for slot in getattr(rule, which):
                    if not classdefbuilder.canAdd(set(slot)):
                        logger.warn("Mitigating. Rule count before=%i", len(rules))
                        self.mitigate(
                            rules, which, classdefbuilder.classes(), set(slot)
                        )
                        logger.warn("Mitigating. Rule count after=%i", len(rules))
                        return False
                    classdefbuilder.add(set(slot))
        return True

    def mitigate(self, rules, which, rule_classes, failing_slot):
        # What kind of fail is this?
        problem = None
        for klass in rule_classes[1:]:
            if not set(klass).isdisjoint(failing_slot):
                problem = klass
                break
        # Split both into two and try again
        additional_rules = []
        intersection = set(problem) & failing_slot
        assert intersection
        for rule in rules:
            for ix, slot in enumerate(getattr(rule, which)):
                if (set(slot) == set(problem) or set(slot) == failing_slot) and set(
                    slot
                ) != intersection:
                    new_rule = Chaining(
                        copy.deepcopy(rule.input),
                        precontext=copy.deepcopy(rule.precontext),
                        postcontext=copy.deepcopy(rule.postcontext),
                        lookups=rule.lookups[:],
                    )
                    setattr(new_rule, which, getattr(new_rule, which)[:])
                    setattr(rule, which, getattr(rule, which)[:])
                    getattr(rule, which)[ix] = list(set(slot) - intersection)
                    getattr(new_rule, which)[ix] = list(intersection)
                    additional_rules.append(new_rule)
        rules.extend(additional_rules)


optimizations = [
    MergeMultipleSingleSubstitutions,
    EnsureFormat2Chaining,
    # MoveLongCoverageToClassDefinition,  # Runs last
]