Skip to content

Commit 5877298

Browse files
committed
Merge pull request #11 from mdboom/subsetting
Add font subsetting support
2 parents 10d2c93 + 2c02ab6 commit 5877298

File tree

4 files changed

+411
-1
lines changed

4 files changed

+411
-1
lines changed

lib/freetypy/subset.py

+343
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright (c) 2015, Michael Droettboom All rights reserved.
4+
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions
7+
# are met:
8+
9+
# 1. Redistributions of source code must retain the above copyright
10+
# notice, this list of conditions and the following disclaimer.
11+
# 2. Redistributions in binary form must reproduce the above copyright
12+
# notice, this list of conditions and the following disclaimer in
13+
# the documentation and/or other materials provided with the
14+
# distribution.
15+
16+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
19+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
20+
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
21+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
22+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
# POSSIBILITY OF SUCH DAMAGE.
28+
29+
# The views and conclusions contained in the software and
30+
# documentation are those of the authors and should not be interpreted
31+
# as representing official policies, either expressed or implied, of
32+
# the FreeBSD Project.
33+
34+
"""
35+
A library to subset SFNT-style fonts.
36+
"""
37+
38+
from __future__ import absolute_import, division, unicode_literals, print_function
39+
40+
41+
__all__ = ['subset_font']
42+
43+
44+
from collections import OrderedDict
45+
import struct
46+
47+
48+
from freetypy import Face
49+
50+
51+
UNDERSTOOD_VERSIONS = (0x00010000, 0x4f54544f)
52+
53+
54+
class _BinaryStruct(object):
55+
"""
56+
A wrapper around the Python stdlib struct module to define a
57+
binary struct more like a dictionary than a tuple.
58+
"""
59+
def __init__(self, descr, endian='>'):
60+
"""
61+
Parameters
62+
----------
63+
descr : list of tuple
64+
Each entry is a pair ``(name, format)``, where ``format``
65+
is one of the format types understood by `struct`.
66+
endian : str, optional
67+
The endianness of the struct. Must be ``>`` or ``<``.
68+
"""
69+
self._fmt = [endian]
70+
self._offsets = {}
71+
self._names = []
72+
i = 0
73+
for name, fmt in descr:
74+
self._fmt.append(fmt)
75+
self._offsets[name] = (i, (endian + fmt).encode('ascii'))
76+
self._names.append(name)
77+
i += struct.calcsize(fmt.encode('ascii'))
78+
self._fmt = ''.join(self._fmt).encode('ascii')
79+
self._size = struct.calcsize(self._fmt)
80+
81+
@property
82+
def size(self):
83+
"""
84+
Return the size of the struct.
85+
"""
86+
return self._size
87+
88+
def pack(self, **kwargs):
89+
"""
90+
Pack the given arguments, which are given as kwargs, and
91+
return the binary struct.
92+
"""
93+
fields = [0] * len(self._names)
94+
for key, val in kwargs.items():
95+
if key not in self._offsets:
96+
raise KeyError("No header field '{0}'".format(key))
97+
i = self._names.index(key)
98+
fields[i] = val
99+
return struct.pack(self._fmt, *fields)
100+
101+
def unpack(self, buff):
102+
"""
103+
Unpack the given binary buffer into the fields. The result
104+
is a dictionary mapping field names to values.
105+
"""
106+
args = struct.unpack_from(self._fmt, buff[:self._size])
107+
return dict(zip(self._names, args))
108+
109+
def read(self, fd):
110+
"""
111+
Read a struct from the current location in the file.
112+
"""
113+
buff = fd.read(self.size)
114+
return self.unpack(buff)
115+
116+
def write(self, fd, data):
117+
"""
118+
Write a struct to the current location in the file.
119+
"""
120+
buff = self.pack(**data)
121+
fd.write(buff)
122+
123+
124+
class Table(object):
125+
header_struct = _BinaryStruct([
126+
('tag', '4s'),
127+
('checkSum', 'I'),
128+
('offset', 'I'),
129+
('length', 'I')
130+
])
131+
132+
def __init__(self, header, content):
133+
self._header = header
134+
self._content = content
135+
136+
def __repr__(self):
137+
return "<Table '{0}'>".format(
138+
self._header['tag'].decode('ascii'))
139+
140+
def _calc_checksum(self, content):
141+
end = ((len(content) + 3) & ~3) - 4
142+
sum = 0
143+
for i in range(0, end, 4):
144+
sum += struct.unpack('I', content[i:i+4])[0]
145+
sum = sum & 0xffffffff
146+
return sum
147+
148+
@classmethod
149+
def read(cls, fd):
150+
header = Table.header_struct.read(fd)
151+
content = fd.read(header['length'])
152+
153+
return cls(header, content)
154+
155+
@property
156+
def header(self):
157+
return self._header
158+
159+
@property
160+
def content(self):
161+
return self._content
162+
@content.setter
163+
def content(self, new_content):
164+
checksum = self._calc_checksum(new_content)
165+
self._header['checkSum'] = checksum
166+
self._header['length'] = len(new_content)
167+
self._content = new_content
168+
169+
170+
class HeadTable(Table):
171+
head_table_struct = _BinaryStruct([
172+
('version', 'I'),
173+
('fontRevision', 'I'),
174+
('checkSumAdjustment', 'I'),
175+
('magicNumber', 'I'),
176+
('flags', 'H'),
177+
('unitsPerEm', 'H'),
178+
('created', 'q'),
179+
('modified', 'q'),
180+
('xMin', 'h'),
181+
('yMin', 'h'),
182+
('xMax', 'h'),
183+
('yMax', 'h'),
184+
('macStyle', 'H'),
185+
('lowestRecPPEM', 'H'),
186+
('fontDirectionHint', 'h'),
187+
('indexToLocFormat', 'h'),
188+
('glyphDataFormat', 'h')])
189+
190+
def __init__(self, header, content):
191+
super(HeadTable, self).__init__(header, content)
192+
193+
self.__dict__.update(self.head_table_struct.unpack(content))
194+
195+
if self.version not in UNDERSTOOD_VERSIONS:
196+
raise ValueError("Not a TrueType or OpenType file")
197+
198+
if self.magicNumber != 0x5F0F3CF5:
199+
raise ValueError("Bad magic number")
200+
201+
202+
class LocaTable(Table):
203+
def _get_formats(self, fontfile):
204+
if fontfile[b'head'].indexToLocFormat == 0: # short offsets
205+
return '>H', 2, 2
206+
else:
207+
return '>I', 4, 1
208+
209+
def get_offsets(self, fontfile):
210+
entry_format, entry_size, scale = self._get_formats(fontfile)
211+
212+
content = self._content
213+
offsets = []
214+
for i in range(0, len(content), entry_size):
215+
value = struct.unpack(
216+
entry_format, content[i:i+entry_size])[0] * scale
217+
offsets.append(value)
218+
219+
return offsets
220+
221+
def subset(self, fontfile, glyphs, offsets):
222+
new_offsets = []
223+
offset = 0
224+
j = 0
225+
for i in range(len(offsets) - 1):
226+
new_offsets.append(offset)
227+
if j < len(glyphs) and i == glyphs[j]:
228+
offset += offsets[i+1] - offsets[i]
229+
j += 1
230+
new_offsets.append(offset)
231+
232+
entry_format, entry_size, scale = self._get_formats(fontfile)
233+
new_content = []
234+
for value in new_offsets:
235+
new_content.append(struct.pack(entry_format, value // scale))
236+
self.content = b''.join(new_content)
237+
238+
239+
class GlyfTable(Table):
240+
def subset(self, glyphs, offsets):
241+
content = self.content
242+
new_content = []
243+
for gind in glyphs:
244+
new_content.append(content[offsets[gind]:offsets[gind+1]])
245+
self.content = b''.join(new_content)
246+
247+
248+
SPECIAL_TABLES = {
249+
b'head': HeadTable,
250+
b'loca': LocaTable,
251+
b'glyf': GlyfTable
252+
}
253+
254+
255+
class FontFile(object):
256+
"""
257+
A class to subset SFNT-style fonts (TrueType and OpenType).
258+
"""
259+
260+
header_struct = _BinaryStruct([
261+
('version', 'I'),
262+
('numTables', 'H'),
263+
('searchRange', 'H'),
264+
('entrySelector', 'H'),
265+
('rangeShift', 'H')
266+
])
267+
268+
def __init__(self, face, header, tables):
269+
self._face = face
270+
self._header = header
271+
self._tables = tables
272+
273+
def __getitem__(self, tag):
274+
return self._tables[tag]
275+
276+
@classmethod
277+
def read(cls, fd):
278+
header = cls.header_struct.read(fd)
279+
280+
if header['version'] not in UNDERSTOOD_VERSIONS:
281+
raise ValueError("Not a TrueType or OpenType file")
282+
283+
table_dir = []
284+
for i in range(header['numTables']):
285+
table_dir.append(Table.header_struct.read(fd))
286+
287+
tables = OrderedDict()
288+
for table_header in table_dir:
289+
fd.seek(table_header['offset'])
290+
content = fd.read(table_header['length'])
291+
table_cls = SPECIAL_TABLES.get(table_header['tag'], Table)
292+
tables[table_header['tag']] = table_cls(table_header, content)
293+
294+
fd.seek(0)
295+
face = Face(fd)
296+
297+
return cls(face, header, tables)
298+
299+
def subset(self, ccodes):
300+
glyphs = []
301+
for ccode in ccodes:
302+
glyphs.append(self._face.get_char_index_unicode(ccode))
303+
glyphs.sort()
304+
305+
offsets = self[b'loca'].get_offsets(self)
306+
self[b'glyf'].subset(glyphs, offsets)
307+
self[b'loca'].subset(self, glyphs, offsets)
308+
309+
def write(self, fd):
310+
self.header_struct.write(fd, self._header)
311+
312+
offset = (self.header_struct.size +
313+
Table.header_struct.size * len(self._tables))
314+
315+
for table in self._tables.values():
316+
table.header['offset'] = offset
317+
offset += len(table.content)
318+
319+
for table in self._tables.values():
320+
Table.header_struct.write(fd, table.header)
321+
322+
for table in self._tables.values():
323+
fd.write(table._content)
324+
325+
326+
def subset_font(input_fd, output_fd, charcodes):
327+
"""
328+
Subset a SFNT-style (TrueType or OpenType) font.
329+
330+
Parameters
331+
----------
332+
input_fd : readable file-like object, for bytes
333+
The font file to read.
334+
335+
output_fd : writable file-like object, for bytes
336+
The file to write a subsetted font file to.
337+
338+
charcodes : list of int or unicode string
339+
The character codes to include in the output font file.
340+
"""
341+
fontfile = FontFile.read(input_fd)
342+
fontfile.subset(charcodes)
343+
fontfile.write(output_fd)

0 commit comments

Comments
 (0)