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