xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/sfnt.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format.
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott HughesDefines two public classes:
4*e1fe3e4aSElliott Hughes	SFNTReader
5*e1fe3e4aSElliott Hughes	SFNTWriter
6*e1fe3e4aSElliott Hughes
7*e1fe3e4aSElliott Hughes(Normally you don't have to use these classes explicitly; they are
8*e1fe3e4aSElliott Hughesused automatically by ttLib.TTFont.)
9*e1fe3e4aSElliott Hughes
10*e1fe3e4aSElliott HughesThe reading and writing of sfnt files is separated in two distinct
11*e1fe3e4aSElliott Hughesclasses, since whenever the number of tables changes or whenever
12*e1fe3e4aSElliott Hughesa table's length changes you need to rewrite the whole file anyway.
13*e1fe3e4aSElliott Hughes"""
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughesfrom io import BytesIO
16*e1fe3e4aSElliott Hughesfrom types import SimpleNamespace
17*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import Tag
18*e1fe3e4aSElliott Hughesfrom fontTools.misc import sstruct
19*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTLibError, TTLibFileIsCollectionError
20*e1fe3e4aSElliott Hughesimport struct
21*e1fe3e4aSElliott Hughesfrom collections import OrderedDict
22*e1fe3e4aSElliott Hughesimport logging
23*e1fe3e4aSElliott Hughes
24*e1fe3e4aSElliott Hughes
25*e1fe3e4aSElliott Hugheslog = logging.getLogger(__name__)
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes
28*e1fe3e4aSElliott Hughesclass SFNTReader(object):
29*e1fe3e4aSElliott Hughes    def __new__(cls, *args, **kwargs):
30*e1fe3e4aSElliott Hughes        """Return an instance of the SFNTReader sub-class which is compatible
31*e1fe3e4aSElliott Hughes        with the input file type.
32*e1fe3e4aSElliott Hughes        """
33*e1fe3e4aSElliott Hughes        if args and cls is SFNTReader:
34*e1fe3e4aSElliott Hughes            infile = args[0]
35*e1fe3e4aSElliott Hughes            infile.seek(0)
36*e1fe3e4aSElliott Hughes            sfntVersion = Tag(infile.read(4))
37*e1fe3e4aSElliott Hughes            infile.seek(0)
38*e1fe3e4aSElliott Hughes            if sfntVersion == "wOF2":
39*e1fe3e4aSElliott Hughes                # return new WOFF2Reader object
40*e1fe3e4aSElliott Hughes                from fontTools.ttLib.woff2 import WOFF2Reader
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughes                return object.__new__(WOFF2Reader)
43*e1fe3e4aSElliott Hughes        # return default object
44*e1fe3e4aSElliott Hughes        return object.__new__(cls)
45*e1fe3e4aSElliott Hughes
46*e1fe3e4aSElliott Hughes    def __init__(self, file, checkChecksums=0, fontNumber=-1):
47*e1fe3e4aSElliott Hughes        self.file = file
48*e1fe3e4aSElliott Hughes        self.checkChecksums = checkChecksums
49*e1fe3e4aSElliott Hughes
50*e1fe3e4aSElliott Hughes        self.flavor = None
51*e1fe3e4aSElliott Hughes        self.flavorData = None
52*e1fe3e4aSElliott Hughes        self.DirectoryEntry = SFNTDirectoryEntry
53*e1fe3e4aSElliott Hughes        self.file.seek(0)
54*e1fe3e4aSElliott Hughes        self.sfntVersion = self.file.read(4)
55*e1fe3e4aSElliott Hughes        self.file.seek(0)
56*e1fe3e4aSElliott Hughes        if self.sfntVersion == b"ttcf":
57*e1fe3e4aSElliott Hughes            header = readTTCHeader(self.file)
58*e1fe3e4aSElliott Hughes            numFonts = header.numFonts
59*e1fe3e4aSElliott Hughes            if not 0 <= fontNumber < numFonts:
60*e1fe3e4aSElliott Hughes                raise TTLibFileIsCollectionError(
61*e1fe3e4aSElliott Hughes                    "specify a font number between 0 and %d (inclusive)"
62*e1fe3e4aSElliott Hughes                    % (numFonts - 1)
63*e1fe3e4aSElliott Hughes                )
64*e1fe3e4aSElliott Hughes            self.numFonts = numFonts
65*e1fe3e4aSElliott Hughes            self.file.seek(header.offsetTable[fontNumber])
66*e1fe3e4aSElliott Hughes            data = self.file.read(sfntDirectorySize)
67*e1fe3e4aSElliott Hughes            if len(data) != sfntDirectorySize:
68*e1fe3e4aSElliott Hughes                raise TTLibError("Not a Font Collection (not enough data)")
69*e1fe3e4aSElliott Hughes            sstruct.unpack(sfntDirectoryFormat, data, self)
70*e1fe3e4aSElliott Hughes        elif self.sfntVersion == b"wOFF":
71*e1fe3e4aSElliott Hughes            self.flavor = "woff"
72*e1fe3e4aSElliott Hughes            self.DirectoryEntry = WOFFDirectoryEntry
73*e1fe3e4aSElliott Hughes            data = self.file.read(woffDirectorySize)
74*e1fe3e4aSElliott Hughes            if len(data) != woffDirectorySize:
75*e1fe3e4aSElliott Hughes                raise TTLibError("Not a WOFF font (not enough data)")
76*e1fe3e4aSElliott Hughes            sstruct.unpack(woffDirectoryFormat, data, self)
77*e1fe3e4aSElliott Hughes        else:
78*e1fe3e4aSElliott Hughes            data = self.file.read(sfntDirectorySize)
79*e1fe3e4aSElliott Hughes            if len(data) != sfntDirectorySize:
80*e1fe3e4aSElliott Hughes                raise TTLibError("Not a TrueType or OpenType font (not enough data)")
81*e1fe3e4aSElliott Hughes            sstruct.unpack(sfntDirectoryFormat, data, self)
82*e1fe3e4aSElliott Hughes        self.sfntVersion = Tag(self.sfntVersion)
83*e1fe3e4aSElliott Hughes
84*e1fe3e4aSElliott Hughes        if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"):
85*e1fe3e4aSElliott Hughes            raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
86*e1fe3e4aSElliott Hughes        tables = {}
87*e1fe3e4aSElliott Hughes        for i in range(self.numTables):
88*e1fe3e4aSElliott Hughes            entry = self.DirectoryEntry()
89*e1fe3e4aSElliott Hughes            entry.fromFile(self.file)
90*e1fe3e4aSElliott Hughes            tag = Tag(entry.tag)
91*e1fe3e4aSElliott Hughes            tables[tag] = entry
92*e1fe3e4aSElliott Hughes        self.tables = OrderedDict(sorted(tables.items(), key=lambda i: i[1].offset))
93*e1fe3e4aSElliott Hughes
94*e1fe3e4aSElliott Hughes        # Load flavor data if any
95*e1fe3e4aSElliott Hughes        if self.flavor == "woff":
96*e1fe3e4aSElliott Hughes            self.flavorData = WOFFFlavorData(self)
97*e1fe3e4aSElliott Hughes
98*e1fe3e4aSElliott Hughes    def has_key(self, tag):
99*e1fe3e4aSElliott Hughes        return tag in self.tables
100*e1fe3e4aSElliott Hughes
101*e1fe3e4aSElliott Hughes    __contains__ = has_key
102*e1fe3e4aSElliott Hughes
103*e1fe3e4aSElliott Hughes    def keys(self):
104*e1fe3e4aSElliott Hughes        return self.tables.keys()
105*e1fe3e4aSElliott Hughes
106*e1fe3e4aSElliott Hughes    def __getitem__(self, tag):
107*e1fe3e4aSElliott Hughes        """Fetch the raw table data."""
108*e1fe3e4aSElliott Hughes        entry = self.tables[Tag(tag)]
109*e1fe3e4aSElliott Hughes        data = entry.loadData(self.file)
110*e1fe3e4aSElliott Hughes        if self.checkChecksums:
111*e1fe3e4aSElliott Hughes            if tag == "head":
112*e1fe3e4aSElliott Hughes                # Beh: we have to special-case the 'head' table.
113*e1fe3e4aSElliott Hughes                checksum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:])
114*e1fe3e4aSElliott Hughes            else:
115*e1fe3e4aSElliott Hughes                checksum = calcChecksum(data)
116*e1fe3e4aSElliott Hughes            if self.checkChecksums > 1:
117*e1fe3e4aSElliott Hughes                # Be obnoxious, and barf when it's wrong
118*e1fe3e4aSElliott Hughes                assert checksum == entry.checkSum, "bad checksum for '%s' table" % tag
119*e1fe3e4aSElliott Hughes            elif checksum != entry.checkSum:
120*e1fe3e4aSElliott Hughes                # Be friendly, and just log a warning.
121*e1fe3e4aSElliott Hughes                log.warning("bad checksum for '%s' table", tag)
122*e1fe3e4aSElliott Hughes        return data
123*e1fe3e4aSElliott Hughes
124*e1fe3e4aSElliott Hughes    def __delitem__(self, tag):
125*e1fe3e4aSElliott Hughes        del self.tables[Tag(tag)]
126*e1fe3e4aSElliott Hughes
127*e1fe3e4aSElliott Hughes    def close(self):
128*e1fe3e4aSElliott Hughes        self.file.close()
129*e1fe3e4aSElliott Hughes
130*e1fe3e4aSElliott Hughes    # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able
131*e1fe3e4aSElliott Hughes    # and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a
132*e1fe3e4aSElliott Hughes    # reference to an external file object which is not pickleable. So in __getstate__
133*e1fe3e4aSElliott Hughes    # we store the file name and current position, and in __setstate__ we reopen the
134*e1fe3e4aSElliott Hughes    # same named file after unpickling.
135*e1fe3e4aSElliott Hughes
136*e1fe3e4aSElliott Hughes    def __getstate__(self):
137*e1fe3e4aSElliott Hughes        if isinstance(self.file, BytesIO):
138*e1fe3e4aSElliott Hughes            # BytesIO is already pickleable, return the state unmodified
139*e1fe3e4aSElliott Hughes            return self.__dict__
140*e1fe3e4aSElliott Hughes
141*e1fe3e4aSElliott Hughes        # remove unpickleable file attribute, and only store its name and pos
142*e1fe3e4aSElliott Hughes        state = self.__dict__.copy()
143*e1fe3e4aSElliott Hughes        del state["file"]
144*e1fe3e4aSElliott Hughes        state["_filename"] = self.file.name
145*e1fe3e4aSElliott Hughes        state["_filepos"] = self.file.tell()
146*e1fe3e4aSElliott Hughes        return state
147*e1fe3e4aSElliott Hughes
148*e1fe3e4aSElliott Hughes    def __setstate__(self, state):
149*e1fe3e4aSElliott Hughes        if "file" not in state:
150*e1fe3e4aSElliott Hughes            self.file = open(state.pop("_filename"), "rb")
151*e1fe3e4aSElliott Hughes            self.file.seek(state.pop("_filepos"))
152*e1fe3e4aSElliott Hughes        self.__dict__.update(state)
153*e1fe3e4aSElliott Hughes
154*e1fe3e4aSElliott Hughes
155*e1fe3e4aSElliott Hughes# default compression level for WOFF 1.0 tables and metadata
156*e1fe3e4aSElliott HughesZLIB_COMPRESSION_LEVEL = 6
157*e1fe3e4aSElliott Hughes
158*e1fe3e4aSElliott Hughes# if set to True, use zopfli instead of zlib for compressing WOFF 1.0.
159*e1fe3e4aSElliott Hughes# The Python bindings are available at https://pypi.python.org/pypi/zopfli
160*e1fe3e4aSElliott HughesUSE_ZOPFLI = False
161*e1fe3e4aSElliott Hughes
162*e1fe3e4aSElliott Hughes# mapping between zlib's compression levels and zopfli's 'numiterations'.
163*e1fe3e4aSElliott Hughes# Use lower values for files over several MB in size or it will be too slow
164*e1fe3e4aSElliott HughesZOPFLI_LEVELS = {
165*e1fe3e4aSElliott Hughes    # 0: 0,  # can't do 0 iterations...
166*e1fe3e4aSElliott Hughes    1: 1,
167*e1fe3e4aSElliott Hughes    2: 3,
168*e1fe3e4aSElliott Hughes    3: 5,
169*e1fe3e4aSElliott Hughes    4: 8,
170*e1fe3e4aSElliott Hughes    5: 10,
171*e1fe3e4aSElliott Hughes    6: 15,
172*e1fe3e4aSElliott Hughes    7: 25,
173*e1fe3e4aSElliott Hughes    8: 50,
174*e1fe3e4aSElliott Hughes    9: 100,
175*e1fe3e4aSElliott Hughes}
176*e1fe3e4aSElliott Hughes
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughesdef compress(data, level=ZLIB_COMPRESSION_LEVEL):
179*e1fe3e4aSElliott Hughes    """Compress 'data' to Zlib format. If 'USE_ZOPFLI' variable is True,
180*e1fe3e4aSElliott Hughes    zopfli is used instead of the zlib module.
181*e1fe3e4aSElliott Hughes    The compression 'level' must be between 0 and 9. 1 gives best speed,
182*e1fe3e4aSElliott Hughes    9 gives best compression (0 gives no compression at all).
183*e1fe3e4aSElliott Hughes    The default value is a compromise between speed and compression (6).
184*e1fe3e4aSElliott Hughes    """
185*e1fe3e4aSElliott Hughes    if not (0 <= level <= 9):
186*e1fe3e4aSElliott Hughes        raise ValueError("Bad compression level: %s" % level)
187*e1fe3e4aSElliott Hughes    if not USE_ZOPFLI or level == 0:
188*e1fe3e4aSElliott Hughes        from zlib import compress
189*e1fe3e4aSElliott Hughes
190*e1fe3e4aSElliott Hughes        return compress(data, level)
191*e1fe3e4aSElliott Hughes    else:
192*e1fe3e4aSElliott Hughes        from zopfli.zlib import compress
193*e1fe3e4aSElliott Hughes
194*e1fe3e4aSElliott Hughes        return compress(data, numiterations=ZOPFLI_LEVELS[level])
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes
197*e1fe3e4aSElliott Hughesclass SFNTWriter(object):
198*e1fe3e4aSElliott Hughes    def __new__(cls, *args, **kwargs):
199*e1fe3e4aSElliott Hughes        """Return an instance of the SFNTWriter sub-class which is compatible
200*e1fe3e4aSElliott Hughes        with the specified 'flavor'.
201*e1fe3e4aSElliott Hughes        """
202*e1fe3e4aSElliott Hughes        flavor = None
203*e1fe3e4aSElliott Hughes        if kwargs and "flavor" in kwargs:
204*e1fe3e4aSElliott Hughes            flavor = kwargs["flavor"]
205*e1fe3e4aSElliott Hughes        elif args and len(args) > 3:
206*e1fe3e4aSElliott Hughes            flavor = args[3]
207*e1fe3e4aSElliott Hughes        if cls is SFNTWriter:
208*e1fe3e4aSElliott Hughes            if flavor == "woff2":
209*e1fe3e4aSElliott Hughes                # return new WOFF2Writer object
210*e1fe3e4aSElliott Hughes                from fontTools.ttLib.woff2 import WOFF2Writer
211*e1fe3e4aSElliott Hughes
212*e1fe3e4aSElliott Hughes                return object.__new__(WOFF2Writer)
213*e1fe3e4aSElliott Hughes        # return default object
214*e1fe3e4aSElliott Hughes        return object.__new__(cls)
215*e1fe3e4aSElliott Hughes
216*e1fe3e4aSElliott Hughes    def __init__(
217*e1fe3e4aSElliott Hughes        self,
218*e1fe3e4aSElliott Hughes        file,
219*e1fe3e4aSElliott Hughes        numTables,
220*e1fe3e4aSElliott Hughes        sfntVersion="\000\001\000\000",
221*e1fe3e4aSElliott Hughes        flavor=None,
222*e1fe3e4aSElliott Hughes        flavorData=None,
223*e1fe3e4aSElliott Hughes    ):
224*e1fe3e4aSElliott Hughes        self.file = file
225*e1fe3e4aSElliott Hughes        self.numTables = numTables
226*e1fe3e4aSElliott Hughes        self.sfntVersion = Tag(sfntVersion)
227*e1fe3e4aSElliott Hughes        self.flavor = flavor
228*e1fe3e4aSElliott Hughes        self.flavorData = flavorData
229*e1fe3e4aSElliott Hughes
230*e1fe3e4aSElliott Hughes        if self.flavor == "woff":
231*e1fe3e4aSElliott Hughes            self.directoryFormat = woffDirectoryFormat
232*e1fe3e4aSElliott Hughes            self.directorySize = woffDirectorySize
233*e1fe3e4aSElliott Hughes            self.DirectoryEntry = WOFFDirectoryEntry
234*e1fe3e4aSElliott Hughes
235*e1fe3e4aSElliott Hughes            self.signature = "wOFF"
236*e1fe3e4aSElliott Hughes
237*e1fe3e4aSElliott Hughes            # to calculate WOFF checksum adjustment, we also need the original SFNT offsets
238*e1fe3e4aSElliott Hughes            self.origNextTableOffset = (
239*e1fe3e4aSElliott Hughes                sfntDirectorySize + numTables * sfntDirectoryEntrySize
240*e1fe3e4aSElliott Hughes            )
241*e1fe3e4aSElliott Hughes        else:
242*e1fe3e4aSElliott Hughes            assert not self.flavor, "Unknown flavor '%s'" % self.flavor
243*e1fe3e4aSElliott Hughes            self.directoryFormat = sfntDirectoryFormat
244*e1fe3e4aSElliott Hughes            self.directorySize = sfntDirectorySize
245*e1fe3e4aSElliott Hughes            self.DirectoryEntry = SFNTDirectoryEntry
246*e1fe3e4aSElliott Hughes
247*e1fe3e4aSElliott Hughes            from fontTools.ttLib import getSearchRange
248*e1fe3e4aSElliott Hughes
249*e1fe3e4aSElliott Hughes            self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(
250*e1fe3e4aSElliott Hughes                numTables, 16
251*e1fe3e4aSElliott Hughes            )
252*e1fe3e4aSElliott Hughes
253*e1fe3e4aSElliott Hughes        self.directoryOffset = self.file.tell()
254*e1fe3e4aSElliott Hughes        self.nextTableOffset = (
255*e1fe3e4aSElliott Hughes            self.directoryOffset
256*e1fe3e4aSElliott Hughes            + self.directorySize
257*e1fe3e4aSElliott Hughes            + numTables * self.DirectoryEntry.formatSize
258*e1fe3e4aSElliott Hughes        )
259*e1fe3e4aSElliott Hughes        # clear out directory area
260*e1fe3e4aSElliott Hughes        self.file.seek(self.nextTableOffset)
261*e1fe3e4aSElliott Hughes        # make sure we're actually where we want to be. (old cStringIO bug)
262*e1fe3e4aSElliott Hughes        self.file.write(b"\0" * (self.nextTableOffset - self.file.tell()))
263*e1fe3e4aSElliott Hughes        self.tables = OrderedDict()
264*e1fe3e4aSElliott Hughes
265*e1fe3e4aSElliott Hughes    def setEntry(self, tag, entry):
266*e1fe3e4aSElliott Hughes        if tag in self.tables:
267*e1fe3e4aSElliott Hughes            raise TTLibError("cannot rewrite '%s' table" % tag)
268*e1fe3e4aSElliott Hughes
269*e1fe3e4aSElliott Hughes        self.tables[tag] = entry
270*e1fe3e4aSElliott Hughes
271*e1fe3e4aSElliott Hughes    def __setitem__(self, tag, data):
272*e1fe3e4aSElliott Hughes        """Write raw table data to disk."""
273*e1fe3e4aSElliott Hughes        if tag in self.tables:
274*e1fe3e4aSElliott Hughes            raise TTLibError("cannot rewrite '%s' table" % tag)
275*e1fe3e4aSElliott Hughes
276*e1fe3e4aSElliott Hughes        entry = self.DirectoryEntry()
277*e1fe3e4aSElliott Hughes        entry.tag = tag
278*e1fe3e4aSElliott Hughes        entry.offset = self.nextTableOffset
279*e1fe3e4aSElliott Hughes        if tag == "head":
280*e1fe3e4aSElliott Hughes            entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:])
281*e1fe3e4aSElliott Hughes            self.headTable = data
282*e1fe3e4aSElliott Hughes            entry.uncompressed = True
283*e1fe3e4aSElliott Hughes        else:
284*e1fe3e4aSElliott Hughes            entry.checkSum = calcChecksum(data)
285*e1fe3e4aSElliott Hughes        entry.saveData(self.file, data)
286*e1fe3e4aSElliott Hughes
287*e1fe3e4aSElliott Hughes        if self.flavor == "woff":
288*e1fe3e4aSElliott Hughes            entry.origOffset = self.origNextTableOffset
289*e1fe3e4aSElliott Hughes            self.origNextTableOffset += (entry.origLength + 3) & ~3
290*e1fe3e4aSElliott Hughes
291*e1fe3e4aSElliott Hughes        self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3)
292*e1fe3e4aSElliott Hughes        # Add NUL bytes to pad the table data to a 4-byte boundary.
293*e1fe3e4aSElliott Hughes        # Don't depend on f.seek() as we need to add the padding even if no
294*e1fe3e4aSElliott Hughes        # subsequent write follows (seek is lazy), ie. after the final table
295*e1fe3e4aSElliott Hughes        # in the font.
296*e1fe3e4aSElliott Hughes        self.file.write(b"\0" * (self.nextTableOffset - self.file.tell()))
297*e1fe3e4aSElliott Hughes        assert self.nextTableOffset == self.file.tell()
298*e1fe3e4aSElliott Hughes
299*e1fe3e4aSElliott Hughes        self.setEntry(tag, entry)
300*e1fe3e4aSElliott Hughes
301*e1fe3e4aSElliott Hughes    def __getitem__(self, tag):
302*e1fe3e4aSElliott Hughes        return self.tables[tag]
303*e1fe3e4aSElliott Hughes
304*e1fe3e4aSElliott Hughes    def close(self):
305*e1fe3e4aSElliott Hughes        """All tables must have been written to disk. Now write the
306*e1fe3e4aSElliott Hughes        directory.
307*e1fe3e4aSElliott Hughes        """
308*e1fe3e4aSElliott Hughes        tables = sorted(self.tables.items())
309*e1fe3e4aSElliott Hughes        if len(tables) != self.numTables:
310*e1fe3e4aSElliott Hughes            raise TTLibError(
311*e1fe3e4aSElliott Hughes                "wrong number of tables; expected %d, found %d"
312*e1fe3e4aSElliott Hughes                % (self.numTables, len(tables))
313*e1fe3e4aSElliott Hughes            )
314*e1fe3e4aSElliott Hughes
315*e1fe3e4aSElliott Hughes        if self.flavor == "woff":
316*e1fe3e4aSElliott Hughes            self.signature = b"wOFF"
317*e1fe3e4aSElliott Hughes            self.reserved = 0
318*e1fe3e4aSElliott Hughes
319*e1fe3e4aSElliott Hughes            self.totalSfntSize = 12
320*e1fe3e4aSElliott Hughes            self.totalSfntSize += 16 * len(tables)
321*e1fe3e4aSElliott Hughes            for tag, entry in tables:
322*e1fe3e4aSElliott Hughes                self.totalSfntSize += (entry.origLength + 3) & ~3
323*e1fe3e4aSElliott Hughes
324*e1fe3e4aSElliott Hughes            data = self.flavorData if self.flavorData else WOFFFlavorData()
325*e1fe3e4aSElliott Hughes            if data.majorVersion is not None and data.minorVersion is not None:
326*e1fe3e4aSElliott Hughes                self.majorVersion = data.majorVersion
327*e1fe3e4aSElliott Hughes                self.minorVersion = data.minorVersion
328*e1fe3e4aSElliott Hughes            else:
329*e1fe3e4aSElliott Hughes                if hasattr(self, "headTable"):
330*e1fe3e4aSElliott Hughes                    self.majorVersion, self.minorVersion = struct.unpack(
331*e1fe3e4aSElliott Hughes                        ">HH", self.headTable[4:8]
332*e1fe3e4aSElliott Hughes                    )
333*e1fe3e4aSElliott Hughes                else:
334*e1fe3e4aSElliott Hughes                    self.majorVersion = self.minorVersion = 0
335*e1fe3e4aSElliott Hughes            if data.metaData:
336*e1fe3e4aSElliott Hughes                self.metaOrigLength = len(data.metaData)
337*e1fe3e4aSElliott Hughes                self.file.seek(0, 2)
338*e1fe3e4aSElliott Hughes                self.metaOffset = self.file.tell()
339*e1fe3e4aSElliott Hughes                compressedMetaData = compress(data.metaData)
340*e1fe3e4aSElliott Hughes                self.metaLength = len(compressedMetaData)
341*e1fe3e4aSElliott Hughes                self.file.write(compressedMetaData)
342*e1fe3e4aSElliott Hughes            else:
343*e1fe3e4aSElliott Hughes                self.metaOffset = self.metaLength = self.metaOrigLength = 0
344*e1fe3e4aSElliott Hughes            if data.privData:
345*e1fe3e4aSElliott Hughes                self.file.seek(0, 2)
346*e1fe3e4aSElliott Hughes                off = self.file.tell()
347*e1fe3e4aSElliott Hughes                paddedOff = (off + 3) & ~3
348*e1fe3e4aSElliott Hughes                self.file.write(b"\0" * (paddedOff - off))
349*e1fe3e4aSElliott Hughes                self.privOffset = self.file.tell()
350*e1fe3e4aSElliott Hughes                self.privLength = len(data.privData)
351*e1fe3e4aSElliott Hughes                self.file.write(data.privData)
352*e1fe3e4aSElliott Hughes            else:
353*e1fe3e4aSElliott Hughes                self.privOffset = self.privLength = 0
354*e1fe3e4aSElliott Hughes
355*e1fe3e4aSElliott Hughes            self.file.seek(0, 2)
356*e1fe3e4aSElliott Hughes            self.length = self.file.tell()
357*e1fe3e4aSElliott Hughes
358*e1fe3e4aSElliott Hughes        else:
359*e1fe3e4aSElliott Hughes            assert not self.flavor, "Unknown flavor '%s'" % self.flavor
360*e1fe3e4aSElliott Hughes            pass
361*e1fe3e4aSElliott Hughes
362*e1fe3e4aSElliott Hughes        directory = sstruct.pack(self.directoryFormat, self)
363*e1fe3e4aSElliott Hughes
364*e1fe3e4aSElliott Hughes        self.file.seek(self.directoryOffset + self.directorySize)
365*e1fe3e4aSElliott Hughes        seenHead = 0
366*e1fe3e4aSElliott Hughes        for tag, entry in tables:
367*e1fe3e4aSElliott Hughes            if tag == "head":
368*e1fe3e4aSElliott Hughes                seenHead = 1
369*e1fe3e4aSElliott Hughes            directory = directory + entry.toString()
370*e1fe3e4aSElliott Hughes        if seenHead:
371*e1fe3e4aSElliott Hughes            self.writeMasterChecksum(directory)
372*e1fe3e4aSElliott Hughes        self.file.seek(self.directoryOffset)
373*e1fe3e4aSElliott Hughes        self.file.write(directory)
374*e1fe3e4aSElliott Hughes
375*e1fe3e4aSElliott Hughes    def _calcMasterChecksum(self, directory):
376*e1fe3e4aSElliott Hughes        # calculate checkSumAdjustment
377*e1fe3e4aSElliott Hughes        tags = list(self.tables.keys())
378*e1fe3e4aSElliott Hughes        checksums = []
379*e1fe3e4aSElliott Hughes        for i in range(len(tags)):
380*e1fe3e4aSElliott Hughes            checksums.append(self.tables[tags[i]].checkSum)
381*e1fe3e4aSElliott Hughes
382*e1fe3e4aSElliott Hughes        if self.DirectoryEntry != SFNTDirectoryEntry:
383*e1fe3e4aSElliott Hughes            # Create a SFNT directory for checksum calculation purposes
384*e1fe3e4aSElliott Hughes            from fontTools.ttLib import getSearchRange
385*e1fe3e4aSElliott Hughes
386*e1fe3e4aSElliott Hughes            self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(
387*e1fe3e4aSElliott Hughes                self.numTables, 16
388*e1fe3e4aSElliott Hughes            )
389*e1fe3e4aSElliott Hughes            directory = sstruct.pack(sfntDirectoryFormat, self)
390*e1fe3e4aSElliott Hughes            tables = sorted(self.tables.items())
391*e1fe3e4aSElliott Hughes            for tag, entry in tables:
392*e1fe3e4aSElliott Hughes                sfntEntry = SFNTDirectoryEntry()
393*e1fe3e4aSElliott Hughes                sfntEntry.tag = entry.tag
394*e1fe3e4aSElliott Hughes                sfntEntry.checkSum = entry.checkSum
395*e1fe3e4aSElliott Hughes                sfntEntry.offset = entry.origOffset
396*e1fe3e4aSElliott Hughes                sfntEntry.length = entry.origLength
397*e1fe3e4aSElliott Hughes                directory = directory + sfntEntry.toString()
398*e1fe3e4aSElliott Hughes
399*e1fe3e4aSElliott Hughes        directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
400*e1fe3e4aSElliott Hughes        assert directory_end == len(directory)
401*e1fe3e4aSElliott Hughes
402*e1fe3e4aSElliott Hughes        checksums.append(calcChecksum(directory))
403*e1fe3e4aSElliott Hughes        checksum = sum(checksums) & 0xFFFFFFFF
404*e1fe3e4aSElliott Hughes        # BiboAfba!
405*e1fe3e4aSElliott Hughes        checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF
406*e1fe3e4aSElliott Hughes        return checksumadjustment
407*e1fe3e4aSElliott Hughes
408*e1fe3e4aSElliott Hughes    def writeMasterChecksum(self, directory):
409*e1fe3e4aSElliott Hughes        checksumadjustment = self._calcMasterChecksum(directory)
410*e1fe3e4aSElliott Hughes        # write the checksum to the file
411*e1fe3e4aSElliott Hughes        self.file.seek(self.tables["head"].offset + 8)
412*e1fe3e4aSElliott Hughes        self.file.write(struct.pack(">L", checksumadjustment))
413*e1fe3e4aSElliott Hughes
414*e1fe3e4aSElliott Hughes    def reordersTables(self):
415*e1fe3e4aSElliott Hughes        return False
416*e1fe3e4aSElliott Hughes
417*e1fe3e4aSElliott Hughes
418*e1fe3e4aSElliott Hughes# -- sfnt directory helpers and cruft
419*e1fe3e4aSElliott Hughes
420*e1fe3e4aSElliott HughesttcHeaderFormat = """
421*e1fe3e4aSElliott Hughes		> # big endian
422*e1fe3e4aSElliott Hughes		TTCTag:                  4s # "ttcf"
423*e1fe3e4aSElliott Hughes		Version:                 L  # 0x00010000 or 0x00020000
424*e1fe3e4aSElliott Hughes		numFonts:                L  # number of fonts
425*e1fe3e4aSElliott Hughes		# OffsetTable[numFonts]: L  # array with offsets from beginning of file
426*e1fe3e4aSElliott Hughes		# ulDsigTag:             L  # version 2.0 only
427*e1fe3e4aSElliott Hughes		# ulDsigLength:          L  # version 2.0 only
428*e1fe3e4aSElliott Hughes		# ulDsigOffset:          L  # version 2.0 only
429*e1fe3e4aSElliott Hughes"""
430*e1fe3e4aSElliott Hughes
431*e1fe3e4aSElliott HughesttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
432*e1fe3e4aSElliott Hughes
433*e1fe3e4aSElliott HughessfntDirectoryFormat = """
434*e1fe3e4aSElliott Hughes		> # big endian
435*e1fe3e4aSElliott Hughes		sfntVersion:    4s
436*e1fe3e4aSElliott Hughes		numTables:      H    # number of tables
437*e1fe3e4aSElliott Hughes		searchRange:    H    # (max2 <= numTables)*16
438*e1fe3e4aSElliott Hughes		entrySelector:  H    # log2(max2 <= numTables)
439*e1fe3e4aSElliott Hughes		rangeShift:     H    # numTables*16-searchRange
440*e1fe3e4aSElliott Hughes"""
441*e1fe3e4aSElliott Hughes
442*e1fe3e4aSElliott HughessfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
443*e1fe3e4aSElliott Hughes
444*e1fe3e4aSElliott HughessfntDirectoryEntryFormat = """
445*e1fe3e4aSElliott Hughes		> # big endian
446*e1fe3e4aSElliott Hughes		tag:            4s
447*e1fe3e4aSElliott Hughes		checkSum:       L
448*e1fe3e4aSElliott Hughes		offset:         L
449*e1fe3e4aSElliott Hughes		length:         L
450*e1fe3e4aSElliott Hughes"""
451*e1fe3e4aSElliott Hughes
452*e1fe3e4aSElliott HughessfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
453*e1fe3e4aSElliott Hughes
454*e1fe3e4aSElliott HugheswoffDirectoryFormat = """
455*e1fe3e4aSElliott Hughes		> # big endian
456*e1fe3e4aSElliott Hughes		signature:      4s   # "wOFF"
457*e1fe3e4aSElliott Hughes		sfntVersion:    4s
458*e1fe3e4aSElliott Hughes		length:         L    # total woff file size
459*e1fe3e4aSElliott Hughes		numTables:      H    # number of tables
460*e1fe3e4aSElliott Hughes		reserved:       H    # set to 0
461*e1fe3e4aSElliott Hughes		totalSfntSize:  L    # uncompressed size
462*e1fe3e4aSElliott Hughes		majorVersion:   H    # major version of WOFF file
463*e1fe3e4aSElliott Hughes		minorVersion:   H    # minor version of WOFF file
464*e1fe3e4aSElliott Hughes		metaOffset:     L    # offset to metadata block
465*e1fe3e4aSElliott Hughes		metaLength:     L    # length of compressed metadata
466*e1fe3e4aSElliott Hughes		metaOrigLength: L    # length of uncompressed metadata
467*e1fe3e4aSElliott Hughes		privOffset:     L    # offset to private data block
468*e1fe3e4aSElliott Hughes		privLength:     L    # length of private data block
469*e1fe3e4aSElliott Hughes"""
470*e1fe3e4aSElliott Hughes
471*e1fe3e4aSElliott HugheswoffDirectorySize = sstruct.calcsize(woffDirectoryFormat)
472*e1fe3e4aSElliott Hughes
473*e1fe3e4aSElliott HugheswoffDirectoryEntryFormat = """
474*e1fe3e4aSElliott Hughes		> # big endian
475*e1fe3e4aSElliott Hughes		tag:            4s
476*e1fe3e4aSElliott Hughes		offset:         L
477*e1fe3e4aSElliott Hughes		length:         L    # compressed length
478*e1fe3e4aSElliott Hughes		origLength:     L    # original length
479*e1fe3e4aSElliott Hughes		checkSum:       L    # original checksum
480*e1fe3e4aSElliott Hughes"""
481*e1fe3e4aSElliott Hughes
482*e1fe3e4aSElliott HugheswoffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat)
483*e1fe3e4aSElliott Hughes
484*e1fe3e4aSElliott Hughes
485*e1fe3e4aSElliott Hughesclass DirectoryEntry(object):
486*e1fe3e4aSElliott Hughes    def __init__(self):
487*e1fe3e4aSElliott Hughes        self.uncompressed = False  # if True, always embed entry raw
488*e1fe3e4aSElliott Hughes
489*e1fe3e4aSElliott Hughes    def fromFile(self, file):
490*e1fe3e4aSElliott Hughes        sstruct.unpack(self.format, file.read(self.formatSize), self)
491*e1fe3e4aSElliott Hughes
492*e1fe3e4aSElliott Hughes    def fromString(self, str):
493*e1fe3e4aSElliott Hughes        sstruct.unpack(self.format, str, self)
494*e1fe3e4aSElliott Hughes
495*e1fe3e4aSElliott Hughes    def toString(self):
496*e1fe3e4aSElliott Hughes        return sstruct.pack(self.format, self)
497*e1fe3e4aSElliott Hughes
498*e1fe3e4aSElliott Hughes    def __repr__(self):
499*e1fe3e4aSElliott Hughes        if hasattr(self, "tag"):
500*e1fe3e4aSElliott Hughes            return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self))
501*e1fe3e4aSElliott Hughes        else:
502*e1fe3e4aSElliott Hughes            return "<%s at %x>" % (self.__class__.__name__, id(self))
503*e1fe3e4aSElliott Hughes
504*e1fe3e4aSElliott Hughes    def loadData(self, file):
505*e1fe3e4aSElliott Hughes        file.seek(self.offset)
506*e1fe3e4aSElliott Hughes        data = file.read(self.length)
507*e1fe3e4aSElliott Hughes        assert len(data) == self.length
508*e1fe3e4aSElliott Hughes        if hasattr(self.__class__, "decodeData"):
509*e1fe3e4aSElliott Hughes            data = self.decodeData(data)
510*e1fe3e4aSElliott Hughes        return data
511*e1fe3e4aSElliott Hughes
512*e1fe3e4aSElliott Hughes    def saveData(self, file, data):
513*e1fe3e4aSElliott Hughes        if hasattr(self.__class__, "encodeData"):
514*e1fe3e4aSElliott Hughes            data = self.encodeData(data)
515*e1fe3e4aSElliott Hughes        self.length = len(data)
516*e1fe3e4aSElliott Hughes        file.seek(self.offset)
517*e1fe3e4aSElliott Hughes        file.write(data)
518*e1fe3e4aSElliott Hughes
519*e1fe3e4aSElliott Hughes    def decodeData(self, rawData):
520*e1fe3e4aSElliott Hughes        return rawData
521*e1fe3e4aSElliott Hughes
522*e1fe3e4aSElliott Hughes    def encodeData(self, data):
523*e1fe3e4aSElliott Hughes        return data
524*e1fe3e4aSElliott Hughes
525*e1fe3e4aSElliott Hughes
526*e1fe3e4aSElliott Hughesclass SFNTDirectoryEntry(DirectoryEntry):
527*e1fe3e4aSElliott Hughes    format = sfntDirectoryEntryFormat
528*e1fe3e4aSElliott Hughes    formatSize = sfntDirectoryEntrySize
529*e1fe3e4aSElliott Hughes
530*e1fe3e4aSElliott Hughes
531*e1fe3e4aSElliott Hughesclass WOFFDirectoryEntry(DirectoryEntry):
532*e1fe3e4aSElliott Hughes    format = woffDirectoryEntryFormat
533*e1fe3e4aSElliott Hughes    formatSize = woffDirectoryEntrySize
534*e1fe3e4aSElliott Hughes
535*e1fe3e4aSElliott Hughes    def __init__(self):
536*e1fe3e4aSElliott Hughes        super(WOFFDirectoryEntry, self).__init__()
537*e1fe3e4aSElliott Hughes        # With fonttools<=3.1.2, the only way to set a different zlib
538*e1fe3e4aSElliott Hughes        # compression level for WOFF directory entries was to set the class
539*e1fe3e4aSElliott Hughes        # attribute 'zlibCompressionLevel'. This is now replaced by a globally
540*e1fe3e4aSElliott Hughes        # defined `ZLIB_COMPRESSION_LEVEL`, which is also applied when
541*e1fe3e4aSElliott Hughes        # compressing the metadata. For backward compatibility, we still
542*e1fe3e4aSElliott Hughes        # use the class attribute if it was already set.
543*e1fe3e4aSElliott Hughes        if not hasattr(WOFFDirectoryEntry, "zlibCompressionLevel"):
544*e1fe3e4aSElliott Hughes            self.zlibCompressionLevel = ZLIB_COMPRESSION_LEVEL
545*e1fe3e4aSElliott Hughes
546*e1fe3e4aSElliott Hughes    def decodeData(self, rawData):
547*e1fe3e4aSElliott Hughes        import zlib
548*e1fe3e4aSElliott Hughes
549*e1fe3e4aSElliott Hughes        if self.length == self.origLength:
550*e1fe3e4aSElliott Hughes            data = rawData
551*e1fe3e4aSElliott Hughes        else:
552*e1fe3e4aSElliott Hughes            assert self.length < self.origLength
553*e1fe3e4aSElliott Hughes            data = zlib.decompress(rawData)
554*e1fe3e4aSElliott Hughes            assert len(data) == self.origLength
555*e1fe3e4aSElliott Hughes        return data
556*e1fe3e4aSElliott Hughes
557*e1fe3e4aSElliott Hughes    def encodeData(self, data):
558*e1fe3e4aSElliott Hughes        self.origLength = len(data)
559*e1fe3e4aSElliott Hughes        if not self.uncompressed:
560*e1fe3e4aSElliott Hughes            compressedData = compress(data, self.zlibCompressionLevel)
561*e1fe3e4aSElliott Hughes        if self.uncompressed or len(compressedData) >= self.origLength:
562*e1fe3e4aSElliott Hughes            # Encode uncompressed
563*e1fe3e4aSElliott Hughes            rawData = data
564*e1fe3e4aSElliott Hughes            self.length = self.origLength
565*e1fe3e4aSElliott Hughes        else:
566*e1fe3e4aSElliott Hughes            rawData = compressedData
567*e1fe3e4aSElliott Hughes            self.length = len(rawData)
568*e1fe3e4aSElliott Hughes        return rawData
569*e1fe3e4aSElliott Hughes
570*e1fe3e4aSElliott Hughes
571*e1fe3e4aSElliott Hughesclass WOFFFlavorData:
572*e1fe3e4aSElliott Hughes    Flavor = "woff"
573*e1fe3e4aSElliott Hughes
574*e1fe3e4aSElliott Hughes    def __init__(self, reader=None):
575*e1fe3e4aSElliott Hughes        self.majorVersion = None
576*e1fe3e4aSElliott Hughes        self.minorVersion = None
577*e1fe3e4aSElliott Hughes        self.metaData = None
578*e1fe3e4aSElliott Hughes        self.privData = None
579*e1fe3e4aSElliott Hughes        if reader:
580*e1fe3e4aSElliott Hughes            self.majorVersion = reader.majorVersion
581*e1fe3e4aSElliott Hughes            self.minorVersion = reader.minorVersion
582*e1fe3e4aSElliott Hughes            if reader.metaLength:
583*e1fe3e4aSElliott Hughes                reader.file.seek(reader.metaOffset)
584*e1fe3e4aSElliott Hughes                rawData = reader.file.read(reader.metaLength)
585*e1fe3e4aSElliott Hughes                assert len(rawData) == reader.metaLength
586*e1fe3e4aSElliott Hughes                data = self._decompress(rawData)
587*e1fe3e4aSElliott Hughes                assert len(data) == reader.metaOrigLength
588*e1fe3e4aSElliott Hughes                self.metaData = data
589*e1fe3e4aSElliott Hughes            if reader.privLength:
590*e1fe3e4aSElliott Hughes                reader.file.seek(reader.privOffset)
591*e1fe3e4aSElliott Hughes                data = reader.file.read(reader.privLength)
592*e1fe3e4aSElliott Hughes                assert len(data) == reader.privLength
593*e1fe3e4aSElliott Hughes                self.privData = data
594*e1fe3e4aSElliott Hughes
595*e1fe3e4aSElliott Hughes    def _decompress(self, rawData):
596*e1fe3e4aSElliott Hughes        import zlib
597*e1fe3e4aSElliott Hughes
598*e1fe3e4aSElliott Hughes        return zlib.decompress(rawData)
599*e1fe3e4aSElliott Hughes
600*e1fe3e4aSElliott Hughes
601*e1fe3e4aSElliott Hughesdef calcChecksum(data):
602*e1fe3e4aSElliott Hughes    """Calculate the checksum for an arbitrary block of data.
603*e1fe3e4aSElliott Hughes
604*e1fe3e4aSElliott Hughes    If the data length is not a multiple of four, it assumes
605*e1fe3e4aSElliott Hughes    it is to be padded with null byte.
606*e1fe3e4aSElliott Hughes
607*e1fe3e4aSElliott Hughes            >>> print(calcChecksum(b"abcd"))
608*e1fe3e4aSElliott Hughes            1633837924
609*e1fe3e4aSElliott Hughes            >>> print(calcChecksum(b"abcdxyz"))
610*e1fe3e4aSElliott Hughes            3655064932
611*e1fe3e4aSElliott Hughes    """
612*e1fe3e4aSElliott Hughes    remainder = len(data) % 4
613*e1fe3e4aSElliott Hughes    if remainder:
614*e1fe3e4aSElliott Hughes        data += b"\0" * (4 - remainder)
615*e1fe3e4aSElliott Hughes    value = 0
616*e1fe3e4aSElliott Hughes    blockSize = 4096
617*e1fe3e4aSElliott Hughes    assert blockSize % 4 == 0
618*e1fe3e4aSElliott Hughes    for i in range(0, len(data), blockSize):
619*e1fe3e4aSElliott Hughes        block = data[i : i + blockSize]
620*e1fe3e4aSElliott Hughes        longs = struct.unpack(">%dL" % (len(block) // 4), block)
621*e1fe3e4aSElliott Hughes        value = (value + sum(longs)) & 0xFFFFFFFF
622*e1fe3e4aSElliott Hughes    return value
623*e1fe3e4aSElliott Hughes
624*e1fe3e4aSElliott Hughes
625*e1fe3e4aSElliott Hughesdef readTTCHeader(file):
626*e1fe3e4aSElliott Hughes    file.seek(0)
627*e1fe3e4aSElliott Hughes    data = file.read(ttcHeaderSize)
628*e1fe3e4aSElliott Hughes    if len(data) != ttcHeaderSize:
629*e1fe3e4aSElliott Hughes        raise TTLibError("Not a Font Collection (not enough data)")
630*e1fe3e4aSElliott Hughes    self = SimpleNamespace()
631*e1fe3e4aSElliott Hughes    sstruct.unpack(ttcHeaderFormat, data, self)
632*e1fe3e4aSElliott Hughes    if self.TTCTag != "ttcf":
633*e1fe3e4aSElliott Hughes        raise TTLibError("Not a Font Collection")
634*e1fe3e4aSElliott Hughes    assert self.Version == 0x00010000 or self.Version == 0x00020000, (
635*e1fe3e4aSElliott Hughes        "unrecognized TTC version 0x%08x" % self.Version
636*e1fe3e4aSElliott Hughes    )
637*e1fe3e4aSElliott Hughes    self.offsetTable = struct.unpack(
638*e1fe3e4aSElliott Hughes        ">%dL" % self.numFonts, file.read(self.numFonts * 4)
639*e1fe3e4aSElliott Hughes    )
640*e1fe3e4aSElliott Hughes    if self.Version == 0x00020000:
641*e1fe3e4aSElliott Hughes        pass  # ignoring version 2.0 signatures
642*e1fe3e4aSElliott Hughes    return self
643*e1fe3e4aSElliott Hughes
644*e1fe3e4aSElliott Hughes
645*e1fe3e4aSElliott Hughesdef writeTTCHeader(file, numFonts):
646*e1fe3e4aSElliott Hughes    self = SimpleNamespace()
647*e1fe3e4aSElliott Hughes    self.TTCTag = "ttcf"
648*e1fe3e4aSElliott Hughes    self.Version = 0x00010000
649*e1fe3e4aSElliott Hughes    self.numFonts = numFonts
650*e1fe3e4aSElliott Hughes    file.seek(0)
651*e1fe3e4aSElliott Hughes    file.write(sstruct.pack(ttcHeaderFormat, self))
652*e1fe3e4aSElliott Hughes    offset = file.tell()
653*e1fe3e4aSElliott Hughes    file.write(struct.pack(">%dL" % self.numFonts, *([0] * self.numFonts)))
654*e1fe3e4aSElliott Hughes    return offset
655*e1fe3e4aSElliott Hughes
656*e1fe3e4aSElliott Hughes
657*e1fe3e4aSElliott Hughesif __name__ == "__main__":
658*e1fe3e4aSElliott Hughes    import sys
659*e1fe3e4aSElliott Hughes    import doctest
660*e1fe3e4aSElliott Hughes
661*e1fe3e4aSElliott Hughes    sys.exit(doctest.testmod().failed)
662