1 """ttLib/sfnt.py -- low-level module to deal with the sfnt file format. 2 3 Defines two public classes: 4 SFNTReader 5 SFNTWriter 6 7 (Normally you don't have to use these classes explicitly; they are 8 used automatically by ttLib.TTFont.) 9 10 The reading and writing of sfnt files is separated in two distinct 11 classes, since whenever to number of tables changes or whenever 12 a table's length chages you need to rewrite the whole file anyway. 13 """ 14 15 from __future__ import print_function, division, absolute_import 16 from fontTools.misc.py23 import * 17 from fontTools.misc import sstruct 18 from fontTools.ttLib import getSearchRange 19 import struct 20 21 22 class 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 98 class 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 273 ttcHeaderFormat = """ 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 284 ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat) 285 286 sfntDirectoryFormat = """ 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 295 sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 296 297 sfntDirectoryEntryFormat = """ 298 > # big endian 299 tag: 4s 300 checkSum: L 301 offset: L 302 length: L 303 """ 304 305 sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 306 307 woffDirectoryFormat = """ 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 324 woffDirectorySize = sstruct.calcsize(woffDirectoryFormat) 325 326 woffDirectoryEntryFormat = """ 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 335 woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat) 336 337 338 class 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 379 class SFNTDirectoryEntry(DirectoryEntry): 380 381 format = sfntDirectoryEntryFormat 382 formatSize = sfntDirectoryEntrySize 383 384 class 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 414 class 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 441 def 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 468 if __name__ == "__main__": 469 import doctest 470 doctest.testmod() 471