xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/woff2.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesfrom io import BytesIO
2*e1fe3e4aSElliott Hughesimport sys
3*e1fe3e4aSElliott Hughesimport array
4*e1fe3e4aSElliott Hughesimport struct
5*e1fe3e4aSElliott Hughesfrom collections import OrderedDict
6*e1fe3e4aSElliott Hughesfrom fontTools.misc import sstruct
7*e1fe3e4aSElliott Hughesfrom fontTools.misc.arrayTools import calcIntBounds
8*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad
9*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import (
10*e1fe3e4aSElliott Hughes    TTFont,
11*e1fe3e4aSElliott Hughes    TTLibError,
12*e1fe3e4aSElliott Hughes    getTableModule,
13*e1fe3e4aSElliott Hughes    getTableClass,
14*e1fe3e4aSElliott Hughes    getSearchRange,
15*e1fe3e4aSElliott Hughes)
16*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.sfnt import (
17*e1fe3e4aSElliott Hughes    SFNTReader,
18*e1fe3e4aSElliott Hughes    SFNTWriter,
19*e1fe3e4aSElliott Hughes    DirectoryEntry,
20*e1fe3e4aSElliott Hughes    WOFFFlavorData,
21*e1fe3e4aSElliott Hughes    sfntDirectoryFormat,
22*e1fe3e4aSElliott Hughes    sfntDirectorySize,
23*e1fe3e4aSElliott Hughes    SFNTDirectoryEntry,
24*e1fe3e4aSElliott Hughes    sfntDirectoryEntrySize,
25*e1fe3e4aSElliott Hughes    calcChecksum,
26*e1fe3e4aSElliott Hughes)
27*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import ttProgram, _g_l_y_f
28*e1fe3e4aSElliott Hughesimport logging
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott Hughes
31*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.ttLib.woff2")
32*e1fe3e4aSElliott Hughes
33*e1fe3e4aSElliott HugheshaveBrotli = False
34*e1fe3e4aSElliott Hughestry:
35*e1fe3e4aSElliott Hughes    try:
36*e1fe3e4aSElliott Hughes        import brotlicffi as brotli
37*e1fe3e4aSElliott Hughes    except ImportError:
38*e1fe3e4aSElliott Hughes        import brotli
39*e1fe3e4aSElliott Hughes    haveBrotli = True
40*e1fe3e4aSElliott Hughesexcept ImportError:
41*e1fe3e4aSElliott Hughes    pass
42*e1fe3e4aSElliott Hughes
43*e1fe3e4aSElliott Hughes
44*e1fe3e4aSElliott Hughesclass WOFF2Reader(SFNTReader):
45*e1fe3e4aSElliott Hughes    flavor = "woff2"
46*e1fe3e4aSElliott Hughes
47*e1fe3e4aSElliott Hughes    def __init__(self, file, checkChecksums=0, fontNumber=-1):
48*e1fe3e4aSElliott Hughes        if not haveBrotli:
49*e1fe3e4aSElliott Hughes            log.error(
50*e1fe3e4aSElliott Hughes                "The WOFF2 decoder requires the Brotli Python extension, available at: "
51*e1fe3e4aSElliott Hughes                "https://github.com/google/brotli"
52*e1fe3e4aSElliott Hughes            )
53*e1fe3e4aSElliott Hughes            raise ImportError("No module named brotli")
54*e1fe3e4aSElliott Hughes
55*e1fe3e4aSElliott Hughes        self.file = file
56*e1fe3e4aSElliott Hughes
57*e1fe3e4aSElliott Hughes        signature = Tag(self.file.read(4))
58*e1fe3e4aSElliott Hughes        if signature != b"wOF2":
59*e1fe3e4aSElliott Hughes            raise TTLibError("Not a WOFF2 font (bad signature)")
60*e1fe3e4aSElliott Hughes
61*e1fe3e4aSElliott Hughes        self.file.seek(0)
62*e1fe3e4aSElliott Hughes        self.DirectoryEntry = WOFF2DirectoryEntry
63*e1fe3e4aSElliott Hughes        data = self.file.read(woff2DirectorySize)
64*e1fe3e4aSElliott Hughes        if len(data) != woff2DirectorySize:
65*e1fe3e4aSElliott Hughes            raise TTLibError("Not a WOFF2 font (not enough data)")
66*e1fe3e4aSElliott Hughes        sstruct.unpack(woff2DirectoryFormat, data, self)
67*e1fe3e4aSElliott Hughes
68*e1fe3e4aSElliott Hughes        self.tables = OrderedDict()
69*e1fe3e4aSElliott Hughes        offset = 0
70*e1fe3e4aSElliott Hughes        for i in range(self.numTables):
71*e1fe3e4aSElliott Hughes            entry = self.DirectoryEntry()
72*e1fe3e4aSElliott Hughes            entry.fromFile(self.file)
73*e1fe3e4aSElliott Hughes            tag = Tag(entry.tag)
74*e1fe3e4aSElliott Hughes            self.tables[tag] = entry
75*e1fe3e4aSElliott Hughes            entry.offset = offset
76*e1fe3e4aSElliott Hughes            offset += entry.length
77*e1fe3e4aSElliott Hughes
78*e1fe3e4aSElliott Hughes        totalUncompressedSize = offset
79*e1fe3e4aSElliott Hughes        compressedData = self.file.read(self.totalCompressedSize)
80*e1fe3e4aSElliott Hughes        decompressedData = brotli.decompress(compressedData)
81*e1fe3e4aSElliott Hughes        if len(decompressedData) != totalUncompressedSize:
82*e1fe3e4aSElliott Hughes            raise TTLibError(
83*e1fe3e4aSElliott Hughes                "unexpected size for decompressed font data: expected %d, found %d"
84*e1fe3e4aSElliott Hughes                % (totalUncompressedSize, len(decompressedData))
85*e1fe3e4aSElliott Hughes            )
86*e1fe3e4aSElliott Hughes        self.transformBuffer = BytesIO(decompressedData)
87*e1fe3e4aSElliott Hughes
88*e1fe3e4aSElliott Hughes        self.file.seek(0, 2)
89*e1fe3e4aSElliott Hughes        if self.length != self.file.tell():
90*e1fe3e4aSElliott Hughes            raise TTLibError("reported 'length' doesn't match the actual file size")
91*e1fe3e4aSElliott Hughes
92*e1fe3e4aSElliott Hughes        self.flavorData = WOFF2FlavorData(self)
93*e1fe3e4aSElliott Hughes
94*e1fe3e4aSElliott Hughes        # make empty TTFont to store data while reconstructing tables
95*e1fe3e4aSElliott Hughes        self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
96*e1fe3e4aSElliott Hughes
97*e1fe3e4aSElliott Hughes    def __getitem__(self, tag):
98*e1fe3e4aSElliott Hughes        """Fetch the raw table data. Reconstruct transformed tables."""
99*e1fe3e4aSElliott Hughes        entry = self.tables[Tag(tag)]
100*e1fe3e4aSElliott Hughes        if not hasattr(entry, "data"):
101*e1fe3e4aSElliott Hughes            if entry.transformed:
102*e1fe3e4aSElliott Hughes                entry.data = self.reconstructTable(tag)
103*e1fe3e4aSElliott Hughes            else:
104*e1fe3e4aSElliott Hughes                entry.data = entry.loadData(self.transformBuffer)
105*e1fe3e4aSElliott Hughes        return entry.data
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughes    def reconstructTable(self, tag):
108*e1fe3e4aSElliott Hughes        """Reconstruct table named 'tag' from transformed data."""
109*e1fe3e4aSElliott Hughes        entry = self.tables[Tag(tag)]
110*e1fe3e4aSElliott Hughes        rawData = entry.loadData(self.transformBuffer)
111*e1fe3e4aSElliott Hughes        if tag == "glyf":
112*e1fe3e4aSElliott Hughes            # no need to pad glyph data when reconstructing
113*e1fe3e4aSElliott Hughes            padding = self.padding if hasattr(self, "padding") else None
114*e1fe3e4aSElliott Hughes            data = self._reconstructGlyf(rawData, padding)
115*e1fe3e4aSElliott Hughes        elif tag == "loca":
116*e1fe3e4aSElliott Hughes            data = self._reconstructLoca()
117*e1fe3e4aSElliott Hughes        elif tag == "hmtx":
118*e1fe3e4aSElliott Hughes            data = self._reconstructHmtx(rawData)
119*e1fe3e4aSElliott Hughes        else:
120*e1fe3e4aSElliott Hughes            raise TTLibError("transform for table '%s' is unknown" % tag)
121*e1fe3e4aSElliott Hughes        return data
122*e1fe3e4aSElliott Hughes
123*e1fe3e4aSElliott Hughes    def _reconstructGlyf(self, data, padding=None):
124*e1fe3e4aSElliott Hughes        """Return recostructed glyf table data, and set the corresponding loca's
125*e1fe3e4aSElliott Hughes        locations. Optionally pad glyph offsets to the specified number of bytes.
126*e1fe3e4aSElliott Hughes        """
127*e1fe3e4aSElliott Hughes        self.ttFont["loca"] = WOFF2LocaTable()
128*e1fe3e4aSElliott Hughes        glyfTable = self.ttFont["glyf"] = WOFF2GlyfTable()
129*e1fe3e4aSElliott Hughes        glyfTable.reconstruct(data, self.ttFont)
130*e1fe3e4aSElliott Hughes        if padding:
131*e1fe3e4aSElliott Hughes            glyfTable.padding = padding
132*e1fe3e4aSElliott Hughes        data = glyfTable.compile(self.ttFont)
133*e1fe3e4aSElliott Hughes        return data
134*e1fe3e4aSElliott Hughes
135*e1fe3e4aSElliott Hughes    def _reconstructLoca(self):
136*e1fe3e4aSElliott Hughes        """Return reconstructed loca table data."""
137*e1fe3e4aSElliott Hughes        if "loca" not in self.ttFont:
138*e1fe3e4aSElliott Hughes            # make sure glyf is reconstructed first
139*e1fe3e4aSElliott Hughes            self.tables["glyf"].data = self.reconstructTable("glyf")
140*e1fe3e4aSElliott Hughes        locaTable = self.ttFont["loca"]
141*e1fe3e4aSElliott Hughes        data = locaTable.compile(self.ttFont)
142*e1fe3e4aSElliott Hughes        if len(data) != self.tables["loca"].origLength:
143*e1fe3e4aSElliott Hughes            raise TTLibError(
144*e1fe3e4aSElliott Hughes                "reconstructed 'loca' table doesn't match original size: "
145*e1fe3e4aSElliott Hughes                "expected %d, found %d" % (self.tables["loca"].origLength, len(data))
146*e1fe3e4aSElliott Hughes            )
147*e1fe3e4aSElliott Hughes        return data
148*e1fe3e4aSElliott Hughes
149*e1fe3e4aSElliott Hughes    def _reconstructHmtx(self, data):
150*e1fe3e4aSElliott Hughes        """Return reconstructed hmtx table data."""
151*e1fe3e4aSElliott Hughes        # Before reconstructing 'hmtx' table we need to parse other tables:
152*e1fe3e4aSElliott Hughes        # 'glyf' is required for reconstructing the sidebearings from the glyphs'
153*e1fe3e4aSElliott Hughes        # bounding box; 'hhea' is needed for the numberOfHMetrics field.
154*e1fe3e4aSElliott Hughes        if "glyf" in self.flavorData.transformedTables:
155*e1fe3e4aSElliott Hughes            # transformed 'glyf' table is self-contained, thus 'loca' not needed
156*e1fe3e4aSElliott Hughes            tableDependencies = ("maxp", "hhea", "glyf")
157*e1fe3e4aSElliott Hughes        else:
158*e1fe3e4aSElliott Hughes            # decompiling untransformed 'glyf' requires 'loca', which requires 'head'
159*e1fe3e4aSElliott Hughes            tableDependencies = ("maxp", "head", "hhea", "loca", "glyf")
160*e1fe3e4aSElliott Hughes        for tag in tableDependencies:
161*e1fe3e4aSElliott Hughes            self._decompileTable(tag)
162*e1fe3e4aSElliott Hughes        hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable()
163*e1fe3e4aSElliott Hughes        hmtxTable.reconstruct(data, self.ttFont)
164*e1fe3e4aSElliott Hughes        data = hmtxTable.compile(self.ttFont)
165*e1fe3e4aSElliott Hughes        return data
166*e1fe3e4aSElliott Hughes
167*e1fe3e4aSElliott Hughes    def _decompileTable(self, tag):
168*e1fe3e4aSElliott Hughes        """Decompile table data and store it inside self.ttFont."""
169*e1fe3e4aSElliott Hughes        data = self[tag]
170*e1fe3e4aSElliott Hughes        if self.ttFont.isLoaded(tag):
171*e1fe3e4aSElliott Hughes            return self.ttFont[tag]
172*e1fe3e4aSElliott Hughes        tableClass = getTableClass(tag)
173*e1fe3e4aSElliott Hughes        table = tableClass(tag)
174*e1fe3e4aSElliott Hughes        self.ttFont.tables[tag] = table
175*e1fe3e4aSElliott Hughes        table.decompile(data, self.ttFont)
176*e1fe3e4aSElliott Hughes
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughesclass WOFF2Writer(SFNTWriter):
179*e1fe3e4aSElliott Hughes    flavor = "woff2"
180*e1fe3e4aSElliott Hughes
181*e1fe3e4aSElliott Hughes    def __init__(
182*e1fe3e4aSElliott Hughes        self,
183*e1fe3e4aSElliott Hughes        file,
184*e1fe3e4aSElliott Hughes        numTables,
185*e1fe3e4aSElliott Hughes        sfntVersion="\000\001\000\000",
186*e1fe3e4aSElliott Hughes        flavor=None,
187*e1fe3e4aSElliott Hughes        flavorData=None,
188*e1fe3e4aSElliott Hughes    ):
189*e1fe3e4aSElliott Hughes        if not haveBrotli:
190*e1fe3e4aSElliott Hughes            log.error(
191*e1fe3e4aSElliott Hughes                "The WOFF2 encoder requires the Brotli Python extension, available at: "
192*e1fe3e4aSElliott Hughes                "https://github.com/google/brotli"
193*e1fe3e4aSElliott Hughes            )
194*e1fe3e4aSElliott Hughes            raise ImportError("No module named brotli")
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes        self.file = file
197*e1fe3e4aSElliott Hughes        self.numTables = numTables
198*e1fe3e4aSElliott Hughes        self.sfntVersion = Tag(sfntVersion)
199*e1fe3e4aSElliott Hughes        self.flavorData = WOFF2FlavorData(data=flavorData)
200*e1fe3e4aSElliott Hughes
201*e1fe3e4aSElliott Hughes        self.directoryFormat = woff2DirectoryFormat
202*e1fe3e4aSElliott Hughes        self.directorySize = woff2DirectorySize
203*e1fe3e4aSElliott Hughes        self.DirectoryEntry = WOFF2DirectoryEntry
204*e1fe3e4aSElliott Hughes
205*e1fe3e4aSElliott Hughes        self.signature = Tag("wOF2")
206*e1fe3e4aSElliott Hughes
207*e1fe3e4aSElliott Hughes        self.nextTableOffset = 0
208*e1fe3e4aSElliott Hughes        self.transformBuffer = BytesIO()
209*e1fe3e4aSElliott Hughes
210*e1fe3e4aSElliott Hughes        self.tables = OrderedDict()
211*e1fe3e4aSElliott Hughes
212*e1fe3e4aSElliott Hughes        # make empty TTFont to store data while normalising and transforming tables
213*e1fe3e4aSElliott Hughes        self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False)
214*e1fe3e4aSElliott Hughes
215*e1fe3e4aSElliott Hughes    def __setitem__(self, tag, data):
216*e1fe3e4aSElliott Hughes        """Associate new entry named 'tag' with raw table data."""
217*e1fe3e4aSElliott Hughes        if tag in self.tables:
218*e1fe3e4aSElliott Hughes            raise TTLibError("cannot rewrite '%s' table" % tag)
219*e1fe3e4aSElliott Hughes        if tag == "DSIG":
220*e1fe3e4aSElliott Hughes            # always drop DSIG table, since the encoding process can invalidate it
221*e1fe3e4aSElliott Hughes            self.numTables -= 1
222*e1fe3e4aSElliott Hughes            return
223*e1fe3e4aSElliott Hughes
224*e1fe3e4aSElliott Hughes        entry = self.DirectoryEntry()
225*e1fe3e4aSElliott Hughes        entry.tag = Tag(tag)
226*e1fe3e4aSElliott Hughes        entry.flags = getKnownTagIndex(entry.tag)
227*e1fe3e4aSElliott Hughes        # WOFF2 table data are written to disk only on close(), after all tags
228*e1fe3e4aSElliott Hughes        # have been specified
229*e1fe3e4aSElliott Hughes        entry.data = data
230*e1fe3e4aSElliott Hughes
231*e1fe3e4aSElliott Hughes        self.tables[tag] = entry
232*e1fe3e4aSElliott Hughes
233*e1fe3e4aSElliott Hughes    def close(self):
234*e1fe3e4aSElliott Hughes        """All tags must have been specified. Now write the table data and directory."""
235*e1fe3e4aSElliott Hughes        if len(self.tables) != self.numTables:
236*e1fe3e4aSElliott Hughes            raise TTLibError(
237*e1fe3e4aSElliott Hughes                "wrong number of tables; expected %d, found %d"
238*e1fe3e4aSElliott Hughes                % (self.numTables, len(self.tables))
239*e1fe3e4aSElliott Hughes            )
240*e1fe3e4aSElliott Hughes
241*e1fe3e4aSElliott Hughes        if self.sfntVersion in ("\x00\x01\x00\x00", "true"):
242*e1fe3e4aSElliott Hughes            isTrueType = True
243*e1fe3e4aSElliott Hughes        elif self.sfntVersion == "OTTO":
244*e1fe3e4aSElliott Hughes            isTrueType = False
245*e1fe3e4aSElliott Hughes        else:
246*e1fe3e4aSElliott Hughes            raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
247*e1fe3e4aSElliott Hughes
248*e1fe3e4aSElliott Hughes        # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned.
249*e1fe3e4aSElliott Hughes        # However, the reference WOFF2 implementation still fails to reconstruct
250*e1fe3e4aSElliott Hughes        # 'unpadded' glyf tables, therefore we need to 'normalise' them.
251*e1fe3e4aSElliott Hughes        # See:
252*e1fe3e4aSElliott Hughes        # https://github.com/khaledhosny/ots/issues/60
253*e1fe3e4aSElliott Hughes        # https://github.com/google/woff2/issues/15
254*e1fe3e4aSElliott Hughes        if (
255*e1fe3e4aSElliott Hughes            isTrueType
256*e1fe3e4aSElliott Hughes            and "glyf" in self.flavorData.transformedTables
257*e1fe3e4aSElliott Hughes            and "glyf" in self.tables
258*e1fe3e4aSElliott Hughes        ):
259*e1fe3e4aSElliott Hughes            self._normaliseGlyfAndLoca(padding=4)
260*e1fe3e4aSElliott Hughes        self._setHeadTransformFlag()
261*e1fe3e4aSElliott Hughes
262*e1fe3e4aSElliott Hughes        # To pass the legacy OpenType Sanitiser currently included in browsers,
263*e1fe3e4aSElliott Hughes        # we must sort the table directory and data alphabetically by tag.
264*e1fe3e4aSElliott Hughes        # See:
265*e1fe3e4aSElliott Hughes        # https://github.com/google/woff2/pull/3
266*e1fe3e4aSElliott Hughes        # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html
267*e1fe3e4aSElliott Hughes        #
268*e1fe3e4aSElliott Hughes        # 2023: We rely on this in _transformTables where we expect that
269*e1fe3e4aSElliott Hughes        # "loca" comes after "glyf" table.
270*e1fe3e4aSElliott Hughes        self.tables = OrderedDict(sorted(self.tables.items()))
271*e1fe3e4aSElliott Hughes
272*e1fe3e4aSElliott Hughes        self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets()
273*e1fe3e4aSElliott Hughes
274*e1fe3e4aSElliott Hughes        fontData = self._transformTables()
275*e1fe3e4aSElliott Hughes        compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT)
276*e1fe3e4aSElliott Hughes
277*e1fe3e4aSElliott Hughes        self.totalCompressedSize = len(compressedFont)
278*e1fe3e4aSElliott Hughes        self.length = self._calcTotalSize()
279*e1fe3e4aSElliott Hughes        self.majorVersion, self.minorVersion = self._getVersion()
280*e1fe3e4aSElliott Hughes        self.reserved = 0
281*e1fe3e4aSElliott Hughes
282*e1fe3e4aSElliott Hughes        directory = self._packTableDirectory()
283*e1fe3e4aSElliott Hughes        self.file.seek(0)
284*e1fe3e4aSElliott Hughes        self.file.write(pad(directory + compressedFont, size=4))
285*e1fe3e4aSElliott Hughes        self._writeFlavorData()
286*e1fe3e4aSElliott Hughes
287*e1fe3e4aSElliott Hughes    def _normaliseGlyfAndLoca(self, padding=4):
288*e1fe3e4aSElliott Hughes        """Recompile glyf and loca tables, aligning glyph offsets to multiples of
289*e1fe3e4aSElliott Hughes        'padding' size. Update the head table's 'indexToLocFormat' accordingly while
290*e1fe3e4aSElliott Hughes        compiling loca.
291*e1fe3e4aSElliott Hughes        """
292*e1fe3e4aSElliott Hughes        if self.sfntVersion == "OTTO":
293*e1fe3e4aSElliott Hughes            return
294*e1fe3e4aSElliott Hughes
295*e1fe3e4aSElliott Hughes        for tag in ("maxp", "head", "loca", "glyf", "fvar"):
296*e1fe3e4aSElliott Hughes            if tag in self.tables:
297*e1fe3e4aSElliott Hughes                self._decompileTable(tag)
298*e1fe3e4aSElliott Hughes        self.ttFont["glyf"].padding = padding
299*e1fe3e4aSElliott Hughes        for tag in ("glyf", "loca"):
300*e1fe3e4aSElliott Hughes            self._compileTable(tag)
301*e1fe3e4aSElliott Hughes
302*e1fe3e4aSElliott Hughes    def _setHeadTransformFlag(self):
303*e1fe3e4aSElliott Hughes        """Set bit 11 of 'head' table flags to indicate that the font has undergone
304*e1fe3e4aSElliott Hughes        a lossless modifying transform. Re-compile head table data."""
305*e1fe3e4aSElliott Hughes        self._decompileTable("head")
306*e1fe3e4aSElliott Hughes        self.ttFont["head"].flags |= 1 << 11
307*e1fe3e4aSElliott Hughes        self._compileTable("head")
308*e1fe3e4aSElliott Hughes
309*e1fe3e4aSElliott Hughes    def _decompileTable(self, tag):
310*e1fe3e4aSElliott Hughes        """Fetch table data, decompile it, and store it inside self.ttFont."""
311*e1fe3e4aSElliott Hughes        tag = Tag(tag)
312*e1fe3e4aSElliott Hughes        if tag not in self.tables:
313*e1fe3e4aSElliott Hughes            raise TTLibError("missing required table: %s" % tag)
314*e1fe3e4aSElliott Hughes        if self.ttFont.isLoaded(tag):
315*e1fe3e4aSElliott Hughes            return
316*e1fe3e4aSElliott Hughes        data = self.tables[tag].data
317*e1fe3e4aSElliott Hughes        if tag == "loca":
318*e1fe3e4aSElliott Hughes            tableClass = WOFF2LocaTable
319*e1fe3e4aSElliott Hughes        elif tag == "glyf":
320*e1fe3e4aSElliott Hughes            tableClass = WOFF2GlyfTable
321*e1fe3e4aSElliott Hughes        elif tag == "hmtx":
322*e1fe3e4aSElliott Hughes            tableClass = WOFF2HmtxTable
323*e1fe3e4aSElliott Hughes        else:
324*e1fe3e4aSElliott Hughes            tableClass = getTableClass(tag)
325*e1fe3e4aSElliott Hughes        table = tableClass(tag)
326*e1fe3e4aSElliott Hughes        self.ttFont.tables[tag] = table
327*e1fe3e4aSElliott Hughes        table.decompile(data, self.ttFont)
328*e1fe3e4aSElliott Hughes
329*e1fe3e4aSElliott Hughes    def _compileTable(self, tag):
330*e1fe3e4aSElliott Hughes        """Compile table and store it in its 'data' attribute."""
331*e1fe3e4aSElliott Hughes        self.tables[tag].data = self.ttFont[tag].compile(self.ttFont)
332*e1fe3e4aSElliott Hughes
333*e1fe3e4aSElliott Hughes    def _calcSFNTChecksumsLengthsAndOffsets(self):
334*e1fe3e4aSElliott Hughes        """Compute the 'original' SFNT checksums, lengths and offsets for checksum
335*e1fe3e4aSElliott Hughes        adjustment calculation. Return the total size of the uncompressed font.
336*e1fe3e4aSElliott Hughes        """
337*e1fe3e4aSElliott Hughes        offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables)
338*e1fe3e4aSElliott Hughes        for tag, entry in self.tables.items():
339*e1fe3e4aSElliott Hughes            data = entry.data
340*e1fe3e4aSElliott Hughes            entry.origOffset = offset
341*e1fe3e4aSElliott Hughes            entry.origLength = len(data)
342*e1fe3e4aSElliott Hughes            if tag == "head":
343*e1fe3e4aSElliott Hughes                entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:])
344*e1fe3e4aSElliott Hughes            else:
345*e1fe3e4aSElliott Hughes                entry.checkSum = calcChecksum(data)
346*e1fe3e4aSElliott Hughes            offset += (entry.origLength + 3) & ~3
347*e1fe3e4aSElliott Hughes        return offset
348*e1fe3e4aSElliott Hughes
349*e1fe3e4aSElliott Hughes    def _transformTables(self):
350*e1fe3e4aSElliott Hughes        """Return transformed font data."""
351*e1fe3e4aSElliott Hughes        transformedTables = self.flavorData.transformedTables
352*e1fe3e4aSElliott Hughes        for tag, entry in self.tables.items():
353*e1fe3e4aSElliott Hughes            data = None
354*e1fe3e4aSElliott Hughes            if tag in transformedTables:
355*e1fe3e4aSElliott Hughes                data = self.transformTable(tag)
356*e1fe3e4aSElliott Hughes                if data is not None:
357*e1fe3e4aSElliott Hughes                    entry.transformed = True
358*e1fe3e4aSElliott Hughes            if data is None:
359*e1fe3e4aSElliott Hughes                if tag == "glyf":
360*e1fe3e4aSElliott Hughes                    # Currently we always sort table tags so
361*e1fe3e4aSElliott Hughes                    # 'loca' comes after 'glyf'.
362*e1fe3e4aSElliott Hughes                    transformedTables.discard("loca")
363*e1fe3e4aSElliott Hughes                # pass-through the table data without transformation
364*e1fe3e4aSElliott Hughes                data = entry.data
365*e1fe3e4aSElliott Hughes                entry.transformed = False
366*e1fe3e4aSElliott Hughes            entry.offset = self.nextTableOffset
367*e1fe3e4aSElliott Hughes            entry.saveData(self.transformBuffer, data)
368*e1fe3e4aSElliott Hughes            self.nextTableOffset += entry.length
369*e1fe3e4aSElliott Hughes        self.writeMasterChecksum()
370*e1fe3e4aSElliott Hughes        fontData = self.transformBuffer.getvalue()
371*e1fe3e4aSElliott Hughes        return fontData
372*e1fe3e4aSElliott Hughes
373*e1fe3e4aSElliott Hughes    def transformTable(self, tag):
374*e1fe3e4aSElliott Hughes        """Return transformed table data, or None if some pre-conditions aren't
375*e1fe3e4aSElliott Hughes        met -- in which case, the non-transformed table data will be used.
376*e1fe3e4aSElliott Hughes        """
377*e1fe3e4aSElliott Hughes        if tag == "loca":
378*e1fe3e4aSElliott Hughes            data = b""
379*e1fe3e4aSElliott Hughes        elif tag == "glyf":
380*e1fe3e4aSElliott Hughes            for tag in ("maxp", "head", "loca", "glyf"):
381*e1fe3e4aSElliott Hughes                self._decompileTable(tag)
382*e1fe3e4aSElliott Hughes            glyfTable = self.ttFont["glyf"]
383*e1fe3e4aSElliott Hughes            data = glyfTable.transform(self.ttFont)
384*e1fe3e4aSElliott Hughes        elif tag == "hmtx":
385*e1fe3e4aSElliott Hughes            if "glyf" not in self.tables:
386*e1fe3e4aSElliott Hughes                return
387*e1fe3e4aSElliott Hughes            for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"):
388*e1fe3e4aSElliott Hughes                self._decompileTable(tag)
389*e1fe3e4aSElliott Hughes            hmtxTable = self.ttFont["hmtx"]
390*e1fe3e4aSElliott Hughes            data = hmtxTable.transform(self.ttFont)  # can be None
391*e1fe3e4aSElliott Hughes        else:
392*e1fe3e4aSElliott Hughes            raise TTLibError("Transform for table '%s' is unknown" % tag)
393*e1fe3e4aSElliott Hughes        return data
394*e1fe3e4aSElliott Hughes
395*e1fe3e4aSElliott Hughes    def _calcMasterChecksum(self):
396*e1fe3e4aSElliott Hughes        """Calculate checkSumAdjustment."""
397*e1fe3e4aSElliott Hughes        tags = list(self.tables.keys())
398*e1fe3e4aSElliott Hughes        checksums = []
399*e1fe3e4aSElliott Hughes        for i in range(len(tags)):
400*e1fe3e4aSElliott Hughes            checksums.append(self.tables[tags[i]].checkSum)
401*e1fe3e4aSElliott Hughes
402*e1fe3e4aSElliott Hughes        # Create a SFNT directory for checksum calculation purposes
403*e1fe3e4aSElliott Hughes        self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(
404*e1fe3e4aSElliott Hughes            self.numTables, 16
405*e1fe3e4aSElliott Hughes        )
406*e1fe3e4aSElliott Hughes        directory = sstruct.pack(sfntDirectoryFormat, self)
407*e1fe3e4aSElliott Hughes        tables = sorted(self.tables.items())
408*e1fe3e4aSElliott Hughes        for tag, entry in tables:
409*e1fe3e4aSElliott Hughes            sfntEntry = SFNTDirectoryEntry()
410*e1fe3e4aSElliott Hughes            sfntEntry.tag = entry.tag
411*e1fe3e4aSElliott Hughes            sfntEntry.checkSum = entry.checkSum
412*e1fe3e4aSElliott Hughes            sfntEntry.offset = entry.origOffset
413*e1fe3e4aSElliott Hughes            sfntEntry.length = entry.origLength
414*e1fe3e4aSElliott Hughes            directory = directory + sfntEntry.toString()
415*e1fe3e4aSElliott Hughes
416*e1fe3e4aSElliott Hughes        directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
417*e1fe3e4aSElliott Hughes        assert directory_end == len(directory)
418*e1fe3e4aSElliott Hughes
419*e1fe3e4aSElliott Hughes        checksums.append(calcChecksum(directory))
420*e1fe3e4aSElliott Hughes        checksum = sum(checksums) & 0xFFFFFFFF
421*e1fe3e4aSElliott Hughes        # BiboAfba!
422*e1fe3e4aSElliott Hughes        checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF
423*e1fe3e4aSElliott Hughes        return checksumadjustment
424*e1fe3e4aSElliott Hughes
425*e1fe3e4aSElliott Hughes    def writeMasterChecksum(self):
426*e1fe3e4aSElliott Hughes        """Write checkSumAdjustment to the transformBuffer."""
427*e1fe3e4aSElliott Hughes        checksumadjustment = self._calcMasterChecksum()
428*e1fe3e4aSElliott Hughes        self.transformBuffer.seek(self.tables["head"].offset + 8)
429*e1fe3e4aSElliott Hughes        self.transformBuffer.write(struct.pack(">L", checksumadjustment))
430*e1fe3e4aSElliott Hughes
431*e1fe3e4aSElliott Hughes    def _calcTotalSize(self):
432*e1fe3e4aSElliott Hughes        """Calculate total size of WOFF2 font, including any meta- and/or private data."""
433*e1fe3e4aSElliott Hughes        offset = self.directorySize
434*e1fe3e4aSElliott Hughes        for entry in self.tables.values():
435*e1fe3e4aSElliott Hughes            offset += len(entry.toString())
436*e1fe3e4aSElliott Hughes        offset += self.totalCompressedSize
437*e1fe3e4aSElliott Hughes        offset = (offset + 3) & ~3
438*e1fe3e4aSElliott Hughes        offset = self._calcFlavorDataOffsetsAndSize(offset)
439*e1fe3e4aSElliott Hughes        return offset
440*e1fe3e4aSElliott Hughes
441*e1fe3e4aSElliott Hughes    def _calcFlavorDataOffsetsAndSize(self, start):
442*e1fe3e4aSElliott Hughes        """Calculate offsets and lengths for any meta- and/or private data."""
443*e1fe3e4aSElliott Hughes        offset = start
444*e1fe3e4aSElliott Hughes        data = self.flavorData
445*e1fe3e4aSElliott Hughes        if data.metaData:
446*e1fe3e4aSElliott Hughes            self.metaOrigLength = len(data.metaData)
447*e1fe3e4aSElliott Hughes            self.metaOffset = offset
448*e1fe3e4aSElliott Hughes            self.compressedMetaData = brotli.compress(
449*e1fe3e4aSElliott Hughes                data.metaData, mode=brotli.MODE_TEXT
450*e1fe3e4aSElliott Hughes            )
451*e1fe3e4aSElliott Hughes            self.metaLength = len(self.compressedMetaData)
452*e1fe3e4aSElliott Hughes            offset += self.metaLength
453*e1fe3e4aSElliott Hughes        else:
454*e1fe3e4aSElliott Hughes            self.metaOffset = self.metaLength = self.metaOrigLength = 0
455*e1fe3e4aSElliott Hughes            self.compressedMetaData = b""
456*e1fe3e4aSElliott Hughes        if data.privData:
457*e1fe3e4aSElliott Hughes            # make sure private data is padded to 4-byte boundary
458*e1fe3e4aSElliott Hughes            offset = (offset + 3) & ~3
459*e1fe3e4aSElliott Hughes            self.privOffset = offset
460*e1fe3e4aSElliott Hughes            self.privLength = len(data.privData)
461*e1fe3e4aSElliott Hughes            offset += self.privLength
462*e1fe3e4aSElliott Hughes        else:
463*e1fe3e4aSElliott Hughes            self.privOffset = self.privLength = 0
464*e1fe3e4aSElliott Hughes        return offset
465*e1fe3e4aSElliott Hughes
466*e1fe3e4aSElliott Hughes    def _getVersion(self):
467*e1fe3e4aSElliott Hughes        """Return the WOFF2 font's (majorVersion, minorVersion) tuple."""
468*e1fe3e4aSElliott Hughes        data = self.flavorData
469*e1fe3e4aSElliott Hughes        if data.majorVersion is not None and data.minorVersion is not None:
470*e1fe3e4aSElliott Hughes            return data.majorVersion, data.minorVersion
471*e1fe3e4aSElliott Hughes        else:
472*e1fe3e4aSElliott Hughes            # if None, return 'fontRevision' from 'head' table
473*e1fe3e4aSElliott Hughes            if "head" in self.tables:
474*e1fe3e4aSElliott Hughes                return struct.unpack(">HH", self.tables["head"].data[4:8])
475*e1fe3e4aSElliott Hughes            else:
476*e1fe3e4aSElliott Hughes                return 0, 0
477*e1fe3e4aSElliott Hughes
478*e1fe3e4aSElliott Hughes    def _packTableDirectory(self):
479*e1fe3e4aSElliott Hughes        """Return WOFF2 table directory data."""
480*e1fe3e4aSElliott Hughes        directory = sstruct.pack(self.directoryFormat, self)
481*e1fe3e4aSElliott Hughes        for entry in self.tables.values():
482*e1fe3e4aSElliott Hughes            directory = directory + entry.toString()
483*e1fe3e4aSElliott Hughes        return directory
484*e1fe3e4aSElliott Hughes
485*e1fe3e4aSElliott Hughes    def _writeFlavorData(self):
486*e1fe3e4aSElliott Hughes        """Write metadata and/or private data using appropiate padding."""
487*e1fe3e4aSElliott Hughes        compressedMetaData = self.compressedMetaData
488*e1fe3e4aSElliott Hughes        privData = self.flavorData.privData
489*e1fe3e4aSElliott Hughes        if compressedMetaData and privData:
490*e1fe3e4aSElliott Hughes            compressedMetaData = pad(compressedMetaData, size=4)
491*e1fe3e4aSElliott Hughes        if compressedMetaData:
492*e1fe3e4aSElliott Hughes            self.file.seek(self.metaOffset)
493*e1fe3e4aSElliott Hughes            assert self.file.tell() == self.metaOffset
494*e1fe3e4aSElliott Hughes            self.file.write(compressedMetaData)
495*e1fe3e4aSElliott Hughes        if privData:
496*e1fe3e4aSElliott Hughes            self.file.seek(self.privOffset)
497*e1fe3e4aSElliott Hughes            assert self.file.tell() == self.privOffset
498*e1fe3e4aSElliott Hughes            self.file.write(privData)
499*e1fe3e4aSElliott Hughes
500*e1fe3e4aSElliott Hughes    def reordersTables(self):
501*e1fe3e4aSElliott Hughes        return True
502*e1fe3e4aSElliott Hughes
503*e1fe3e4aSElliott Hughes
504*e1fe3e4aSElliott Hughes# -- woff2 directory helpers and cruft
505*e1fe3e4aSElliott Hughes
506*e1fe3e4aSElliott Hugheswoff2DirectoryFormat = """
507*e1fe3e4aSElliott Hughes		> # big endian
508*e1fe3e4aSElliott Hughes		signature:           4s   # "wOF2"
509*e1fe3e4aSElliott Hughes		sfntVersion:         4s
510*e1fe3e4aSElliott Hughes		length:              L    # total woff2 file size
511*e1fe3e4aSElliott Hughes		numTables:           H    # number of tables
512*e1fe3e4aSElliott Hughes		reserved:            H    # set to 0
513*e1fe3e4aSElliott Hughes		totalSfntSize:       L    # uncompressed size
514*e1fe3e4aSElliott Hughes		totalCompressedSize: L    # compressed size
515*e1fe3e4aSElliott Hughes		majorVersion:        H    # major version of WOFF file
516*e1fe3e4aSElliott Hughes		minorVersion:        H    # minor version of WOFF file
517*e1fe3e4aSElliott Hughes		metaOffset:          L    # offset to metadata block
518*e1fe3e4aSElliott Hughes		metaLength:          L    # length of compressed metadata
519*e1fe3e4aSElliott Hughes		metaOrigLength:      L    # length of uncompressed metadata
520*e1fe3e4aSElliott Hughes		privOffset:          L    # offset to private data block
521*e1fe3e4aSElliott Hughes		privLength:          L    # length of private data block
522*e1fe3e4aSElliott Hughes"""
523*e1fe3e4aSElliott Hughes
524*e1fe3e4aSElliott Hugheswoff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat)
525*e1fe3e4aSElliott Hughes
526*e1fe3e4aSElliott Hugheswoff2KnownTags = (
527*e1fe3e4aSElliott Hughes    "cmap",
528*e1fe3e4aSElliott Hughes    "head",
529*e1fe3e4aSElliott Hughes    "hhea",
530*e1fe3e4aSElliott Hughes    "hmtx",
531*e1fe3e4aSElliott Hughes    "maxp",
532*e1fe3e4aSElliott Hughes    "name",
533*e1fe3e4aSElliott Hughes    "OS/2",
534*e1fe3e4aSElliott Hughes    "post",
535*e1fe3e4aSElliott Hughes    "cvt ",
536*e1fe3e4aSElliott Hughes    "fpgm",
537*e1fe3e4aSElliott Hughes    "glyf",
538*e1fe3e4aSElliott Hughes    "loca",
539*e1fe3e4aSElliott Hughes    "prep",
540*e1fe3e4aSElliott Hughes    "CFF ",
541*e1fe3e4aSElliott Hughes    "VORG",
542*e1fe3e4aSElliott Hughes    "EBDT",
543*e1fe3e4aSElliott Hughes    "EBLC",
544*e1fe3e4aSElliott Hughes    "gasp",
545*e1fe3e4aSElliott Hughes    "hdmx",
546*e1fe3e4aSElliott Hughes    "kern",
547*e1fe3e4aSElliott Hughes    "LTSH",
548*e1fe3e4aSElliott Hughes    "PCLT",
549*e1fe3e4aSElliott Hughes    "VDMX",
550*e1fe3e4aSElliott Hughes    "vhea",
551*e1fe3e4aSElliott Hughes    "vmtx",
552*e1fe3e4aSElliott Hughes    "BASE",
553*e1fe3e4aSElliott Hughes    "GDEF",
554*e1fe3e4aSElliott Hughes    "GPOS",
555*e1fe3e4aSElliott Hughes    "GSUB",
556*e1fe3e4aSElliott Hughes    "EBSC",
557*e1fe3e4aSElliott Hughes    "JSTF",
558*e1fe3e4aSElliott Hughes    "MATH",
559*e1fe3e4aSElliott Hughes    "CBDT",
560*e1fe3e4aSElliott Hughes    "CBLC",
561*e1fe3e4aSElliott Hughes    "COLR",
562*e1fe3e4aSElliott Hughes    "CPAL",
563*e1fe3e4aSElliott Hughes    "SVG ",
564*e1fe3e4aSElliott Hughes    "sbix",
565*e1fe3e4aSElliott Hughes    "acnt",
566*e1fe3e4aSElliott Hughes    "avar",
567*e1fe3e4aSElliott Hughes    "bdat",
568*e1fe3e4aSElliott Hughes    "bloc",
569*e1fe3e4aSElliott Hughes    "bsln",
570*e1fe3e4aSElliott Hughes    "cvar",
571*e1fe3e4aSElliott Hughes    "fdsc",
572*e1fe3e4aSElliott Hughes    "feat",
573*e1fe3e4aSElliott Hughes    "fmtx",
574*e1fe3e4aSElliott Hughes    "fvar",
575*e1fe3e4aSElliott Hughes    "gvar",
576*e1fe3e4aSElliott Hughes    "hsty",
577*e1fe3e4aSElliott Hughes    "just",
578*e1fe3e4aSElliott Hughes    "lcar",
579*e1fe3e4aSElliott Hughes    "mort",
580*e1fe3e4aSElliott Hughes    "morx",
581*e1fe3e4aSElliott Hughes    "opbd",
582*e1fe3e4aSElliott Hughes    "prop",
583*e1fe3e4aSElliott Hughes    "trak",
584*e1fe3e4aSElliott Hughes    "Zapf",
585*e1fe3e4aSElliott Hughes    "Silf",
586*e1fe3e4aSElliott Hughes    "Glat",
587*e1fe3e4aSElliott Hughes    "Gloc",
588*e1fe3e4aSElliott Hughes    "Feat",
589*e1fe3e4aSElliott Hughes    "Sill",
590*e1fe3e4aSElliott Hughes)
591*e1fe3e4aSElliott Hughes
592*e1fe3e4aSElliott Hugheswoff2FlagsFormat = """
593*e1fe3e4aSElliott Hughes		> # big endian
594*e1fe3e4aSElliott Hughes		flags: B  # table type and flags
595*e1fe3e4aSElliott Hughes"""
596*e1fe3e4aSElliott Hughes
597*e1fe3e4aSElliott Hugheswoff2FlagsSize = sstruct.calcsize(woff2FlagsFormat)
598*e1fe3e4aSElliott Hughes
599*e1fe3e4aSElliott Hugheswoff2UnknownTagFormat = """
600*e1fe3e4aSElliott Hughes		> # big endian
601*e1fe3e4aSElliott Hughes		tag: 4s  # 4-byte tag (optional)
602*e1fe3e4aSElliott Hughes"""
603*e1fe3e4aSElliott Hughes
604*e1fe3e4aSElliott Hugheswoff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat)
605*e1fe3e4aSElliott Hughes
606*e1fe3e4aSElliott Hugheswoff2UnknownTagIndex = 0x3F
607*e1fe3e4aSElliott Hughes
608*e1fe3e4aSElliott Hugheswoff2Base128MaxSize = 5
609*e1fe3e4aSElliott Hugheswoff2DirectoryEntryMaxSize = (
610*e1fe3e4aSElliott Hughes    woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize
611*e1fe3e4aSElliott Hughes)
612*e1fe3e4aSElliott Hughes
613*e1fe3e4aSElliott Hugheswoff2TransformedTableTags = ("glyf", "loca")
614*e1fe3e4aSElliott Hughes
615*e1fe3e4aSElliott Hugheswoff2GlyfTableFormat = """
616*e1fe3e4aSElliott Hughes		> # big endian
617*e1fe3e4aSElliott Hughes		version:                  H  # = 0x0000
618*e1fe3e4aSElliott Hughes		optionFlags:              H  # Bit 0: we have overlapSimpleBitmap[], Bits 1-15: reserved
619*e1fe3e4aSElliott Hughes		numGlyphs:                H  # Number of glyphs
620*e1fe3e4aSElliott Hughes		indexFormat:              H  # Offset format for loca table
621*e1fe3e4aSElliott Hughes		nContourStreamSize:       L  # Size of nContour stream
622*e1fe3e4aSElliott Hughes		nPointsStreamSize:        L  # Size of nPoints stream
623*e1fe3e4aSElliott Hughes		flagStreamSize:           L  # Size of flag stream
624*e1fe3e4aSElliott Hughes		glyphStreamSize:          L  # Size of glyph stream
625*e1fe3e4aSElliott Hughes		compositeStreamSize:      L  # Size of composite stream
626*e1fe3e4aSElliott Hughes		bboxStreamSize:           L  # Comnined size of bboxBitmap and bboxStream
627*e1fe3e4aSElliott Hughes		instructionStreamSize:    L  # Size of instruction stream
628*e1fe3e4aSElliott Hughes"""
629*e1fe3e4aSElliott Hughes
630*e1fe3e4aSElliott Hugheswoff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat)
631*e1fe3e4aSElliott Hughes
632*e1fe3e4aSElliott HughesbboxFormat = """
633*e1fe3e4aSElliott Hughes		>	# big endian
634*e1fe3e4aSElliott Hughes		xMin:				h
635*e1fe3e4aSElliott Hughes		yMin:				h
636*e1fe3e4aSElliott Hughes		xMax:				h
637*e1fe3e4aSElliott Hughes		yMax:				h
638*e1fe3e4aSElliott Hughes"""
639*e1fe3e4aSElliott Hughes
640*e1fe3e4aSElliott Hugheswoff2OverlapSimpleBitmapFlag = 0x0001
641*e1fe3e4aSElliott Hughes
642*e1fe3e4aSElliott Hughes
643*e1fe3e4aSElliott Hughesdef getKnownTagIndex(tag):
644*e1fe3e4aSElliott Hughes    """Return index of 'tag' in woff2KnownTags list. Return 63 if not found."""
645*e1fe3e4aSElliott Hughes    for i in range(len(woff2KnownTags)):
646*e1fe3e4aSElliott Hughes        if tag == woff2KnownTags[i]:
647*e1fe3e4aSElliott Hughes            return i
648*e1fe3e4aSElliott Hughes    return woff2UnknownTagIndex
649*e1fe3e4aSElliott Hughes
650*e1fe3e4aSElliott Hughes
651*e1fe3e4aSElliott Hughesclass WOFF2DirectoryEntry(DirectoryEntry):
652*e1fe3e4aSElliott Hughes    def fromFile(self, file):
653*e1fe3e4aSElliott Hughes        pos = file.tell()
654*e1fe3e4aSElliott Hughes        data = file.read(woff2DirectoryEntryMaxSize)
655*e1fe3e4aSElliott Hughes        left = self.fromString(data)
656*e1fe3e4aSElliott Hughes        consumed = len(data) - len(left)
657*e1fe3e4aSElliott Hughes        file.seek(pos + consumed)
658*e1fe3e4aSElliott Hughes
659*e1fe3e4aSElliott Hughes    def fromString(self, data):
660*e1fe3e4aSElliott Hughes        if len(data) < 1:
661*e1fe3e4aSElliott Hughes            raise TTLibError("can't read table 'flags': not enough data")
662*e1fe3e4aSElliott Hughes        dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self)
663*e1fe3e4aSElliott Hughes        if self.flags & 0x3F == 0x3F:
664*e1fe3e4aSElliott Hughes            # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value
665*e1fe3e4aSElliott Hughes            if len(data) < woff2UnknownTagSize:
666*e1fe3e4aSElliott Hughes                raise TTLibError("can't read table 'tag': not enough data")
667*e1fe3e4aSElliott Hughes            dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self)
668*e1fe3e4aSElliott Hughes        else:
669*e1fe3e4aSElliott Hughes            # otherwise, tag is derived from a fixed 'Known Tags' table
670*e1fe3e4aSElliott Hughes            self.tag = woff2KnownTags[self.flags & 0x3F]
671*e1fe3e4aSElliott Hughes        self.tag = Tag(self.tag)
672*e1fe3e4aSElliott Hughes        self.origLength, data = unpackBase128(data)
673*e1fe3e4aSElliott Hughes        self.length = self.origLength
674*e1fe3e4aSElliott Hughes        if self.transformed:
675*e1fe3e4aSElliott Hughes            self.length, data = unpackBase128(data)
676*e1fe3e4aSElliott Hughes            if self.tag == "loca" and self.length != 0:
677*e1fe3e4aSElliott Hughes                raise TTLibError("the transformLength of the 'loca' table must be 0")
678*e1fe3e4aSElliott Hughes        # return left over data
679*e1fe3e4aSElliott Hughes        return data
680*e1fe3e4aSElliott Hughes
681*e1fe3e4aSElliott Hughes    def toString(self):
682*e1fe3e4aSElliott Hughes        data = bytechr(self.flags)
683*e1fe3e4aSElliott Hughes        if (self.flags & 0x3F) == 0x3F:
684*e1fe3e4aSElliott Hughes            data += struct.pack(">4s", self.tag.tobytes())
685*e1fe3e4aSElliott Hughes        data += packBase128(self.origLength)
686*e1fe3e4aSElliott Hughes        if self.transformed:
687*e1fe3e4aSElliott Hughes            data += packBase128(self.length)
688*e1fe3e4aSElliott Hughes        return data
689*e1fe3e4aSElliott Hughes
690*e1fe3e4aSElliott Hughes    @property
691*e1fe3e4aSElliott Hughes    def transformVersion(self):
692*e1fe3e4aSElliott Hughes        """Return bits 6-7 of table entry's flags, which indicate the preprocessing
693*e1fe3e4aSElliott Hughes        transformation version number (between 0 and 3).
694*e1fe3e4aSElliott Hughes        """
695*e1fe3e4aSElliott Hughes        return self.flags >> 6
696*e1fe3e4aSElliott Hughes
697*e1fe3e4aSElliott Hughes    @transformVersion.setter
698*e1fe3e4aSElliott Hughes    def transformVersion(self, value):
699*e1fe3e4aSElliott Hughes        assert 0 <= value <= 3
700*e1fe3e4aSElliott Hughes        self.flags |= value << 6
701*e1fe3e4aSElliott Hughes
702*e1fe3e4aSElliott Hughes    @property
703*e1fe3e4aSElliott Hughes    def transformed(self):
704*e1fe3e4aSElliott Hughes        """Return True if the table has any transformation, else return False."""
705*e1fe3e4aSElliott Hughes        # For all tables in a font, except for 'glyf' and 'loca', the transformation
706*e1fe3e4aSElliott Hughes        # version 0 indicates the null transform (where the original table data is
707*e1fe3e4aSElliott Hughes        # passed directly to the Brotli compressor). For 'glyf' and 'loca' tables,
708*e1fe3e4aSElliott Hughes        # transformation version 3 indicates the null transform
709*e1fe3e4aSElliott Hughes        if self.tag in {"glyf", "loca"}:
710*e1fe3e4aSElliott Hughes            return self.transformVersion != 3
711*e1fe3e4aSElliott Hughes        else:
712*e1fe3e4aSElliott Hughes            return self.transformVersion != 0
713*e1fe3e4aSElliott Hughes
714*e1fe3e4aSElliott Hughes    @transformed.setter
715*e1fe3e4aSElliott Hughes    def transformed(self, booleanValue):
716*e1fe3e4aSElliott Hughes        # here we assume that a non-null transform means version 0 for 'glyf' and
717*e1fe3e4aSElliott Hughes        # 'loca' and 1 for every other table (e.g. hmtx); but that may change as
718*e1fe3e4aSElliott Hughes        # new transformation formats are introduced in the future (if ever).
719*e1fe3e4aSElliott Hughes        if self.tag in {"glyf", "loca"}:
720*e1fe3e4aSElliott Hughes            self.transformVersion = 3 if not booleanValue else 0
721*e1fe3e4aSElliott Hughes        else:
722*e1fe3e4aSElliott Hughes            self.transformVersion = int(booleanValue)
723*e1fe3e4aSElliott Hughes
724*e1fe3e4aSElliott Hughes
725*e1fe3e4aSElliott Hughesclass WOFF2LocaTable(getTableClass("loca")):
726*e1fe3e4aSElliott Hughes    """Same as parent class. The only difference is that it attempts to preserve
727*e1fe3e4aSElliott Hughes    the 'indexFormat' as encoded in the WOFF2 glyf table.
728*e1fe3e4aSElliott Hughes    """
729*e1fe3e4aSElliott Hughes
730*e1fe3e4aSElliott Hughes    def __init__(self, tag=None):
731*e1fe3e4aSElliott Hughes        self.tableTag = Tag(tag or "loca")
732*e1fe3e4aSElliott Hughes
733*e1fe3e4aSElliott Hughes    def compile(self, ttFont):
734*e1fe3e4aSElliott Hughes        try:
735*e1fe3e4aSElliott Hughes            max_location = max(self.locations)
736*e1fe3e4aSElliott Hughes        except AttributeError:
737*e1fe3e4aSElliott Hughes            self.set([])
738*e1fe3e4aSElliott Hughes            max_location = 0
739*e1fe3e4aSElliott Hughes        if "glyf" in ttFont and hasattr(ttFont["glyf"], "indexFormat"):
740*e1fe3e4aSElliott Hughes            # copile loca using the indexFormat specified in the WOFF2 glyf table
741*e1fe3e4aSElliott Hughes            indexFormat = ttFont["glyf"].indexFormat
742*e1fe3e4aSElliott Hughes            if indexFormat == 0:
743*e1fe3e4aSElliott Hughes                if max_location >= 0x20000:
744*e1fe3e4aSElliott Hughes                    raise TTLibError("indexFormat is 0 but local offsets > 0x20000")
745*e1fe3e4aSElliott Hughes                if not all(l % 2 == 0 for l in self.locations):
746*e1fe3e4aSElliott Hughes                    raise TTLibError(
747*e1fe3e4aSElliott Hughes                        "indexFormat is 0 but local offsets not multiples of 2"
748*e1fe3e4aSElliott Hughes                    )
749*e1fe3e4aSElliott Hughes                locations = array.array("H")
750*e1fe3e4aSElliott Hughes                for i in range(len(self.locations)):
751*e1fe3e4aSElliott Hughes                    locations.append(self.locations[i] // 2)
752*e1fe3e4aSElliott Hughes            else:
753*e1fe3e4aSElliott Hughes                locations = array.array("I", self.locations)
754*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
755*e1fe3e4aSElliott Hughes                locations.byteswap()
756*e1fe3e4aSElliott Hughes            data = locations.tobytes()
757*e1fe3e4aSElliott Hughes        else:
758*e1fe3e4aSElliott Hughes            # use the most compact indexFormat given the current glyph offsets
759*e1fe3e4aSElliott Hughes            data = super(WOFF2LocaTable, self).compile(ttFont)
760*e1fe3e4aSElliott Hughes        return data
761*e1fe3e4aSElliott Hughes
762*e1fe3e4aSElliott Hughes
763*e1fe3e4aSElliott Hughesclass WOFF2GlyfTable(getTableClass("glyf")):
764*e1fe3e4aSElliott Hughes    """Decoder/Encoder for WOFF2 'glyf' table transform."""
765*e1fe3e4aSElliott Hughes
766*e1fe3e4aSElliott Hughes    subStreams = (
767*e1fe3e4aSElliott Hughes        "nContourStream",
768*e1fe3e4aSElliott Hughes        "nPointsStream",
769*e1fe3e4aSElliott Hughes        "flagStream",
770*e1fe3e4aSElliott Hughes        "glyphStream",
771*e1fe3e4aSElliott Hughes        "compositeStream",
772*e1fe3e4aSElliott Hughes        "bboxStream",
773*e1fe3e4aSElliott Hughes        "instructionStream",
774*e1fe3e4aSElliott Hughes    )
775*e1fe3e4aSElliott Hughes
776*e1fe3e4aSElliott Hughes    def __init__(self, tag=None):
777*e1fe3e4aSElliott Hughes        self.tableTag = Tag(tag or "glyf")
778*e1fe3e4aSElliott Hughes
779*e1fe3e4aSElliott Hughes    def reconstruct(self, data, ttFont):
780*e1fe3e4aSElliott Hughes        """Decompile transformed 'glyf' data."""
781*e1fe3e4aSElliott Hughes        inputDataSize = len(data)
782*e1fe3e4aSElliott Hughes
783*e1fe3e4aSElliott Hughes        if inputDataSize < woff2GlyfTableFormatSize:
784*e1fe3e4aSElliott Hughes            raise TTLibError("not enough 'glyf' data")
785*e1fe3e4aSElliott Hughes        dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self)
786*e1fe3e4aSElliott Hughes        offset = woff2GlyfTableFormatSize
787*e1fe3e4aSElliott Hughes
788*e1fe3e4aSElliott Hughes        for stream in self.subStreams:
789*e1fe3e4aSElliott Hughes            size = getattr(self, stream + "Size")
790*e1fe3e4aSElliott Hughes            setattr(self, stream, data[:size])
791*e1fe3e4aSElliott Hughes            data = data[size:]
792*e1fe3e4aSElliott Hughes            offset += size
793*e1fe3e4aSElliott Hughes
794*e1fe3e4aSElliott Hughes        hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag
795*e1fe3e4aSElliott Hughes        self.overlapSimpleBitmap = None
796*e1fe3e4aSElliott Hughes        if hasOverlapSimpleBitmap:
797*e1fe3e4aSElliott Hughes            overlapSimpleBitmapSize = (self.numGlyphs + 7) >> 3
798*e1fe3e4aSElliott Hughes            self.overlapSimpleBitmap = array.array("B", data[:overlapSimpleBitmapSize])
799*e1fe3e4aSElliott Hughes            offset += overlapSimpleBitmapSize
800*e1fe3e4aSElliott Hughes
801*e1fe3e4aSElliott Hughes        if offset != inputDataSize:
802*e1fe3e4aSElliott Hughes            raise TTLibError(
803*e1fe3e4aSElliott Hughes                "incorrect size of transformed 'glyf' table: expected %d, received %d bytes"
804*e1fe3e4aSElliott Hughes                % (offset, inputDataSize)
805*e1fe3e4aSElliott Hughes            )
806*e1fe3e4aSElliott Hughes
807*e1fe3e4aSElliott Hughes        bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
808*e1fe3e4aSElliott Hughes        bboxBitmap = self.bboxStream[:bboxBitmapSize]
809*e1fe3e4aSElliott Hughes        self.bboxBitmap = array.array("B", bboxBitmap)
810*e1fe3e4aSElliott Hughes        self.bboxStream = self.bboxStream[bboxBitmapSize:]
811*e1fe3e4aSElliott Hughes
812*e1fe3e4aSElliott Hughes        self.nContourStream = array.array("h", self.nContourStream)
813*e1fe3e4aSElliott Hughes        if sys.byteorder != "big":
814*e1fe3e4aSElliott Hughes            self.nContourStream.byteswap()
815*e1fe3e4aSElliott Hughes        assert len(self.nContourStream) == self.numGlyphs
816*e1fe3e4aSElliott Hughes
817*e1fe3e4aSElliott Hughes        if "head" in ttFont:
818*e1fe3e4aSElliott Hughes            ttFont["head"].indexToLocFormat = self.indexFormat
819*e1fe3e4aSElliott Hughes        try:
820*e1fe3e4aSElliott Hughes            self.glyphOrder = ttFont.getGlyphOrder()
821*e1fe3e4aSElliott Hughes        except:
822*e1fe3e4aSElliott Hughes            self.glyphOrder = None
823*e1fe3e4aSElliott Hughes        if self.glyphOrder is None:
824*e1fe3e4aSElliott Hughes            self.glyphOrder = [".notdef"]
825*e1fe3e4aSElliott Hughes            self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)])
826*e1fe3e4aSElliott Hughes        else:
827*e1fe3e4aSElliott Hughes            if len(self.glyphOrder) != self.numGlyphs:
828*e1fe3e4aSElliott Hughes                raise TTLibError(
829*e1fe3e4aSElliott Hughes                    "incorrect glyphOrder: expected %d glyphs, found %d"
830*e1fe3e4aSElliott Hughes                    % (len(self.glyphOrder), self.numGlyphs)
831*e1fe3e4aSElliott Hughes                )
832*e1fe3e4aSElliott Hughes
833*e1fe3e4aSElliott Hughes        glyphs = self.glyphs = {}
834*e1fe3e4aSElliott Hughes        for glyphID, glyphName in enumerate(self.glyphOrder):
835*e1fe3e4aSElliott Hughes            glyph = self._decodeGlyph(glyphID)
836*e1fe3e4aSElliott Hughes            glyphs[glyphName] = glyph
837*e1fe3e4aSElliott Hughes
838*e1fe3e4aSElliott Hughes    def transform(self, ttFont):
839*e1fe3e4aSElliott Hughes        """Return transformed 'glyf' data"""
840*e1fe3e4aSElliott Hughes        self.numGlyphs = len(self.glyphs)
841*e1fe3e4aSElliott Hughes        assert len(self.glyphOrder) == self.numGlyphs
842*e1fe3e4aSElliott Hughes        if "maxp" in ttFont:
843*e1fe3e4aSElliott Hughes            ttFont["maxp"].numGlyphs = self.numGlyphs
844*e1fe3e4aSElliott Hughes        self.indexFormat = ttFont["head"].indexToLocFormat
845*e1fe3e4aSElliott Hughes
846*e1fe3e4aSElliott Hughes        for stream in self.subStreams:
847*e1fe3e4aSElliott Hughes            setattr(self, stream, b"")
848*e1fe3e4aSElliott Hughes        bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2
849*e1fe3e4aSElliott Hughes        self.bboxBitmap = array.array("B", [0] * bboxBitmapSize)
850*e1fe3e4aSElliott Hughes
851*e1fe3e4aSElliott Hughes        self.overlapSimpleBitmap = array.array("B", [0] * ((self.numGlyphs + 7) >> 3))
852*e1fe3e4aSElliott Hughes        for glyphID in range(self.numGlyphs):
853*e1fe3e4aSElliott Hughes            try:
854*e1fe3e4aSElliott Hughes                self._encodeGlyph(glyphID)
855*e1fe3e4aSElliott Hughes            except NotImplementedError:
856*e1fe3e4aSElliott Hughes                return None
857*e1fe3e4aSElliott Hughes        hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap)
858*e1fe3e4aSElliott Hughes
859*e1fe3e4aSElliott Hughes        self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream
860*e1fe3e4aSElliott Hughes        for stream in self.subStreams:
861*e1fe3e4aSElliott Hughes            setattr(self, stream + "Size", len(getattr(self, stream)))
862*e1fe3e4aSElliott Hughes        self.version = 0
863*e1fe3e4aSElliott Hughes        self.optionFlags = 0
864*e1fe3e4aSElliott Hughes        if hasOverlapSimpleBitmap:
865*e1fe3e4aSElliott Hughes            self.optionFlags |= woff2OverlapSimpleBitmapFlag
866*e1fe3e4aSElliott Hughes        data = sstruct.pack(woff2GlyfTableFormat, self)
867*e1fe3e4aSElliott Hughes        data += bytesjoin([getattr(self, s) for s in self.subStreams])
868*e1fe3e4aSElliott Hughes        if hasOverlapSimpleBitmap:
869*e1fe3e4aSElliott Hughes            data += self.overlapSimpleBitmap.tobytes()
870*e1fe3e4aSElliott Hughes        return data
871*e1fe3e4aSElliott Hughes
872*e1fe3e4aSElliott Hughes    def _decodeGlyph(self, glyphID):
873*e1fe3e4aSElliott Hughes        glyph = getTableModule("glyf").Glyph()
874*e1fe3e4aSElliott Hughes        glyph.numberOfContours = self.nContourStream[glyphID]
875*e1fe3e4aSElliott Hughes        if glyph.numberOfContours == 0:
876*e1fe3e4aSElliott Hughes            return glyph
877*e1fe3e4aSElliott Hughes        elif glyph.isComposite():
878*e1fe3e4aSElliott Hughes            self._decodeComponents(glyph)
879*e1fe3e4aSElliott Hughes        else:
880*e1fe3e4aSElliott Hughes            self._decodeCoordinates(glyph)
881*e1fe3e4aSElliott Hughes            self._decodeOverlapSimpleFlag(glyph, glyphID)
882*e1fe3e4aSElliott Hughes        self._decodeBBox(glyphID, glyph)
883*e1fe3e4aSElliott Hughes        return glyph
884*e1fe3e4aSElliott Hughes
885*e1fe3e4aSElliott Hughes    def _decodeComponents(self, glyph):
886*e1fe3e4aSElliott Hughes        data = self.compositeStream
887*e1fe3e4aSElliott Hughes        glyph.components = []
888*e1fe3e4aSElliott Hughes        more = 1
889*e1fe3e4aSElliott Hughes        haveInstructions = 0
890*e1fe3e4aSElliott Hughes        while more:
891*e1fe3e4aSElliott Hughes            component = getTableModule("glyf").GlyphComponent()
892*e1fe3e4aSElliott Hughes            more, haveInstr, data = component.decompile(data, self)
893*e1fe3e4aSElliott Hughes            haveInstructions = haveInstructions | haveInstr
894*e1fe3e4aSElliott Hughes            glyph.components.append(component)
895*e1fe3e4aSElliott Hughes        self.compositeStream = data
896*e1fe3e4aSElliott Hughes        if haveInstructions:
897*e1fe3e4aSElliott Hughes            self._decodeInstructions(glyph)
898*e1fe3e4aSElliott Hughes
899*e1fe3e4aSElliott Hughes    def _decodeCoordinates(self, glyph):
900*e1fe3e4aSElliott Hughes        data = self.nPointsStream
901*e1fe3e4aSElliott Hughes        endPtsOfContours = []
902*e1fe3e4aSElliott Hughes        endPoint = -1
903*e1fe3e4aSElliott Hughes        for i in range(glyph.numberOfContours):
904*e1fe3e4aSElliott Hughes            ptsOfContour, data = unpack255UShort(data)
905*e1fe3e4aSElliott Hughes            endPoint += ptsOfContour
906*e1fe3e4aSElliott Hughes            endPtsOfContours.append(endPoint)
907*e1fe3e4aSElliott Hughes        glyph.endPtsOfContours = endPtsOfContours
908*e1fe3e4aSElliott Hughes        self.nPointsStream = data
909*e1fe3e4aSElliott Hughes        self._decodeTriplets(glyph)
910*e1fe3e4aSElliott Hughes        self._decodeInstructions(glyph)
911*e1fe3e4aSElliott Hughes
912*e1fe3e4aSElliott Hughes    def _decodeOverlapSimpleFlag(self, glyph, glyphID):
913*e1fe3e4aSElliott Hughes        if self.overlapSimpleBitmap is None or glyph.numberOfContours <= 0:
914*e1fe3e4aSElliott Hughes            return
915*e1fe3e4aSElliott Hughes        byte = glyphID >> 3
916*e1fe3e4aSElliott Hughes        bit = glyphID & 7
917*e1fe3e4aSElliott Hughes        if self.overlapSimpleBitmap[byte] & (0x80 >> bit):
918*e1fe3e4aSElliott Hughes            glyph.flags[0] |= _g_l_y_f.flagOverlapSimple
919*e1fe3e4aSElliott Hughes
920*e1fe3e4aSElliott Hughes    def _decodeInstructions(self, glyph):
921*e1fe3e4aSElliott Hughes        glyphStream = self.glyphStream
922*e1fe3e4aSElliott Hughes        instructionStream = self.instructionStream
923*e1fe3e4aSElliott Hughes        instructionLength, glyphStream = unpack255UShort(glyphStream)
924*e1fe3e4aSElliott Hughes        glyph.program = ttProgram.Program()
925*e1fe3e4aSElliott Hughes        glyph.program.fromBytecode(instructionStream[:instructionLength])
926*e1fe3e4aSElliott Hughes        self.glyphStream = glyphStream
927*e1fe3e4aSElliott Hughes        self.instructionStream = instructionStream[instructionLength:]
928*e1fe3e4aSElliott Hughes
929*e1fe3e4aSElliott Hughes    def _decodeBBox(self, glyphID, glyph):
930*e1fe3e4aSElliott Hughes        haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7)))
931*e1fe3e4aSElliott Hughes        if glyph.isComposite() and not haveBBox:
932*e1fe3e4aSElliott Hughes            raise TTLibError("no bbox values for composite glyph %d" % glyphID)
933*e1fe3e4aSElliott Hughes        if haveBBox:
934*e1fe3e4aSElliott Hughes            dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph)
935*e1fe3e4aSElliott Hughes        else:
936*e1fe3e4aSElliott Hughes            glyph.recalcBounds(self)
937*e1fe3e4aSElliott Hughes
938*e1fe3e4aSElliott Hughes    def _decodeTriplets(self, glyph):
939*e1fe3e4aSElliott Hughes        def withSign(flag, baseval):
940*e1fe3e4aSElliott Hughes            assert 0 <= baseval and baseval < 65536, "integer overflow"
941*e1fe3e4aSElliott Hughes            return baseval if flag & 1 else -baseval
942*e1fe3e4aSElliott Hughes
943*e1fe3e4aSElliott Hughes        nPoints = glyph.endPtsOfContours[-1] + 1
944*e1fe3e4aSElliott Hughes        flagSize = nPoints
945*e1fe3e4aSElliott Hughes        if flagSize > len(self.flagStream):
946*e1fe3e4aSElliott Hughes            raise TTLibError("not enough 'flagStream' data")
947*e1fe3e4aSElliott Hughes        flagsData = self.flagStream[:flagSize]
948*e1fe3e4aSElliott Hughes        self.flagStream = self.flagStream[flagSize:]
949*e1fe3e4aSElliott Hughes        flags = array.array("B", flagsData)
950*e1fe3e4aSElliott Hughes
951*e1fe3e4aSElliott Hughes        triplets = array.array("B", self.glyphStream)
952*e1fe3e4aSElliott Hughes        nTriplets = len(triplets)
953*e1fe3e4aSElliott Hughes        assert nPoints <= nTriplets
954*e1fe3e4aSElliott Hughes
955*e1fe3e4aSElliott Hughes        x = 0
956*e1fe3e4aSElliott Hughes        y = 0
957*e1fe3e4aSElliott Hughes        glyph.coordinates = getTableModule("glyf").GlyphCoordinates.zeros(nPoints)
958*e1fe3e4aSElliott Hughes        glyph.flags = array.array("B")
959*e1fe3e4aSElliott Hughes        tripletIndex = 0
960*e1fe3e4aSElliott Hughes        for i in range(nPoints):
961*e1fe3e4aSElliott Hughes            flag = flags[i]
962*e1fe3e4aSElliott Hughes            onCurve = not bool(flag >> 7)
963*e1fe3e4aSElliott Hughes            flag &= 0x7F
964*e1fe3e4aSElliott Hughes            if flag < 84:
965*e1fe3e4aSElliott Hughes                nBytes = 1
966*e1fe3e4aSElliott Hughes            elif flag < 120:
967*e1fe3e4aSElliott Hughes                nBytes = 2
968*e1fe3e4aSElliott Hughes            elif flag < 124:
969*e1fe3e4aSElliott Hughes                nBytes = 3
970*e1fe3e4aSElliott Hughes            else:
971*e1fe3e4aSElliott Hughes                nBytes = 4
972*e1fe3e4aSElliott Hughes            assert (tripletIndex + nBytes) <= nTriplets
973*e1fe3e4aSElliott Hughes            if flag < 10:
974*e1fe3e4aSElliott Hughes                dx = 0
975*e1fe3e4aSElliott Hughes                dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex])
976*e1fe3e4aSElliott Hughes            elif flag < 20:
977*e1fe3e4aSElliott Hughes                dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex])
978*e1fe3e4aSElliott Hughes                dy = 0
979*e1fe3e4aSElliott Hughes            elif flag < 84:
980*e1fe3e4aSElliott Hughes                b0 = flag - 20
981*e1fe3e4aSElliott Hughes                b1 = triplets[tripletIndex]
982*e1fe3e4aSElliott Hughes                dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4))
983*e1fe3e4aSElliott Hughes                dy = withSign(flag >> 1, 1 + ((b0 & 0x0C) << 2) + (b1 & 0x0F))
984*e1fe3e4aSElliott Hughes            elif flag < 120:
985*e1fe3e4aSElliott Hughes                b0 = flag - 84
986*e1fe3e4aSElliott Hughes                dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex])
987*e1fe3e4aSElliott Hughes                dy = withSign(
988*e1fe3e4aSElliott Hughes                    flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1]
989*e1fe3e4aSElliott Hughes                )
990*e1fe3e4aSElliott Hughes            elif flag < 124:
991*e1fe3e4aSElliott Hughes                b2 = triplets[tripletIndex + 1]
992*e1fe3e4aSElliott Hughes                dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4))
993*e1fe3e4aSElliott Hughes                dy = withSign(
994*e1fe3e4aSElliott Hughes                    flag >> 1, ((b2 & 0x0F) << 8) + triplets[tripletIndex + 2]
995*e1fe3e4aSElliott Hughes                )
996*e1fe3e4aSElliott Hughes            else:
997*e1fe3e4aSElliott Hughes                dx = withSign(
998*e1fe3e4aSElliott Hughes                    flag, (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1]
999*e1fe3e4aSElliott Hughes                )
1000*e1fe3e4aSElliott Hughes                dy = withSign(
1001*e1fe3e4aSElliott Hughes                    flag >> 1,
1002*e1fe3e4aSElliott Hughes                    (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3],
1003*e1fe3e4aSElliott Hughes                )
1004*e1fe3e4aSElliott Hughes            tripletIndex += nBytes
1005*e1fe3e4aSElliott Hughes            x += dx
1006*e1fe3e4aSElliott Hughes            y += dy
1007*e1fe3e4aSElliott Hughes            glyph.coordinates[i] = (x, y)
1008*e1fe3e4aSElliott Hughes            glyph.flags.append(int(onCurve))
1009*e1fe3e4aSElliott Hughes        bytesConsumed = tripletIndex
1010*e1fe3e4aSElliott Hughes        self.glyphStream = self.glyphStream[bytesConsumed:]
1011*e1fe3e4aSElliott Hughes
1012*e1fe3e4aSElliott Hughes    def _encodeGlyph(self, glyphID):
1013*e1fe3e4aSElliott Hughes        glyphName = self.getGlyphName(glyphID)
1014*e1fe3e4aSElliott Hughes        glyph = self[glyphName]
1015*e1fe3e4aSElliott Hughes        self.nContourStream += struct.pack(">h", glyph.numberOfContours)
1016*e1fe3e4aSElliott Hughes        if glyph.numberOfContours == 0:
1017*e1fe3e4aSElliott Hughes            return
1018*e1fe3e4aSElliott Hughes        elif glyph.isComposite():
1019*e1fe3e4aSElliott Hughes            self._encodeComponents(glyph)
1020*e1fe3e4aSElliott Hughes        elif glyph.isVarComposite():
1021*e1fe3e4aSElliott Hughes            raise NotImplementedError
1022*e1fe3e4aSElliott Hughes        else:
1023*e1fe3e4aSElliott Hughes            self._encodeCoordinates(glyph)
1024*e1fe3e4aSElliott Hughes            self._encodeOverlapSimpleFlag(glyph, glyphID)
1025*e1fe3e4aSElliott Hughes        self._encodeBBox(glyphID, glyph)
1026*e1fe3e4aSElliott Hughes
1027*e1fe3e4aSElliott Hughes    def _encodeComponents(self, glyph):
1028*e1fe3e4aSElliott Hughes        lastcomponent = len(glyph.components) - 1
1029*e1fe3e4aSElliott Hughes        more = 1
1030*e1fe3e4aSElliott Hughes        haveInstructions = 0
1031*e1fe3e4aSElliott Hughes        for i in range(len(glyph.components)):
1032*e1fe3e4aSElliott Hughes            if i == lastcomponent:
1033*e1fe3e4aSElliott Hughes                haveInstructions = hasattr(glyph, "program")
1034*e1fe3e4aSElliott Hughes                more = 0
1035*e1fe3e4aSElliott Hughes            component = glyph.components[i]
1036*e1fe3e4aSElliott Hughes            self.compositeStream += component.compile(more, haveInstructions, self)
1037*e1fe3e4aSElliott Hughes        if haveInstructions:
1038*e1fe3e4aSElliott Hughes            self._encodeInstructions(glyph)
1039*e1fe3e4aSElliott Hughes
1040*e1fe3e4aSElliott Hughes    def _encodeCoordinates(self, glyph):
1041*e1fe3e4aSElliott Hughes        lastEndPoint = -1
1042*e1fe3e4aSElliott Hughes        if _g_l_y_f.flagCubic in glyph.flags:
1043*e1fe3e4aSElliott Hughes            raise NotImplementedError
1044*e1fe3e4aSElliott Hughes        for endPoint in glyph.endPtsOfContours:
1045*e1fe3e4aSElliott Hughes            ptsOfContour = endPoint - lastEndPoint
1046*e1fe3e4aSElliott Hughes            self.nPointsStream += pack255UShort(ptsOfContour)
1047*e1fe3e4aSElliott Hughes            lastEndPoint = endPoint
1048*e1fe3e4aSElliott Hughes        self._encodeTriplets(glyph)
1049*e1fe3e4aSElliott Hughes        self._encodeInstructions(glyph)
1050*e1fe3e4aSElliott Hughes
1051*e1fe3e4aSElliott Hughes    def _encodeOverlapSimpleFlag(self, glyph, glyphID):
1052*e1fe3e4aSElliott Hughes        if glyph.numberOfContours <= 0:
1053*e1fe3e4aSElliott Hughes            return
1054*e1fe3e4aSElliott Hughes        if glyph.flags[0] & _g_l_y_f.flagOverlapSimple:
1055*e1fe3e4aSElliott Hughes            byte = glyphID >> 3
1056*e1fe3e4aSElliott Hughes            bit = glyphID & 7
1057*e1fe3e4aSElliott Hughes            self.overlapSimpleBitmap[byte] |= 0x80 >> bit
1058*e1fe3e4aSElliott Hughes
1059*e1fe3e4aSElliott Hughes    def _encodeInstructions(self, glyph):
1060*e1fe3e4aSElliott Hughes        instructions = glyph.program.getBytecode()
1061*e1fe3e4aSElliott Hughes        self.glyphStream += pack255UShort(len(instructions))
1062*e1fe3e4aSElliott Hughes        self.instructionStream += instructions
1063*e1fe3e4aSElliott Hughes
1064*e1fe3e4aSElliott Hughes    def _encodeBBox(self, glyphID, glyph):
1065*e1fe3e4aSElliott Hughes        assert glyph.numberOfContours != 0, "empty glyph has no bbox"
1066*e1fe3e4aSElliott Hughes        if not glyph.isComposite():
1067*e1fe3e4aSElliott Hughes            # for simple glyphs, compare the encoded bounding box info with the calculated
1068*e1fe3e4aSElliott Hughes            # values, and if they match omit the bounding box info
1069*e1fe3e4aSElliott Hughes            currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax
1070*e1fe3e4aSElliott Hughes            calculatedBBox = calcIntBounds(glyph.coordinates)
1071*e1fe3e4aSElliott Hughes            if currentBBox == calculatedBBox:
1072*e1fe3e4aSElliott Hughes                return
1073*e1fe3e4aSElliott Hughes        self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7)
1074*e1fe3e4aSElliott Hughes        self.bboxStream += sstruct.pack(bboxFormat, glyph)
1075*e1fe3e4aSElliott Hughes
1076*e1fe3e4aSElliott Hughes    def _encodeTriplets(self, glyph):
1077*e1fe3e4aSElliott Hughes        assert len(glyph.coordinates) == len(glyph.flags)
1078*e1fe3e4aSElliott Hughes        coordinates = glyph.coordinates.copy()
1079*e1fe3e4aSElliott Hughes        coordinates.absoluteToRelative()
1080*e1fe3e4aSElliott Hughes
1081*e1fe3e4aSElliott Hughes        flags = array.array("B")
1082*e1fe3e4aSElliott Hughes        triplets = array.array("B")
1083*e1fe3e4aSElliott Hughes        for i in range(len(coordinates)):
1084*e1fe3e4aSElliott Hughes            onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve
1085*e1fe3e4aSElliott Hughes            x, y = coordinates[i]
1086*e1fe3e4aSElliott Hughes            absX = abs(x)
1087*e1fe3e4aSElliott Hughes            absY = abs(y)
1088*e1fe3e4aSElliott Hughes            onCurveBit = 0 if onCurve else 128
1089*e1fe3e4aSElliott Hughes            xSignBit = 0 if (x < 0) else 1
1090*e1fe3e4aSElliott Hughes            ySignBit = 0 if (y < 0) else 1
1091*e1fe3e4aSElliott Hughes            xySignBits = xSignBit + 2 * ySignBit
1092*e1fe3e4aSElliott Hughes
1093*e1fe3e4aSElliott Hughes            if x == 0 and absY < 1280:
1094*e1fe3e4aSElliott Hughes                flags.append(onCurveBit + ((absY & 0xF00) >> 7) + ySignBit)
1095*e1fe3e4aSElliott Hughes                triplets.append(absY & 0xFF)
1096*e1fe3e4aSElliott Hughes            elif y == 0 and absX < 1280:
1097*e1fe3e4aSElliott Hughes                flags.append(onCurveBit + 10 + ((absX & 0xF00) >> 7) + xSignBit)
1098*e1fe3e4aSElliott Hughes                triplets.append(absX & 0xFF)
1099*e1fe3e4aSElliott Hughes            elif absX < 65 and absY < 65:
1100*e1fe3e4aSElliott Hughes                flags.append(
1101*e1fe3e4aSElliott Hughes                    onCurveBit
1102*e1fe3e4aSElliott Hughes                    + 20
1103*e1fe3e4aSElliott Hughes                    + ((absX - 1) & 0x30)
1104*e1fe3e4aSElliott Hughes                    + (((absY - 1) & 0x30) >> 2)
1105*e1fe3e4aSElliott Hughes                    + xySignBits
1106*e1fe3e4aSElliott Hughes                )
1107*e1fe3e4aSElliott Hughes                triplets.append((((absX - 1) & 0xF) << 4) | ((absY - 1) & 0xF))
1108*e1fe3e4aSElliott Hughes            elif absX < 769 and absY < 769:
1109*e1fe3e4aSElliott Hughes                flags.append(
1110*e1fe3e4aSElliott Hughes                    onCurveBit
1111*e1fe3e4aSElliott Hughes                    + 84
1112*e1fe3e4aSElliott Hughes                    + 12 * (((absX - 1) & 0x300) >> 8)
1113*e1fe3e4aSElliott Hughes                    + (((absY - 1) & 0x300) >> 6)
1114*e1fe3e4aSElliott Hughes                    + xySignBits
1115*e1fe3e4aSElliott Hughes                )
1116*e1fe3e4aSElliott Hughes                triplets.append((absX - 1) & 0xFF)
1117*e1fe3e4aSElliott Hughes                triplets.append((absY - 1) & 0xFF)
1118*e1fe3e4aSElliott Hughes            elif absX < 4096 and absY < 4096:
1119*e1fe3e4aSElliott Hughes                flags.append(onCurveBit + 120 + xySignBits)
1120*e1fe3e4aSElliott Hughes                triplets.append(absX >> 4)
1121*e1fe3e4aSElliott Hughes                triplets.append(((absX & 0xF) << 4) | (absY >> 8))
1122*e1fe3e4aSElliott Hughes                triplets.append(absY & 0xFF)
1123*e1fe3e4aSElliott Hughes            else:
1124*e1fe3e4aSElliott Hughes                flags.append(onCurveBit + 124 + xySignBits)
1125*e1fe3e4aSElliott Hughes                triplets.append(absX >> 8)
1126*e1fe3e4aSElliott Hughes                triplets.append(absX & 0xFF)
1127*e1fe3e4aSElliott Hughes                triplets.append(absY >> 8)
1128*e1fe3e4aSElliott Hughes                triplets.append(absY & 0xFF)
1129*e1fe3e4aSElliott Hughes
1130*e1fe3e4aSElliott Hughes        self.flagStream += flags.tobytes()
1131*e1fe3e4aSElliott Hughes        self.glyphStream += triplets.tobytes()
1132*e1fe3e4aSElliott Hughes
1133*e1fe3e4aSElliott Hughes
1134*e1fe3e4aSElliott Hughesclass WOFF2HmtxTable(getTableClass("hmtx")):
1135*e1fe3e4aSElliott Hughes    def __init__(self, tag=None):
1136*e1fe3e4aSElliott Hughes        self.tableTag = Tag(tag or "hmtx")
1137*e1fe3e4aSElliott Hughes
1138*e1fe3e4aSElliott Hughes    def reconstruct(self, data, ttFont):
1139*e1fe3e4aSElliott Hughes        (flags,) = struct.unpack(">B", data[:1])
1140*e1fe3e4aSElliott Hughes        data = data[1:]
1141*e1fe3e4aSElliott Hughes        if flags & 0b11111100 != 0:
1142*e1fe3e4aSElliott Hughes            raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag)
1143*e1fe3e4aSElliott Hughes
1144*e1fe3e4aSElliott Hughes        # When bit 0 is _not_ set, the lsb[] array is present
1145*e1fe3e4aSElliott Hughes        hasLsbArray = flags & 1 == 0
1146*e1fe3e4aSElliott Hughes        # When bit 1 is _not_ set, the leftSideBearing[] array is present
1147*e1fe3e4aSElliott Hughes        hasLeftSideBearingArray = flags & 2 == 0
1148*e1fe3e4aSElliott Hughes        if hasLsbArray and hasLeftSideBearingArray:
1149*e1fe3e4aSElliott Hughes            raise TTLibError(
1150*e1fe3e4aSElliott Hughes                "either bits 0 or 1 (or both) must set in transformed '%s' flags"
1151*e1fe3e4aSElliott Hughes                % self.tableTag
1152*e1fe3e4aSElliott Hughes            )
1153*e1fe3e4aSElliott Hughes
1154*e1fe3e4aSElliott Hughes        glyfTable = ttFont["glyf"]
1155*e1fe3e4aSElliott Hughes        headerTable = ttFont["hhea"]
1156*e1fe3e4aSElliott Hughes        glyphOrder = glyfTable.glyphOrder
1157*e1fe3e4aSElliott Hughes        numGlyphs = len(glyphOrder)
1158*e1fe3e4aSElliott Hughes        numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs)
1159*e1fe3e4aSElliott Hughes
1160*e1fe3e4aSElliott Hughes        assert len(data) >= 2 * numberOfHMetrics
1161*e1fe3e4aSElliott Hughes        advanceWidthArray = array.array("H", data[: 2 * numberOfHMetrics])
1162*e1fe3e4aSElliott Hughes        if sys.byteorder != "big":
1163*e1fe3e4aSElliott Hughes            advanceWidthArray.byteswap()
1164*e1fe3e4aSElliott Hughes        data = data[2 * numberOfHMetrics :]
1165*e1fe3e4aSElliott Hughes
1166*e1fe3e4aSElliott Hughes        if hasLsbArray:
1167*e1fe3e4aSElliott Hughes            assert len(data) >= 2 * numberOfHMetrics
1168*e1fe3e4aSElliott Hughes            lsbArray = array.array("h", data[: 2 * numberOfHMetrics])
1169*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
1170*e1fe3e4aSElliott Hughes                lsbArray.byteswap()
1171*e1fe3e4aSElliott Hughes            data = data[2 * numberOfHMetrics :]
1172*e1fe3e4aSElliott Hughes        else:
1173*e1fe3e4aSElliott Hughes            # compute (proportional) glyphs' lsb from their xMin
1174*e1fe3e4aSElliott Hughes            lsbArray = array.array("h")
1175*e1fe3e4aSElliott Hughes            for i, glyphName in enumerate(glyphOrder):
1176*e1fe3e4aSElliott Hughes                if i >= numberOfHMetrics:
1177*e1fe3e4aSElliott Hughes                    break
1178*e1fe3e4aSElliott Hughes                glyph = glyfTable[glyphName]
1179*e1fe3e4aSElliott Hughes                xMin = getattr(glyph, "xMin", 0)
1180*e1fe3e4aSElliott Hughes                lsbArray.append(xMin)
1181*e1fe3e4aSElliott Hughes
1182*e1fe3e4aSElliott Hughes        numberOfSideBearings = numGlyphs - numberOfHMetrics
1183*e1fe3e4aSElliott Hughes        if hasLeftSideBearingArray:
1184*e1fe3e4aSElliott Hughes            assert len(data) >= 2 * numberOfSideBearings
1185*e1fe3e4aSElliott Hughes            leftSideBearingArray = array.array("h", data[: 2 * numberOfSideBearings])
1186*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
1187*e1fe3e4aSElliott Hughes                leftSideBearingArray.byteswap()
1188*e1fe3e4aSElliott Hughes            data = data[2 * numberOfSideBearings :]
1189*e1fe3e4aSElliott Hughes        else:
1190*e1fe3e4aSElliott Hughes            # compute (monospaced) glyphs' leftSideBearing from their xMin
1191*e1fe3e4aSElliott Hughes            leftSideBearingArray = array.array("h")
1192*e1fe3e4aSElliott Hughes            for i, glyphName in enumerate(glyphOrder):
1193*e1fe3e4aSElliott Hughes                if i < numberOfHMetrics:
1194*e1fe3e4aSElliott Hughes                    continue
1195*e1fe3e4aSElliott Hughes                glyph = glyfTable[glyphName]
1196*e1fe3e4aSElliott Hughes                xMin = getattr(glyph, "xMin", 0)
1197*e1fe3e4aSElliott Hughes                leftSideBearingArray.append(xMin)
1198*e1fe3e4aSElliott Hughes
1199*e1fe3e4aSElliott Hughes        if data:
1200*e1fe3e4aSElliott Hughes            raise TTLibError("too much '%s' table data" % self.tableTag)
1201*e1fe3e4aSElliott Hughes
1202*e1fe3e4aSElliott Hughes        self.metrics = {}
1203*e1fe3e4aSElliott Hughes        for i in range(numberOfHMetrics):
1204*e1fe3e4aSElliott Hughes            glyphName = glyphOrder[i]
1205*e1fe3e4aSElliott Hughes            advanceWidth, lsb = advanceWidthArray[i], lsbArray[i]
1206*e1fe3e4aSElliott Hughes            self.metrics[glyphName] = (advanceWidth, lsb)
1207*e1fe3e4aSElliott Hughes        lastAdvance = advanceWidthArray[-1]
1208*e1fe3e4aSElliott Hughes        for i in range(numberOfSideBearings):
1209*e1fe3e4aSElliott Hughes            glyphName = glyphOrder[i + numberOfHMetrics]
1210*e1fe3e4aSElliott Hughes            self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i])
1211*e1fe3e4aSElliott Hughes
1212*e1fe3e4aSElliott Hughes    def transform(self, ttFont):
1213*e1fe3e4aSElliott Hughes        glyphOrder = ttFont.getGlyphOrder()
1214*e1fe3e4aSElliott Hughes        glyf = ttFont["glyf"]
1215*e1fe3e4aSElliott Hughes        hhea = ttFont["hhea"]
1216*e1fe3e4aSElliott Hughes        numberOfHMetrics = hhea.numberOfHMetrics
1217*e1fe3e4aSElliott Hughes
1218*e1fe3e4aSElliott Hughes        # check if any of the proportional glyphs has left sidebearings that
1219*e1fe3e4aSElliott Hughes        # differ from their xMin bounding box values.
1220*e1fe3e4aSElliott Hughes        hasLsbArray = False
1221*e1fe3e4aSElliott Hughes        for i in range(numberOfHMetrics):
1222*e1fe3e4aSElliott Hughes            glyphName = glyphOrder[i]
1223*e1fe3e4aSElliott Hughes            lsb = self.metrics[glyphName][1]
1224*e1fe3e4aSElliott Hughes            if lsb != getattr(glyf[glyphName], "xMin", 0):
1225*e1fe3e4aSElliott Hughes                hasLsbArray = True
1226*e1fe3e4aSElliott Hughes                break
1227*e1fe3e4aSElliott Hughes
1228*e1fe3e4aSElliott Hughes        # do the same for the monospaced glyphs (if any) at the end of hmtx table
1229*e1fe3e4aSElliott Hughes        hasLeftSideBearingArray = False
1230*e1fe3e4aSElliott Hughes        for i in range(numberOfHMetrics, len(glyphOrder)):
1231*e1fe3e4aSElliott Hughes            glyphName = glyphOrder[i]
1232*e1fe3e4aSElliott Hughes            lsb = self.metrics[glyphName][1]
1233*e1fe3e4aSElliott Hughes            if lsb != getattr(glyf[glyphName], "xMin", 0):
1234*e1fe3e4aSElliott Hughes                hasLeftSideBearingArray = True
1235*e1fe3e4aSElliott Hughes                break
1236*e1fe3e4aSElliott Hughes
1237*e1fe3e4aSElliott Hughes        # if we need to encode both sidebearings arrays, then no transformation is
1238*e1fe3e4aSElliott Hughes        # applicable, and we must use the untransformed hmtx data
1239*e1fe3e4aSElliott Hughes        if hasLsbArray and hasLeftSideBearingArray:
1240*e1fe3e4aSElliott Hughes            return
1241*e1fe3e4aSElliott Hughes
1242*e1fe3e4aSElliott Hughes        # set bit 0 and 1 when the respective arrays are _not_ present
1243*e1fe3e4aSElliott Hughes        flags = 0
1244*e1fe3e4aSElliott Hughes        if not hasLsbArray:
1245*e1fe3e4aSElliott Hughes            flags |= 1 << 0
1246*e1fe3e4aSElliott Hughes        if not hasLeftSideBearingArray:
1247*e1fe3e4aSElliott Hughes            flags |= 1 << 1
1248*e1fe3e4aSElliott Hughes
1249*e1fe3e4aSElliott Hughes        data = struct.pack(">B", flags)
1250*e1fe3e4aSElliott Hughes
1251*e1fe3e4aSElliott Hughes        advanceWidthArray = array.array(
1252*e1fe3e4aSElliott Hughes            "H",
1253*e1fe3e4aSElliott Hughes            [
1254*e1fe3e4aSElliott Hughes                self.metrics[glyphName][0]
1255*e1fe3e4aSElliott Hughes                for i, glyphName in enumerate(glyphOrder)
1256*e1fe3e4aSElliott Hughes                if i < numberOfHMetrics
1257*e1fe3e4aSElliott Hughes            ],
1258*e1fe3e4aSElliott Hughes        )
1259*e1fe3e4aSElliott Hughes        if sys.byteorder != "big":
1260*e1fe3e4aSElliott Hughes            advanceWidthArray.byteswap()
1261*e1fe3e4aSElliott Hughes        data += advanceWidthArray.tobytes()
1262*e1fe3e4aSElliott Hughes
1263*e1fe3e4aSElliott Hughes        if hasLsbArray:
1264*e1fe3e4aSElliott Hughes            lsbArray = array.array(
1265*e1fe3e4aSElliott Hughes                "h",
1266*e1fe3e4aSElliott Hughes                [
1267*e1fe3e4aSElliott Hughes                    self.metrics[glyphName][1]
1268*e1fe3e4aSElliott Hughes                    for i, glyphName in enumerate(glyphOrder)
1269*e1fe3e4aSElliott Hughes                    if i < numberOfHMetrics
1270*e1fe3e4aSElliott Hughes                ],
1271*e1fe3e4aSElliott Hughes            )
1272*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
1273*e1fe3e4aSElliott Hughes                lsbArray.byteswap()
1274*e1fe3e4aSElliott Hughes            data += lsbArray.tobytes()
1275*e1fe3e4aSElliott Hughes
1276*e1fe3e4aSElliott Hughes        if hasLeftSideBearingArray:
1277*e1fe3e4aSElliott Hughes            leftSideBearingArray = array.array(
1278*e1fe3e4aSElliott Hughes                "h",
1279*e1fe3e4aSElliott Hughes                [
1280*e1fe3e4aSElliott Hughes                    self.metrics[glyphOrder[i]][1]
1281*e1fe3e4aSElliott Hughes                    for i in range(numberOfHMetrics, len(glyphOrder))
1282*e1fe3e4aSElliott Hughes                ],
1283*e1fe3e4aSElliott Hughes            )
1284*e1fe3e4aSElliott Hughes            if sys.byteorder != "big":
1285*e1fe3e4aSElliott Hughes                leftSideBearingArray.byteswap()
1286*e1fe3e4aSElliott Hughes            data += leftSideBearingArray.tobytes()
1287*e1fe3e4aSElliott Hughes
1288*e1fe3e4aSElliott Hughes        return data
1289*e1fe3e4aSElliott Hughes
1290*e1fe3e4aSElliott Hughes
1291*e1fe3e4aSElliott Hughesclass WOFF2FlavorData(WOFFFlavorData):
1292*e1fe3e4aSElliott Hughes    Flavor = "woff2"
1293*e1fe3e4aSElliott Hughes
1294*e1fe3e4aSElliott Hughes    def __init__(self, reader=None, data=None, transformedTables=None):
1295*e1fe3e4aSElliott Hughes        """Data class that holds the WOFF2 header major/minor version, any
1296*e1fe3e4aSElliott Hughes        metadata or private data (as bytes strings), and the set of
1297*e1fe3e4aSElliott Hughes        table tags that have transformations applied (if reader is not None),
1298*e1fe3e4aSElliott Hughes        or will have once the WOFF2 font is compiled.
1299*e1fe3e4aSElliott Hughes
1300*e1fe3e4aSElliott Hughes        Args:
1301*e1fe3e4aSElliott Hughes                reader: an SFNTReader (or subclass) object to read flavor data from.
1302*e1fe3e4aSElliott Hughes                data: another WOFFFlavorData object to initialise data from.
1303*e1fe3e4aSElliott Hughes                transformedTables: set of strings containing table tags to be transformed.
1304*e1fe3e4aSElliott Hughes
1305*e1fe3e4aSElliott Hughes        Raises:
1306*e1fe3e4aSElliott Hughes                ImportError if the brotli module is not installed.
1307*e1fe3e4aSElliott Hughes
1308*e1fe3e4aSElliott Hughes        NOTE: The 'reader' argument, on the one hand, and the 'data' and
1309*e1fe3e4aSElliott Hughes        'transformedTables' arguments, on the other hand, are mutually exclusive.
1310*e1fe3e4aSElliott Hughes        """
1311*e1fe3e4aSElliott Hughes        if not haveBrotli:
1312*e1fe3e4aSElliott Hughes            raise ImportError("No module named brotli")
1313*e1fe3e4aSElliott Hughes
1314*e1fe3e4aSElliott Hughes        if reader is not None:
1315*e1fe3e4aSElliott Hughes            if data is not None:
1316*e1fe3e4aSElliott Hughes                raise TypeError("'reader' and 'data' arguments are mutually exclusive")
1317*e1fe3e4aSElliott Hughes            if transformedTables is not None:
1318*e1fe3e4aSElliott Hughes                raise TypeError(
1319*e1fe3e4aSElliott Hughes                    "'reader' and 'transformedTables' arguments are mutually exclusive"
1320*e1fe3e4aSElliott Hughes                )
1321*e1fe3e4aSElliott Hughes
1322*e1fe3e4aSElliott Hughes        if transformedTables is not None and (
1323*e1fe3e4aSElliott Hughes            "glyf" in transformedTables
1324*e1fe3e4aSElliott Hughes            and "loca" not in transformedTables
1325*e1fe3e4aSElliott Hughes            or "loca" in transformedTables
1326*e1fe3e4aSElliott Hughes            and "glyf" not in transformedTables
1327*e1fe3e4aSElliott Hughes        ):
1328*e1fe3e4aSElliott Hughes            raise ValueError("'glyf' and 'loca' must be transformed (or not) together")
1329*e1fe3e4aSElliott Hughes        super(WOFF2FlavorData, self).__init__(reader=reader)
1330*e1fe3e4aSElliott Hughes        if reader:
1331*e1fe3e4aSElliott Hughes            transformedTables = [
1332*e1fe3e4aSElliott Hughes                tag for tag, entry in reader.tables.items() if entry.transformed
1333*e1fe3e4aSElliott Hughes            ]
1334*e1fe3e4aSElliott Hughes        elif data:
1335*e1fe3e4aSElliott Hughes            self.majorVersion = data.majorVersion
1336*e1fe3e4aSElliott Hughes            self.majorVersion = data.minorVersion
1337*e1fe3e4aSElliott Hughes            self.metaData = data.metaData
1338*e1fe3e4aSElliott Hughes            self.privData = data.privData
1339*e1fe3e4aSElliott Hughes            if transformedTables is None and hasattr(data, "transformedTables"):
1340*e1fe3e4aSElliott Hughes                transformedTables = data.transformedTables
1341*e1fe3e4aSElliott Hughes
1342*e1fe3e4aSElliott Hughes        if transformedTables is None:
1343*e1fe3e4aSElliott Hughes            transformedTables = woff2TransformedTableTags
1344*e1fe3e4aSElliott Hughes
1345*e1fe3e4aSElliott Hughes        self.transformedTables = set(transformedTables)
1346*e1fe3e4aSElliott Hughes
1347*e1fe3e4aSElliott Hughes    def _decompress(self, rawData):
1348*e1fe3e4aSElliott Hughes        return brotli.decompress(rawData)
1349*e1fe3e4aSElliott Hughes
1350*e1fe3e4aSElliott Hughes
1351*e1fe3e4aSElliott Hughesdef unpackBase128(data):
1352*e1fe3e4aSElliott Hughes    r"""Read one to five bytes from UIntBase128-encoded input string, and return
1353*e1fe3e4aSElliott Hughes    a tuple containing the decoded integer plus any leftover data.
1354*e1fe3e4aSElliott Hughes
1355*e1fe3e4aSElliott Hughes    >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00")
1356*e1fe3e4aSElliott Hughes    True
1357*e1fe3e4aSElliott Hughes    >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295
1358*e1fe3e4aSElliott Hughes    True
1359*e1fe3e4aSElliott Hughes    >>> unpackBase128(b'\x80\x80\x3f')  # doctest: +IGNORE_EXCEPTION_DETAIL
1360*e1fe3e4aSElliott Hughes    Traceback (most recent call last):
1361*e1fe3e4aSElliott Hughes      File "<stdin>", line 1, in ?
1362*e1fe3e4aSElliott Hughes    TTLibError: UIntBase128 value must not start with leading zeros
1363*e1fe3e4aSElliott Hughes    >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0]  # doctest: +IGNORE_EXCEPTION_DETAIL
1364*e1fe3e4aSElliott Hughes    Traceback (most recent call last):
1365*e1fe3e4aSElliott Hughes      File "<stdin>", line 1, in ?
1366*e1fe3e4aSElliott Hughes    TTLibError: UIntBase128-encoded sequence is longer than 5 bytes
1367*e1fe3e4aSElliott Hughes    >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0]  # doctest: +IGNORE_EXCEPTION_DETAIL
1368*e1fe3e4aSElliott Hughes    Traceback (most recent call last):
1369*e1fe3e4aSElliott Hughes      File "<stdin>", line 1, in ?
1370*e1fe3e4aSElliott Hughes    TTLibError: UIntBase128 value exceeds 2**32-1
1371*e1fe3e4aSElliott Hughes    """
1372*e1fe3e4aSElliott Hughes    if len(data) == 0:
1373*e1fe3e4aSElliott Hughes        raise TTLibError("not enough data to unpack UIntBase128")
1374*e1fe3e4aSElliott Hughes    result = 0
1375*e1fe3e4aSElliott Hughes    if byteord(data[0]) == 0x80:
1376*e1fe3e4aSElliott Hughes        # font must be rejected if UIntBase128 value starts with 0x80
1377*e1fe3e4aSElliott Hughes        raise TTLibError("UIntBase128 value must not start with leading zeros")
1378*e1fe3e4aSElliott Hughes    for i in range(woff2Base128MaxSize):
1379*e1fe3e4aSElliott Hughes        if len(data) == 0:
1380*e1fe3e4aSElliott Hughes            raise TTLibError("not enough data to unpack UIntBase128")
1381*e1fe3e4aSElliott Hughes        code = byteord(data[0])
1382*e1fe3e4aSElliott Hughes        data = data[1:]
1383*e1fe3e4aSElliott Hughes        # if any of the top seven bits are set then we're about to overflow
1384*e1fe3e4aSElliott Hughes        if result & 0xFE000000:
1385*e1fe3e4aSElliott Hughes            raise TTLibError("UIntBase128 value exceeds 2**32-1")
1386*e1fe3e4aSElliott Hughes        # set current value = old value times 128 bitwise-or (byte bitwise-and 127)
1387*e1fe3e4aSElliott Hughes        result = (result << 7) | (code & 0x7F)
1388*e1fe3e4aSElliott Hughes        # repeat until the most significant bit of byte is false
1389*e1fe3e4aSElliott Hughes        if (code & 0x80) == 0:
1390*e1fe3e4aSElliott Hughes            # return result plus left over data
1391*e1fe3e4aSElliott Hughes            return result, data
1392*e1fe3e4aSElliott Hughes    # make sure not to exceed the size bound
1393*e1fe3e4aSElliott Hughes    raise TTLibError("UIntBase128-encoded sequence is longer than 5 bytes")
1394*e1fe3e4aSElliott Hughes
1395*e1fe3e4aSElliott Hughes
1396*e1fe3e4aSElliott Hughesdef base128Size(n):
1397*e1fe3e4aSElliott Hughes    """Return the length in bytes of a UIntBase128-encoded sequence with value n.
1398*e1fe3e4aSElliott Hughes
1399*e1fe3e4aSElliott Hughes    >>> base128Size(0)
1400*e1fe3e4aSElliott Hughes    1
1401*e1fe3e4aSElliott Hughes    >>> base128Size(24567)
1402*e1fe3e4aSElliott Hughes    3
1403*e1fe3e4aSElliott Hughes    >>> base128Size(2**32-1)
1404*e1fe3e4aSElliott Hughes    5
1405*e1fe3e4aSElliott Hughes    """
1406*e1fe3e4aSElliott Hughes    assert n >= 0
1407*e1fe3e4aSElliott Hughes    size = 1
1408*e1fe3e4aSElliott Hughes    while n >= 128:
1409*e1fe3e4aSElliott Hughes        size += 1
1410*e1fe3e4aSElliott Hughes        n >>= 7
1411*e1fe3e4aSElliott Hughes    return size
1412*e1fe3e4aSElliott Hughes
1413*e1fe3e4aSElliott Hughes
1414*e1fe3e4aSElliott Hughesdef packBase128(n):
1415*e1fe3e4aSElliott Hughes    r"""Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of
1416*e1fe3e4aSElliott Hughes    bytes using UIntBase128 variable-length encoding. Produce the shortest possible
1417*e1fe3e4aSElliott Hughes    encoding.
1418*e1fe3e4aSElliott Hughes
1419*e1fe3e4aSElliott Hughes    >>> packBase128(63) == b"\x3f"
1420*e1fe3e4aSElliott Hughes    True
1421*e1fe3e4aSElliott Hughes    >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f'
1422*e1fe3e4aSElliott Hughes    True
1423*e1fe3e4aSElliott Hughes    """
1424*e1fe3e4aSElliott Hughes    if n < 0 or n >= 2**32:
1425*e1fe3e4aSElliott Hughes        raise TTLibError("UIntBase128 format requires 0 <= integer <= 2**32-1")
1426*e1fe3e4aSElliott Hughes    data = b""
1427*e1fe3e4aSElliott Hughes    size = base128Size(n)
1428*e1fe3e4aSElliott Hughes    for i in range(size):
1429*e1fe3e4aSElliott Hughes        b = (n >> (7 * (size - i - 1))) & 0x7F
1430*e1fe3e4aSElliott Hughes        if i < size - 1:
1431*e1fe3e4aSElliott Hughes            b |= 0x80
1432*e1fe3e4aSElliott Hughes        data += struct.pack("B", b)
1433*e1fe3e4aSElliott Hughes    return data
1434*e1fe3e4aSElliott Hughes
1435*e1fe3e4aSElliott Hughes
1436*e1fe3e4aSElliott Hughesdef unpack255UShort(data):
1437*e1fe3e4aSElliott Hughes    """Read one to three bytes from 255UInt16-encoded input string, and return a
1438*e1fe3e4aSElliott Hughes    tuple containing the decoded integer plus any leftover data.
1439*e1fe3e4aSElliott Hughes
1440*e1fe3e4aSElliott Hughes    >>> unpack255UShort(bytechr(252))[0]
1441*e1fe3e4aSElliott Hughes    252
1442*e1fe3e4aSElliott Hughes
1443*e1fe3e4aSElliott Hughes    Note that some numbers (e.g. 506) can have multiple encodings:
1444*e1fe3e4aSElliott Hughes    >>> unpack255UShort(struct.pack("BB", 254, 0))[0]
1445*e1fe3e4aSElliott Hughes    506
1446*e1fe3e4aSElliott Hughes    >>> unpack255UShort(struct.pack("BB", 255, 253))[0]
1447*e1fe3e4aSElliott Hughes    506
1448*e1fe3e4aSElliott Hughes    >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0]
1449*e1fe3e4aSElliott Hughes    506
1450*e1fe3e4aSElliott Hughes    """
1451*e1fe3e4aSElliott Hughes    code = byteord(data[:1])
1452*e1fe3e4aSElliott Hughes    data = data[1:]
1453*e1fe3e4aSElliott Hughes    if code == 253:
1454*e1fe3e4aSElliott Hughes        # read two more bytes as an unsigned short
1455*e1fe3e4aSElliott Hughes        if len(data) < 2:
1456*e1fe3e4aSElliott Hughes            raise TTLibError("not enough data to unpack 255UInt16")
1457*e1fe3e4aSElliott Hughes        (result,) = struct.unpack(">H", data[:2])
1458*e1fe3e4aSElliott Hughes        data = data[2:]
1459*e1fe3e4aSElliott Hughes    elif code == 254:
1460*e1fe3e4aSElliott Hughes        # read another byte, plus 253 * 2
1461*e1fe3e4aSElliott Hughes        if len(data) == 0:
1462*e1fe3e4aSElliott Hughes            raise TTLibError("not enough data to unpack 255UInt16")
1463*e1fe3e4aSElliott Hughes        result = byteord(data[:1])
1464*e1fe3e4aSElliott Hughes        result += 506
1465*e1fe3e4aSElliott Hughes        data = data[1:]
1466*e1fe3e4aSElliott Hughes    elif code == 255:
1467*e1fe3e4aSElliott Hughes        # read another byte, plus 253
1468*e1fe3e4aSElliott Hughes        if len(data) == 0:
1469*e1fe3e4aSElliott Hughes            raise TTLibError("not enough data to unpack 255UInt16")
1470*e1fe3e4aSElliott Hughes        result = byteord(data[:1])
1471*e1fe3e4aSElliott Hughes        result += 253
1472*e1fe3e4aSElliott Hughes        data = data[1:]
1473*e1fe3e4aSElliott Hughes    else:
1474*e1fe3e4aSElliott Hughes        # leave as is if lower than 253
1475*e1fe3e4aSElliott Hughes        result = code
1476*e1fe3e4aSElliott Hughes    # return result plus left over data
1477*e1fe3e4aSElliott Hughes    return result, data
1478*e1fe3e4aSElliott Hughes
1479*e1fe3e4aSElliott Hughes
1480*e1fe3e4aSElliott Hughesdef pack255UShort(value):
1481*e1fe3e4aSElliott Hughes    r"""Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring
1482*e1fe3e4aSElliott Hughes    using 255UInt16 variable-length encoding.
1483*e1fe3e4aSElliott Hughes
1484*e1fe3e4aSElliott Hughes    >>> pack255UShort(252) == b'\xfc'
1485*e1fe3e4aSElliott Hughes    True
1486*e1fe3e4aSElliott Hughes    >>> pack255UShort(506) == b'\xfe\x00'
1487*e1fe3e4aSElliott Hughes    True
1488*e1fe3e4aSElliott Hughes    >>> pack255UShort(762) == b'\xfd\x02\xfa'
1489*e1fe3e4aSElliott Hughes    True
1490*e1fe3e4aSElliott Hughes    """
1491*e1fe3e4aSElliott Hughes    if value < 0 or value > 0xFFFF:
1492*e1fe3e4aSElliott Hughes        raise TTLibError("255UInt16 format requires 0 <= integer <= 65535")
1493*e1fe3e4aSElliott Hughes    if value < 253:
1494*e1fe3e4aSElliott Hughes        return struct.pack(">B", value)
1495*e1fe3e4aSElliott Hughes    elif value < 506:
1496*e1fe3e4aSElliott Hughes        return struct.pack(">BB", 255, value - 253)
1497*e1fe3e4aSElliott Hughes    elif value < 762:
1498*e1fe3e4aSElliott Hughes        return struct.pack(">BB", 254, value - 506)
1499*e1fe3e4aSElliott Hughes    else:
1500*e1fe3e4aSElliott Hughes        return struct.pack(">BH", 253, value)
1501*e1fe3e4aSElliott Hughes
1502*e1fe3e4aSElliott Hughes
1503*e1fe3e4aSElliott Hughesdef compress(input_file, output_file, transform_tables=None):
1504*e1fe3e4aSElliott Hughes    """Compress OpenType font to WOFF2.
1505*e1fe3e4aSElliott Hughes
1506*e1fe3e4aSElliott Hughes    Args:
1507*e1fe3e4aSElliott Hughes            input_file: a file path, file or file-like object (open in binary mode)
1508*e1fe3e4aSElliott Hughes                    containing an OpenType font (either CFF- or TrueType-flavored).
1509*e1fe3e4aSElliott Hughes            output_file: a file path, file or file-like object where to save the
1510*e1fe3e4aSElliott Hughes                    compressed WOFF2 font.
1511*e1fe3e4aSElliott Hughes            transform_tables: Optional[Iterable[str]]: a set of table tags for which
1512*e1fe3e4aSElliott Hughes                    to enable preprocessing transformations. By default, only 'glyf'
1513*e1fe3e4aSElliott Hughes                    and 'loca' tables are transformed. An empty set means disable all
1514*e1fe3e4aSElliott Hughes                    transformations.
1515*e1fe3e4aSElliott Hughes    """
1516*e1fe3e4aSElliott Hughes    log.info("Processing %s => %s" % (input_file, output_file))
1517*e1fe3e4aSElliott Hughes
1518*e1fe3e4aSElliott Hughes    font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
1519*e1fe3e4aSElliott Hughes    font.flavor = "woff2"
1520*e1fe3e4aSElliott Hughes
1521*e1fe3e4aSElliott Hughes    if transform_tables is not None:
1522*e1fe3e4aSElliott Hughes        font.flavorData = WOFF2FlavorData(
1523*e1fe3e4aSElliott Hughes            data=font.flavorData, transformedTables=transform_tables
1524*e1fe3e4aSElliott Hughes        )
1525*e1fe3e4aSElliott Hughes
1526*e1fe3e4aSElliott Hughes    font.save(output_file, reorderTables=False)
1527*e1fe3e4aSElliott Hughes
1528*e1fe3e4aSElliott Hughes
1529*e1fe3e4aSElliott Hughesdef decompress(input_file, output_file):
1530*e1fe3e4aSElliott Hughes    """Decompress WOFF2 font to OpenType font.
1531*e1fe3e4aSElliott Hughes
1532*e1fe3e4aSElliott Hughes    Args:
1533*e1fe3e4aSElliott Hughes            input_file: a file path, file or file-like object (open in binary mode)
1534*e1fe3e4aSElliott Hughes                    containing a compressed WOFF2 font.
1535*e1fe3e4aSElliott Hughes            output_file: a file path, file or file-like object where to save the
1536*e1fe3e4aSElliott Hughes                    decompressed OpenType font.
1537*e1fe3e4aSElliott Hughes    """
1538*e1fe3e4aSElliott Hughes    log.info("Processing %s => %s" % (input_file, output_file))
1539*e1fe3e4aSElliott Hughes
1540*e1fe3e4aSElliott Hughes    font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False)
1541*e1fe3e4aSElliott Hughes    font.flavor = None
1542*e1fe3e4aSElliott Hughes    font.flavorData = None
1543*e1fe3e4aSElliott Hughes    font.save(output_file, reorderTables=True)
1544*e1fe3e4aSElliott Hughes
1545*e1fe3e4aSElliott Hughes
1546*e1fe3e4aSElliott Hughesdef main(args=None):
1547*e1fe3e4aSElliott Hughes    """Compress and decompress WOFF2 fonts"""
1548*e1fe3e4aSElliott Hughes    import argparse
1549*e1fe3e4aSElliott Hughes    from fontTools import configLogger
1550*e1fe3e4aSElliott Hughes    from fontTools.ttx import makeOutputFileName
1551*e1fe3e4aSElliott Hughes
1552*e1fe3e4aSElliott Hughes    class _HelpAction(argparse._HelpAction):
1553*e1fe3e4aSElliott Hughes        def __call__(self, parser, namespace, values, option_string=None):
1554*e1fe3e4aSElliott Hughes            subparsers_actions = [
1555*e1fe3e4aSElliott Hughes                action
1556*e1fe3e4aSElliott Hughes                for action in parser._actions
1557*e1fe3e4aSElliott Hughes                if isinstance(action, argparse._SubParsersAction)
1558*e1fe3e4aSElliott Hughes            ]
1559*e1fe3e4aSElliott Hughes            for subparsers_action in subparsers_actions:
1560*e1fe3e4aSElliott Hughes                for choice, subparser in subparsers_action.choices.items():
1561*e1fe3e4aSElliott Hughes                    print(subparser.format_help())
1562*e1fe3e4aSElliott Hughes            parser.exit()
1563*e1fe3e4aSElliott Hughes
1564*e1fe3e4aSElliott Hughes    class _NoGlyfTransformAction(argparse.Action):
1565*e1fe3e4aSElliott Hughes        def __call__(self, parser, namespace, values, option_string=None):
1566*e1fe3e4aSElliott Hughes            namespace.transform_tables.difference_update({"glyf", "loca"})
1567*e1fe3e4aSElliott Hughes
1568*e1fe3e4aSElliott Hughes    class _HmtxTransformAction(argparse.Action):
1569*e1fe3e4aSElliott Hughes        def __call__(self, parser, namespace, values, option_string=None):
1570*e1fe3e4aSElliott Hughes            namespace.transform_tables.add("hmtx")
1571*e1fe3e4aSElliott Hughes
1572*e1fe3e4aSElliott Hughes    parser = argparse.ArgumentParser(
1573*e1fe3e4aSElliott Hughes        prog="fonttools ttLib.woff2", description=main.__doc__, add_help=False
1574*e1fe3e4aSElliott Hughes    )
1575*e1fe3e4aSElliott Hughes
1576*e1fe3e4aSElliott Hughes    parser.add_argument(
1577*e1fe3e4aSElliott Hughes        "-h", "--help", action=_HelpAction, help="show this help message and exit"
1578*e1fe3e4aSElliott Hughes    )
1579*e1fe3e4aSElliott Hughes
1580*e1fe3e4aSElliott Hughes    parser_group = parser.add_subparsers(title="sub-commands")
1581*e1fe3e4aSElliott Hughes    parser_compress = parser_group.add_parser(
1582*e1fe3e4aSElliott Hughes        "compress", description="Compress a TTF or OTF font to WOFF2"
1583*e1fe3e4aSElliott Hughes    )
1584*e1fe3e4aSElliott Hughes    parser_decompress = parser_group.add_parser(
1585*e1fe3e4aSElliott Hughes        "decompress", description="Decompress a WOFF2 font to OTF"
1586*e1fe3e4aSElliott Hughes    )
1587*e1fe3e4aSElliott Hughes
1588*e1fe3e4aSElliott Hughes    for subparser in (parser_compress, parser_decompress):
1589*e1fe3e4aSElliott Hughes        group = subparser.add_mutually_exclusive_group(required=False)
1590*e1fe3e4aSElliott Hughes        group.add_argument(
1591*e1fe3e4aSElliott Hughes            "-v",
1592*e1fe3e4aSElliott Hughes            "--verbose",
1593*e1fe3e4aSElliott Hughes            action="store_true",
1594*e1fe3e4aSElliott Hughes            help="print more messages to console",
1595*e1fe3e4aSElliott Hughes        )
1596*e1fe3e4aSElliott Hughes        group.add_argument(
1597*e1fe3e4aSElliott Hughes            "-q",
1598*e1fe3e4aSElliott Hughes            "--quiet",
1599*e1fe3e4aSElliott Hughes            action="store_true",
1600*e1fe3e4aSElliott Hughes            help="do not print messages to console",
1601*e1fe3e4aSElliott Hughes        )
1602*e1fe3e4aSElliott Hughes
1603*e1fe3e4aSElliott Hughes    parser_compress.add_argument(
1604*e1fe3e4aSElliott Hughes        "input_file",
1605*e1fe3e4aSElliott Hughes        metavar="INPUT",
1606*e1fe3e4aSElliott Hughes        help="the input OpenType font (.ttf or .otf)",
1607*e1fe3e4aSElliott Hughes    )
1608*e1fe3e4aSElliott Hughes    parser_decompress.add_argument(
1609*e1fe3e4aSElliott Hughes        "input_file",
1610*e1fe3e4aSElliott Hughes        metavar="INPUT",
1611*e1fe3e4aSElliott Hughes        help="the input WOFF2 font",
1612*e1fe3e4aSElliott Hughes    )
1613*e1fe3e4aSElliott Hughes
1614*e1fe3e4aSElliott Hughes    parser_compress.add_argument(
1615*e1fe3e4aSElliott Hughes        "-o",
1616*e1fe3e4aSElliott Hughes        "--output-file",
1617*e1fe3e4aSElliott Hughes        metavar="OUTPUT",
1618*e1fe3e4aSElliott Hughes        help="the output WOFF2 font",
1619*e1fe3e4aSElliott Hughes    )
1620*e1fe3e4aSElliott Hughes    parser_decompress.add_argument(
1621*e1fe3e4aSElliott Hughes        "-o",
1622*e1fe3e4aSElliott Hughes        "--output-file",
1623*e1fe3e4aSElliott Hughes        metavar="OUTPUT",
1624*e1fe3e4aSElliott Hughes        help="the output OpenType font",
1625*e1fe3e4aSElliott Hughes    )
1626*e1fe3e4aSElliott Hughes
1627*e1fe3e4aSElliott Hughes    transform_group = parser_compress.add_argument_group()
1628*e1fe3e4aSElliott Hughes    transform_group.add_argument(
1629*e1fe3e4aSElliott Hughes        "--no-glyf-transform",
1630*e1fe3e4aSElliott Hughes        dest="transform_tables",
1631*e1fe3e4aSElliott Hughes        nargs=0,
1632*e1fe3e4aSElliott Hughes        action=_NoGlyfTransformAction,
1633*e1fe3e4aSElliott Hughes        help="Do not transform glyf (and loca) tables",
1634*e1fe3e4aSElliott Hughes    )
1635*e1fe3e4aSElliott Hughes    transform_group.add_argument(
1636*e1fe3e4aSElliott Hughes        "--hmtx-transform",
1637*e1fe3e4aSElliott Hughes        dest="transform_tables",
1638*e1fe3e4aSElliott Hughes        nargs=0,
1639*e1fe3e4aSElliott Hughes        action=_HmtxTransformAction,
1640*e1fe3e4aSElliott Hughes        help="Enable optional transformation for 'hmtx' table",
1641*e1fe3e4aSElliott Hughes    )
1642*e1fe3e4aSElliott Hughes
1643*e1fe3e4aSElliott Hughes    parser_compress.set_defaults(
1644*e1fe3e4aSElliott Hughes        subcommand=compress,
1645*e1fe3e4aSElliott Hughes        transform_tables={"glyf", "loca"},
1646*e1fe3e4aSElliott Hughes    )
1647*e1fe3e4aSElliott Hughes    parser_decompress.set_defaults(subcommand=decompress)
1648*e1fe3e4aSElliott Hughes
1649*e1fe3e4aSElliott Hughes    options = vars(parser.parse_args(args))
1650*e1fe3e4aSElliott Hughes
1651*e1fe3e4aSElliott Hughes    subcommand = options.pop("subcommand", None)
1652*e1fe3e4aSElliott Hughes    if not subcommand:
1653*e1fe3e4aSElliott Hughes        parser.print_help()
1654*e1fe3e4aSElliott Hughes        return
1655*e1fe3e4aSElliott Hughes
1656*e1fe3e4aSElliott Hughes    quiet = options.pop("quiet")
1657*e1fe3e4aSElliott Hughes    verbose = options.pop("verbose")
1658*e1fe3e4aSElliott Hughes    configLogger(
1659*e1fe3e4aSElliott Hughes        level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"),
1660*e1fe3e4aSElliott Hughes    )
1661*e1fe3e4aSElliott Hughes
1662*e1fe3e4aSElliott Hughes    if not options["output_file"]:
1663*e1fe3e4aSElliott Hughes        if subcommand is compress:
1664*e1fe3e4aSElliott Hughes            extension = ".woff2"
1665*e1fe3e4aSElliott Hughes        elif subcommand is decompress:
1666*e1fe3e4aSElliott Hughes            # choose .ttf/.otf file extension depending on sfntVersion
1667*e1fe3e4aSElliott Hughes            with open(options["input_file"], "rb") as f:
1668*e1fe3e4aSElliott Hughes                f.seek(4)  # skip 'wOF2' signature
1669*e1fe3e4aSElliott Hughes                sfntVersion = f.read(4)
1670*e1fe3e4aSElliott Hughes            assert len(sfntVersion) == 4, "not enough data"
1671*e1fe3e4aSElliott Hughes            extension = ".otf" if sfntVersion == b"OTTO" else ".ttf"
1672*e1fe3e4aSElliott Hughes        else:
1673*e1fe3e4aSElliott Hughes            raise AssertionError(subcommand)
1674*e1fe3e4aSElliott Hughes        options["output_file"] = makeOutputFileName(
1675*e1fe3e4aSElliott Hughes            options["input_file"], outputDir=None, extension=extension
1676*e1fe3e4aSElliott Hughes        )
1677*e1fe3e4aSElliott Hughes
1678*e1fe3e4aSElliott Hughes    try:
1679*e1fe3e4aSElliott Hughes        subcommand(**options)
1680*e1fe3e4aSElliott Hughes    except TTLibError as e:
1681*e1fe3e4aSElliott Hughes        parser.error(e)
1682*e1fe3e4aSElliott Hughes
1683*e1fe3e4aSElliott Hughes
1684*e1fe3e4aSElliott Hughesif __name__ == "__main__":
1685*e1fe3e4aSElliott Hughes    sys.exit(main())
1686