Add support for loading WOFF file format

This commit is contained in:
Behdad Esfahbod 2013-08-15 15:30:55 -04:00
parent f09f1d4064
commit 58d7416124
2 changed files with 118 additions and 22 deletions

View File

@ -69,7 +69,7 @@ class TTFont:
""" """
def __init__(self, file=None, res_name_or_index=None, def __init__(self, file=None, res_name_or_index=None,
sfntVersion="\000\001\000\000", checkChecksums=0, sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0,
verbose=0, recalcBBoxes=1, allowVID=0, ignoreDecompileErrors=False, verbose=0, recalcBBoxes=1, allowVID=0, ignoreDecompileErrors=False,
fontNumber=-1): fontNumber=-1):
@ -91,7 +91,8 @@ class TTFont:
The TTFont constructor can also be called without a 'file' The TTFont constructor can also be called without a 'file'
argument: this is the way to create a new empty font. argument: this is the way to create a new empty font.
In this case you can optionally supply the 'sfntVersion' argument. In this case you can optionally supply the 'sfntVersion' argument,
and a 'flavor' which can be None, or 'woff'.
If the recalcBBoxes argument is false, a number of things will *not* If the recalcBBoxes argument is false, a number of things will *not*
be recalculated upon save/compile: be recalculated upon save/compile:
@ -135,6 +136,8 @@ class TTFont:
if not file: if not file:
self.sfntVersion = sfntVersion self.sfntVersion = sfntVersion
self.flavor = flavor
self.flavorData = None
return return
if not hasattr(file, "read"): if not hasattr(file, "read"):
# assume file is a string # assume file is a string
@ -155,6 +158,8 @@ class TTFont:
pass # assume "file" is a readable file object pass # assume "file" is a readable file object
self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber) self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber)
self.sfntVersion = self.reader.sfntVersion self.sfntVersion = self.reader.sfntVersion
self.flavor = self.reader.flavor
self.flavorData = self.reader.flavorData
def close(self): def close(self):
"""If we still have a reader object, close it.""" """If we still have a reader object, close it."""

View File

@ -22,14 +22,14 @@ class SFNTReader:
def __init__(self, file, checkChecksums=1, fontNumber=-1): def __init__(self, file, checkChecksums=1, fontNumber=-1):
self.file = file self.file = file
self.checkChecksums = checkChecksums self.checkChecksums = checkChecksums
data = self.file.read(sfntDirectorySize)
if len(data) <> sfntDirectorySize: self.flavor = None
from fontTools import ttLib self.flavorData = None
raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)" self.DirectoryEntry = SFNTDirectoryEntry
sstruct.unpack(sfntDirectoryFormat, data, self) self.sfntVersion = self.file.read(4)
self.file.seek(0)
if self.sfntVersion == "ttcf": if self.sfntVersion == "ttcf":
assert ttcHeaderSize == sfntDirectorySize sstruct.unpack(ttcHeaderFormat, self.file.read(ttcHeaderSize), self)
sstruct.unpack(ttcHeaderFormat, data, self)
assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version
if not 0 <= fontNumber < self.numFonts: if not 0 <= fontNumber < self.numFonts:
from fontTools import ttLib from fontTools import ttLib
@ -38,14 +38,20 @@ class SFNTReader:
if self.Version == 0x00020000: if self.Version == 0x00020000:
pass # ignoring version 2.0 signatures pass # ignoring version 2.0 signatures
self.file.seek(offsetTable[fontNumber]) self.file.seek(offsetTable[fontNumber])
data = self.file.read(sfntDirectorySize) sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self)
sstruct.unpack(sfntDirectoryFormat, data, self) elif self.sfntVersion == "wOFF":
self.flavor = "woff"
self.DirectoryEntry = WOFFDirectoryEntry
sstruct.unpack(woffDirectoryFormat, self.file.read(woffDirectorySize), self)
else:
sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self)
if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"): if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
from fontTools import ttLib from fontTools import ttLib
raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)" raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
self.tables = {} self.tables = {}
for i in range(self.numTables): for i in range(self.numTables):
entry = SFNTDirectoryEntry() entry = self.DirectoryEntry()
entry.fromFile(self.file) entry.fromFile(self.file)
if entry.length > 0: if entry.length > 0:
self.tables[entry.tag] = entry self.tables[entry.tag] = entry
@ -56,6 +62,10 @@ class SFNTReader:
# *has* a zero-length table. # *has* a zero-length table.
pass pass
# Load flavor data if any
if self.flavor == "woff":
self.flavorData = WOFFFlavorData(self)
def has_key(self, tag): def has_key(self, tag):
return self.tables.has_key(tag) return self.tables.has_key(tag)
@ -65,8 +75,7 @@ class SFNTReader:
def __getitem__(self, tag): def __getitem__(self, tag):
"""Fetch the raw table data.""" """Fetch the raw table data."""
entry = self.tables[tag] entry = self.tables[tag]
self.file.seek(entry.offset) data = entry.loadData (self.file)
data = self.file.read(entry.length)
if self.checkChecksums: if self.checkChecksums:
if tag == 'head': if tag == 'head':
# Beh: we have to special-case the 'head' table. # Beh: we have to special-case the 'head' table.
@ -214,23 +223,105 @@ sfntDirectoryEntryFormat = """
sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
class SFNTDirectoryEntry: woffDirectoryFormat = """
> # big endian
signature: 4s # "wOFF"
sfntVersion: 4s
length: L # total woff file size
numTables: H # number of tables
reserved: H # set to 0
totalSfntSize: L # uncompressed size
majorVersion: H # major version of WOFF file
minorVersion: H # minor version of WOFF file
metaOffset: L # offset to metadata block
metaLength: L # length of compressed metadata
metaOrigLength: L # length of uncompressed metadata
privOffset: L # offset to private data block
privLength: L # length of private data block
"""
woffDirectorySize = sstruct.calcsize(woffDirectoryFormat)
woffDirectoryEntryFormat = """
> # big endian
tag: 4s
offset: L
length: L # compressed length
origLength: L # original length
checksum: L # original checksum
"""
woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat)
class DirectoryEntry:
def fromFile(self, file): def fromFile(self, file):
sstruct.unpack(sfntDirectoryEntryFormat, sstruct.unpack(self.format, file.read(self.formatSize), self)
file.read(sfntDirectoryEntrySize), self)
def fromString(self, str): def fromString(self, str):
sstruct.unpack(sfntDirectoryEntryFormat, str, self) sstruct.unpack(self.format, str, self)
def toString(self): def toString(self):
return sstruct.pack(sfntDirectoryEntryFormat, self) return sstruct.pack(self.format, self)
def __repr__(self): def __repr__(self):
if hasattr(self, "tag"): if hasattr(self, "tag"):
return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self)) return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self))
else: else:
return "<SFNTDirectoryEntry at %x>" % id(self) return "<%s at %x>" % (self.__class__.__name__, id(self))
def loadData(self, file):
file.seek(self.offset)
data = file.read(self.length)
assert len(data) == self.length
return self.decodeData (data)
def decodeData(self, rawData):
return rawData
class SFNTDirectoryEntry(DirectoryEntry):
format = sfntDirectoryEntryFormat
formatSize = sfntDirectoryEntrySize
class WOFFDirectoryEntry(DirectoryEntry):
format = woffDirectoryEntryFormat
formatSize = woffDirectoryEntrySize
def decodeData(self, rawData):
import zlib
if self.length == self.origLength:
data = rawData
else:
assert self.length < self.origLength
data = zlib.decompress(rawData)
assert len (data) == self.origLength
return data
class WOFFFlavorData():
def __init__(self, reader=None):
self.majorVersion = None
self.minorVersion = None
self.metaData = None
self.privData = None
if reader:
self.majorVersion = reader.majorVersion
self.minorVersion = reader.minorVersion
if reader.metaLength:
reader.file.seek(reader.metaOffset)
rawData = read.file.read(reader.metaLength)
assert len(rawData) == reader.metaLength
data = zlib.decompress(rawData)
assert len(data) == reader.metaOrigLength
self.metaData = data
if reader.privLength:
reader.file.seek(reader.privOffset)
data = read.file.read(reader.privLength)
assert len(data) == reader.privLength
self.privData = data
def calcChecksum(data): def calcChecksum(data):