xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/tables/otBase.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesfrom fontTools.config import OPTIONS
2*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import Tag, bytesjoin
3*e1fe3e4aSElliott Hughesfrom .DefaultTable import DefaultTable
4*e1fe3e4aSElliott Hughesfrom enum import IntEnum
5*e1fe3e4aSElliott Hughesimport sys
6*e1fe3e4aSElliott Hughesimport array
7*e1fe3e4aSElliott Hughesimport struct
8*e1fe3e4aSElliott Hughesimport logging
9*e1fe3e4aSElliott Hughesfrom functools import lru_cache
10*e1fe3e4aSElliott Hughesfrom typing import Iterator, NamedTuple, Optional, Tuple
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott Hugheslog = logging.getLogger(__name__)
13*e1fe3e4aSElliott Hughes
14*e1fe3e4aSElliott Hugheshave_uharfbuzz = False
15*e1fe3e4aSElliott Hughestry:
16*e1fe3e4aSElliott Hughes    import uharfbuzz as hb
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott Hughes    # repack method added in uharfbuzz >= 0.23; if uharfbuzz *can* be
19*e1fe3e4aSElliott Hughes    # imported but repack method is missing, behave as if uharfbuzz
20*e1fe3e4aSElliott Hughes    # is not available (fallback to the slower Python implementation)
21*e1fe3e4aSElliott Hughes    have_uharfbuzz = callable(getattr(hb, "repack", None))
22*e1fe3e4aSElliott Hughesexcept ImportError:
23*e1fe3e4aSElliott Hughes    pass
24*e1fe3e4aSElliott Hughes
25*e1fe3e4aSElliott HughesUSE_HARFBUZZ_REPACKER = OPTIONS[f"{__name__}:USE_HARFBUZZ_REPACKER"]
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes
28*e1fe3e4aSElliott Hughesclass OverflowErrorRecord(object):
29*e1fe3e4aSElliott Hughes    def __init__(self, overflowTuple):
30*e1fe3e4aSElliott Hughes        self.tableType = overflowTuple[0]
31*e1fe3e4aSElliott Hughes        self.LookupListIndex = overflowTuple[1]
32*e1fe3e4aSElliott Hughes        self.SubTableIndex = overflowTuple[2]
33*e1fe3e4aSElliott Hughes        self.itemName = overflowTuple[3]
34*e1fe3e4aSElliott Hughes        self.itemIndex = overflowTuple[4]
35*e1fe3e4aSElliott Hughes
36*e1fe3e4aSElliott Hughes    def __repr__(self):
37*e1fe3e4aSElliott Hughes        return str(
38*e1fe3e4aSElliott Hughes            (
39*e1fe3e4aSElliott Hughes                self.tableType,
40*e1fe3e4aSElliott Hughes                "LookupIndex:",
41*e1fe3e4aSElliott Hughes                self.LookupListIndex,
42*e1fe3e4aSElliott Hughes                "SubTableIndex:",
43*e1fe3e4aSElliott Hughes                self.SubTableIndex,
44*e1fe3e4aSElliott Hughes                "ItemName:",
45*e1fe3e4aSElliott Hughes                self.itemName,
46*e1fe3e4aSElliott Hughes                "ItemIndex:",
47*e1fe3e4aSElliott Hughes                self.itemIndex,
48*e1fe3e4aSElliott Hughes            )
49*e1fe3e4aSElliott Hughes        )
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hughes
52*e1fe3e4aSElliott Hughesclass OTLOffsetOverflowError(Exception):
53*e1fe3e4aSElliott Hughes    def __init__(self, overflowErrorRecord):
54*e1fe3e4aSElliott Hughes        self.value = overflowErrorRecord
55*e1fe3e4aSElliott Hughes
56*e1fe3e4aSElliott Hughes    def __str__(self):
57*e1fe3e4aSElliott Hughes        return repr(self.value)
58*e1fe3e4aSElliott Hughes
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughesclass RepackerState(IntEnum):
61*e1fe3e4aSElliott Hughes    # Repacking control flow is implemnted using a state machine. The state machine table:
62*e1fe3e4aSElliott Hughes    #
63*e1fe3e4aSElliott Hughes    # State       | Packing Success | Packing Failed | Exception Raised |
64*e1fe3e4aSElliott Hughes    # ------------+-----------------+----------------+------------------+
65*e1fe3e4aSElliott Hughes    # PURE_FT     | Return result   | PURE_FT        | Return failure   |
66*e1fe3e4aSElliott Hughes    # HB_FT       | Return result   | HB_FT          | FT_FALLBACK      |
67*e1fe3e4aSElliott Hughes    # FT_FALLBACK | HB_FT           | FT_FALLBACK    | Return failure   |
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughes    # Pack only with fontTools, don't allow sharing between extensions.
70*e1fe3e4aSElliott Hughes    PURE_FT = 1
71*e1fe3e4aSElliott Hughes
72*e1fe3e4aSElliott Hughes    # Attempt to pack with harfbuzz (allowing sharing between extensions)
73*e1fe3e4aSElliott Hughes    # use fontTools to attempt overflow resolution.
74*e1fe3e4aSElliott Hughes    HB_FT = 2
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes    # Fallback if HB/FT packing gets stuck. Pack only with fontTools, don't allow sharing between
77*e1fe3e4aSElliott Hughes    # extensions.
78*e1fe3e4aSElliott Hughes    FT_FALLBACK = 3
79*e1fe3e4aSElliott Hughes
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughesclass BaseTTXConverter(DefaultTable):
82*e1fe3e4aSElliott Hughes    """Generic base class for TTX table converters. It functions as an
83*e1fe3e4aSElliott Hughes    adapter between the TTX (ttLib actually) table model and the model
84*e1fe3e4aSElliott Hughes    we use for OpenType tables, which is necessarily subtly different.
85*e1fe3e4aSElliott Hughes    """
86*e1fe3e4aSElliott Hughes
87*e1fe3e4aSElliott Hughes    def decompile(self, data, font):
88*e1fe3e4aSElliott Hughes        """Create an object from the binary data. Called automatically on access."""
89*e1fe3e4aSElliott Hughes        from . import otTables
90*e1fe3e4aSElliott Hughes
91*e1fe3e4aSElliott Hughes        reader = OTTableReader(data, tableTag=self.tableTag)
92*e1fe3e4aSElliott Hughes        tableClass = getattr(otTables, self.tableTag)
93*e1fe3e4aSElliott Hughes        self.table = tableClass()
94*e1fe3e4aSElliott Hughes        self.table.decompile(reader, font)
95*e1fe3e4aSElliott Hughes
96*e1fe3e4aSElliott Hughes    def compile(self, font):
97*e1fe3e4aSElliott Hughes        """Compiles the table into binary. Called automatically on save."""
98*e1fe3e4aSElliott Hughes
99*e1fe3e4aSElliott Hughes        # General outline:
100*e1fe3e4aSElliott Hughes        # Create a top-level OTTableWriter for the GPOS/GSUB table.
101*e1fe3e4aSElliott Hughes        # 	Call the compile method for the the table
102*e1fe3e4aSElliott Hughes        # 		for each 'converter' record in the table converter list
103*e1fe3e4aSElliott Hughes        # 			call converter's write method for each item in the value.
104*e1fe3e4aSElliott Hughes        # 				- For simple items, the write method adds a string to the
105*e1fe3e4aSElliott Hughes        # 				writer's self.items list.
106*e1fe3e4aSElliott Hughes        # 				- For Struct/Table/Subtable items, it add first adds new writer to the
107*e1fe3e4aSElliott Hughes        # 				to the writer's self.items, then calls the item's compile method.
108*e1fe3e4aSElliott Hughes        # 				This creates a tree of writers, rooted at the GUSB/GPOS writer, with
109*e1fe3e4aSElliott Hughes        # 				each writer representing a table, and the writer.items list containing
110*e1fe3e4aSElliott Hughes        # 				the child data strings and writers.
111*e1fe3e4aSElliott Hughes        # 	call the getAllData method
112*e1fe3e4aSElliott Hughes        # 		call _doneWriting, which removes duplicates
113*e1fe3e4aSElliott Hughes        # 		call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables
114*e1fe3e4aSElliott Hughes        # 		Traverse the flat list of tables, calling getDataLength on each to update their position
115*e1fe3e4aSElliott Hughes        # 		Traverse the flat list of tables again, calling getData each get the data in the table, now that
116*e1fe3e4aSElliott Hughes        # 		pos's and offset are known.
117*e1fe3e4aSElliott Hughes
118*e1fe3e4aSElliott Hughes        # 		If a lookup subtable overflows an offset, we have to start all over.
119*e1fe3e4aSElliott Hughes        overflowRecord = None
120*e1fe3e4aSElliott Hughes        # this is 3-state option: default (None) means automatically use hb.repack or
121*e1fe3e4aSElliott Hughes        # silently fall back if it fails; True, use it and raise error if not possible
122*e1fe3e4aSElliott Hughes        # or it errors out; False, don't use it, even if you can.
123*e1fe3e4aSElliott Hughes        use_hb_repack = font.cfg[USE_HARFBUZZ_REPACKER]
124*e1fe3e4aSElliott Hughes        if self.tableTag in ("GSUB", "GPOS"):
125*e1fe3e4aSElliott Hughes            if use_hb_repack is False:
126*e1fe3e4aSElliott Hughes                log.debug(
127*e1fe3e4aSElliott Hughes                    "hb.repack disabled, compiling '%s' with pure-python serializer",
128*e1fe3e4aSElliott Hughes                    self.tableTag,
129*e1fe3e4aSElliott Hughes                )
130*e1fe3e4aSElliott Hughes            elif not have_uharfbuzz:
131*e1fe3e4aSElliott Hughes                if use_hb_repack is True:
132*e1fe3e4aSElliott Hughes                    raise ImportError("No module named 'uharfbuzz'")
133*e1fe3e4aSElliott Hughes                else:
134*e1fe3e4aSElliott Hughes                    assert use_hb_repack is None
135*e1fe3e4aSElliott Hughes                    log.debug(
136*e1fe3e4aSElliott Hughes                        "uharfbuzz not found, compiling '%s' with pure-python serializer",
137*e1fe3e4aSElliott Hughes                        self.tableTag,
138*e1fe3e4aSElliott Hughes                    )
139*e1fe3e4aSElliott Hughes
140*e1fe3e4aSElliott Hughes        if (
141*e1fe3e4aSElliott Hughes            use_hb_repack in (None, True)
142*e1fe3e4aSElliott Hughes            and have_uharfbuzz
143*e1fe3e4aSElliott Hughes            and self.tableTag in ("GSUB", "GPOS")
144*e1fe3e4aSElliott Hughes        ):
145*e1fe3e4aSElliott Hughes            state = RepackerState.HB_FT
146*e1fe3e4aSElliott Hughes        else:
147*e1fe3e4aSElliott Hughes            state = RepackerState.PURE_FT
148*e1fe3e4aSElliott Hughes
149*e1fe3e4aSElliott Hughes        hb_first_error_logged = False
150*e1fe3e4aSElliott Hughes        lastOverflowRecord = None
151*e1fe3e4aSElliott Hughes        while True:
152*e1fe3e4aSElliott Hughes            try:
153*e1fe3e4aSElliott Hughes                writer = OTTableWriter(tableTag=self.tableTag)
154*e1fe3e4aSElliott Hughes                self.table.compile(writer, font)
155*e1fe3e4aSElliott Hughes                if state == RepackerState.HB_FT:
156*e1fe3e4aSElliott Hughes                    return self.tryPackingHarfbuzz(writer, hb_first_error_logged)
157*e1fe3e4aSElliott Hughes                elif state == RepackerState.PURE_FT:
158*e1fe3e4aSElliott Hughes                    return self.tryPackingFontTools(writer)
159*e1fe3e4aSElliott Hughes                elif state == RepackerState.FT_FALLBACK:
160*e1fe3e4aSElliott Hughes                    # Run packing with FontTools only, but don't return the result as it will
161*e1fe3e4aSElliott Hughes                    # not be optimally packed. Once a successful packing has been found, state is
162*e1fe3e4aSElliott Hughes                    # changed back to harfbuzz packing to produce the final, optimal, packing.
163*e1fe3e4aSElliott Hughes                    self.tryPackingFontTools(writer)
164*e1fe3e4aSElliott Hughes                    log.debug(
165*e1fe3e4aSElliott Hughes                        "Re-enabling sharing between extensions and switching back to "
166*e1fe3e4aSElliott Hughes                        "harfbuzz+fontTools packing."
167*e1fe3e4aSElliott Hughes                    )
168*e1fe3e4aSElliott Hughes                    state = RepackerState.HB_FT
169*e1fe3e4aSElliott Hughes
170*e1fe3e4aSElliott Hughes            except OTLOffsetOverflowError as e:
171*e1fe3e4aSElliott Hughes                hb_first_error_logged = True
172*e1fe3e4aSElliott Hughes                ok = self.tryResolveOverflow(font, e, lastOverflowRecord)
173*e1fe3e4aSElliott Hughes                lastOverflowRecord = e.value
174*e1fe3e4aSElliott Hughes
175*e1fe3e4aSElliott Hughes                if ok:
176*e1fe3e4aSElliott Hughes                    continue
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughes                if state is RepackerState.HB_FT:
179*e1fe3e4aSElliott Hughes                    log.debug(
180*e1fe3e4aSElliott Hughes                        "Harfbuzz packing out of resolutions, disabling sharing between extensions and "
181*e1fe3e4aSElliott Hughes                        "switching to fontTools only packing."
182*e1fe3e4aSElliott Hughes                    )
183*e1fe3e4aSElliott Hughes                    state = RepackerState.FT_FALLBACK
184*e1fe3e4aSElliott Hughes                else:
185*e1fe3e4aSElliott Hughes                    raise
186*e1fe3e4aSElliott Hughes
187*e1fe3e4aSElliott Hughes    def tryPackingHarfbuzz(self, writer, hb_first_error_logged):
188*e1fe3e4aSElliott Hughes        try:
189*e1fe3e4aSElliott Hughes            log.debug("serializing '%s' with hb.repack", self.tableTag)
190*e1fe3e4aSElliott Hughes            return writer.getAllDataUsingHarfbuzz(self.tableTag)
191*e1fe3e4aSElliott Hughes        except (ValueError, MemoryError, hb.RepackerError) as e:
192*e1fe3e4aSElliott Hughes            # Only log hb repacker errors the first time they occur in
193*e1fe3e4aSElliott Hughes            # the offset-overflow resolution loop, they are just noisy.
194*e1fe3e4aSElliott Hughes            # Maybe we can revisit this if/when uharfbuzz actually gives
195*e1fe3e4aSElliott Hughes            # us more info as to why hb.repack failed...
196*e1fe3e4aSElliott Hughes            if not hb_first_error_logged:
197*e1fe3e4aSElliott Hughes                error_msg = f"{type(e).__name__}"
198*e1fe3e4aSElliott Hughes                if str(e) != "":
199*e1fe3e4aSElliott Hughes                    error_msg += f": {e}"
200*e1fe3e4aSElliott Hughes                log.warning(
201*e1fe3e4aSElliott Hughes                    "hb.repack failed to serialize '%s', attempting fonttools resolutions "
202*e1fe3e4aSElliott Hughes                    "; the error message was: %s",
203*e1fe3e4aSElliott Hughes                    self.tableTag,
204*e1fe3e4aSElliott Hughes                    error_msg,
205*e1fe3e4aSElliott Hughes                )
206*e1fe3e4aSElliott Hughes                hb_first_error_logged = True
207*e1fe3e4aSElliott Hughes            return writer.getAllData(remove_duplicate=False)
208*e1fe3e4aSElliott Hughes
209*e1fe3e4aSElliott Hughes    def tryPackingFontTools(self, writer):
210*e1fe3e4aSElliott Hughes        return writer.getAllData()
211*e1fe3e4aSElliott Hughes
212*e1fe3e4aSElliott Hughes    def tryResolveOverflow(self, font, e, lastOverflowRecord):
213*e1fe3e4aSElliott Hughes        ok = 0
214*e1fe3e4aSElliott Hughes        if lastOverflowRecord == e.value:
215*e1fe3e4aSElliott Hughes            # Oh well...
216*e1fe3e4aSElliott Hughes            return ok
217*e1fe3e4aSElliott Hughes
218*e1fe3e4aSElliott Hughes        overflowRecord = e.value
219*e1fe3e4aSElliott Hughes        log.info("Attempting to fix OTLOffsetOverflowError %s", e)
220*e1fe3e4aSElliott Hughes
221*e1fe3e4aSElliott Hughes        if overflowRecord.itemName is None:
222*e1fe3e4aSElliott Hughes            from .otTables import fixLookupOverFlows
223*e1fe3e4aSElliott Hughes
224*e1fe3e4aSElliott Hughes            ok = fixLookupOverFlows(font, overflowRecord)
225*e1fe3e4aSElliott Hughes        else:
226*e1fe3e4aSElliott Hughes            from .otTables import fixSubTableOverFlows
227*e1fe3e4aSElliott Hughes
228*e1fe3e4aSElliott Hughes            ok = fixSubTableOverFlows(font, overflowRecord)
229*e1fe3e4aSElliott Hughes
230*e1fe3e4aSElliott Hughes        if ok:
231*e1fe3e4aSElliott Hughes            return ok
232*e1fe3e4aSElliott Hughes
233*e1fe3e4aSElliott Hughes        # Try upgrading lookup to Extension and hope
234*e1fe3e4aSElliott Hughes        # that cross-lookup sharing not happening would
235*e1fe3e4aSElliott Hughes        # fix overflow...
236*e1fe3e4aSElliott Hughes        from .otTables import fixLookupOverFlows
237*e1fe3e4aSElliott Hughes
238*e1fe3e4aSElliott Hughes        return fixLookupOverFlows(font, overflowRecord)
239*e1fe3e4aSElliott Hughes
240*e1fe3e4aSElliott Hughes    def toXML(self, writer, font):
241*e1fe3e4aSElliott Hughes        self.table.toXML2(writer, font)
242*e1fe3e4aSElliott Hughes
243*e1fe3e4aSElliott Hughes    def fromXML(self, name, attrs, content, font):
244*e1fe3e4aSElliott Hughes        from . import otTables
245*e1fe3e4aSElliott Hughes
246*e1fe3e4aSElliott Hughes        if not hasattr(self, "table"):
247*e1fe3e4aSElliott Hughes            tableClass = getattr(otTables, self.tableTag)
248*e1fe3e4aSElliott Hughes            self.table = tableClass()
249*e1fe3e4aSElliott Hughes        self.table.fromXML(name, attrs, content, font)
250*e1fe3e4aSElliott Hughes        self.table.populateDefaults()
251*e1fe3e4aSElliott Hughes
252*e1fe3e4aSElliott Hughes    def ensureDecompiled(self, recurse=True):
253*e1fe3e4aSElliott Hughes        self.table.ensureDecompiled(recurse=recurse)
254*e1fe3e4aSElliott Hughes
255*e1fe3e4aSElliott Hughes
256*e1fe3e4aSElliott Hughes# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
257*e1fe3e4aSElliott Hughesassert len(struct.pack("i", 0)) == 4
258*e1fe3e4aSElliott Hughesassert array.array("i").itemsize == 4, "Oops, file a bug against fonttools."
259*e1fe3e4aSElliott Hughes
260*e1fe3e4aSElliott Hughes
261*e1fe3e4aSElliott Hughesclass OTTableReader(object):
262*e1fe3e4aSElliott Hughes    """Helper class to retrieve data from an OpenType table."""
263*e1fe3e4aSElliott Hughes
264*e1fe3e4aSElliott Hughes    __slots__ = ("data", "offset", "pos", "localState", "tableTag")
265*e1fe3e4aSElliott Hughes
266*e1fe3e4aSElliott Hughes    def __init__(self, data, localState=None, offset=0, tableTag=None):
267*e1fe3e4aSElliott Hughes        self.data = data
268*e1fe3e4aSElliott Hughes        self.offset = offset
269*e1fe3e4aSElliott Hughes        self.pos = offset
270*e1fe3e4aSElliott Hughes        self.localState = localState
271*e1fe3e4aSElliott Hughes        self.tableTag = tableTag
272*e1fe3e4aSElliott Hughes
273*e1fe3e4aSElliott Hughes    def advance(self, count):
274*e1fe3e4aSElliott Hughes        self.pos += count
275*e1fe3e4aSElliott Hughes
276*e1fe3e4aSElliott Hughes    def seek(self, pos):
277*e1fe3e4aSElliott Hughes        self.pos = pos
278*e1fe3e4aSElliott Hughes
279*e1fe3e4aSElliott Hughes    def copy(self):
280*e1fe3e4aSElliott Hughes        other = self.__class__(self.data, self.localState, self.offset, self.tableTag)
281*e1fe3e4aSElliott Hughes        other.pos = self.pos
282*e1fe3e4aSElliott Hughes        return other
283*e1fe3e4aSElliott Hughes
284*e1fe3e4aSElliott Hughes    def getSubReader(self, offset):
285*e1fe3e4aSElliott Hughes        offset = self.offset + offset
286*e1fe3e4aSElliott Hughes        return self.__class__(self.data, self.localState, offset, self.tableTag)
287*e1fe3e4aSElliott Hughes
288*e1fe3e4aSElliott Hughes    def readValue(self, typecode, staticSize):
289*e1fe3e4aSElliott Hughes        pos = self.pos
290*e1fe3e4aSElliott Hughes        newpos = pos + staticSize
291*e1fe3e4aSElliott Hughes        (value,) = struct.unpack(f">{typecode}", self.data[pos:newpos])
292*e1fe3e4aSElliott Hughes        self.pos = newpos
293*e1fe3e4aSElliott Hughes        return value
294*e1fe3e4aSElliott Hughes
295*e1fe3e4aSElliott Hughes    def readArray(self, typecode, staticSize, count):
296*e1fe3e4aSElliott Hughes        pos = self.pos
297*e1fe3e4aSElliott Hughes        newpos = pos + count * staticSize
298*e1fe3e4aSElliott Hughes        value = array.array(typecode, self.data[pos:newpos])
299*e1fe3e4aSElliott Hughes        if sys.byteorder != "big":
300*e1fe3e4aSElliott Hughes            value.byteswap()
301*e1fe3e4aSElliott Hughes        self.pos = newpos
302*e1fe3e4aSElliott Hughes        return value.tolist()
303*e1fe3e4aSElliott Hughes
304*e1fe3e4aSElliott Hughes    def readInt8(self):
305*e1fe3e4aSElliott Hughes        return self.readValue("b", staticSize=1)
306*e1fe3e4aSElliott Hughes
307*e1fe3e4aSElliott Hughes    def readInt8Array(self, count):
308*e1fe3e4aSElliott Hughes        return self.readArray("b", staticSize=1, count=count)
309*e1fe3e4aSElliott Hughes
310*e1fe3e4aSElliott Hughes    def readShort(self):
311*e1fe3e4aSElliott Hughes        return self.readValue("h", staticSize=2)
312*e1fe3e4aSElliott Hughes
313*e1fe3e4aSElliott Hughes    def readShortArray(self, count):
314*e1fe3e4aSElliott Hughes        return self.readArray("h", staticSize=2, count=count)
315*e1fe3e4aSElliott Hughes
316*e1fe3e4aSElliott Hughes    def readLong(self):
317*e1fe3e4aSElliott Hughes        return self.readValue("i", staticSize=4)
318*e1fe3e4aSElliott Hughes
319*e1fe3e4aSElliott Hughes    def readLongArray(self, count):
320*e1fe3e4aSElliott Hughes        return self.readArray("i", staticSize=4, count=count)
321*e1fe3e4aSElliott Hughes
322*e1fe3e4aSElliott Hughes    def readUInt8(self):
323*e1fe3e4aSElliott Hughes        return self.readValue("B", staticSize=1)
324*e1fe3e4aSElliott Hughes
325*e1fe3e4aSElliott Hughes    def readUInt8Array(self, count):
326*e1fe3e4aSElliott Hughes        return self.readArray("B", staticSize=1, count=count)
327*e1fe3e4aSElliott Hughes
328*e1fe3e4aSElliott Hughes    def readUShort(self):
329*e1fe3e4aSElliott Hughes        return self.readValue("H", staticSize=2)
330*e1fe3e4aSElliott Hughes
331*e1fe3e4aSElliott Hughes    def readUShortArray(self, count):
332*e1fe3e4aSElliott Hughes        return self.readArray("H", staticSize=2, count=count)
333*e1fe3e4aSElliott Hughes
334*e1fe3e4aSElliott Hughes    def readULong(self):
335*e1fe3e4aSElliott Hughes        return self.readValue("I", staticSize=4)
336*e1fe3e4aSElliott Hughes
337*e1fe3e4aSElliott Hughes    def readULongArray(self, count):
338*e1fe3e4aSElliott Hughes        return self.readArray("I", staticSize=4, count=count)
339*e1fe3e4aSElliott Hughes
340*e1fe3e4aSElliott Hughes    def readUInt24(self):
341*e1fe3e4aSElliott Hughes        pos = self.pos
342*e1fe3e4aSElliott Hughes        newpos = pos + 3
343*e1fe3e4aSElliott Hughes        (value,) = struct.unpack(">l", b"\0" + self.data[pos:newpos])
344*e1fe3e4aSElliott Hughes        self.pos = newpos
345*e1fe3e4aSElliott Hughes        return value
346*e1fe3e4aSElliott Hughes
347*e1fe3e4aSElliott Hughes    def readUInt24Array(self, count):
348*e1fe3e4aSElliott Hughes        return [self.readUInt24() for _ in range(count)]
349*e1fe3e4aSElliott Hughes
350*e1fe3e4aSElliott Hughes    def readTag(self):
351*e1fe3e4aSElliott Hughes        pos = self.pos
352*e1fe3e4aSElliott Hughes        newpos = pos + 4
353*e1fe3e4aSElliott Hughes        value = Tag(self.data[pos:newpos])
354*e1fe3e4aSElliott Hughes        assert len(value) == 4, value
355*e1fe3e4aSElliott Hughes        self.pos = newpos
356*e1fe3e4aSElliott Hughes        return value
357*e1fe3e4aSElliott Hughes
358*e1fe3e4aSElliott Hughes    def readData(self, count):
359*e1fe3e4aSElliott Hughes        pos = self.pos
360*e1fe3e4aSElliott Hughes        newpos = pos + count
361*e1fe3e4aSElliott Hughes        value = self.data[pos:newpos]
362*e1fe3e4aSElliott Hughes        self.pos = newpos
363*e1fe3e4aSElliott Hughes        return value
364*e1fe3e4aSElliott Hughes
365*e1fe3e4aSElliott Hughes    def __setitem__(self, name, value):
366*e1fe3e4aSElliott Hughes        state = self.localState.copy() if self.localState else dict()
367*e1fe3e4aSElliott Hughes        state[name] = value
368*e1fe3e4aSElliott Hughes        self.localState = state
369*e1fe3e4aSElliott Hughes
370*e1fe3e4aSElliott Hughes    def __getitem__(self, name):
371*e1fe3e4aSElliott Hughes        return self.localState and self.localState[name]
372*e1fe3e4aSElliott Hughes
373*e1fe3e4aSElliott Hughes    def __contains__(self, name):
374*e1fe3e4aSElliott Hughes        return self.localState and name in self.localState
375*e1fe3e4aSElliott Hughes
376*e1fe3e4aSElliott Hughes
377*e1fe3e4aSElliott Hughesclass OffsetToWriter(object):
378*e1fe3e4aSElliott Hughes    def __init__(self, subWriter, offsetSize):
379*e1fe3e4aSElliott Hughes        self.subWriter = subWriter
380*e1fe3e4aSElliott Hughes        self.offsetSize = offsetSize
381*e1fe3e4aSElliott Hughes
382*e1fe3e4aSElliott Hughes    def __eq__(self, other):
383*e1fe3e4aSElliott Hughes        if type(self) != type(other):
384*e1fe3e4aSElliott Hughes            return NotImplemented
385*e1fe3e4aSElliott Hughes        return self.subWriter == other.subWriter and self.offsetSize == other.offsetSize
386*e1fe3e4aSElliott Hughes
387*e1fe3e4aSElliott Hughes    def __hash__(self):
388*e1fe3e4aSElliott Hughes        # only works after self._doneWriting() has been called
389*e1fe3e4aSElliott Hughes        return hash((self.subWriter, self.offsetSize))
390*e1fe3e4aSElliott Hughes
391*e1fe3e4aSElliott Hughes
392*e1fe3e4aSElliott Hughesclass OTTableWriter(object):
393*e1fe3e4aSElliott Hughes    """Helper class to gather and assemble data for OpenType tables."""
394*e1fe3e4aSElliott Hughes
395*e1fe3e4aSElliott Hughes    def __init__(self, localState=None, tableTag=None):
396*e1fe3e4aSElliott Hughes        self.items = []
397*e1fe3e4aSElliott Hughes        self.pos = None
398*e1fe3e4aSElliott Hughes        self.localState = localState
399*e1fe3e4aSElliott Hughes        self.tableTag = tableTag
400*e1fe3e4aSElliott Hughes        self.parent = None
401*e1fe3e4aSElliott Hughes
402*e1fe3e4aSElliott Hughes    def __setitem__(self, name, value):
403*e1fe3e4aSElliott Hughes        state = self.localState.copy() if self.localState else dict()
404*e1fe3e4aSElliott Hughes        state[name] = value
405*e1fe3e4aSElliott Hughes        self.localState = state
406*e1fe3e4aSElliott Hughes
407*e1fe3e4aSElliott Hughes    def __getitem__(self, name):
408*e1fe3e4aSElliott Hughes        return self.localState[name]
409*e1fe3e4aSElliott Hughes
410*e1fe3e4aSElliott Hughes    def __delitem__(self, name):
411*e1fe3e4aSElliott Hughes        del self.localState[name]
412*e1fe3e4aSElliott Hughes
413*e1fe3e4aSElliott Hughes    # assembler interface
414*e1fe3e4aSElliott Hughes
415*e1fe3e4aSElliott Hughes    def getDataLength(self):
416*e1fe3e4aSElliott Hughes        """Return the length of this table in bytes, without subtables."""
417*e1fe3e4aSElliott Hughes        l = 0
418*e1fe3e4aSElliott Hughes        for item in self.items:
419*e1fe3e4aSElliott Hughes            if hasattr(item, "getCountData"):
420*e1fe3e4aSElliott Hughes                l += item.size
421*e1fe3e4aSElliott Hughes            elif hasattr(item, "subWriter"):
422*e1fe3e4aSElliott Hughes                l += item.offsetSize
423*e1fe3e4aSElliott Hughes            else:
424*e1fe3e4aSElliott Hughes                l = l + len(item)
425*e1fe3e4aSElliott Hughes        return l
426*e1fe3e4aSElliott Hughes
427*e1fe3e4aSElliott Hughes    def getData(self):
428*e1fe3e4aSElliott Hughes        """Assemble the data for this writer/table, without subtables."""
429*e1fe3e4aSElliott Hughes        items = list(self.items)  # make a shallow copy
430*e1fe3e4aSElliott Hughes        pos = self.pos
431*e1fe3e4aSElliott Hughes        numItems = len(items)
432*e1fe3e4aSElliott Hughes        for i in range(numItems):
433*e1fe3e4aSElliott Hughes            item = items[i]
434*e1fe3e4aSElliott Hughes
435*e1fe3e4aSElliott Hughes            if hasattr(item, "subWriter"):
436*e1fe3e4aSElliott Hughes                if item.offsetSize == 4:
437*e1fe3e4aSElliott Hughes                    items[i] = packULong(item.subWriter.pos - pos)
438*e1fe3e4aSElliott Hughes                elif item.offsetSize == 2:
439*e1fe3e4aSElliott Hughes                    try:
440*e1fe3e4aSElliott Hughes                        items[i] = packUShort(item.subWriter.pos - pos)
441*e1fe3e4aSElliott Hughes                    except struct.error:
442*e1fe3e4aSElliott Hughes                        # provide data to fix overflow problem.
443*e1fe3e4aSElliott Hughes                        overflowErrorRecord = self.getOverflowErrorRecord(
444*e1fe3e4aSElliott Hughes                            item.subWriter
445*e1fe3e4aSElliott Hughes                        )
446*e1fe3e4aSElliott Hughes
447*e1fe3e4aSElliott Hughes                        raise OTLOffsetOverflowError(overflowErrorRecord)
448*e1fe3e4aSElliott Hughes                elif item.offsetSize == 3:
449*e1fe3e4aSElliott Hughes                    items[i] = packUInt24(item.subWriter.pos - pos)
450*e1fe3e4aSElliott Hughes                else:
451*e1fe3e4aSElliott Hughes                    raise ValueError(item.offsetSize)
452*e1fe3e4aSElliott Hughes
453*e1fe3e4aSElliott Hughes        return bytesjoin(items)
454*e1fe3e4aSElliott Hughes
455*e1fe3e4aSElliott Hughes    def getDataForHarfbuzz(self):
456*e1fe3e4aSElliott Hughes        """Assemble the data for this writer/table with all offset field set to 0"""
457*e1fe3e4aSElliott Hughes        items = list(self.items)
458*e1fe3e4aSElliott Hughes        packFuncs = {2: packUShort, 3: packUInt24, 4: packULong}
459*e1fe3e4aSElliott Hughes        for i, item in enumerate(items):
460*e1fe3e4aSElliott Hughes            if hasattr(item, "subWriter"):
461*e1fe3e4aSElliott Hughes                # Offset value is not needed in harfbuzz repacker, so setting offset to 0 to avoid overflow here
462*e1fe3e4aSElliott Hughes                if item.offsetSize in packFuncs:
463*e1fe3e4aSElliott Hughes                    items[i] = packFuncs[item.offsetSize](0)
464*e1fe3e4aSElliott Hughes                else:
465*e1fe3e4aSElliott Hughes                    raise ValueError(item.offsetSize)
466*e1fe3e4aSElliott Hughes
467*e1fe3e4aSElliott Hughes        return bytesjoin(items)
468*e1fe3e4aSElliott Hughes
469*e1fe3e4aSElliott Hughes    def __hash__(self):
470*e1fe3e4aSElliott Hughes        # only works after self._doneWriting() has been called
471*e1fe3e4aSElliott Hughes        return hash(self.items)
472*e1fe3e4aSElliott Hughes
473*e1fe3e4aSElliott Hughes    def __ne__(self, other):
474*e1fe3e4aSElliott Hughes        result = self.__eq__(other)
475*e1fe3e4aSElliott Hughes        return result if result is NotImplemented else not result
476*e1fe3e4aSElliott Hughes
477*e1fe3e4aSElliott Hughes    def __eq__(self, other):
478*e1fe3e4aSElliott Hughes        if type(self) != type(other):
479*e1fe3e4aSElliott Hughes            return NotImplemented
480*e1fe3e4aSElliott Hughes        return self.items == other.items
481*e1fe3e4aSElliott Hughes
482*e1fe3e4aSElliott Hughes    def _doneWriting(self, internedTables, shareExtension=False):
483*e1fe3e4aSElliott Hughes        # Convert CountData references to data string items
484*e1fe3e4aSElliott Hughes        # collapse duplicate table references to a unique entry
485*e1fe3e4aSElliott Hughes        # "tables" are OTTableWriter objects.
486*e1fe3e4aSElliott Hughes
487*e1fe3e4aSElliott Hughes        # For Extension Lookup types, we can
488*e1fe3e4aSElliott Hughes        # eliminate duplicates only within the tree under the Extension Lookup,
489*e1fe3e4aSElliott Hughes        # as offsets may exceed 64K even between Extension LookupTable subtables.
490*e1fe3e4aSElliott Hughes        isExtension = hasattr(self, "Extension")
491*e1fe3e4aSElliott Hughes
492*e1fe3e4aSElliott Hughes        # Certain versions of Uniscribe reject the font if the GSUB/GPOS top-level
493*e1fe3e4aSElliott Hughes        # arrays (ScriptList, FeatureList, LookupList) point to the same, possibly
494*e1fe3e4aSElliott Hughes        # empty, array.  So, we don't share those.
495*e1fe3e4aSElliott Hughes        # See: https://github.com/fonttools/fonttools/issues/518
496*e1fe3e4aSElliott Hughes        dontShare = hasattr(self, "DontShare")
497*e1fe3e4aSElliott Hughes
498*e1fe3e4aSElliott Hughes        if isExtension and not shareExtension:
499*e1fe3e4aSElliott Hughes            internedTables = {}
500*e1fe3e4aSElliott Hughes
501*e1fe3e4aSElliott Hughes        items = self.items
502*e1fe3e4aSElliott Hughes        for i in range(len(items)):
503*e1fe3e4aSElliott Hughes            item = items[i]
504*e1fe3e4aSElliott Hughes            if hasattr(item, "getCountData"):
505*e1fe3e4aSElliott Hughes                items[i] = item.getCountData()
506*e1fe3e4aSElliott Hughes            elif hasattr(item, "subWriter"):
507*e1fe3e4aSElliott Hughes                item.subWriter._doneWriting(
508*e1fe3e4aSElliott Hughes                    internedTables, shareExtension=shareExtension
509*e1fe3e4aSElliott Hughes                )
510*e1fe3e4aSElliott Hughes                # At this point, all subwriters are hashable based on their items.
511*e1fe3e4aSElliott Hughes                # (See hash and comparison magic methods above.) So the ``setdefault``
512*e1fe3e4aSElliott Hughes                # call here will return the first writer object we've seen with
513*e1fe3e4aSElliott Hughes                # equal content, or store it in the dictionary if it's not been
514*e1fe3e4aSElliott Hughes                # seen yet. We therefore replace the subwriter object with an equivalent
515*e1fe3e4aSElliott Hughes                # object, which deduplicates the tree.
516*e1fe3e4aSElliott Hughes                if not dontShare:
517*e1fe3e4aSElliott Hughes                    items[i].subWriter = internedTables.setdefault(
518*e1fe3e4aSElliott Hughes                        item.subWriter, item.subWriter
519*e1fe3e4aSElliott Hughes                    )
520*e1fe3e4aSElliott Hughes        self.items = tuple(items)
521*e1fe3e4aSElliott Hughes
522*e1fe3e4aSElliott Hughes    def _gatherTables(self, tables, extTables, done):
523*e1fe3e4aSElliott Hughes        # Convert table references in self.items tree to a flat
524*e1fe3e4aSElliott Hughes        # list of tables in depth-first traversal order.
525*e1fe3e4aSElliott Hughes        # "tables" are OTTableWriter objects.
526*e1fe3e4aSElliott Hughes        # We do the traversal in reverse order at each level, in order to
527*e1fe3e4aSElliott Hughes        # resolve duplicate references to be the last reference in the list of tables.
528*e1fe3e4aSElliott Hughes        # For extension lookups, duplicate references can be merged only within the
529*e1fe3e4aSElliott Hughes        # writer tree under the  extension lookup.
530*e1fe3e4aSElliott Hughes
531*e1fe3e4aSElliott Hughes        done[id(self)] = True
532*e1fe3e4aSElliott Hughes
533*e1fe3e4aSElliott Hughes        numItems = len(self.items)
534*e1fe3e4aSElliott Hughes        iRange = list(range(numItems))
535*e1fe3e4aSElliott Hughes        iRange.reverse()
536*e1fe3e4aSElliott Hughes
537*e1fe3e4aSElliott Hughes        isExtension = hasattr(self, "Extension")
538*e1fe3e4aSElliott Hughes
539*e1fe3e4aSElliott Hughes        selfTables = tables
540*e1fe3e4aSElliott Hughes
541*e1fe3e4aSElliott Hughes        if isExtension:
542*e1fe3e4aSElliott Hughes            assert (
543*e1fe3e4aSElliott Hughes                extTables is not None
544*e1fe3e4aSElliott Hughes            ), "Program or XML editing error. Extension subtables cannot contain extensions subtables"
545*e1fe3e4aSElliott Hughes            tables, extTables, done = extTables, None, {}
546*e1fe3e4aSElliott Hughes
547*e1fe3e4aSElliott Hughes        # add Coverage table if it is sorted last.
548*e1fe3e4aSElliott Hughes        sortCoverageLast = False
549*e1fe3e4aSElliott Hughes        if hasattr(self, "sortCoverageLast"):
550*e1fe3e4aSElliott Hughes            # Find coverage table
551*e1fe3e4aSElliott Hughes            for i in range(numItems):
552*e1fe3e4aSElliott Hughes                item = self.items[i]
553*e1fe3e4aSElliott Hughes                if (
554*e1fe3e4aSElliott Hughes                    hasattr(item, "subWriter")
555*e1fe3e4aSElliott Hughes                    and getattr(item.subWriter, "name", None) == "Coverage"
556*e1fe3e4aSElliott Hughes                ):
557*e1fe3e4aSElliott Hughes                    sortCoverageLast = True
558*e1fe3e4aSElliott Hughes                    break
559*e1fe3e4aSElliott Hughes            if id(item.subWriter) not in done:
560*e1fe3e4aSElliott Hughes                item.subWriter._gatherTables(tables, extTables, done)
561*e1fe3e4aSElliott Hughes            else:
562*e1fe3e4aSElliott Hughes                # We're a new parent of item
563*e1fe3e4aSElliott Hughes                pass
564*e1fe3e4aSElliott Hughes
565*e1fe3e4aSElliott Hughes        for i in iRange:
566*e1fe3e4aSElliott Hughes            item = self.items[i]
567*e1fe3e4aSElliott Hughes            if not hasattr(item, "subWriter"):
568*e1fe3e4aSElliott Hughes                continue
569*e1fe3e4aSElliott Hughes
570*e1fe3e4aSElliott Hughes            if (
571*e1fe3e4aSElliott Hughes                sortCoverageLast
572*e1fe3e4aSElliott Hughes                and (i == 1)
573*e1fe3e4aSElliott Hughes                and getattr(item.subWriter, "name", None) == "Coverage"
574*e1fe3e4aSElliott Hughes            ):
575*e1fe3e4aSElliott Hughes                # we've already 'gathered' it above
576*e1fe3e4aSElliott Hughes                continue
577*e1fe3e4aSElliott Hughes
578*e1fe3e4aSElliott Hughes            if id(item.subWriter) not in done:
579*e1fe3e4aSElliott Hughes                item.subWriter._gatherTables(tables, extTables, done)
580*e1fe3e4aSElliott Hughes            else:
581*e1fe3e4aSElliott Hughes                # Item is already written out by other parent
582*e1fe3e4aSElliott Hughes                pass
583*e1fe3e4aSElliott Hughes
584*e1fe3e4aSElliott Hughes        selfTables.append(self)
585*e1fe3e4aSElliott Hughes
586*e1fe3e4aSElliott Hughes    def _gatherGraphForHarfbuzz(self, tables, obj_list, done, objidx, virtual_edges):
587*e1fe3e4aSElliott Hughes        real_links = []
588*e1fe3e4aSElliott Hughes        virtual_links = []
589*e1fe3e4aSElliott Hughes        item_idx = objidx
590*e1fe3e4aSElliott Hughes
591*e1fe3e4aSElliott Hughes        # Merge virtual_links from parent
592*e1fe3e4aSElliott Hughes        for idx in virtual_edges:
593*e1fe3e4aSElliott Hughes            virtual_links.append((0, 0, idx))
594*e1fe3e4aSElliott Hughes
595*e1fe3e4aSElliott Hughes        sortCoverageLast = False
596*e1fe3e4aSElliott Hughes        coverage_idx = 0
597*e1fe3e4aSElliott Hughes        if hasattr(self, "sortCoverageLast"):
598*e1fe3e4aSElliott Hughes            # Find coverage table
599*e1fe3e4aSElliott Hughes            for i, item in enumerate(self.items):
600*e1fe3e4aSElliott Hughes                if getattr(item, "name", None) == "Coverage":
601*e1fe3e4aSElliott Hughes                    sortCoverageLast = True
602*e1fe3e4aSElliott Hughes                    if id(item) not in done:
603*e1fe3e4aSElliott Hughes                        coverage_idx = item_idx = item._gatherGraphForHarfbuzz(
604*e1fe3e4aSElliott Hughes                            tables, obj_list, done, item_idx, virtual_edges
605*e1fe3e4aSElliott Hughes                        )
606*e1fe3e4aSElliott Hughes                    else:
607*e1fe3e4aSElliott Hughes                        coverage_idx = done[id(item)]
608*e1fe3e4aSElliott Hughes                    virtual_edges.append(coverage_idx)
609*e1fe3e4aSElliott Hughes                    break
610*e1fe3e4aSElliott Hughes
611*e1fe3e4aSElliott Hughes        child_idx = 0
612*e1fe3e4aSElliott Hughes        offset_pos = 0
613*e1fe3e4aSElliott Hughes        for i, item in enumerate(self.items):
614*e1fe3e4aSElliott Hughes            if hasattr(item, "subWriter"):
615*e1fe3e4aSElliott Hughes                pos = offset_pos
616*e1fe3e4aSElliott Hughes            elif hasattr(item, "getCountData"):
617*e1fe3e4aSElliott Hughes                offset_pos += item.size
618*e1fe3e4aSElliott Hughes                continue
619*e1fe3e4aSElliott Hughes            else:
620*e1fe3e4aSElliott Hughes                offset_pos = offset_pos + len(item)
621*e1fe3e4aSElliott Hughes                continue
622*e1fe3e4aSElliott Hughes
623*e1fe3e4aSElliott Hughes            if id(item.subWriter) not in done:
624*e1fe3e4aSElliott Hughes                child_idx = item_idx = item.subWriter._gatherGraphForHarfbuzz(
625*e1fe3e4aSElliott Hughes                    tables, obj_list, done, item_idx, virtual_edges
626*e1fe3e4aSElliott Hughes                )
627*e1fe3e4aSElliott Hughes            else:
628*e1fe3e4aSElliott Hughes                child_idx = done[id(item.subWriter)]
629*e1fe3e4aSElliott Hughes
630*e1fe3e4aSElliott Hughes            real_edge = (pos, item.offsetSize, child_idx)
631*e1fe3e4aSElliott Hughes            real_links.append(real_edge)
632*e1fe3e4aSElliott Hughes            offset_pos += item.offsetSize
633*e1fe3e4aSElliott Hughes
634*e1fe3e4aSElliott Hughes        tables.append(self)
635*e1fe3e4aSElliott Hughes        obj_list.append((real_links, virtual_links))
636*e1fe3e4aSElliott Hughes        item_idx += 1
637*e1fe3e4aSElliott Hughes        done[id(self)] = item_idx
638*e1fe3e4aSElliott Hughes        if sortCoverageLast:
639*e1fe3e4aSElliott Hughes            virtual_edges.pop()
640*e1fe3e4aSElliott Hughes
641*e1fe3e4aSElliott Hughes        return item_idx
642*e1fe3e4aSElliott Hughes
643*e1fe3e4aSElliott Hughes    def getAllDataUsingHarfbuzz(self, tableTag):
644*e1fe3e4aSElliott Hughes        """The Whole table is represented as a Graph.
645*e1fe3e4aSElliott Hughes        Assemble graph data and call Harfbuzz repacker to pack the table.
646*e1fe3e4aSElliott Hughes        Harfbuzz repacker is faster and retain as much sub-table sharing as possible, see also:
647*e1fe3e4aSElliott Hughes        https://github.com/harfbuzz/harfbuzz/blob/main/docs/repacker.md
648*e1fe3e4aSElliott Hughes        The input format for hb.repack() method is explained here:
649*e1fe3e4aSElliott Hughes        https://github.com/harfbuzz/uharfbuzz/blob/main/src/uharfbuzz/_harfbuzz.pyx#L1149
650*e1fe3e4aSElliott Hughes        """
651*e1fe3e4aSElliott Hughes        internedTables = {}
652*e1fe3e4aSElliott Hughes        self._doneWriting(internedTables, shareExtension=True)
653*e1fe3e4aSElliott Hughes        tables = []
654*e1fe3e4aSElliott Hughes        obj_list = []
655*e1fe3e4aSElliott Hughes        done = {}
656*e1fe3e4aSElliott Hughes        objidx = 0
657*e1fe3e4aSElliott Hughes        virtual_edges = []
658*e1fe3e4aSElliott Hughes        self._gatherGraphForHarfbuzz(tables, obj_list, done, objidx, virtual_edges)
659*e1fe3e4aSElliott Hughes        # Gather all data in two passes: the absolute positions of all
660*e1fe3e4aSElliott Hughes        # subtable are needed before the actual data can be assembled.
661*e1fe3e4aSElliott Hughes        pos = 0
662*e1fe3e4aSElliott Hughes        for table in tables:
663*e1fe3e4aSElliott Hughes            table.pos = pos
664*e1fe3e4aSElliott Hughes            pos = pos + table.getDataLength()
665*e1fe3e4aSElliott Hughes
666*e1fe3e4aSElliott Hughes        data = []
667*e1fe3e4aSElliott Hughes        for table in tables:
668*e1fe3e4aSElliott Hughes            tableData = table.getDataForHarfbuzz()
669*e1fe3e4aSElliott Hughes            data.append(tableData)
670*e1fe3e4aSElliott Hughes
671*e1fe3e4aSElliott Hughes        if hasattr(hb, "repack_with_tag"):
672*e1fe3e4aSElliott Hughes            return hb.repack_with_tag(str(tableTag), data, obj_list)
673*e1fe3e4aSElliott Hughes        else:
674*e1fe3e4aSElliott Hughes            return hb.repack(data, obj_list)
675*e1fe3e4aSElliott Hughes
676*e1fe3e4aSElliott Hughes    def getAllData(self, remove_duplicate=True):
677*e1fe3e4aSElliott Hughes        """Assemble all data, including all subtables."""
678*e1fe3e4aSElliott Hughes        if remove_duplicate:
679*e1fe3e4aSElliott Hughes            internedTables = {}
680*e1fe3e4aSElliott Hughes            self._doneWriting(internedTables)
681*e1fe3e4aSElliott Hughes        tables = []
682*e1fe3e4aSElliott Hughes        extTables = []
683*e1fe3e4aSElliott Hughes        done = {}
684*e1fe3e4aSElliott Hughes        self._gatherTables(tables, extTables, done)
685*e1fe3e4aSElliott Hughes        tables.reverse()
686*e1fe3e4aSElliott Hughes        extTables.reverse()
687*e1fe3e4aSElliott Hughes        # Gather all data in two passes: the absolute positions of all
688*e1fe3e4aSElliott Hughes        # subtable are needed before the actual data can be assembled.
689*e1fe3e4aSElliott Hughes        pos = 0
690*e1fe3e4aSElliott Hughes        for table in tables:
691*e1fe3e4aSElliott Hughes            table.pos = pos
692*e1fe3e4aSElliott Hughes            pos = pos + table.getDataLength()
693*e1fe3e4aSElliott Hughes
694*e1fe3e4aSElliott Hughes        for table in extTables:
695*e1fe3e4aSElliott Hughes            table.pos = pos
696*e1fe3e4aSElliott Hughes            pos = pos + table.getDataLength()
697*e1fe3e4aSElliott Hughes
698*e1fe3e4aSElliott Hughes        data = []
699*e1fe3e4aSElliott Hughes        for table in tables:
700*e1fe3e4aSElliott Hughes            tableData = table.getData()
701*e1fe3e4aSElliott Hughes            data.append(tableData)
702*e1fe3e4aSElliott Hughes
703*e1fe3e4aSElliott Hughes        for table in extTables:
704*e1fe3e4aSElliott Hughes            tableData = table.getData()
705*e1fe3e4aSElliott Hughes            data.append(tableData)
706*e1fe3e4aSElliott Hughes
707*e1fe3e4aSElliott Hughes        return bytesjoin(data)
708*e1fe3e4aSElliott Hughes
709*e1fe3e4aSElliott Hughes    # interface for gathering data, as used by table.compile()
710*e1fe3e4aSElliott Hughes
711*e1fe3e4aSElliott Hughes    def getSubWriter(self):
712*e1fe3e4aSElliott Hughes        subwriter = self.__class__(self.localState, self.tableTag)
713*e1fe3e4aSElliott Hughes        subwriter.parent = (
714*e1fe3e4aSElliott Hughes            self  # because some subtables have idential values, we discard
715*e1fe3e4aSElliott Hughes        )
716*e1fe3e4aSElliott Hughes        # the duplicates under the getAllData method. Hence some
717*e1fe3e4aSElliott Hughes        # subtable writers can have more than one parent writer.
718*e1fe3e4aSElliott Hughes        # But we just care about first one right now.
719*e1fe3e4aSElliott Hughes        return subwriter
720*e1fe3e4aSElliott Hughes
721*e1fe3e4aSElliott Hughes    def writeValue(self, typecode, value):
722*e1fe3e4aSElliott Hughes        self.items.append(struct.pack(f">{typecode}", value))
723*e1fe3e4aSElliott Hughes
724*e1fe3e4aSElliott Hughes    def writeArray(self, typecode, values):
725*e1fe3e4aSElliott Hughes        a = array.array(typecode, values)
726*e1fe3e4aSElliott Hughes        if sys.byteorder != "big":
727*e1fe3e4aSElliott Hughes            a.byteswap()
728*e1fe3e4aSElliott Hughes        self.items.append(a.tobytes())
729*e1fe3e4aSElliott Hughes
730*e1fe3e4aSElliott Hughes    def writeInt8(self, value):
731*e1fe3e4aSElliott Hughes        assert -128 <= value < 128, value
732*e1fe3e4aSElliott Hughes        self.items.append(struct.pack(">b", value))
733*e1fe3e4aSElliott Hughes
734*e1fe3e4aSElliott Hughes    def writeInt8Array(self, values):
735*e1fe3e4aSElliott Hughes        self.writeArray("b", values)
736*e1fe3e4aSElliott Hughes
737*e1fe3e4aSElliott Hughes    def writeShort(self, value):
738*e1fe3e4aSElliott Hughes        assert -32768 <= value < 32768, value
739*e1fe3e4aSElliott Hughes        self.items.append(struct.pack(">h", value))
740*e1fe3e4aSElliott Hughes
741*e1fe3e4aSElliott Hughes    def writeShortArray(self, values):
742*e1fe3e4aSElliott Hughes        self.writeArray("h", values)
743*e1fe3e4aSElliott Hughes
744*e1fe3e4aSElliott Hughes    def writeLong(self, value):
745*e1fe3e4aSElliott Hughes        self.items.append(struct.pack(">i", value))
746*e1fe3e4aSElliott Hughes
747*e1fe3e4aSElliott Hughes    def writeLongArray(self, values):
748*e1fe3e4aSElliott Hughes        self.writeArray("i", values)
749*e1fe3e4aSElliott Hughes
750*e1fe3e4aSElliott Hughes    def writeUInt8(self, value):
751*e1fe3e4aSElliott Hughes        assert 0 <= value < 256, value
752*e1fe3e4aSElliott Hughes        self.items.append(struct.pack(">B", value))
753*e1fe3e4aSElliott Hughes
754*e1fe3e4aSElliott Hughes    def writeUInt8Array(self, values):
755*e1fe3e4aSElliott Hughes        self.writeArray("B", values)
756*e1fe3e4aSElliott Hughes
757*e1fe3e4aSElliott Hughes    def writeUShort(self, value):
758*e1fe3e4aSElliott Hughes        assert 0 <= value < 0x10000, value
759*e1fe3e4aSElliott Hughes        self.items.append(struct.pack(">H", value))
760*e1fe3e4aSElliott Hughes
761*e1fe3e4aSElliott Hughes    def writeUShortArray(self, values):
762*e1fe3e4aSElliott Hughes        self.writeArray("H", values)
763*e1fe3e4aSElliott Hughes
764*e1fe3e4aSElliott Hughes    def writeULong(self, value):
765*e1fe3e4aSElliott Hughes        self.items.append(struct.pack(">I", value))
766*e1fe3e4aSElliott Hughes
767*e1fe3e4aSElliott Hughes    def writeULongArray(self, values):
768*e1fe3e4aSElliott Hughes        self.writeArray("I", values)
769*e1fe3e4aSElliott Hughes
770*e1fe3e4aSElliott Hughes    def writeUInt24(self, value):
771*e1fe3e4aSElliott Hughes        assert 0 <= value < 0x1000000, value
772*e1fe3e4aSElliott Hughes        b = struct.pack(">L", value)
773*e1fe3e4aSElliott Hughes        self.items.append(b[1:])
774*e1fe3e4aSElliott Hughes
775*e1fe3e4aSElliott Hughes    def writeUInt24Array(self, values):
776*e1fe3e4aSElliott Hughes        for value in values:
777*e1fe3e4aSElliott Hughes            self.writeUInt24(value)
778*e1fe3e4aSElliott Hughes
779*e1fe3e4aSElliott Hughes    def writeTag(self, tag):
780*e1fe3e4aSElliott Hughes        tag = Tag(tag).tobytes()
781*e1fe3e4aSElliott Hughes        assert len(tag) == 4, tag
782*e1fe3e4aSElliott Hughes        self.items.append(tag)
783*e1fe3e4aSElliott Hughes
784*e1fe3e4aSElliott Hughes    def writeSubTable(self, subWriter, offsetSize):
785*e1fe3e4aSElliott Hughes        self.items.append(OffsetToWriter(subWriter, offsetSize))
786*e1fe3e4aSElliott Hughes
787*e1fe3e4aSElliott Hughes    def writeCountReference(self, table, name, size=2, value=None):
788*e1fe3e4aSElliott Hughes        ref = CountReference(table, name, size=size, value=value)
789*e1fe3e4aSElliott Hughes        self.items.append(ref)
790*e1fe3e4aSElliott Hughes        return ref
791*e1fe3e4aSElliott Hughes
792*e1fe3e4aSElliott Hughes    def writeStruct(self, format, values):
793*e1fe3e4aSElliott Hughes        data = struct.pack(*(format,) + values)
794*e1fe3e4aSElliott Hughes        self.items.append(data)
795*e1fe3e4aSElliott Hughes
796*e1fe3e4aSElliott Hughes    def writeData(self, data):
797*e1fe3e4aSElliott Hughes        self.items.append(data)
798*e1fe3e4aSElliott Hughes
799*e1fe3e4aSElliott Hughes    def getOverflowErrorRecord(self, item):
800*e1fe3e4aSElliott Hughes        LookupListIndex = SubTableIndex = itemName = itemIndex = None
801*e1fe3e4aSElliott Hughes        if self.name == "LookupList":
802*e1fe3e4aSElliott Hughes            LookupListIndex = item.repeatIndex
803*e1fe3e4aSElliott Hughes        elif self.name == "Lookup":
804*e1fe3e4aSElliott Hughes            LookupListIndex = self.repeatIndex
805*e1fe3e4aSElliott Hughes            SubTableIndex = item.repeatIndex
806*e1fe3e4aSElliott Hughes        else:
807*e1fe3e4aSElliott Hughes            itemName = getattr(item, "name", "<none>")
808*e1fe3e4aSElliott Hughes            if hasattr(item, "repeatIndex"):
809*e1fe3e4aSElliott Hughes                itemIndex = item.repeatIndex
810*e1fe3e4aSElliott Hughes            if self.name == "SubTable":
811*e1fe3e4aSElliott Hughes                LookupListIndex = self.parent.repeatIndex
812*e1fe3e4aSElliott Hughes                SubTableIndex = self.repeatIndex
813*e1fe3e4aSElliott Hughes            elif self.name == "ExtSubTable":
814*e1fe3e4aSElliott Hughes                LookupListIndex = self.parent.parent.repeatIndex
815*e1fe3e4aSElliott Hughes                SubTableIndex = self.parent.repeatIndex
816*e1fe3e4aSElliott Hughes            else:  # who knows how far below the SubTable level we are! Climb back up to the nearest subtable.
817*e1fe3e4aSElliott Hughes                itemName = ".".join([self.name, itemName])
818*e1fe3e4aSElliott Hughes                p1 = self.parent
819*e1fe3e4aSElliott Hughes                while p1 and p1.name not in ["ExtSubTable", "SubTable"]:
820*e1fe3e4aSElliott Hughes                    itemName = ".".join([p1.name, itemName])
821*e1fe3e4aSElliott Hughes                    p1 = p1.parent
822*e1fe3e4aSElliott Hughes                if p1:
823*e1fe3e4aSElliott Hughes                    if p1.name == "ExtSubTable":
824*e1fe3e4aSElliott Hughes                        LookupListIndex = p1.parent.parent.repeatIndex
825*e1fe3e4aSElliott Hughes                        SubTableIndex = p1.parent.repeatIndex
826*e1fe3e4aSElliott Hughes                    else:
827*e1fe3e4aSElliott Hughes                        LookupListIndex = p1.parent.repeatIndex
828*e1fe3e4aSElliott Hughes                        SubTableIndex = p1.repeatIndex
829*e1fe3e4aSElliott Hughes
830*e1fe3e4aSElliott Hughes        return OverflowErrorRecord(
831*e1fe3e4aSElliott Hughes            (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex)
832*e1fe3e4aSElliott Hughes        )
833*e1fe3e4aSElliott Hughes
834*e1fe3e4aSElliott Hughes
835*e1fe3e4aSElliott Hughesclass CountReference(object):
836*e1fe3e4aSElliott Hughes    """A reference to a Count value, not a count of references."""
837*e1fe3e4aSElliott Hughes
838*e1fe3e4aSElliott Hughes    def __init__(self, table, name, size=None, value=None):
839*e1fe3e4aSElliott Hughes        self.table = table
840*e1fe3e4aSElliott Hughes        self.name = name
841*e1fe3e4aSElliott Hughes        self.size = size
842*e1fe3e4aSElliott Hughes        if value is not None:
843*e1fe3e4aSElliott Hughes            self.setValue(value)
844*e1fe3e4aSElliott Hughes
845*e1fe3e4aSElliott Hughes    def setValue(self, value):
846*e1fe3e4aSElliott Hughes        table = self.table
847*e1fe3e4aSElliott Hughes        name = self.name
848*e1fe3e4aSElliott Hughes        if table[name] is None:
849*e1fe3e4aSElliott Hughes            table[name] = value
850*e1fe3e4aSElliott Hughes        else:
851*e1fe3e4aSElliott Hughes            assert table[name] == value, (name, table[name], value)
852*e1fe3e4aSElliott Hughes
853*e1fe3e4aSElliott Hughes    def getValue(self):
854*e1fe3e4aSElliott Hughes        return self.table[self.name]
855*e1fe3e4aSElliott Hughes
856*e1fe3e4aSElliott Hughes    def getCountData(self):
857*e1fe3e4aSElliott Hughes        v = self.table[self.name]
858*e1fe3e4aSElliott Hughes        if v is None:
859*e1fe3e4aSElliott Hughes            v = 0
860*e1fe3e4aSElliott Hughes        return {1: packUInt8, 2: packUShort, 4: packULong}[self.size](v)
861*e1fe3e4aSElliott Hughes
862*e1fe3e4aSElliott Hughes
863*e1fe3e4aSElliott Hughesdef packUInt8(value):
864*e1fe3e4aSElliott Hughes    return struct.pack(">B", value)
865*e1fe3e4aSElliott Hughes
866*e1fe3e4aSElliott Hughes
867*e1fe3e4aSElliott Hughesdef packUShort(value):
868*e1fe3e4aSElliott Hughes    return struct.pack(">H", value)
869*e1fe3e4aSElliott Hughes
870*e1fe3e4aSElliott Hughes
871*e1fe3e4aSElliott Hughesdef packULong(value):
872*e1fe3e4aSElliott Hughes    assert 0 <= value < 0x100000000, value
873*e1fe3e4aSElliott Hughes    return struct.pack(">I", value)
874*e1fe3e4aSElliott Hughes
875*e1fe3e4aSElliott Hughes
876*e1fe3e4aSElliott Hughesdef packUInt24(value):
877*e1fe3e4aSElliott Hughes    assert 0 <= value < 0x1000000, value
878*e1fe3e4aSElliott Hughes    return struct.pack(">I", value)[1:]
879*e1fe3e4aSElliott Hughes
880*e1fe3e4aSElliott Hughes
881*e1fe3e4aSElliott Hughesclass BaseTable(object):
882*e1fe3e4aSElliott Hughes    """Generic base class for all OpenType (sub)tables."""
883*e1fe3e4aSElliott Hughes
884*e1fe3e4aSElliott Hughes    def __getattr__(self, attr):
885*e1fe3e4aSElliott Hughes        reader = self.__dict__.get("reader")
886*e1fe3e4aSElliott Hughes        if reader:
887*e1fe3e4aSElliott Hughes            del self.reader
888*e1fe3e4aSElliott Hughes            font = self.font
889*e1fe3e4aSElliott Hughes            del self.font
890*e1fe3e4aSElliott Hughes            self.decompile(reader, font)
891*e1fe3e4aSElliott Hughes            return getattr(self, attr)
892*e1fe3e4aSElliott Hughes
893*e1fe3e4aSElliott Hughes        raise AttributeError(attr)
894*e1fe3e4aSElliott Hughes
895*e1fe3e4aSElliott Hughes    def ensureDecompiled(self, recurse=False):
896*e1fe3e4aSElliott Hughes        reader = self.__dict__.get("reader")
897*e1fe3e4aSElliott Hughes        if reader:
898*e1fe3e4aSElliott Hughes            del self.reader
899*e1fe3e4aSElliott Hughes            font = self.font
900*e1fe3e4aSElliott Hughes            del self.font
901*e1fe3e4aSElliott Hughes            self.decompile(reader, font)
902*e1fe3e4aSElliott Hughes        if recurse:
903*e1fe3e4aSElliott Hughes            for subtable in self.iterSubTables():
904*e1fe3e4aSElliott Hughes                subtable.value.ensureDecompiled(recurse)
905*e1fe3e4aSElliott Hughes
906*e1fe3e4aSElliott Hughes    def __getstate__(self):
907*e1fe3e4aSElliott Hughes        # before copying/pickling 'lazy' objects, make a shallow copy of OTTableReader
908*e1fe3e4aSElliott Hughes        # https://github.com/fonttools/fonttools/issues/2965
909*e1fe3e4aSElliott Hughes        if "reader" in self.__dict__:
910*e1fe3e4aSElliott Hughes            state = self.__dict__.copy()
911*e1fe3e4aSElliott Hughes            state["reader"] = self.__dict__["reader"].copy()
912*e1fe3e4aSElliott Hughes            return state
913*e1fe3e4aSElliott Hughes        return self.__dict__
914*e1fe3e4aSElliott Hughes
915*e1fe3e4aSElliott Hughes    @classmethod
916*e1fe3e4aSElliott Hughes    def getRecordSize(cls, reader):
917*e1fe3e4aSElliott Hughes        totalSize = 0
918*e1fe3e4aSElliott Hughes        for conv in cls.converters:
919*e1fe3e4aSElliott Hughes            size = conv.getRecordSize(reader)
920*e1fe3e4aSElliott Hughes            if size is NotImplemented:
921*e1fe3e4aSElliott Hughes                return NotImplemented
922*e1fe3e4aSElliott Hughes            countValue = 1
923*e1fe3e4aSElliott Hughes            if conv.repeat:
924*e1fe3e4aSElliott Hughes                if conv.repeat in reader:
925*e1fe3e4aSElliott Hughes                    countValue = reader[conv.repeat] + conv.aux
926*e1fe3e4aSElliott Hughes                else:
927*e1fe3e4aSElliott Hughes                    return NotImplemented
928*e1fe3e4aSElliott Hughes            totalSize += size * countValue
929*e1fe3e4aSElliott Hughes        return totalSize
930*e1fe3e4aSElliott Hughes
931*e1fe3e4aSElliott Hughes    def getConverters(self):
932*e1fe3e4aSElliott Hughes        return self.converters
933*e1fe3e4aSElliott Hughes
934*e1fe3e4aSElliott Hughes    def getConverterByName(self, name):
935*e1fe3e4aSElliott Hughes        return self.convertersByName[name]
936*e1fe3e4aSElliott Hughes
937*e1fe3e4aSElliott Hughes    def populateDefaults(self, propagator=None):
938*e1fe3e4aSElliott Hughes        for conv in self.getConverters():
939*e1fe3e4aSElliott Hughes            if conv.repeat:
940*e1fe3e4aSElliott Hughes                if not hasattr(self, conv.name):
941*e1fe3e4aSElliott Hughes                    setattr(self, conv.name, [])
942*e1fe3e4aSElliott Hughes                countValue = len(getattr(self, conv.name)) - conv.aux
943*e1fe3e4aSElliott Hughes                try:
944*e1fe3e4aSElliott Hughes                    count_conv = self.getConverterByName(conv.repeat)
945*e1fe3e4aSElliott Hughes                    setattr(self, conv.repeat, countValue)
946*e1fe3e4aSElliott Hughes                except KeyError:
947*e1fe3e4aSElliott Hughes                    # conv.repeat is a propagated count
948*e1fe3e4aSElliott Hughes                    if propagator and conv.repeat in propagator:
949*e1fe3e4aSElliott Hughes                        propagator[conv.repeat].setValue(countValue)
950*e1fe3e4aSElliott Hughes            else:
951*e1fe3e4aSElliott Hughes                if conv.aux and not eval(conv.aux, None, self.__dict__):
952*e1fe3e4aSElliott Hughes                    continue
953*e1fe3e4aSElliott Hughes                if hasattr(self, conv.name):
954*e1fe3e4aSElliott Hughes                    continue  # Warn if it should NOT be present?!
955*e1fe3e4aSElliott Hughes                if hasattr(conv, "writeNullOffset"):
956*e1fe3e4aSElliott Hughes                    setattr(self, conv.name, None)  # Warn?
957*e1fe3e4aSElliott Hughes                # elif not conv.isCount:
958*e1fe3e4aSElliott Hughes                # 	# Warn?
959*e1fe3e4aSElliott Hughes                # 	pass
960*e1fe3e4aSElliott Hughes                if hasattr(conv, "DEFAULT"):
961*e1fe3e4aSElliott Hughes                    # OptionalValue converters (e.g. VarIndex)
962*e1fe3e4aSElliott Hughes                    setattr(self, conv.name, conv.DEFAULT)
963*e1fe3e4aSElliott Hughes
964*e1fe3e4aSElliott Hughes    def decompile(self, reader, font):
965*e1fe3e4aSElliott Hughes        self.readFormat(reader)
966*e1fe3e4aSElliott Hughes        table = {}
967*e1fe3e4aSElliott Hughes        self.__rawTable = table  # for debugging
968*e1fe3e4aSElliott Hughes        for conv in self.getConverters():
969*e1fe3e4aSElliott Hughes            if conv.name == "SubTable":
970*e1fe3e4aSElliott Hughes                conv = conv.getConverter(reader.tableTag, table["LookupType"])
971*e1fe3e4aSElliott Hughes            if conv.name == "ExtSubTable":
972*e1fe3e4aSElliott Hughes                conv = conv.getConverter(reader.tableTag, table["ExtensionLookupType"])
973*e1fe3e4aSElliott Hughes            if conv.name == "FeatureParams":
974*e1fe3e4aSElliott Hughes                conv = conv.getConverter(reader["FeatureTag"])
975*e1fe3e4aSElliott Hughes            if conv.name == "SubStruct":
976*e1fe3e4aSElliott Hughes                conv = conv.getConverter(reader.tableTag, table["MorphType"])
977*e1fe3e4aSElliott Hughes            try:
978*e1fe3e4aSElliott Hughes                if conv.repeat:
979*e1fe3e4aSElliott Hughes                    if isinstance(conv.repeat, int):
980*e1fe3e4aSElliott Hughes                        countValue = conv.repeat
981*e1fe3e4aSElliott Hughes                    elif conv.repeat in table:
982*e1fe3e4aSElliott Hughes                        countValue = table[conv.repeat]
983*e1fe3e4aSElliott Hughes                    else:
984*e1fe3e4aSElliott Hughes                        # conv.repeat is a propagated count
985*e1fe3e4aSElliott Hughes                        countValue = reader[conv.repeat]
986*e1fe3e4aSElliott Hughes                    countValue += conv.aux
987*e1fe3e4aSElliott Hughes                    table[conv.name] = conv.readArray(reader, font, table, countValue)
988*e1fe3e4aSElliott Hughes                else:
989*e1fe3e4aSElliott Hughes                    if conv.aux and not eval(conv.aux, None, table):
990*e1fe3e4aSElliott Hughes                        continue
991*e1fe3e4aSElliott Hughes                    table[conv.name] = conv.read(reader, font, table)
992*e1fe3e4aSElliott Hughes                    if conv.isPropagated:
993*e1fe3e4aSElliott Hughes                        reader[conv.name] = table[conv.name]
994*e1fe3e4aSElliott Hughes            except Exception as e:
995*e1fe3e4aSElliott Hughes                name = conv.name
996*e1fe3e4aSElliott Hughes                e.args = e.args + (name,)
997*e1fe3e4aSElliott Hughes                raise
998*e1fe3e4aSElliott Hughes
999*e1fe3e4aSElliott Hughes        if hasattr(self, "postRead"):
1000*e1fe3e4aSElliott Hughes            self.postRead(table, font)
1001*e1fe3e4aSElliott Hughes        else:
1002*e1fe3e4aSElliott Hughes            self.__dict__.update(table)
1003*e1fe3e4aSElliott Hughes
1004*e1fe3e4aSElliott Hughes        del self.__rawTable  # succeeded, get rid of debugging info
1005*e1fe3e4aSElliott Hughes
1006*e1fe3e4aSElliott Hughes    def compile(self, writer, font):
1007*e1fe3e4aSElliott Hughes        self.ensureDecompiled()
1008*e1fe3e4aSElliott Hughes        # TODO Following hack to be removed by rewriting how FormatSwitching tables
1009*e1fe3e4aSElliott Hughes        # are handled.
1010*e1fe3e4aSElliott Hughes        # https://github.com/fonttools/fonttools/pull/2238#issuecomment-805192631
1011*e1fe3e4aSElliott Hughes        if hasattr(self, "preWrite"):
1012*e1fe3e4aSElliott Hughes            deleteFormat = not hasattr(self, "Format")
1013*e1fe3e4aSElliott Hughes            table = self.preWrite(font)
1014*e1fe3e4aSElliott Hughes            deleteFormat = deleteFormat and hasattr(self, "Format")
1015*e1fe3e4aSElliott Hughes        else:
1016*e1fe3e4aSElliott Hughes            deleteFormat = False
1017*e1fe3e4aSElliott Hughes            table = self.__dict__.copy()
1018*e1fe3e4aSElliott Hughes
1019*e1fe3e4aSElliott Hughes        # some count references may have been initialized in a custom preWrite; we set
1020*e1fe3e4aSElliott Hughes        # these in the writer's state beforehand (instead of sequentially) so they will
1021*e1fe3e4aSElliott Hughes        # be propagated to all nested subtables even if the count appears in the current
1022*e1fe3e4aSElliott Hughes        # table only *after* the offset to the subtable that it is counting.
1023*e1fe3e4aSElliott Hughes        for conv in self.getConverters():
1024*e1fe3e4aSElliott Hughes            if conv.isCount and conv.isPropagated:
1025*e1fe3e4aSElliott Hughes                value = table.get(conv.name)
1026*e1fe3e4aSElliott Hughes                if isinstance(value, CountReference):
1027*e1fe3e4aSElliott Hughes                    writer[conv.name] = value
1028*e1fe3e4aSElliott Hughes
1029*e1fe3e4aSElliott Hughes        if hasattr(self, "sortCoverageLast"):
1030*e1fe3e4aSElliott Hughes            writer.sortCoverageLast = 1
1031*e1fe3e4aSElliott Hughes
1032*e1fe3e4aSElliott Hughes        if hasattr(self, "DontShare"):
1033*e1fe3e4aSElliott Hughes            writer.DontShare = True
1034*e1fe3e4aSElliott Hughes
1035*e1fe3e4aSElliott Hughes        if hasattr(self.__class__, "LookupType"):
1036*e1fe3e4aSElliott Hughes            writer["LookupType"].setValue(self.__class__.LookupType)
1037*e1fe3e4aSElliott Hughes
1038*e1fe3e4aSElliott Hughes        self.writeFormat(writer)
1039*e1fe3e4aSElliott Hughes        for conv in self.getConverters():
1040*e1fe3e4aSElliott Hughes            value = table.get(
1041*e1fe3e4aSElliott Hughes                conv.name
1042*e1fe3e4aSElliott Hughes            )  # TODO Handle defaults instead of defaulting to None!
1043*e1fe3e4aSElliott Hughes            if conv.repeat:
1044*e1fe3e4aSElliott Hughes                if value is None:
1045*e1fe3e4aSElliott Hughes                    value = []
1046*e1fe3e4aSElliott Hughes                countValue = len(value) - conv.aux
1047*e1fe3e4aSElliott Hughes                if isinstance(conv.repeat, int):
1048*e1fe3e4aSElliott Hughes                    assert len(value) == conv.repeat, "expected %d values, got %d" % (
1049*e1fe3e4aSElliott Hughes                        conv.repeat,
1050*e1fe3e4aSElliott Hughes                        len(value),
1051*e1fe3e4aSElliott Hughes                    )
1052*e1fe3e4aSElliott Hughes                elif conv.repeat in table:
1053*e1fe3e4aSElliott Hughes                    CountReference(table, conv.repeat, value=countValue)
1054*e1fe3e4aSElliott Hughes                else:
1055*e1fe3e4aSElliott Hughes                    # conv.repeat is a propagated count
1056*e1fe3e4aSElliott Hughes                    writer[conv.repeat].setValue(countValue)
1057*e1fe3e4aSElliott Hughes                try:
1058*e1fe3e4aSElliott Hughes                    conv.writeArray(writer, font, table, value)
1059*e1fe3e4aSElliott Hughes                except Exception as e:
1060*e1fe3e4aSElliott Hughes                    e.args = e.args + (conv.name + "[]",)
1061*e1fe3e4aSElliott Hughes                    raise
1062*e1fe3e4aSElliott Hughes            elif conv.isCount:
1063*e1fe3e4aSElliott Hughes                # Special-case Count values.
1064*e1fe3e4aSElliott Hughes                # Assumption: a Count field will *always* precede
1065*e1fe3e4aSElliott Hughes                # the actual array(s).
1066*e1fe3e4aSElliott Hughes                # We need a default value, as it may be set later by a nested
1067*e1fe3e4aSElliott Hughes                # table. We will later store it here.
1068*e1fe3e4aSElliott Hughes                # We add a reference: by the time the data is assembled
1069*e1fe3e4aSElliott Hughes                # the Count value will be filled in.
1070*e1fe3e4aSElliott Hughes                # We ignore the current count value since it will be recomputed,
1071*e1fe3e4aSElliott Hughes                # unless it's a CountReference that was already initialized in a custom preWrite.
1072*e1fe3e4aSElliott Hughes                if isinstance(value, CountReference):
1073*e1fe3e4aSElliott Hughes                    ref = value
1074*e1fe3e4aSElliott Hughes                    ref.size = conv.staticSize
1075*e1fe3e4aSElliott Hughes                    writer.writeData(ref)
1076*e1fe3e4aSElliott Hughes                    table[conv.name] = ref.getValue()
1077*e1fe3e4aSElliott Hughes                else:
1078*e1fe3e4aSElliott Hughes                    ref = writer.writeCountReference(table, conv.name, conv.staticSize)
1079*e1fe3e4aSElliott Hughes                    table[conv.name] = None
1080*e1fe3e4aSElliott Hughes                if conv.isPropagated:
1081*e1fe3e4aSElliott Hughes                    writer[conv.name] = ref
1082*e1fe3e4aSElliott Hughes            elif conv.isLookupType:
1083*e1fe3e4aSElliott Hughes                # We make sure that subtables have the same lookup type,
1084*e1fe3e4aSElliott Hughes                # and that the type is the same as the one set on the
1085*e1fe3e4aSElliott Hughes                # Lookup object, if any is set.
1086*e1fe3e4aSElliott Hughes                if conv.name not in table:
1087*e1fe3e4aSElliott Hughes                    table[conv.name] = None
1088*e1fe3e4aSElliott Hughes                ref = writer.writeCountReference(
1089*e1fe3e4aSElliott Hughes                    table, conv.name, conv.staticSize, table[conv.name]
1090*e1fe3e4aSElliott Hughes                )
1091*e1fe3e4aSElliott Hughes                writer["LookupType"] = ref
1092*e1fe3e4aSElliott Hughes            else:
1093*e1fe3e4aSElliott Hughes                if conv.aux and not eval(conv.aux, None, table):
1094*e1fe3e4aSElliott Hughes                    continue
1095*e1fe3e4aSElliott Hughes                try:
1096*e1fe3e4aSElliott Hughes                    conv.write(writer, font, table, value)
1097*e1fe3e4aSElliott Hughes                except Exception as e:
1098*e1fe3e4aSElliott Hughes                    name = value.__class__.__name__ if value is not None else conv.name
1099*e1fe3e4aSElliott Hughes                    e.args = e.args + (name,)
1100*e1fe3e4aSElliott Hughes                    raise
1101*e1fe3e4aSElliott Hughes                if conv.isPropagated:
1102*e1fe3e4aSElliott Hughes                    writer[conv.name] = value
1103*e1fe3e4aSElliott Hughes
1104*e1fe3e4aSElliott Hughes        if deleteFormat:
1105*e1fe3e4aSElliott Hughes            del self.Format
1106*e1fe3e4aSElliott Hughes
1107*e1fe3e4aSElliott Hughes    def readFormat(self, reader):
1108*e1fe3e4aSElliott Hughes        pass
1109*e1fe3e4aSElliott Hughes
1110*e1fe3e4aSElliott Hughes    def writeFormat(self, writer):
1111*e1fe3e4aSElliott Hughes        pass
1112*e1fe3e4aSElliott Hughes
1113*e1fe3e4aSElliott Hughes    def toXML(self, xmlWriter, font, attrs=None, name=None):
1114*e1fe3e4aSElliott Hughes        tableName = name if name else self.__class__.__name__
1115*e1fe3e4aSElliott Hughes        if attrs is None:
1116*e1fe3e4aSElliott Hughes            attrs = []
1117*e1fe3e4aSElliott Hughes        if hasattr(self, "Format"):
1118*e1fe3e4aSElliott Hughes            attrs = attrs + [("Format", self.Format)]
1119*e1fe3e4aSElliott Hughes        xmlWriter.begintag(tableName, attrs)
1120*e1fe3e4aSElliott Hughes        xmlWriter.newline()
1121*e1fe3e4aSElliott Hughes        self.toXML2(xmlWriter, font)
1122*e1fe3e4aSElliott Hughes        xmlWriter.endtag(tableName)
1123*e1fe3e4aSElliott Hughes        xmlWriter.newline()
1124*e1fe3e4aSElliott Hughes
1125*e1fe3e4aSElliott Hughes    def toXML2(self, xmlWriter, font):
1126*e1fe3e4aSElliott Hughes        # Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB).
1127*e1fe3e4aSElliott Hughes        # This is because in TTX our parent writes our main tag, and in otBase.py we
1128*e1fe3e4aSElliott Hughes        # do it ourselves. I think I'm getting schizophrenic...
1129*e1fe3e4aSElliott Hughes        for conv in self.getConverters():
1130*e1fe3e4aSElliott Hughes            if conv.repeat:
1131*e1fe3e4aSElliott Hughes                value = getattr(self, conv.name, [])
1132*e1fe3e4aSElliott Hughes                for i in range(len(value)):
1133*e1fe3e4aSElliott Hughes                    item = value[i]
1134*e1fe3e4aSElliott Hughes                    conv.xmlWrite(xmlWriter, font, item, conv.name, [("index", i)])
1135*e1fe3e4aSElliott Hughes            else:
1136*e1fe3e4aSElliott Hughes                if conv.aux and not eval(conv.aux, None, vars(self)):
1137*e1fe3e4aSElliott Hughes                    continue
1138*e1fe3e4aSElliott Hughes                value = getattr(
1139*e1fe3e4aSElliott Hughes                    self, conv.name, None
1140*e1fe3e4aSElliott Hughes                )  # TODO Handle defaults instead of defaulting to None!
1141*e1fe3e4aSElliott Hughes                conv.xmlWrite(xmlWriter, font, value, conv.name, [])
1142*e1fe3e4aSElliott Hughes
1143*e1fe3e4aSElliott Hughes    def fromXML(self, name, attrs, content, font):
1144*e1fe3e4aSElliott Hughes        try:
1145*e1fe3e4aSElliott Hughes            conv = self.getConverterByName(name)
1146*e1fe3e4aSElliott Hughes        except KeyError:
1147*e1fe3e4aSElliott Hughes            raise  # XXX on KeyError, raise nice error
1148*e1fe3e4aSElliott Hughes        value = conv.xmlRead(attrs, content, font)
1149*e1fe3e4aSElliott Hughes        if conv.repeat:
1150*e1fe3e4aSElliott Hughes            seq = getattr(self, conv.name, None)
1151*e1fe3e4aSElliott Hughes            if seq is None:
1152*e1fe3e4aSElliott Hughes                seq = []
1153*e1fe3e4aSElliott Hughes                setattr(self, conv.name, seq)
1154*e1fe3e4aSElliott Hughes            seq.append(value)
1155*e1fe3e4aSElliott Hughes        else:
1156*e1fe3e4aSElliott Hughes            setattr(self, conv.name, value)
1157*e1fe3e4aSElliott Hughes
1158*e1fe3e4aSElliott Hughes    def __ne__(self, other):
1159*e1fe3e4aSElliott Hughes        result = self.__eq__(other)
1160*e1fe3e4aSElliott Hughes        return result if result is NotImplemented else not result
1161*e1fe3e4aSElliott Hughes
1162*e1fe3e4aSElliott Hughes    def __eq__(self, other):
1163*e1fe3e4aSElliott Hughes        if type(self) != type(other):
1164*e1fe3e4aSElliott Hughes            return NotImplemented
1165*e1fe3e4aSElliott Hughes
1166*e1fe3e4aSElliott Hughes        self.ensureDecompiled()
1167*e1fe3e4aSElliott Hughes        other.ensureDecompiled()
1168*e1fe3e4aSElliott Hughes
1169*e1fe3e4aSElliott Hughes        return self.__dict__ == other.__dict__
1170*e1fe3e4aSElliott Hughes
1171*e1fe3e4aSElliott Hughes    class SubTableEntry(NamedTuple):
1172*e1fe3e4aSElliott Hughes        """See BaseTable.iterSubTables()"""
1173*e1fe3e4aSElliott Hughes
1174*e1fe3e4aSElliott Hughes        name: str
1175*e1fe3e4aSElliott Hughes        value: "BaseTable"
1176*e1fe3e4aSElliott Hughes        index: Optional[int] = None  # index into given array, None for single values
1177*e1fe3e4aSElliott Hughes
1178*e1fe3e4aSElliott Hughes    def iterSubTables(self) -> Iterator[SubTableEntry]:
1179*e1fe3e4aSElliott Hughes        """Yield (name, value, index) namedtuples for all subtables of current table.
1180*e1fe3e4aSElliott Hughes
1181*e1fe3e4aSElliott Hughes        A sub-table is an instance of BaseTable (or subclass thereof) that is a child
1182*e1fe3e4aSElliott Hughes        of self, the current parent table.
1183*e1fe3e4aSElliott Hughes        The tuples also contain the attribute name (str) of the of parent table to get
1184*e1fe3e4aSElliott Hughes        a subtable, and optionally, for lists of subtables (i.e. attributes associated
1185*e1fe3e4aSElliott Hughes        with a converter that has a 'repeat'), an index into the list containing the
1186*e1fe3e4aSElliott Hughes        given subtable value.
1187*e1fe3e4aSElliott Hughes        This method can be useful to traverse trees of otTables.
1188*e1fe3e4aSElliott Hughes        """
1189*e1fe3e4aSElliott Hughes        for conv in self.getConverters():
1190*e1fe3e4aSElliott Hughes            name = conv.name
1191*e1fe3e4aSElliott Hughes            value = getattr(self, name, None)
1192*e1fe3e4aSElliott Hughes            if value is None:
1193*e1fe3e4aSElliott Hughes                continue
1194*e1fe3e4aSElliott Hughes            if isinstance(value, BaseTable):
1195*e1fe3e4aSElliott Hughes                yield self.SubTableEntry(name, value)
1196*e1fe3e4aSElliott Hughes            elif isinstance(value, list):
1197*e1fe3e4aSElliott Hughes                yield from (
1198*e1fe3e4aSElliott Hughes                    self.SubTableEntry(name, v, index=i)
1199*e1fe3e4aSElliott Hughes                    for i, v in enumerate(value)
1200*e1fe3e4aSElliott Hughes                    if isinstance(v, BaseTable)
1201*e1fe3e4aSElliott Hughes                )
1202*e1fe3e4aSElliott Hughes
1203*e1fe3e4aSElliott Hughes    # instance (not @class)method for consistency with FormatSwitchingBaseTable
1204*e1fe3e4aSElliott Hughes    def getVariableAttrs(self):
1205*e1fe3e4aSElliott Hughes        return getVariableAttrs(self.__class__)
1206*e1fe3e4aSElliott Hughes
1207*e1fe3e4aSElliott Hughes
1208*e1fe3e4aSElliott Hughesclass FormatSwitchingBaseTable(BaseTable):
1209*e1fe3e4aSElliott Hughes    """Minor specialization of BaseTable, for tables that have multiple
1210*e1fe3e4aSElliott Hughes    formats, eg. CoverageFormat1 vs. CoverageFormat2."""
1211*e1fe3e4aSElliott Hughes
1212*e1fe3e4aSElliott Hughes    @classmethod
1213*e1fe3e4aSElliott Hughes    def getRecordSize(cls, reader):
1214*e1fe3e4aSElliott Hughes        return NotImplemented
1215*e1fe3e4aSElliott Hughes
1216*e1fe3e4aSElliott Hughes    def getConverters(self):
1217*e1fe3e4aSElliott Hughes        try:
1218*e1fe3e4aSElliott Hughes            fmt = self.Format
1219*e1fe3e4aSElliott Hughes        except AttributeError:
1220*e1fe3e4aSElliott Hughes            # some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format'
1221*e1fe3e4aSElliott Hughes            # attribute after fully decompiled, only gain one in preWrite before being
1222*e1fe3e4aSElliott Hughes            # recompiled. In the decompiled state, these hand-coded classes defined in
1223*e1fe3e4aSElliott Hughes            # otTables.py lose their format-specific nature and gain more high-level
1224*e1fe3e4aSElliott Hughes            # attributes that are not tied to converters.
1225*e1fe3e4aSElliott Hughes            return []
1226*e1fe3e4aSElliott Hughes        return self.converters.get(self.Format, [])
1227*e1fe3e4aSElliott Hughes
1228*e1fe3e4aSElliott Hughes    def getConverterByName(self, name):
1229*e1fe3e4aSElliott Hughes        return self.convertersByName[self.Format][name]
1230*e1fe3e4aSElliott Hughes
1231*e1fe3e4aSElliott Hughes    def readFormat(self, reader):
1232*e1fe3e4aSElliott Hughes        self.Format = reader.readUShort()
1233*e1fe3e4aSElliott Hughes
1234*e1fe3e4aSElliott Hughes    def writeFormat(self, writer):
1235*e1fe3e4aSElliott Hughes        writer.writeUShort(self.Format)
1236*e1fe3e4aSElliott Hughes
1237*e1fe3e4aSElliott Hughes    def toXML(self, xmlWriter, font, attrs=None, name=None):
1238*e1fe3e4aSElliott Hughes        BaseTable.toXML(self, xmlWriter, font, attrs, name)
1239*e1fe3e4aSElliott Hughes
1240*e1fe3e4aSElliott Hughes    def getVariableAttrs(self):
1241*e1fe3e4aSElliott Hughes        return getVariableAttrs(self.__class__, self.Format)
1242*e1fe3e4aSElliott Hughes
1243*e1fe3e4aSElliott Hughes
1244*e1fe3e4aSElliott Hughesclass UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable):
1245*e1fe3e4aSElliott Hughes    def readFormat(self, reader):
1246*e1fe3e4aSElliott Hughes        self.Format = reader.readUInt8()
1247*e1fe3e4aSElliott Hughes
1248*e1fe3e4aSElliott Hughes    def writeFormat(self, writer):
1249*e1fe3e4aSElliott Hughes        writer.writeUInt8(self.Format)
1250*e1fe3e4aSElliott Hughes
1251*e1fe3e4aSElliott Hughes
1252*e1fe3e4aSElliott HughesformatSwitchingBaseTables = {
1253*e1fe3e4aSElliott Hughes    "uint16": FormatSwitchingBaseTable,
1254*e1fe3e4aSElliott Hughes    "uint8": UInt8FormatSwitchingBaseTable,
1255*e1fe3e4aSElliott Hughes}
1256*e1fe3e4aSElliott Hughes
1257*e1fe3e4aSElliott Hughes
1258*e1fe3e4aSElliott Hughesdef getFormatSwitchingBaseTableClass(formatType):
1259*e1fe3e4aSElliott Hughes    try:
1260*e1fe3e4aSElliott Hughes        return formatSwitchingBaseTables[formatType]
1261*e1fe3e4aSElliott Hughes    except KeyError:
1262*e1fe3e4aSElliott Hughes        raise TypeError(f"Unsupported format type: {formatType!r}")
1263*e1fe3e4aSElliott Hughes
1264*e1fe3e4aSElliott Hughes
1265*e1fe3e4aSElliott Hughes# memoize since these are parsed from otData.py, thus stay constant
1266*e1fe3e4aSElliott Hughes@lru_cache()
1267*e1fe3e4aSElliott Hughesdef getVariableAttrs(cls: BaseTable, fmt: Optional[int] = None) -> Tuple[str]:
1268*e1fe3e4aSElliott Hughes    """Return sequence of variable table field names (can be empty).
1269*e1fe3e4aSElliott Hughes
1270*e1fe3e4aSElliott Hughes    Attributes are deemed "variable" when their otData.py's description contain
1271*e1fe3e4aSElliott Hughes    'VarIndexBase + {offset}', e.g. COLRv1 PaintVar* tables.
1272*e1fe3e4aSElliott Hughes    """
1273*e1fe3e4aSElliott Hughes    if not issubclass(cls, BaseTable):
1274*e1fe3e4aSElliott Hughes        raise TypeError(cls)
1275*e1fe3e4aSElliott Hughes    if issubclass(cls, FormatSwitchingBaseTable):
1276*e1fe3e4aSElliott Hughes        if fmt is None:
1277*e1fe3e4aSElliott Hughes            raise TypeError(f"'fmt' is required for format-switching {cls.__name__}")
1278*e1fe3e4aSElliott Hughes        converters = cls.convertersByName[fmt]
1279*e1fe3e4aSElliott Hughes    else:
1280*e1fe3e4aSElliott Hughes        converters = cls.convertersByName
1281*e1fe3e4aSElliott Hughes    # assume if no 'VarIndexBase' field is present, table has no variable fields
1282*e1fe3e4aSElliott Hughes    if "VarIndexBase" not in converters:
1283*e1fe3e4aSElliott Hughes        return ()
1284*e1fe3e4aSElliott Hughes    varAttrs = {}
1285*e1fe3e4aSElliott Hughes    for name, conv in converters.items():
1286*e1fe3e4aSElliott Hughes        offset = conv.getVarIndexOffset()
1287*e1fe3e4aSElliott Hughes        if offset is not None:
1288*e1fe3e4aSElliott Hughes            varAttrs[name] = offset
1289*e1fe3e4aSElliott Hughes    return tuple(sorted(varAttrs, key=varAttrs.__getitem__))
1290*e1fe3e4aSElliott Hughes
1291*e1fe3e4aSElliott Hughes
1292*e1fe3e4aSElliott Hughes#
1293*e1fe3e4aSElliott Hughes# Support for ValueRecords
1294*e1fe3e4aSElliott Hughes#
1295*e1fe3e4aSElliott Hughes# This data type is so different from all other OpenType data types that
1296*e1fe3e4aSElliott Hughes# it requires quite a bit of code for itself. It even has special support
1297*e1fe3e4aSElliott Hughes# in OTTableReader and OTTableWriter...
1298*e1fe3e4aSElliott Hughes#
1299*e1fe3e4aSElliott Hughes
1300*e1fe3e4aSElliott HughesvalueRecordFormat = [
1301*e1fe3e4aSElliott Hughes    # 	Mask	 Name		isDevice signed
1302*e1fe3e4aSElliott Hughes    (0x0001, "XPlacement", 0, 1),
1303*e1fe3e4aSElliott Hughes    (0x0002, "YPlacement", 0, 1),
1304*e1fe3e4aSElliott Hughes    (0x0004, "XAdvance", 0, 1),
1305*e1fe3e4aSElliott Hughes    (0x0008, "YAdvance", 0, 1),
1306*e1fe3e4aSElliott Hughes    (0x0010, "XPlaDevice", 1, 0),
1307*e1fe3e4aSElliott Hughes    (0x0020, "YPlaDevice", 1, 0),
1308*e1fe3e4aSElliott Hughes    (0x0040, "XAdvDevice", 1, 0),
1309*e1fe3e4aSElliott Hughes    (0x0080, "YAdvDevice", 1, 0),
1310*e1fe3e4aSElliott Hughes    # 	reserved:
1311*e1fe3e4aSElliott Hughes    (0x0100, "Reserved1", 0, 0),
1312*e1fe3e4aSElliott Hughes    (0x0200, "Reserved2", 0, 0),
1313*e1fe3e4aSElliott Hughes    (0x0400, "Reserved3", 0, 0),
1314*e1fe3e4aSElliott Hughes    (0x0800, "Reserved4", 0, 0),
1315*e1fe3e4aSElliott Hughes    (0x1000, "Reserved5", 0, 0),
1316*e1fe3e4aSElliott Hughes    (0x2000, "Reserved6", 0, 0),
1317*e1fe3e4aSElliott Hughes    (0x4000, "Reserved7", 0, 0),
1318*e1fe3e4aSElliott Hughes    (0x8000, "Reserved8", 0, 0),
1319*e1fe3e4aSElliott Hughes]
1320*e1fe3e4aSElliott Hughes
1321*e1fe3e4aSElliott Hughes
1322*e1fe3e4aSElliott Hughesdef _buildDict():
1323*e1fe3e4aSElliott Hughes    d = {}
1324*e1fe3e4aSElliott Hughes    for mask, name, isDevice, signed in valueRecordFormat:
1325*e1fe3e4aSElliott Hughes        d[name] = mask, isDevice, signed
1326*e1fe3e4aSElliott Hughes    return d
1327*e1fe3e4aSElliott Hughes
1328*e1fe3e4aSElliott Hughes
1329*e1fe3e4aSElliott HughesvalueRecordFormatDict = _buildDict()
1330*e1fe3e4aSElliott Hughes
1331*e1fe3e4aSElliott Hughes
1332*e1fe3e4aSElliott Hughesclass ValueRecordFactory(object):
1333*e1fe3e4aSElliott Hughes    """Given a format code, this object convert ValueRecords."""
1334*e1fe3e4aSElliott Hughes
1335*e1fe3e4aSElliott Hughes    def __init__(self, valueFormat):
1336*e1fe3e4aSElliott Hughes        format = []
1337*e1fe3e4aSElliott Hughes        for mask, name, isDevice, signed in valueRecordFormat:
1338*e1fe3e4aSElliott Hughes            if valueFormat & mask:
1339*e1fe3e4aSElliott Hughes                format.append((name, isDevice, signed))
1340*e1fe3e4aSElliott Hughes        self.format = format
1341*e1fe3e4aSElliott Hughes
1342*e1fe3e4aSElliott Hughes    def __len__(self):
1343*e1fe3e4aSElliott Hughes        return len(self.format)
1344*e1fe3e4aSElliott Hughes
1345*e1fe3e4aSElliott Hughes    def readValueRecord(self, reader, font):
1346*e1fe3e4aSElliott Hughes        format = self.format
1347*e1fe3e4aSElliott Hughes        if not format:
1348*e1fe3e4aSElliott Hughes            return None
1349*e1fe3e4aSElliott Hughes        valueRecord = ValueRecord()
1350*e1fe3e4aSElliott Hughes        for name, isDevice, signed in format:
1351*e1fe3e4aSElliott Hughes            if signed:
1352*e1fe3e4aSElliott Hughes                value = reader.readShort()
1353*e1fe3e4aSElliott Hughes            else:
1354*e1fe3e4aSElliott Hughes                value = reader.readUShort()
1355*e1fe3e4aSElliott Hughes            if isDevice:
1356*e1fe3e4aSElliott Hughes                if value:
1357*e1fe3e4aSElliott Hughes                    from . import otTables
1358*e1fe3e4aSElliott Hughes
1359*e1fe3e4aSElliott Hughes                    subReader = reader.getSubReader(value)
1360*e1fe3e4aSElliott Hughes                    value = getattr(otTables, name)()
1361*e1fe3e4aSElliott Hughes                    value.decompile(subReader, font)
1362*e1fe3e4aSElliott Hughes                else:
1363*e1fe3e4aSElliott Hughes                    value = None
1364*e1fe3e4aSElliott Hughes            setattr(valueRecord, name, value)
1365*e1fe3e4aSElliott Hughes        return valueRecord
1366*e1fe3e4aSElliott Hughes
1367*e1fe3e4aSElliott Hughes    def writeValueRecord(self, writer, font, valueRecord):
1368*e1fe3e4aSElliott Hughes        for name, isDevice, signed in self.format:
1369*e1fe3e4aSElliott Hughes            value = getattr(valueRecord, name, 0)
1370*e1fe3e4aSElliott Hughes            if isDevice:
1371*e1fe3e4aSElliott Hughes                if value:
1372*e1fe3e4aSElliott Hughes                    subWriter = writer.getSubWriter()
1373*e1fe3e4aSElliott Hughes                    writer.writeSubTable(subWriter, offsetSize=2)
1374*e1fe3e4aSElliott Hughes                    value.compile(subWriter, font)
1375*e1fe3e4aSElliott Hughes                else:
1376*e1fe3e4aSElliott Hughes                    writer.writeUShort(0)
1377*e1fe3e4aSElliott Hughes            elif signed:
1378*e1fe3e4aSElliott Hughes                writer.writeShort(value)
1379*e1fe3e4aSElliott Hughes            else:
1380*e1fe3e4aSElliott Hughes                writer.writeUShort(value)
1381*e1fe3e4aSElliott Hughes
1382*e1fe3e4aSElliott Hughes
1383*e1fe3e4aSElliott Hughesclass ValueRecord(object):
1384*e1fe3e4aSElliott Hughes    # see ValueRecordFactory
1385*e1fe3e4aSElliott Hughes
1386*e1fe3e4aSElliott Hughes    def __init__(self, valueFormat=None, src=None):
1387*e1fe3e4aSElliott Hughes        if valueFormat is not None:
1388*e1fe3e4aSElliott Hughes            for mask, name, isDevice, signed in valueRecordFormat:
1389*e1fe3e4aSElliott Hughes                if valueFormat & mask:
1390*e1fe3e4aSElliott Hughes                    setattr(self, name, None if isDevice else 0)
1391*e1fe3e4aSElliott Hughes            if src is not None:
1392*e1fe3e4aSElliott Hughes                for key, val in src.__dict__.items():
1393*e1fe3e4aSElliott Hughes                    if not hasattr(self, key):
1394*e1fe3e4aSElliott Hughes                        continue
1395*e1fe3e4aSElliott Hughes                    setattr(self, key, val)
1396*e1fe3e4aSElliott Hughes        elif src is not None:
1397*e1fe3e4aSElliott Hughes            self.__dict__ = src.__dict__.copy()
1398*e1fe3e4aSElliott Hughes
1399*e1fe3e4aSElliott Hughes    def getFormat(self):
1400*e1fe3e4aSElliott Hughes        format = 0
1401*e1fe3e4aSElliott Hughes        for name in self.__dict__.keys():
1402*e1fe3e4aSElliott Hughes            format = format | valueRecordFormatDict[name][0]
1403*e1fe3e4aSElliott Hughes        return format
1404*e1fe3e4aSElliott Hughes
1405*e1fe3e4aSElliott Hughes    def getEffectiveFormat(self):
1406*e1fe3e4aSElliott Hughes        format = 0
1407*e1fe3e4aSElliott Hughes        for name, value in self.__dict__.items():
1408*e1fe3e4aSElliott Hughes            if value:
1409*e1fe3e4aSElliott Hughes                format = format | valueRecordFormatDict[name][0]
1410*e1fe3e4aSElliott Hughes        return format
1411*e1fe3e4aSElliott Hughes
1412*e1fe3e4aSElliott Hughes    def toXML(self, xmlWriter, font, valueName, attrs=None):
1413*e1fe3e4aSElliott Hughes        if attrs is None:
1414*e1fe3e4aSElliott Hughes            simpleItems = []
1415*e1fe3e4aSElliott Hughes        else:
1416*e1fe3e4aSElliott Hughes            simpleItems = list(attrs)
1417*e1fe3e4aSElliott Hughes        for mask, name, isDevice, format in valueRecordFormat[:4]:  # "simple" values
1418*e1fe3e4aSElliott Hughes            if hasattr(self, name):
1419*e1fe3e4aSElliott Hughes                simpleItems.append((name, getattr(self, name)))
1420*e1fe3e4aSElliott Hughes        deviceItems = []
1421*e1fe3e4aSElliott Hughes        for mask, name, isDevice, format in valueRecordFormat[4:8]:  # device records
1422*e1fe3e4aSElliott Hughes            if hasattr(self, name):
1423*e1fe3e4aSElliott Hughes                device = getattr(self, name)
1424*e1fe3e4aSElliott Hughes                if device is not None:
1425*e1fe3e4aSElliott Hughes                    deviceItems.append((name, device))
1426*e1fe3e4aSElliott Hughes        if deviceItems:
1427*e1fe3e4aSElliott Hughes            xmlWriter.begintag(valueName, simpleItems)
1428*e1fe3e4aSElliott Hughes            xmlWriter.newline()
1429*e1fe3e4aSElliott Hughes            for name, deviceRecord in deviceItems:
1430*e1fe3e4aSElliott Hughes                if deviceRecord is not None:
1431*e1fe3e4aSElliott Hughes                    deviceRecord.toXML(xmlWriter, font, name=name)
1432*e1fe3e4aSElliott Hughes            xmlWriter.endtag(valueName)
1433*e1fe3e4aSElliott Hughes            xmlWriter.newline()
1434*e1fe3e4aSElliott Hughes        else:
1435*e1fe3e4aSElliott Hughes            xmlWriter.simpletag(valueName, simpleItems)
1436*e1fe3e4aSElliott Hughes            xmlWriter.newline()
1437*e1fe3e4aSElliott Hughes
1438*e1fe3e4aSElliott Hughes    def fromXML(self, name, attrs, content, font):
1439*e1fe3e4aSElliott Hughes        from . import otTables
1440*e1fe3e4aSElliott Hughes
1441*e1fe3e4aSElliott Hughes        for k, v in attrs.items():
1442*e1fe3e4aSElliott Hughes            setattr(self, k, int(v))
1443*e1fe3e4aSElliott Hughes        for element in content:
1444*e1fe3e4aSElliott Hughes            if not isinstance(element, tuple):
1445*e1fe3e4aSElliott Hughes                continue
1446*e1fe3e4aSElliott Hughes            name, attrs, content = element
1447*e1fe3e4aSElliott Hughes            value = getattr(otTables, name)()
1448*e1fe3e4aSElliott Hughes            for elem2 in content:
1449*e1fe3e4aSElliott Hughes                if not isinstance(elem2, tuple):
1450*e1fe3e4aSElliott Hughes                    continue
1451*e1fe3e4aSElliott Hughes                name2, attrs2, content2 = elem2
1452*e1fe3e4aSElliott Hughes                value.fromXML(name2, attrs2, content2, font)
1453*e1fe3e4aSElliott Hughes            setattr(self, name, value)
1454*e1fe3e4aSElliott Hughes
1455*e1fe3e4aSElliott Hughes    def __ne__(self, other):
1456*e1fe3e4aSElliott Hughes        result = self.__eq__(other)
1457*e1fe3e4aSElliott Hughes        return result if result is NotImplemented else not result
1458*e1fe3e4aSElliott Hughes
1459*e1fe3e4aSElliott Hughes    def __eq__(self, other):
1460*e1fe3e4aSElliott Hughes        if type(self) != type(other):
1461*e1fe3e4aSElliott Hughes            return NotImplemented
1462*e1fe3e4aSElliott Hughes        return self.__dict__ == other.__dict__
1463