blob: c0d8ff1afe3bb0798b58f235282349e4b1a43d76 [file] [log] [blame]
Just7842e561999-12-16 21:34:53 +00001"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format.
2
3Defines two public classes:
4 SFNTReader
5 SFNTWriter
6
7(Normally you don't have to use these classes explicitly; they are
8used automatically by ttLib.TTFont.)
9
10The reading and writing of sfnt files is separated in two distinct
11classes, since whenever to number of tables changes or whenever
12a table's length chages you need to rewrite the whole file anyway.
13"""
14
jvr9be387c2008-03-01 11:43:01 +000015import sys
Just7842e561999-12-16 21:34:53 +000016import struct, sstruct
jvr1b7d54f2008-03-04 15:25:27 +000017import numpy
Just7842e561999-12-16 21:34:53 +000018import os
19
jvr04b32042002-05-14 12:09:10 +000020
Just7842e561999-12-16 21:34:53 +000021class SFNTReader:
22
pabs37e91e772009-02-22 08:55:00 +000023 def __init__(self, file, checkChecksums=1, fontNumber=-1):
Just7842e561999-12-16 21:34:53 +000024 self.file = file
jvrea9dfa92002-05-12 17:14:50 +000025 self.checkChecksums = checkChecksums
Just7842e561999-12-16 21:34:53 +000026 data = self.file.read(sfntDirectorySize)
27 if len(data) <> sfntDirectorySize:
28 from fontTools import ttLib
29 raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)"
30 sstruct.unpack(sfntDirectoryFormat, data, self)
pabs37e91e772009-02-22 08:55:00 +000031 if self.sfntVersion == "ttcf":
32 assert ttcHeaderSize == sfntDirectorySize
33 sstruct.unpack(ttcHeaderFormat, data, self)
34 assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version
35 if not 0 <= fontNumber < self.numFonts:
36 from fontTools import ttLib
37 raise ttLib.TTLibError, "specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)
38 offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4))
39 if self.Version == 0x00020000:
40 pass # ignoring version 2.0 signatures
41 self.file.seek(offsetTable[fontNumber])
42 data = self.file.read(sfntDirectorySize)
43 sstruct.unpack(sfntDirectoryFormat, data, self)
Just7842e561999-12-16 21:34:53 +000044 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
45 from fontTools import ttLib
46 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
47 self.tables = {}
48 for i in range(self.numTables):
49 entry = SFNTDirectoryEntry()
jvrea9dfa92002-05-12 17:14:50 +000050 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000051 if entry.length > 0:
52 self.tables[entry.tag] = entry
53 else:
54 # Ignore zero-length tables. This doesn't seem to be documented,
55 # yet it's apparently how the Windows TT rasterizer behaves.
56 # Besides, at least one font has been sighted which actually
57 # *has* a zero-length table.
58 pass
Just7842e561999-12-16 21:34:53 +000059
60 def has_key(self, tag):
61 return self.tables.has_key(tag)
62
63 def keys(self):
64 return self.tables.keys()
65
66 def __getitem__(self, tag):
67 """Fetch the raw table data."""
68 entry = self.tables[tag]
69 self.file.seek(entry.offset)
70 data = self.file.read(entry.length)
jvrea9dfa92002-05-12 17:14:50 +000071 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000072 if tag == 'head':
73 # Beh: we have to special-case the 'head' table.
jvrea9dfa92002-05-12 17:14:50 +000074 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000075 else:
jvrea9dfa92002-05-12 17:14:50 +000076 checksum = calcChecksum(data)
77 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000078 # Be obnoxious, and barf when it's wrong
79 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
80 elif checksum <> entry.checkSum:
81 # Be friendly, and just print a warning.
82 print "bad checksum for '%s' table" % tag
83 return data
84
jvrf7074632002-05-04 22:04:02 +000085 def __delitem__(self, tag):
86 del self.tables[tag]
87
Just7842e561999-12-16 21:34:53 +000088 def close(self):
89 self.file.close()
90
91
92class SFNTWriter:
93
94 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
95 self.file = file
96 self.numTables = numTables
97 self.sfntVersion = sfntVersion
jvrea9dfa92002-05-12 17:14:50 +000098 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
Just7842e561999-12-16 21:34:53 +000099 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
100 # clear out directory area
101 self.file.seek(self.nextTableOffset)
102 # make sure we're actually where we want to be. (XXX old cStringIO bug)
103 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
104 self.tables = {}
105
106 def __setitem__(self, tag, data):
107 """Write raw table data to disk."""
108 if self.tables.has_key(tag):
109 # We've written this table to file before. If the length
jvr04b32042002-05-14 12:09:10 +0000110 # of the data is still the same, we allow overwriting it.
Just7842e561999-12-16 21:34:53 +0000111 entry = self.tables[tag]
112 if len(data) <> entry.length:
113 from fontTools import ttLib
114 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
115 else:
116 entry = SFNTDirectoryEntry()
117 entry.tag = tag
118 entry.offset = self.nextTableOffset
119 entry.length = len(data)
120 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
121 self.file.seek(entry.offset)
122 self.file.write(data)
jvrc63ac642008-06-17 20:41:15 +0000123 # Add NUL bytes to pad the table data to a 4-byte boundary.
124 # Don't depend on f.seek() as we need to add the padding even if no
125 # subsequent write follows (seek is lazy), ie. after the final table
126 # in the font.
Just7842e561999-12-16 21:34:53 +0000127 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
jvrc63ac642008-06-17 20:41:15 +0000128 assert self.nextTableOffset == self.file.tell()
Just7842e561999-12-16 21:34:53 +0000129
130 if tag == 'head':
jvrea9dfa92002-05-12 17:14:50 +0000131 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +0000132 else:
jvrea9dfa92002-05-12 17:14:50 +0000133 entry.checkSum = calcChecksum(data)
Just7842e561999-12-16 21:34:53 +0000134 self.tables[tag] = entry
135
jvr28ae1962004-11-16 10:37:59 +0000136 def close(self):
Just7842e561999-12-16 21:34:53 +0000137 """All tables must have been written to disk. Now write the
138 directory.
139 """
140 tables = self.tables.items()
141 tables.sort()
142 if len(tables) <> self.numTables:
143 from fontTools import ttLib
144 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
145
146 directory = sstruct.pack(sfntDirectoryFormat, self)
147
148 self.file.seek(sfntDirectorySize)
jvrf509c0f2003-08-22 19:38:37 +0000149 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000150 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000151 if tag == "head":
152 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000153 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000154 if seenHead:
155 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000156 self.file.seek(0)
157 self.file.write(directory)
Just7842e561999-12-16 21:34:53 +0000158
jvrea9dfa92002-05-12 17:14:50 +0000159 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000160 # calculate checkSumAdjustment
161 tags = self.tables.keys()
jvr1ebda672008-03-08 20:29:30 +0000162 checksums = numpy.zeros(len(tags)+1, numpy.int32)
Just7842e561999-12-16 21:34:53 +0000163 for i in range(len(tags)):
164 checksums[i] = self.tables[tags[i]].checkSum
165
166 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
167 assert directory_end == len(directory)
168
jvrea9dfa92002-05-12 17:14:50 +0000169 checksums[-1] = calcChecksum(directory)
jvr1b7d54f2008-03-04 15:25:27 +0000170 checksum = numpy.add.reduce(checksums)
Just7842e561999-12-16 21:34:53 +0000171 # BiboAfba!
jvr1b7d54f2008-03-04 15:25:27 +0000172 checksumadjustment = numpy.array(0xb1b0afbaL - 0x100000000L,
173 numpy.int32) - checksum
Just7842e561999-12-16 21:34:53 +0000174 # write the checksum to the file
175 self.file.seek(self.tables['head'].offset + 8)
jvr58629632002-07-21 20:05:52 +0000176 self.file.write(struct.pack(">l", checksumadjustment))
jvr1ebda672008-03-08 20:29:30 +0000177
Just7842e561999-12-16 21:34:53 +0000178
179# -- sfnt directory helpers and cruft
180
pabs37e91e772009-02-22 08:55:00 +0000181ttcHeaderFormat = """
182 > # big endian
183 TTCTag: 4s # "ttcf"
184 Version: L # 0x00010000 or 0x00020000
185 numFonts: L # number of fonts
186 # OffsetTable[numFonts]: L # array with offsets from beginning of file
187 # ulDsigTag: L # version 2.0 only
188 # ulDsigLength: L # version 2.0 only
189 # ulDsigOffset: L # version 2.0 only
190"""
191
192ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
193
Just7842e561999-12-16 21:34:53 +0000194sfntDirectoryFormat = """
195 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000196 sfntVersion: 4s
197 numTables: H # number of tables
198 searchRange: H # (max2 <= numTables)*16
199 entrySelector: H # log2(max2 <= numTables)
200 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000201"""
202
203sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
204
205sfntDirectoryEntryFormat = """
206 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000207 tag: 4s
208 checkSum: l
209 offset: l
210 length: l
Just7842e561999-12-16 21:34:53 +0000211"""
212
213sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
214
215class SFNTDirectoryEntry:
216
jvrea9dfa92002-05-12 17:14:50 +0000217 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000218 sstruct.unpack(sfntDirectoryEntryFormat,
219 file.read(sfntDirectoryEntrySize), self)
220
jvrea9dfa92002-05-12 17:14:50 +0000221 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000222 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
223
jvrea9dfa92002-05-12 17:14:50 +0000224 def toString(self):
Just7842e561999-12-16 21:34:53 +0000225 return sstruct.pack(sfntDirectoryEntryFormat, self)
226
227 def __repr__(self):
228 if hasattr(self, "tag"):
229 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
230 else:
231 return "<SFNTDirectoryEntry at %x>" % id(self)
232
233
jvrea9dfa92002-05-12 17:14:50 +0000234def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000235 """Calculate the checksum for an arbitrary block of data.
236 Optionally takes a 'start' argument, which allows you to
237 calculate a checksum in chunks by feeding it a previous
238 result.
239
240 If the data length is not a multiple of four, it assumes
241 it is to be padded with null byte.
242 """
243 from fontTools import ttLib
244 remainder = len(data) % 4
245 if remainder:
246 data = data + '\0' * (4-remainder)
jvr1b7d54f2008-03-04 15:25:27 +0000247 a = numpy.fromstring(struct.pack(">l", start) + data, numpy.int32)
jvr9be387c2008-03-01 11:43:01 +0000248 if sys.byteorder <> "big":
jvr1b7d54f2008-03-04 15:25:27 +0000249 a = a.byteswap()
250 return numpy.add.reduce(a)
Just7842e561999-12-16 21:34:53 +0000251
252
jvrea9dfa92002-05-12 17:14:50 +0000253def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000254 """Return the highest exponent of two, so that
255 (2 ** exponent) <= x
256 """
257 exponent = 0
258 while x:
259 x = x >> 1
260 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000261 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000262
263
jvrea9dfa92002-05-12 17:14:50 +0000264def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000265 """Calculate searchRange, entrySelector, rangeShift for the
266 sfnt directory. 'n' is the number of tables.
267 """
268 # This stuff needs to be stored in the file, because?
269 import math
jvrea9dfa92002-05-12 17:14:50 +0000270 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000271 searchRange = (2 ** exponent) * 16
272 entrySelector = exponent
273 rangeShift = n * 16 - searchRange
274 return searchRange, entrySelector, rangeShift
275