Skip to content

Commit 449edd0

Browse files
committed
Add font subsetting support.
This will replace matplotlib._ttconv
1 parent 5eb0867 commit 449edd0

File tree

2 files changed

+404
-0
lines changed

2 files changed

+404
-0
lines changed

lib/freetypy/subset.py

+340
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
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+
from collections import OrderedDict
42+
import struct
43+
44+
45+
from freetypy import Face
46+
47+
48+
UNDERSTOOD_VERSIONS = (0x00010000, 0x4f54544f)
49+
50+
51+
class _BinaryStruct(object):
52+
"""
53+
A wrapper around the Python stdlib struct module to define a
54+
binary struct more like a dictionary than a tuple.
55+
"""
56+
def __init__(self, descr, endian='>'):
57+
"""
58+
Parameters
59+
----------
60+
descr : list of tuple
61+
Each entry is a pair ``(name, format)``, where ``format``
62+
is one of the format types understood by `struct`.
63+
endian : str, optional
64+
The endianness of the struct. Must be ``>`` or ``<``.
65+
"""
66+
self._fmt = [endian]
67+
self._offsets = {}
68+
self._names = []
69+
i = 0
70+
for name, fmt in descr:
71+
self._fmt.append(fmt)
72+
self._offsets[name] = (i, (endian + fmt).encode('ascii'))
73+
self._names.append(name)
74+
i += struct.calcsize(fmt.encode('ascii'))
75+
self._fmt = ''.join(self._fmt).encode('ascii')
76+
self._size = struct.calcsize(self._fmt)
77+
78+
@property
79+
def size(self):
80+
"""
81+
Return the size of the struct.
82+
"""
83+
return self._size
84+
85+
def pack(self, **kwargs):
86+
"""
87+
Pack the given arguments, which are given as kwargs, and
88+
return the binary struct.
89+
"""
90+
fields = [0] * len(self._names)
91+
for key, val in kwargs.items():
92+
if key not in self._offsets:
93+
raise KeyError("No header field '{0}'".format(key))
94+
i = self._names.index(key)
95+
fields[i] = val
96+
return struct.pack(self._fmt, *fields)
97+
98+
def unpack(self, buff):
99+
"""
100+
Unpack the given binary buffer into the fields. The result
101+
is a dictionary mapping field names to values.
102+
"""
103+
args = struct.unpack_from(self._fmt, buff[:self._size])
104+
return dict(zip(self._names, args))
105+
106+
def read(self, fd):
107+
"""
108+
Read a struct from the current location in the file.
109+
"""
110+
buff = fd.read(self.size)
111+
return self.unpack(buff)
112+
113+
def write(self, fd, data):
114+
"""
115+
Write a struct to the current location in the file.
116+
"""
117+
buff = self.pack(**data)
118+
fd.write(buff)
119+
120+
121+
class Table(object):
122+
header_struct = _BinaryStruct([
123+
('tag', '4s'),
124+
('checkSum', 'I'),
125+
('offset', 'I'),
126+
('length', 'I')
127+
])
128+
129+
def __init__(self, header, content):
130+
self._header = header
131+
self._content = content
132+
133+
def __repr__(self):
134+
return "<Table '{0}'>".format(
135+
self._header['tag'].decode('ascii'))
136+
137+
def _calc_checksum(self, content):
138+
end = ((len(content) + 3) & ~3) - 4
139+
sum = 0
140+
for i in range(0, end, 4):
141+
sum += struct.unpack('I', content[i:i+4])[0]
142+
sum = sum & 0xffffffff
143+
return sum
144+
145+
@classmethod
146+
def read(cls, fd):
147+
header = Table.header_struct.read(fd)
148+
content = fd.read(header['length'])
149+
150+
return cls(header, content)
151+
152+
@property
153+
def header(self):
154+
return self._header
155+
156+
@property
157+
def content(self):
158+
return self._content
159+
@content.setter
160+
def content(self, new_content):
161+
checksum = self._calc_checksum(new_content)
162+
self._header['checkSum'] = checksum
163+
self._header['length'] = len(new_content)
164+
self._content = new_content
165+
166+
167+
class HeadTable(Table):
168+
head_table_struct = _BinaryStruct([
169+
('version', 'I'),
170+
('fontRevision', 'I'),
171+
('checkSumAdjustment', 'I'),
172+
('magicNumber', 'I'),
173+
('flags', 'H'),
174+
('unitsPerEm', 'H'),
175+
('created', 'q'),
176+
('modified', 'q'),
177+
('xMin', 'h'),
178+
('yMin', 'h'),
179+
('xMax', 'h'),
180+
('yMax', 'h'),
181+
('macStyle', 'H'),
182+
('lowestRecPPEM', 'H'),
183+
('fontDirectionHint', 'h'),
184+
('indexToLocFormat', 'h'),
185+
('glyphDataFormat', 'h')])
186+
187+
def __init__(self, header, content):
188+
super(HeadTable, self).__init__(header, content)
189+
190+
self.__dict__.update(self.head_table_struct.unpack(content))
191+
192+
if self.version not in UNDERSTOOD_VERSIONS:
193+
raise ValueError("Not a TrueType or OpenType file")
194+
195+
if self.magicNumber != 0x5F0F3CF5:
196+
raise ValueError("Bad magic number")
197+
198+
199+
class LocaTable(Table):
200+
def _get_formats(self, fontfile):
201+
if fontfile[b'head'].indexToLocFormat == 0: # short offsets
202+
return '>H', 2, 2
203+
else:
204+
return '>I', 4, 1
205+
206+
def get_offsets(self, fontfile):
207+
entry_format, entry_size, scale = self._get_formats(fontfile)
208+
209+
content = self._content
210+
offsets = []
211+
for i in range(0, len(content), entry_size):
212+
value = struct.unpack(
213+
entry_format, content[i:i+entry_size])[0] * scale
214+
offsets.append(value)
215+
216+
return offsets
217+
218+
def subset(self, fontfile, glyphs, offsets):
219+
new_offsets = []
220+
offset = 0
221+
j = 0
222+
for i in range(len(offsets) - 1):
223+
new_offsets.append(offset)
224+
if j < len(glyphs) and i == glyphs[j]:
225+
offset += offsets[i+1] - offsets[i]
226+
j += 1
227+
new_offsets.append(offset)
228+
229+
entry_format, entry_size, scale = self._get_formats(fontfile)
230+
new_content = []
231+
for value in new_offsets:
232+
new_content.append(struct.pack(entry_format, value // scale))
233+
self.content = b''.join(new_content)
234+
235+
236+
class GlyfTable(Table):
237+
def subset(self, glyphs, offsets):
238+
content = self.content
239+
new_content = []
240+
for gind in glyphs:
241+
new_content.append(content[offsets[gind]:offsets[gind+1]])
242+
self.content = b''.join(new_content)
243+
244+
245+
SPECIAL_TABLES = {
246+
b'head': HeadTable,
247+
b'loca': LocaTable,
248+
b'glyf': GlyfTable
249+
}
250+
251+
252+
class FontFile(object):
253+
"""
254+
A class to subset SFNT-style fonts (TrueType and OpenType).
255+
"""
256+
257+
header_struct = _BinaryStruct([
258+
('version', 'I'),
259+
('numTables', 'H'),
260+
('searchRange', 'H'),
261+
('entrySelector', 'H'),
262+
('rangeShift', 'H')
263+
])
264+
265+
def __init__(self, face, header, tables):
266+
self._face = face
267+
self._header = header
268+
self._tables = tables
269+
270+
def __getitem__(self, tag):
271+
return self._tables[tag]
272+
273+
@classmethod
274+
def read(cls, fd):
275+
header = cls.header_struct.read(fd)
276+
277+
if header['version'] not in UNDERSTOOD_VERSIONS:
278+
raise ValueError("Not a TrueType or OpenType file")
279+
280+
table_dir = []
281+
for i in range(header['numTables']):
282+
table_dir.append(Table.header_struct.read(fd))
283+
284+
tables = OrderedDict()
285+
for table_header in table_dir:
286+
fd.seek(table_header['offset'])
287+
content = fd.read(table_header['length'])
288+
table_cls = SPECIAL_TABLES.get(table_header['tag'], Table)
289+
tables[table_header['tag']] = table_cls(table_header, content)
290+
291+
fd.seek(0)
292+
face = Face(fd)
293+
294+
return cls(face, header, tables)
295+
296+
def subset(self, ccodes):
297+
glyphs = []
298+
for ccode in ccodes:
299+
glyphs.append(self._face.get_char_index_unicode(ccode))
300+
glyphs.sort()
301+
302+
offsets = self[b'loca'].get_offsets(self)
303+
self[b'glyf'].subset(glyphs, offsets)
304+
self[b'loca'].subset(self, glyphs, offsets)
305+
306+
def write(self, fd):
307+
self.header_struct.write(fd, self._header)
308+
309+
offset = (self.header_struct.size +
310+
Table.header_struct.size * len(self._tables))
311+
312+
for table in self._tables.values():
313+
table.header['offset'] = offset
314+
offset += len(table.content)
315+
316+
for table in self._tables.values():
317+
Table.header_struct.write(fd, table.header)
318+
319+
for table in self._tables.values():
320+
fd.write(table._content)
321+
322+
323+
def subset_font(input_fd, output_fd, charcodes):
324+
"""
325+
Subset a SFNT-style (TrueType or OpenType) font.
326+
327+
Parameters
328+
----------
329+
input_fd : readable file-like object, for bytes
330+
The font file to read.
331+
332+
output_fd : writable file-like object, for bytes
333+
The file to write a subsetted font file to.
334+
335+
charcodes : list of int or unicode string
336+
The character codes to include in the output font file.
337+
"""
338+
fontfile = FontFile.read(input_fd)
339+
fontfile.subset(charcodes)
340+
fontfile.write(output_fd)

0 commit comments

Comments
 (0)