sfnt.py revision 28ae1962292b66ad67117aef2a99d5735a70b779
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 struct, sstruct 16import Numeric 17import os 18 19 20class SFNTReader: 21 22 def __init__(self, file, checkChecksums=1): 23 self.file = file 24 self.checkChecksums = checkChecksums 25 data = self.file.read(sfntDirectorySize) 26 if len(data) <> sfntDirectorySize: 27 from fontTools import ttLib 28 raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)" 29 sstruct.unpack(sfntDirectoryFormat, data, self) 30 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"): 31 from fontTools import ttLib 32 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)" 33 self.tables = {} 34 for i in range(self.numTables): 35 entry = SFNTDirectoryEntry() 36 entry.fromFile(self.file) 37 if entry.length > 0: 38 self.tables[entry.tag] = entry 39 else: 40 # Ignore zero-length tables. This doesn't seem to be documented, 41 # yet it's apparently how the Windows TT rasterizer behaves. 42 # Besides, at least one font has been sighted which actually 43 # *has* a zero-length table. 44 pass 45 46 def has_key(self, tag): 47 return self.tables.has_key(tag) 48 49 def keys(self): 50 return self.tables.keys() 51 52 def __getitem__(self, tag): 53 """Fetch the raw table data.""" 54 entry = self.tables[tag] 55 self.file.seek(entry.offset) 56 data = self.file.read(entry.length) 57 if self.checkChecksums: 58 if tag == 'head': 59 # Beh: we have to special-case the 'head' table. 60 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:]) 61 else: 62 checksum = calcChecksum(data) 63 if self.checkChecksums > 1: 64 # Be obnoxious, and barf when it's wrong 65 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag 66 elif checksum <> entry.checkSum: 67 # Be friendly, and just print a warning. 68 print "bad checksum for '%s' table" % tag 69 return data 70 71 def __delitem__(self, tag): 72 del self.tables[tag] 73 74 def close(self): 75 self.file.close() 76 77 78class SFNTWriter: 79 80 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"): 81 self.file = file 82 self.numTables = numTables 83 self.sfntVersion = sfntVersion 84 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables) 85 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize 86 # clear out directory area 87 self.file.seek(self.nextTableOffset) 88 # make sure we're actually where we want to be. (XXX old cStringIO bug) 89 self.file.write('\0' * (self.nextTableOffset - self.file.tell())) 90 self.tables = {} 91 92 def __setitem__(self, tag, data): 93 """Write raw table data to disk.""" 94 if self.tables.has_key(tag): 95 # We've written this table to file before. If the length 96 # of the data is still the same, we allow overwriting it. 97 entry = self.tables[tag] 98 if len(data) <> entry.length: 99 from fontTools import ttLib 100 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag 101 else: 102 entry = SFNTDirectoryEntry() 103 entry.tag = tag 104 entry.offset = self.nextTableOffset 105 entry.length = len(data) 106 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3) 107 self.file.seek(entry.offset) 108 self.file.write(data) 109 self.file.seek(self.nextTableOffset) 110 # make sure we're actually where we want to be. (XXX old cStringIO bug) 111 self.file.write('\0' * (self.nextTableOffset - self.file.tell())) 112 113 if tag == 'head': 114 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:]) 115 else: 116 entry.checkSum = calcChecksum(data) 117 self.tables[tag] = entry 118 119 def close(self): 120 """All tables must have been written to disk. Now write the 121 directory. 122 """ 123 tables = self.tables.items() 124 tables.sort() 125 if len(tables) <> self.numTables: 126 from fontTools import ttLib 127 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables)) 128 129 directory = sstruct.pack(sfntDirectoryFormat, self) 130 131 self.file.seek(sfntDirectorySize) 132 seenHead = 0 133 for tag, entry in tables: 134 if tag == "head": 135 seenHead = 1 136 directory = directory + entry.toString() 137 if seenHead: 138 self.calcMasterChecksum(directory) 139 self.file.seek(0) 140 self.file.write(directory) 141 142 def calcMasterChecksum(self, directory): 143 # calculate checkSumAdjustment 144 tags = self.tables.keys() 145 checksums = Numeric.zeros(len(tags)+1) 146 for i in range(len(tags)): 147 checksums[i] = self.tables[tags[i]].checkSum 148 149 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize 150 assert directory_end == len(directory) 151 152 checksums[-1] = calcChecksum(directory) 153 checksum = Numeric.add.reduce(checksums) 154 # BiboAfba! 155 checksumadjustment = Numeric.array(0xb1b0afbaL - 0x100000000L, 156 Numeric.Int32) - checksum 157 # write the checksum to the file 158 self.file.seek(self.tables['head'].offset + 8) 159 self.file.write(struct.pack(">l", checksumadjustment)) 160 161 162# -- sfnt directory helpers and cruft 163 164sfntDirectoryFormat = """ 165 > # big endian 166 sfntVersion: 4s 167 numTables: H # number of tables 168 searchRange: H # (max2 <= numTables)*16 169 entrySelector: H # log2(max2 <= numTables) 170 rangeShift: H # numTables*16-searchRange 171""" 172 173sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 174 175sfntDirectoryEntryFormat = """ 176 > # big endian 177 tag: 4s 178 checkSum: l 179 offset: l 180 length: l 181""" 182 183sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 184 185class SFNTDirectoryEntry: 186 187 def fromFile(self, file): 188 sstruct.unpack(sfntDirectoryEntryFormat, 189 file.read(sfntDirectoryEntrySize), self) 190 191 def fromString(self, str): 192 sstruct.unpack(sfntDirectoryEntryFormat, str, self) 193 194 def toString(self): 195 return sstruct.pack(sfntDirectoryEntryFormat, self) 196 197 def __repr__(self): 198 if hasattr(self, "tag"): 199 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self)) 200 else: 201 return "<SFNTDirectoryEntry at %x>" % id(self) 202 203 204def calcChecksum(data, start=0): 205 """Calculate the checksum for an arbitrary block of data. 206 Optionally takes a 'start' argument, which allows you to 207 calculate a checksum in chunks by feeding it a previous 208 result. 209 210 If the data length is not a multiple of four, it assumes 211 it is to be padded with null byte. 212 """ 213 from fontTools import ttLib 214 remainder = len(data) % 4 215 if remainder: 216 data = data + '\0' * (4-remainder) 217 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32) 218 if ttLib.endian <> "big": 219 a = a.byteswapped() 220 return Numeric.add.reduce(a) 221 222 223def maxPowerOfTwo(x): 224 """Return the highest exponent of two, so that 225 (2 ** exponent) <= x 226 """ 227 exponent = 0 228 while x: 229 x = x >> 1 230 exponent = exponent + 1 231 return max(exponent - 1, 0) 232 233 234def getSearchRange(n): 235 """Calculate searchRange, entrySelector, rangeShift for the 236 sfnt directory. 'n' is the number of tables. 237 """ 238 # This stuff needs to be stored in the file, because? 239 import math 240 exponent = maxPowerOfTwo(n) 241 searchRange = (2 ** exponent) * 16 242 entrySelector = exponent 243 rangeShift = n * 16 - searchRange 244 return searchRange, entrySelector, rangeShift 245 246