xref: /aosp_15_r20/external/fonttools/Lib/fontTools/colorLib/table_builder.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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 fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}"
71
72    return fmt, remainder
73
74
75class TableBuilder:
76    """
77    Helps to populate things derived from BaseTable from maps, tuples, etc.
78
79    A table of lifecycle callbacks may be provided to add logic beyond what is possible
80    based on otData info for the target class. See BuildCallbacks.
81    """
82
83    def __init__(self, callbackTable=None):
84        if callbackTable is None:
85            callbackTable = {}
86        self._callbackTable = callbackTable
87
88    def _convert(self, dest, field, converter, value):
89        enumClass = getattr(converter, "enumClass", None)
90
91        if enumClass:
92            if isinstance(value, enumClass):
93                pass
94            elif isinstance(value, str):
95                try:
96                    value = getattr(enumClass, value.upper())
97                except AttributeError:
98                    raise ValueError(f"{value} is not a valid {enumClass}")
99            else:
100                value = enumClass(value)
101
102        elif isinstance(converter, IntValue):
103            value = otRound(value)
104        elif isinstance(converter, FloatValue):
105            value = float(value)
106
107        elif isinstance(converter, Struct):
108            if converter.repeat:
109                if _isNonStrSequence(value):
110                    value = [self.build(converter.tableClass, v) for v in value]
111                else:
112                    value = [self.build(converter.tableClass, value)]
113                setattr(dest, converter.repeat, len(value))
114            else:
115                value = self.build(converter.tableClass, value)
116        elif callable(converter):
117            value = converter(value)
118
119        setattr(dest, field, value)
120
121    def build(self, cls, source):
122        assert issubclass(cls, BaseTable)
123
124        if isinstance(source, cls):
125            return source
126
127        callbackKey = (cls,)
128        fmt = None
129        if issubclass(cls, FormatSwitchingBaseTable):
130            fmt, source = _split_format(cls, source)
131            callbackKey = (cls, fmt)
132
133        dest = self._callbackTable.get(
134            (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls()
135        )()
136        assert isinstance(dest, cls)
137
138        convByName = _assignable(cls.convertersByName)
139        skippedFields = set()
140
141        # For format switchers we need to resolve converters based on format
142        if issubclass(cls, FormatSwitchingBaseTable):
143            dest.Format = fmt
144            convByName = _assignable(convByName[dest.Format])
145            skippedFields.add("Format")
146
147        # Convert sequence => mapping so before thunk only has to handle one format
148        if _isNonStrSequence(source):
149            # Sequence (typically list or tuple) assumed to match fields in declaration order
150            assert len(source) <= len(
151                convByName
152            ), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values"
153            source = dict(zip(convByName.keys(), source))
154
155        dest, source = self._callbackTable.get(
156            (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s)
157        )(dest, source)
158
159        if isinstance(source, collections.abc.Mapping):
160            for field, value in source.items():
161                if field in skippedFields:
162                    continue
163                converter = convByName.get(field, None)
164                if not converter:
165                    raise ValueError(
166                        f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}"
167                    )
168                self._convert(dest, field, converter, value)
169        else:
170            # let's try as a 1-tuple
171            dest = self.build(cls, (source,))
172
173        for field, conv in convByName.items():
174            if not hasattr(dest, field) and isinstance(conv, OptionalValue):
175                setattr(dest, field, conv.DEFAULT)
176
177        dest = self._callbackTable.get(
178            (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d
179        )(dest)
180
181        return dest
182
183
184class TableUnbuilder:
185    def __init__(self, callbackTable=None):
186        if callbackTable is None:
187            callbackTable = {}
188        self._callbackTable = callbackTable
189
190    def unbuild(self, table):
191        assert isinstance(table, BaseTable)
192
193        source = {}
194
195        callbackKey = (type(table),)
196        if isinstance(table, FormatSwitchingBaseTable):
197            source["Format"] = int(table.Format)
198            callbackKey += (table.Format,)
199
200        for converter in table.getConverters():
201            if isinstance(converter, ComputedInt):
202                continue
203            value = getattr(table, converter.name)
204
205            enumClass = getattr(converter, "enumClass", None)
206            if enumClass:
207                source[converter.name] = value.name.lower()
208            elif isinstance(converter, Struct):
209                if converter.repeat:
210                    source[converter.name] = [self.unbuild(v) for v in value]
211                else:
212                    source[converter.name] = self.unbuild(value)
213            elif isinstance(converter, SimpleValue):
214                # "simple" values (e.g. int, float, str) need no further un-building
215                source[converter.name] = value
216            else:
217                raise NotImplementedError(
218                    "Don't know how unbuild {value!r} with {converter!r}"
219                )
220
221        source = self._callbackTable.get(callbackKey, lambda s: s)(source)
222
223        return source
224