blob: 6019d5fad561370f7541a01e7769102bd218649d [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()
pabs30e2aece2009-03-24 09:42:15 +0000162 checksums = numpy.zeros(len(tags)+1, numpy.uint32)
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)
pabs30e2aece2009-03-24 09:42:15 +0000170 checksum = numpy.add.reduce(checksums,dtype=numpy.uint32)
Just7842e561999-12-16 21:34:53 +0000171 # BiboAfba!
pabs30e2aece2009-03-24 09:42:15 +0000172 checksumadjustment = int(numpy.subtract.reduce(numpy.array([0xB1B0AFBA, checksum], numpy.uint32)))
Just7842e561999-12-16 21:34:53 +0000173 # write the checksum to the file
174 self.file.seek(self.tables['head'].offset + 8)
pabs30e2aece2009-03-24 09:42:15 +0000175 self.file.write(struct.pack(">L", checksumadjustment))
jvr1ebda672008-03-08 20:29:30 +0000176
Just7842e561999-12-16 21:34:53 +0000177
178# -- sfnt directory helpers and cruft
179
pabs37e91e772009-02-22 08:55:00 +0000180ttcHeaderFormat = """
181 > # big endian
182 TTCTag: 4s # "ttcf"
183 Version: L # 0x00010000 or 0x00020000
184 numFonts: L # number of fonts
185 # OffsetTable[numFonts]: L # array with offsets from beginning of file
186 # ulDsigTag: L # version 2.0 only
187 # ulDsigLength: L # version 2.0 only
188 # ulDsigOffset: L # version 2.0 only
189"""
190
191ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
192
Just7842e561999-12-16 21:34:53 +0000193sfntDirectoryFormat = """
194 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000195 sfntVersion: 4s
196 numTables: H # number of tables
197 searchRange: H # (max2 <= numTables)*16
198 entrySelector: H # log2(max2 <= numTables)
199 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000200"""
201
202sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
203
204sfntDirectoryEntryFormat = """
205 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000206 tag: 4s
pabs30e2aece2009-03-24 09:42:15 +0000207 checkSum: L
208 offset: L
209 length: L
Just7842e561999-12-16 21:34:53 +0000210"""
211
212sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
213
214class SFNTDirectoryEntry:
215
jvrea9dfa92002-05-12 17:14:50 +0000216 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000217 sstruct.unpack(sfntDirectoryEntryFormat,
218 file.read(sfntDirectoryEntrySize), self)
219
jvrea9dfa92002-05-12 17:14:50 +0000220 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000221 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
222
jvrea9dfa92002-05-12 17:14:50 +0000223 def toString(self):
Just7842e561999-12-16 21:34:53 +0000224 return sstruct.pack(sfntDirectoryEntryFormat, self)
225
226 def __repr__(self):
227 if hasattr(self, "tag"):
228 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
229 else:
230 return "<SFNTDirectoryEntry at %x>" % id(self)
231
232
jvrea9dfa92002-05-12 17:14:50 +0000233def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000234 """Calculate the checksum for an arbitrary block of data.
235 Optionally takes a 'start' argument, which allows you to
236 calculate a checksum in chunks by feeding it a previous
237 result.
238
239 If the data length is not a multiple of four, it assumes
240 it is to be padded with null byte.
241 """
242 from fontTools import ttLib
243 remainder = len(data) % 4
244 if remainder:
245 data = data + '\0' * (4-remainder)
pabs30e2aece2009-03-24 09:42:15 +0000246 data = struct.unpack(">%dL"%(len(data)/4), data)
247 a = numpy.array((start,)+data, numpy.uint32)
248 return int(numpy.sum(a,dtype=numpy.uint32))
Just7842e561999-12-16 21:34:53 +0000249
250
jvrea9dfa92002-05-12 17:14:50 +0000251def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000252 """Return the highest exponent of two, so that
253 (2 ** exponent) <= x
254 """
255 exponent = 0
256 while x:
257 x = x >> 1
258 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000259 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000260
261
jvrea9dfa92002-05-12 17:14:50 +0000262def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000263 """Calculate searchRange, entrySelector, rangeShift for the
264 sfnt directory. 'n' is the number of tables.
265 """
266 # This stuff needs to be stored in the file, because?
267 import math
jvrea9dfa92002-05-12 17:14:50 +0000268 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000269 searchRange = (2 ** exponent) * 16
270 entrySelector = exponent
271 rangeShift = n * 16 - searchRange
272 return searchRange, entrySelector, rangeShift
273