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 15from __future__ import print_function, division, absolute_import 16from fontTools.misc.py23 import * 17from fontTools.misc import sstruct 18from fontTools.ttLib import getSearchRange 19import struct 20 21 22class SFNTReader(object): 23 24 def __init__(self, file, checkChecksums=1, fontNumber=-1): 25 self.file = file 26 self.checkChecksums = checkChecksums 27 28 self.flavor = None 29 self.flavorData = None 30 self.DirectoryEntry = SFNTDirectoryEntry 31 self.sfntVersion = self.file.read(4) 32 self.file.seek(0) 33 if self.sfntVersion == b"ttcf": 34 sstruct.unpack(ttcHeaderFormat, self.file.read(ttcHeaderSize), self) 35 assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version 36 if not 0 <= fontNumber < self.numFonts: 37 from fontTools import ttLib 38 raise ttLib.TTLibError("specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)) 39 offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4)) 40 if self.Version == 0x00020000: 41 pass # ignoring version 2.0 signatures 42 self.file.seek(offsetTable[fontNumber]) 43 sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self) 44 elif self.sfntVersion == b"wOFF": 45 self.flavor = "woff" 46 self.DirectoryEntry = WOFFDirectoryEntry 47 sstruct.unpack(woffDirectoryFormat, self.file.read(woffDirectorySize), self) 48 else: 49 sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self) 50 self.sfntVersion = Tag(self.sfntVersion) 51 52 if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): 53 from fontTools import ttLib 54 raise ttLib.TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") 55 self.tables = {} 56 for i in range(self.numTables): 57 entry = self.DirectoryEntry() 58 entry.fromFile(self.file) 59 self.tables[Tag(entry.tag)] = entry 60 61 # Load flavor data if any 62 if self.flavor == "woff": 63 self.flavorData = WOFFFlavorData(self) 64 65 def has_key(self, tag): 66 return tag in self.tables 67 68 __contains__ = has_key 69 70 def keys(self): 71 return self.tables.keys() 72 73 def __getitem__(self, tag): 74 """Fetch the raw table data.""" 75 entry = self.tables[Tag(tag)] 76 data = entry.loadData (self.file) 77 if self.checkChecksums: 78 if tag == 'head': 79 # Beh: we have to special-case the 'head' table. 80 checksum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) 81 else: 82 checksum = calcChecksum(data) 83 if self.checkChecksums > 1: 84 # Be obnoxious, and barf when it's wrong 85 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag 86 elif checksum != entry.checkSum: 87 # Be friendly, and just print a warning. 88 print("bad checksum for '%s' table" % tag) 89 return data 90 91 def __delitem__(self, tag): 92 del self.tables[Tag(tag)] 93 94 def close(self): 95 self.file.close() 96 97 98class SFNTWriter(object): 99 100 def __init__(self, file, numTables, sfntVersion="\000\001\000\000", 101 flavor=None, flavorData=None): 102 self.file = file 103 self.numTables = numTables 104 self.sfntVersion = Tag(sfntVersion) 105 self.flavor = flavor 106 self.flavorData = flavorData 107 108 if self.flavor == "woff": 109 self.directoryFormat = woffDirectoryFormat 110 self.directorySize = woffDirectorySize 111 self.DirectoryEntry = WOFFDirectoryEntry 112 113 self.signature = "wOFF" 114 else: 115 assert not self.flavor, "Unknown flavor '%s'" % self.flavor 116 self.directoryFormat = sfntDirectoryFormat 117 self.directorySize = sfntDirectorySize 118 self.DirectoryEntry = SFNTDirectoryEntry 119 120 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables, 16) 121 122 self.nextTableOffset = self.directorySize + numTables * self.DirectoryEntry.formatSize 123 # clear out directory area 124 self.file.seek(self.nextTableOffset) 125 # make sure we're actually where we want to be. (old cStringIO bug) 126 self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) 127 self.tables = {} 128 129 def __setitem__(self, tag, data): 130 """Write raw table data to disk.""" 131 reuse = False 132 if tag in self.tables: 133 # We've written this table to file before. If the length 134 # of the data is still the same, we allow overwriting it. 135 entry = self.tables[tag] 136 assert not hasattr(entry.__class__, 'encodeData') 137 if len(data) != entry.length: 138 from fontTools import ttLib 139 raise ttLib.TTLibError("cannot rewrite '%s' table: length does not match directory entry" % tag) 140 reuse = True 141 else: 142 entry = self.DirectoryEntry() 143 entry.tag = tag 144 145 if tag == 'head': 146 entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) 147 self.headTable = data 148 entry.uncompressed = True 149 else: 150 entry.checkSum = calcChecksum(data) 151 152 entry.offset = self.nextTableOffset 153 entry.saveData (self.file, data) 154 155 if not reuse: 156 self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3) 157 158 # Add NUL bytes to pad the table data to a 4-byte boundary. 159 # Don't depend on f.seek() as we need to add the padding even if no 160 # subsequent write follows (seek is lazy), ie. after the final table 161 # in the font. 162 self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) 163 assert self.nextTableOffset == self.file.tell() 164 165 self.tables[tag] = entry 166 167 def close(self): 168 """All tables must have been written to disk. Now write the 169 directory. 170 """ 171 tables = sorted(self.tables.items()) 172 if len(tables) != self.numTables: 173 from fontTools import ttLib 174 raise ttLib.TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) 175 176 if self.flavor == "woff": 177 self.signature = b"wOFF" 178 self.reserved = 0 179 180 self.totalSfntSize = 12 181 self.totalSfntSize += 16 * len(tables) 182 for tag, entry in tables: 183 self.totalSfntSize += (entry.origLength + 3) & ~3 184 185 data = self.flavorData if self.flavorData else WOFFFlavorData() 186 if data.majorVersion is not None and data.minorVersion is not None: 187 self.majorVersion = data.majorVersion 188 self.minorVersion = data.minorVersion 189 else: 190 if hasattr(self, 'headTable'): 191 self.majorVersion, self.minorVersion = struct.unpack(">HH", self.headTable[4:8]) 192 else: 193 self.majorVersion = self.minorVersion = 0 194 if data.metaData: 195 self.metaOrigLength = len(data.metaData) 196 self.file.seek(0,2) 197 self.metaOffset = self.file.tell() 198 import zlib 199 compressedMetaData = zlib.compress(data.metaData) 200 self.metaLength = len(compressedMetaData) 201 self.file.write(compressedMetaData) 202 else: 203 self.metaOffset = self.metaLength = self.metaOrigLength = 0 204 if data.privData: 205 self.file.seek(0,2) 206 off = self.file.tell() 207 paddedOff = (off + 3) & ~3 208 self.file.write('\0' * (paddedOff - off)) 209 self.privOffset = self.file.tell() 210 self.privLength = len(data.privData) 211 self.file.write(data.privData) 212 else: 213 self.privOffset = self.privLength = 0 214 215 self.file.seek(0,2) 216 self.length = self.file.tell() 217 218 else: 219 assert not self.flavor, "Unknown flavor '%s'" % self.flavor 220 pass 221 222 directory = sstruct.pack(self.directoryFormat, self) 223 224 self.file.seek(self.directorySize) 225 seenHead = 0 226 for tag, entry in tables: 227 if tag == "head": 228 seenHead = 1 229 directory = directory + entry.toString() 230 if seenHead: 231 self.writeMasterChecksum(directory) 232 self.file.seek(0) 233 self.file.write(directory) 234 235 def _calcMasterChecksum(self, directory): 236 # calculate checkSumAdjustment 237 tags = list(self.tables.keys()) 238 checksums = [] 239 for i in range(len(tags)): 240 checksums.append(self.tables[tags[i]].checkSum) 241 242 # TODO(behdad) I'm fairly sure the checksum for woff is not working correctly. 243 # Haven't debugged. 244 if self.DirectoryEntry != SFNTDirectoryEntry: 245 # Create a SFNT directory for checksum calculation purposes 246 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) 247 directory = sstruct.pack(sfntDirectoryFormat, self) 248 tables = sorted(self.tables.items()) 249 for tag, entry in tables: 250 sfntEntry = SFNTDirectoryEntry() 251 for item in ['tag', 'checkSum', 'offset', 'length']: 252 setattr(sfntEntry, item, getattr(entry, item)) 253 directory = directory + sfntEntry.toString() 254 255 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize 256 assert directory_end == len(directory) 257 258 checksums.append(calcChecksum(directory)) 259 checksum = sum(checksums) & 0xffffffff 260 # BiboAfba! 261 checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff 262 return checksumadjustment 263 264 def writeMasterChecksum(self, directory): 265 checksumadjustment = self._calcMasterChecksum(directory) 266 # write the checksum to the file 267 self.file.seek(self.tables['head'].offset + 8) 268 self.file.write(struct.pack(">L", checksumadjustment)) 269 270 271# -- sfnt directory helpers and cruft 272 273ttcHeaderFormat = """ 274 > # big endian 275 TTCTag: 4s # "ttcf" 276 Version: L # 0x00010000 or 0x00020000 277 numFonts: L # number of fonts 278 # OffsetTable[numFonts]: L # array with offsets from beginning of file 279 # ulDsigTag: L # version 2.0 only 280 # ulDsigLength: L # version 2.0 only 281 # ulDsigOffset: L # version 2.0 only 282""" 283 284ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat) 285 286sfntDirectoryFormat = """ 287 > # big endian 288 sfntVersion: 4s 289 numTables: H # number of tables 290 searchRange: H # (max2 <= numTables)*16 291 entrySelector: H # log2(max2 <= numTables) 292 rangeShift: H # numTables*16-searchRange 293""" 294 295sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 296 297sfntDirectoryEntryFormat = """ 298 > # big endian 299 tag: 4s 300 checkSum: L 301 offset: L 302 length: L 303""" 304 305sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 306 307woffDirectoryFormat = """ 308 > # big endian 309 signature: 4s # "wOFF" 310 sfntVersion: 4s 311 length: L # total woff file size 312 numTables: H # number of tables 313 reserved: H # set to 0 314 totalSfntSize: L # uncompressed size 315 majorVersion: H # major version of WOFF file 316 minorVersion: H # minor version of WOFF file 317 metaOffset: L # offset to metadata block 318 metaLength: L # length of compressed metadata 319 metaOrigLength: L # length of uncompressed metadata 320 privOffset: L # offset to private data block 321 privLength: L # length of private data block 322""" 323 324woffDirectorySize = sstruct.calcsize(woffDirectoryFormat) 325 326woffDirectoryEntryFormat = """ 327 > # big endian 328 tag: 4s 329 offset: L 330 length: L # compressed length 331 origLength: L # original length 332 checkSum: L # original checksum 333""" 334 335woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat) 336 337 338class DirectoryEntry(object): 339 340 def __init__(self): 341 self.uncompressed = False # if True, always embed entry raw 342 343 def fromFile(self, file): 344 sstruct.unpack(self.format, file.read(self.formatSize), self) 345 346 def fromString(self, str): 347 sstruct.unpack(self.format, str, self) 348 349 def toString(self): 350 return sstruct.pack(self.format, self) 351 352 def __repr__(self): 353 if hasattr(self, "tag"): 354 return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self)) 355 else: 356 return "<%s at %x>" % (self.__class__.__name__, id(self)) 357 358 def loadData(self, file): 359 file.seek(self.offset) 360 data = file.read(self.length) 361 assert len(data) == self.length 362 if hasattr(self.__class__, 'decodeData'): 363 data = self.decodeData(data) 364 return data 365 366 def saveData(self, file, data): 367 if hasattr(self.__class__, 'encodeData'): 368 data = self.encodeData(data) 369 self.length = len(data) 370 file.seek(self.offset) 371 file.write(data) 372 373 def decodeData(self, rawData): 374 return rawData 375 376 def encodeData(self, data): 377 return data 378 379class SFNTDirectoryEntry(DirectoryEntry): 380 381 format = sfntDirectoryEntryFormat 382 formatSize = sfntDirectoryEntrySize 383 384class WOFFDirectoryEntry(DirectoryEntry): 385 386 format = woffDirectoryEntryFormat 387 formatSize = woffDirectoryEntrySize 388 zlibCompressionLevel = 6 389 390 def decodeData(self, rawData): 391 import zlib 392 if self.length == self.origLength: 393 data = rawData 394 else: 395 assert self.length < self.origLength 396 data = zlib.decompress(rawData) 397 assert len (data) == self.origLength 398 return data 399 400 def encodeData(self, data): 401 import zlib 402 self.origLength = len(data) 403 if not self.uncompressed: 404 compressedData = zlib.compress(data, self.zlibCompressionLevel) 405 if self.uncompressed or len(compressedData) >= self.origLength: 406 # Encode uncompressed 407 rawData = data 408 self.length = self.origLength 409 else: 410 rawData = compressedData 411 self.length = len(rawData) 412 return rawData 413 414class WOFFFlavorData(): 415 416 Flavor = 'woff' 417 418 def __init__(self, reader=None): 419 self.majorVersion = None 420 self.minorVersion = None 421 self.metaData = None 422 self.privData = None 423 if reader: 424 self.majorVersion = reader.majorVersion 425 self.minorVersion = reader.minorVersion 426 if reader.metaLength: 427 reader.file.seek(reader.metaOffset) 428 rawData = reader.file.read(reader.metaLength) 429 assert len(rawData) == reader.metaLength 430 import zlib 431 data = zlib.decompress(rawData) 432 assert len(data) == reader.metaOrigLength 433 self.metaData = data 434 if reader.privLength: 435 reader.file.seek(reader.privOffset) 436 data = reader.file.read(reader.privLength) 437 assert len(data) == reader.privLength 438 self.privData = data 439 440 441def calcChecksum(data): 442 """Calculate the checksum for an arbitrary block of data. 443 Optionally takes a 'start' argument, which allows you to 444 calculate a checksum in chunks by feeding it a previous 445 result. 446 447 If the data length is not a multiple of four, it assumes 448 it is to be padded with null byte. 449 450 >>> print calcChecksum(b"abcd") 451 1633837924 452 >>> print calcChecksum(b"abcdxyz") 453 3655064932 454 """ 455 remainder = len(data) % 4 456 if remainder: 457 data += b"\0" * (4 - remainder) 458 value = 0 459 blockSize = 4096 460 assert blockSize % 4 == 0 461 for i in range(0, len(data), blockSize): 462 block = data[i:i+blockSize] 463 longs = struct.unpack(">%dL" % (len(block) // 4), block) 464 value = (value + sum(longs)) & 0xffffffff 465 return value 466 467 468if __name__ == "__main__": 469 import doctest 470 doctest.testmod() 471