• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such.
3
4"""
5
6import collections
7import enum
8from fontTools.ttLib.tables.otBase import (
9    BaseTable,
10    FormatSwitchingBaseTable,
11    UInt8FormatSwitchingBaseTable,
12)
13from fontTools.ttLib.tables.otConverters import (
14    ComputedInt,
15    SimpleValue,
16    Struct,
17    Short,
18    UInt8,
19    UShort,
20    IntValue,
21    FloatValue,
22    OptionalValue,
23)
24from fontTools.misc.roundTools import otRound
25
26
27class BuildCallback(enum.Enum):
28    """Keyed on (BEFORE_BUILD, class[, Format if available]).
29    Receives (dest, source).
30    Should return (dest, source), which can be new objects.
31    """
32
33    BEFORE_BUILD = enum.auto()
34
35    """Keyed on (AFTER_BUILD, class[, Format if available]).
36    Receives (dest).
37    Should return dest, which can be a new object.
38    """
39    AFTER_BUILD = enum.auto()
40
41    """Keyed on (CREATE_DEFAULT, class[, Format if available]).
42    Receives no arguments.
43    Should return a new instance of class.
44    """
45    CREATE_DEFAULT = enum.auto()
46
47
48def _assignable(convertersByName):
49    return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)}
50
51
52def _isNonStrSequence(value):
53    return isinstance(value, collections.abc.Sequence) and not isinstance(value, str)
54
55
56def _split_format(cls, source):
57    if _isNonStrSequence(source):
58        assert len(source) > 0, f"{cls} needs at least format from {source}"
59        fmt, remainder = source[0], source[1:]
60    elif isinstance(source, collections.abc.Mapping):
61        assert "Format" in source, f"{cls} needs at least Format from {source}"
62        remainder = source.copy()
63        fmt = remainder.pop("Format")
64    else:
65        raise ValueError(f"Not sure how to populate {cls} from {source}")
66
67    assert isinstance(
68        fmt, collections.abc.Hashable
69    ), f"{cls} Format is not hashable: {fmt!r}"
70    assert (
71        fmt in cls.convertersByName
72    ), f"{cls} invalid Format: {fmt!r}"
73
74    return fmt, remainder
75
76
77class TableBuilder:
78    """
79    Helps to populate things derived from BaseTable from maps, tuples, etc.
80
81    A table of lifecycle callbacks may be provided to add logic beyond what is possible
82    based on otData info for the target class. See BuildCallbacks.
83    """
84
85    def __init__(self, callbackTable=None):
86        if callbackTable is None:
87            callbackTable = {}
88        self._callbackTable = callbackTable
89
90    def _convert(self, dest, field, converter, value):
91        enumClass = getattr(converter, "enumClass", None)
92
93        if enumClass:
94            if isinstance(value, enumClass):
95                pass
96            elif isinstance(value, str):
97                try:
98                    value = getattr(enumClass, value.upper())
99                except AttributeError:
100                    raise ValueError(f"{value} is not a valid {enumClass}")
101            else:
102                value = enumClass(value)
103
104        elif isinstance(converter, IntValue):
105            value = otRound(value)
106        elif isinstance(converter, FloatValue):
107            value = float(value)
108
109        elif isinstance(converter, Struct):
110            if converter.repeat:
111                if _isNonStrSequence(value):
112                    value = [self.build(converter.tableClass, v) for v in value]
113                else:
114                    value = [self.build(converter.tableClass, value)]
115                setattr(dest, converter.repeat, len(value))
116            else:
117                value = self.build(converter.tableClass, value)
118        elif callable(converter):
119            value = converter(value)
120
121        setattr(dest, field, value)
122
123    def build(self, cls, source):
124        assert issubclass(cls, BaseTable)
125
126        if isinstance(source, cls):
127            return source
128
129        callbackKey = (cls,)
130        fmt = None
131        if issubclass(cls, FormatSwitchingBaseTable):
132            fmt, source = _split_format(cls, source)
133            callbackKey = (cls, fmt)
134
135        dest = self._callbackTable.get(
136            (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls()
137        )()
138        assert isinstance(dest, cls)
139
140        convByName = _assignable(cls.convertersByName)
141        skippedFields = set()
142
143        # For format switchers we need to resolve converters based on format
144        if issubclass(cls, FormatSwitchingBaseTable):
145            dest.Format = fmt
146            convByName = _assignable(convByName[dest.Format])
147            skippedFields.add("Format")
148
149        # Convert sequence => mapping so before thunk only has to handle one format
150        if _isNonStrSequence(source):
151            # Sequence (typically list or tuple) assumed to match fields in declaration order
152            assert len(source) <= len(
153                convByName
154            ), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values"
155            source = dict(zip(convByName.keys(), source))
156
157        dest, source = self._callbackTable.get(
158            (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s)
159        )(dest, source)
160
161        if isinstance(source, collections.abc.Mapping):
162            for field, value in source.items():
163                if field in skippedFields:
164                    continue
165                converter = convByName.get(field, None)
166                if not converter:
167                    raise ValueError(
168                        f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}"
169                    )
170                self._convert(dest, field, converter, value)
171        else:
172            # let's try as a 1-tuple
173            dest = self.build(cls, (source,))
174
175        for field, conv in convByName.items():
176            if not hasattr(dest, field) and isinstance(conv, OptionalValue):
177                setattr(dest, field, conv.DEFAULT)
178
179        dest = self._callbackTable.get(
180            (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d
181        )(dest)
182
183        return dest
184
185
186class TableUnbuilder:
187    def __init__(self, callbackTable=None):
188        if callbackTable is None:
189            callbackTable = {}
190        self._callbackTable = callbackTable
191
192    def unbuild(self, table):
193        assert isinstance(table, BaseTable)
194
195        source = {}
196
197        callbackKey = (type(table),)
198        if isinstance(table, FormatSwitchingBaseTable):
199            source["Format"] = int(table.Format)
200            callbackKey += (table.Format,)
201
202        for converter in table.getConverters():
203            if isinstance(converter, ComputedInt):
204                continue
205            value = getattr(table, converter.name)
206
207            enumClass = getattr(converter, "enumClass", None)
208            if enumClass:
209                source[converter.name] = value.name.lower()
210            elif isinstance(converter, Struct):
211                if converter.repeat:
212                    source[converter.name] = [self.unbuild(v) for v in value]
213                else:
214                    source[converter.name] = self.unbuild(value)
215            elif isinstance(converter, SimpleValue):
216                # "simple" values (e.g. int, float, str) need no further un-building
217                source[converter.name] = value
218            else:
219                raise NotImplementedError(
220                    "Don't know how unbuild {value!r} with {converter!r}"
221                )
222
223        source = self._callbackTable.get(callbackKey, lambda s: s)(source)
224
225        return source
226