sfnt.py revision 0e2aecec53da493c44d6a5c253910a9475da218a
1"""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 15import sys 16import struct, sstruct 17import numpy 18import os 19 20 21class SFNTReader: 22 23 def __init__(self, file, checkChecksums=1, fontNumber=-1): 24 self.file = file 25 self.checkChecksums = checkChecksums 26 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) 31 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) 44 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() 50 entry.fromFile(self.file) 51 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 59 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) 71 if self.checkChecksums: 72 if tag == 'head': 73 # Beh: we have to special-case the 'head' table. 74 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:]) 75 else: 76 checksum = calcChecksum(data) 77 if self.checkChecksums > 1: 78 # 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 85 def __delitem__(self, tag): 86 del self.tables[tag] 87 88 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 98 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables) 99 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 110 # of the data is still the same, we allow overwriting it. 111 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) 123 # 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. 127 self.file.write('\0' * (self.nextTableOffset - self.file.tell())) 128 assert self.nextTableOffset == self.file.tell() 129 130 if tag == 'head': 131 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:]) 132 else: 133 entry.checkSum = calcChecksum(data) 134 self.tables[tag] = entry 135 136 def close(self): 137 """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) 149 seenHead = 0 150 for tag, entry in tables: 151 if tag == "head": 152 seenHead = 1 153 directory = directory + entry.toString() 154 if seenHead: 155 self.calcMasterChecksum(directory) 156 self.file.seek(0) 157 self.file.write(directory) 158 159 def calcMasterChecksum(self, directory): 160 # calculate checkSumAdjustment 161 tags = self.tables.keys() 162 checksums = numpy.zeros(len(tags)+1, numpy.uint32) 163 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 169 checksums[-1] = calcChecksum(directory) 170 checksum = numpy.add.reduce(checksums,dtype=numpy.uint32) 171 # BiboAfba! 172 checksumadjustment = int(numpy.subtract.reduce(numpy.array([0xB1B0AFBA, checksum], numpy.uint32))) 173 # write the checksum to the file 174 self.file.seek(self.tables['head'].offset + 8) 175 self.file.write(struct.pack(">L", checksumadjustment)) 176 177 178# -- sfnt directory helpers and cruft 179 180ttcHeaderFormat = """ 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 193sfntDirectoryFormat = """ 194 > # big endian 195 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 200""" 201 202sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 203 204sfntDirectoryEntryFormat = """ 205 > # big endian 206 tag: 4s 207 checkSum: L 208 offset: L 209 length: L 210""" 211 212sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 213 214class SFNTDirectoryEntry: 215 216 def fromFile(self, file): 217 sstruct.unpack(sfntDirectoryEntryFormat, 218 file.read(sfntDirectoryEntrySize), self) 219 220 def fromString(self, str): 221 sstruct.unpack(sfntDirectoryEntryFormat, str, self) 222 223 def toString(self): 224 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 233def calcChecksum(data, start=0): 234 """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) 246 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)) 249 250 251def maxPowerOfTwo(x): 252 """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 259 return max(exponent - 1, 0) 260 261 262def getSearchRange(n): 263 """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 268 exponent = maxPowerOfTwo(n) 269 searchRange = (2 ** exponent) * 16 270 entrySelector = exponent 271 rangeShift = n * 16 - searchRange 272 return searchRange, entrySelector, rangeShift 273 274