File: sfnt.py

package info (click to toggle)
python-enable 4.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 7,220 kB
  • sloc: cpp: 57,417; python: 28,437; makefile: 314; sh: 43
file content (247 lines) | stat: -rw-r--r-- 10,162 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format.

Defines two public classes:
        SFNTReader
        SFNTWriter

(Normally you don't have to use these classes explicitly; they are
used automatically by ttLib.TTFont.)

The reading and writing of sfnt files is separated in two distinct
classes, since whenever to number of tables changes or whenever
a table's length chages you need to rewrite the whole file anyway.
"""

import struct
from kiva.fonttools import sstruct
import numpy
import os

# magic value corresponding to 0xb1b0afba; used in lieu of the hex
# constant to avoid signedness issues with python 2.4
CHECKSUM_MAGIC = -1313820742


class SFNTReader:

        def __init__(self, file, checkChecksums=1):
                self.file = file
                self.checkChecksums = checkChecksums
                data = self.file.read(sfntDirectorySize)
                if len(data) <> sfntDirectorySize:
                        from kiva.fonttools.fontTools import ttLib
                        raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)"
                sstruct.unpack(sfntDirectoryFormat, data, self)
                if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
                        from kiva.fonttools.fontTools import ttLib
                        raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
                self.tables = {}
                for i in range(self.numTables):
                        entry = SFNTDirectoryEntry()
                        entry.fromFile(self.file)
                        if entry.length > 0:
                                self.tables[entry.tag] = entry
                        else:
                                # Ignore zero-length tables. This doesn't seem to be documented,
                                # yet it's apparently how the Windows TT rasterizer behaves.
                                # Besides, at least one font has been sighted which actually
                                # *has* a zero-length table.
                                pass

        def has_key(self, tag):
                return self.tables.has_key(tag)

        def keys(self):
                return self.tables.keys()

        def __getitem__(self, tag):
                """Fetch the raw table data."""
                entry = self.tables[tag]
                self.file.seek(entry.offset)
                data = self.file.read(entry.length)
                if self.checkChecksums:
                        if tag == 'head':
                                # Beh: we have to special-case the 'head' table.
                                checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
                        else:
                                checksum = calcChecksum(data)
                        if self.checkChecksums > 1:
                                # Be obnoxious, and barf when it's wrong
                                assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
                        elif checksum <> entry.checkSum:
                                # Be friendly, and just print a warning.
                                print "bad checksum for '%s' table" % tag
                return data

        def __delitem__(self, tag):
                del self.tables[tag]

        def close(self):
                self.file.close()


class SFNTWriter:

        def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
                self.file = file
                self.numTables = numTables
                self.sfntVersion = sfntVersion
                self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
                self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
                # clear out directory area
                self.file.seek(self.nextTableOffset)
                # make sure we're actually where we want to be. (XXX old cStringIO bug)
                self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
                self.tables = {}

        def __setitem__(self, tag, data):
                """Write raw table data to disk."""
                if self.tables.has_key(tag):
                        # We've written this table to file before. If the length
                        # of the data is still the same, we allow overwriting it.
                        entry = self.tables[tag]
                        if len(data) <> entry.length:
                                from kiva.fonttools.fontTools import ttLib
                                raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
                else:
                        entry = SFNTDirectoryEntry()
                        entry.tag = tag
                        entry.offset = self.nextTableOffset
                        entry.length = len(data)
                        self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
                self.file.seek(entry.offset)
                self.file.write(data)
                self.file.seek(self.nextTableOffset)
                # make sure we're actually where we want to be. (XXX old cStringIO bug)
                self.file.write('\0' * (self.nextTableOffset - self.file.tell()))

                if tag == 'head':
                        entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
                else:
                        entry.checkSum = calcChecksum(data)
                self.tables[tag] = entry

        def close(self, closeStream=1):
                """All tables must have been written to disk. Now write the
                directory.
                """
                tables = self.tables.items()
                tables.sort()
                if len(tables) <> self.numTables:
                        from kiva.fonttools.fontTools import ttLib
                        raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))

                directory = sstruct.pack(sfntDirectoryFormat, self)

                self.file.seek(sfntDirectorySize)
                for tag, entry in tables:
                        directory = directory + entry.toString()
                self.calcMasterChecksum(directory)
                self.file.seek(0)
                self.file.write(directory)
                if closeStream:
                        self.file.close()

        def calcMasterChecksum(self, directory):
                # calculate checkSumAdjustment
                tags = self.tables.keys()
                checksums = numpy.zeros(len(tags)+1)
                for i in range(len(tags)):
                        checksums[i] = self.tables[tags[i]].checkSum

                directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
                assert directory_end == len(directory)

                checksums[-1] = calcChecksum(directory)
                checksum = numpy.add.reduce(checksums)
                # BiboAfba!
                checksumadjustment = numpy.array(CHECKSUM_MAGIC) - checksum
                # write the checksum to the file
                self.file.seek(self.tables['head'].offset + 8)
                self.file.write(struct.pack(">l", checksumadjustment))


# -- sfnt directory helpers and cruft

sfntDirectoryFormat = """
                > # big endian
                sfntVersion:    4s
                numTables:      H    # number of tables
                searchRange:    H    # (max2 <= numTables)*16
                entrySelector:  H    # log2(max2 <= numTables)
                rangeShift:     H    # numTables*16-searchRange
"""

sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)

sfntDirectoryEntryFormat = """
                > # big endian
                tag:            4s
                checkSum:       l
                offset:         l
                length:         l
"""

sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)

class SFNTDirectoryEntry:

        def fromFile(self, file):
                sstruct.unpack(sfntDirectoryEntryFormat,
                                file.read(sfntDirectoryEntrySize), self)

        def fromString(self, str):
                sstruct.unpack(sfntDirectoryEntryFormat, str, self)

        def toString(self):
                return sstruct.pack(sfntDirectoryEntryFormat, self)

        def __repr__(self):
                if hasattr(self, "tag"):
                        return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
                else:
                        return "<SFNTDirectoryEntry at %x>" % id(self)


def calcChecksum(data, start=0):
        """Calculate the checksum for an arbitrary block of data.
        Optionally takes a 'start' argument, which allows you to
        calculate a checksum in chunks by feeding it a previous
        result.

        If the data length is not a multiple of four, it assumes
        it is to be padded with null byte.
        """
        from kiva.fonttools.fontTools import ttLib
        remainder = len(data) % 4
        if remainder:
                data = data + '\0' * (4-remainder)
        a = numpy.fromstring(struct.pack(">l", start) + data, numpy.int32)
        if ttLib.endian <> "big":
                a = a.byteswapped()
        return numpy.add.reduce(a)


def maxPowerOfTwo(x):
        """Return the highest exponent of two, so that
        (2 ** exponent) <= x
        """
        exponent = 0
        while x:
                x = x >> 1
                exponent = exponent + 1
        return max(exponent - 1, 0)


def getSearchRange(n):
        """Calculate searchRange, entrySelector, rangeShift for the
        sfnt directory. 'n' is the number of tables.
        """
        # This stuff needs to be stored in the file, because?
        import math
        exponent = maxPowerOfTwo(n)
        searchRange = (2 ** exponent) * 16
        entrySelector = exponent
        rangeShift = n * 16 - searchRange
        return searchRange, entrySelector, rangeShift