diff --git a/lib/freetypy/subset.py b/lib/freetypy/subset.py
new file mode 100644
index 0000000..1a19ded
--- /dev/null
+++ b/lib/freetypy/subset.py
@@ -0,0 +1,343 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015, Michael Droettboom All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be interpreted
+# as representing official policies, either expressed or implied, of
+# the FreeBSD Project.
+
+"""
+A library to subset SFNT-style fonts.
+"""
+
+from __future__ import absolute_import, division, unicode_literals, print_function
+
+
+__all__ = ['subset_font']
+
+
+from collections import OrderedDict
+import struct
+
+
+from freetypy import Face
+
+
+UNDERSTOOD_VERSIONS = (0x00010000, 0x4f54544f)
+
+
+class _BinaryStruct(object):
+ """
+ A wrapper around the Python stdlib struct module to define a
+ binary struct more like a dictionary than a tuple.
+ """
+ def __init__(self, descr, endian='>'):
+ """
+ Parameters
+ ----------
+ descr : list of tuple
+ Each entry is a pair ``(name, format)``, where ``format``
+ is one of the format types understood by `struct`.
+ endian : str, optional
+ The endianness of the struct. Must be ``>`` or ``<``.
+ """
+ self._fmt = [endian]
+ self._offsets = {}
+ self._names = []
+ i = 0
+ for name, fmt in descr:
+ self._fmt.append(fmt)
+ self._offsets[name] = (i, (endian + fmt).encode('ascii'))
+ self._names.append(name)
+ i += struct.calcsize(fmt.encode('ascii'))
+ self._fmt = ''.join(self._fmt).encode('ascii')
+ self._size = struct.calcsize(self._fmt)
+
+ @property
+ def size(self):
+ """
+ Return the size of the struct.
+ """
+ return self._size
+
+ def pack(self, **kwargs):
+ """
+ Pack the given arguments, which are given as kwargs, and
+ return the binary struct.
+ """
+ fields = [0] * len(self._names)
+ for key, val in kwargs.items():
+ if key not in self._offsets:
+ raise KeyError("No header field '{0}'".format(key))
+ i = self._names.index(key)
+ fields[i] = val
+ return struct.pack(self._fmt, *fields)
+
+ def unpack(self, buff):
+ """
+ Unpack the given binary buffer into the fields. The result
+ is a dictionary mapping field names to values.
+ """
+ args = struct.unpack_from(self._fmt, buff[:self._size])
+ return dict(zip(self._names, args))
+
+ def read(self, fd):
+ """
+ Read a struct from the current location in the file.
+ """
+ buff = fd.read(self.size)
+ return self.unpack(buff)
+
+ def write(self, fd, data):
+ """
+ Write a struct to the current location in the file.
+ """
+ buff = self.pack(**data)
+ fd.write(buff)
+
+
+class Table(object):
+ header_struct = _BinaryStruct([
+ ('tag', '4s'),
+ ('checkSum', 'I'),
+ ('offset', 'I'),
+ ('length', 'I')
+ ])
+
+ def __init__(self, header, content):
+ self._header = header
+ self._content = content
+
+ def __repr__(self):
+ return "
".format(
+ self._header['tag'].decode('ascii'))
+
+ def _calc_checksum(self, content):
+ end = ((len(content) + 3) & ~3) - 4
+ sum = 0
+ for i in range(0, end, 4):
+ sum += struct.unpack('I', content[i:i+4])[0]
+ sum = sum & 0xffffffff
+ return sum
+
+ @classmethod
+ def read(cls, fd):
+ header = Table.header_struct.read(fd)
+ content = fd.read(header['length'])
+
+ return cls(header, content)
+
+ @property
+ def header(self):
+ return self._header
+
+ @property
+ def content(self):
+ return self._content
+ @content.setter
+ def content(self, new_content):
+ checksum = self._calc_checksum(new_content)
+ self._header['checkSum'] = checksum
+ self._header['length'] = len(new_content)
+ self._content = new_content
+
+
+class HeadTable(Table):
+ head_table_struct = _BinaryStruct([
+ ('version', 'I'),
+ ('fontRevision', 'I'),
+ ('checkSumAdjustment', 'I'),
+ ('magicNumber', 'I'),
+ ('flags', 'H'),
+ ('unitsPerEm', 'H'),
+ ('created', 'q'),
+ ('modified', 'q'),
+ ('xMin', 'h'),
+ ('yMin', 'h'),
+ ('xMax', 'h'),
+ ('yMax', 'h'),
+ ('macStyle', 'H'),
+ ('lowestRecPPEM', 'H'),
+ ('fontDirectionHint', 'h'),
+ ('indexToLocFormat', 'h'),
+ ('glyphDataFormat', 'h')])
+
+ def __init__(self, header, content):
+ super(HeadTable, self).__init__(header, content)
+
+ self.__dict__.update(self.head_table_struct.unpack(content))
+
+ if self.version not in UNDERSTOOD_VERSIONS:
+ raise ValueError("Not a TrueType or OpenType file")
+
+ if self.magicNumber != 0x5F0F3CF5:
+ raise ValueError("Bad magic number")
+
+
+class LocaTable(Table):
+ def _get_formats(self, fontfile):
+ if fontfile[b'head'].indexToLocFormat == 0: # short offsets
+ return '>H', 2, 2
+ else:
+ return '>I', 4, 1
+
+ def get_offsets(self, fontfile):
+ entry_format, entry_size, scale = self._get_formats(fontfile)
+
+ content = self._content
+ offsets = []
+ for i in range(0, len(content), entry_size):
+ value = struct.unpack(
+ entry_format, content[i:i+entry_size])[0] * scale
+ offsets.append(value)
+
+ return offsets
+
+ def subset(self, fontfile, glyphs, offsets):
+ new_offsets = []
+ offset = 0
+ j = 0
+ for i in range(len(offsets) - 1):
+ new_offsets.append(offset)
+ if j < len(glyphs) and i == glyphs[j]:
+ offset += offsets[i+1] - offsets[i]
+ j += 1
+ new_offsets.append(offset)
+
+ entry_format, entry_size, scale = self._get_formats(fontfile)
+ new_content = []
+ for value in new_offsets:
+ new_content.append(struct.pack(entry_format, value // scale))
+ self.content = b''.join(new_content)
+
+
+class GlyfTable(Table):
+ def subset(self, glyphs, offsets):
+ content = self.content
+ new_content = []
+ for gind in glyphs:
+ new_content.append(content[offsets[gind]:offsets[gind+1]])
+ self.content = b''.join(new_content)
+
+
+SPECIAL_TABLES = {
+ b'head': HeadTable,
+ b'loca': LocaTable,
+ b'glyf': GlyfTable
+}
+
+
+class FontFile(object):
+ """
+ A class to subset SFNT-style fonts (TrueType and OpenType).
+ """
+
+ header_struct = _BinaryStruct([
+ ('version', 'I'),
+ ('numTables', 'H'),
+ ('searchRange', 'H'),
+ ('entrySelector', 'H'),
+ ('rangeShift', 'H')
+ ])
+
+ def __init__(self, face, header, tables):
+ self._face = face
+ self._header = header
+ self._tables = tables
+
+ def __getitem__(self, tag):
+ return self._tables[tag]
+
+ @classmethod
+ def read(cls, fd):
+ header = cls.header_struct.read(fd)
+
+ if header['version'] not in UNDERSTOOD_VERSIONS:
+ raise ValueError("Not a TrueType or OpenType file")
+
+ table_dir = []
+ for i in range(header['numTables']):
+ table_dir.append(Table.header_struct.read(fd))
+
+ tables = OrderedDict()
+ for table_header in table_dir:
+ fd.seek(table_header['offset'])
+ content = fd.read(table_header['length'])
+ table_cls = SPECIAL_TABLES.get(table_header['tag'], Table)
+ tables[table_header['tag']] = table_cls(table_header, content)
+
+ fd.seek(0)
+ face = Face(fd)
+
+ return cls(face, header, tables)
+
+ def subset(self, ccodes):
+ glyphs = []
+ for ccode in ccodes:
+ glyphs.append(self._face.get_char_index_unicode(ccode))
+ glyphs.sort()
+
+ offsets = self[b'loca'].get_offsets(self)
+ self[b'glyf'].subset(glyphs, offsets)
+ self[b'loca'].subset(self, glyphs, offsets)
+
+ def write(self, fd):
+ self.header_struct.write(fd, self._header)
+
+ offset = (self.header_struct.size +
+ Table.header_struct.size * len(self._tables))
+
+ for table in self._tables.values():
+ table.header['offset'] = offset
+ offset += len(table.content)
+
+ for table in self._tables.values():
+ Table.header_struct.write(fd, table.header)
+
+ for table in self._tables.values():
+ fd.write(table._content)
+
+
+def subset_font(input_fd, output_fd, charcodes):
+ """
+ Subset a SFNT-style (TrueType or OpenType) font.
+
+ Parameters
+ ----------
+ input_fd : readable file-like object, for bytes
+ The font file to read.
+
+ output_fd : writable file-like object, for bytes
+ The file to write a subsetted font file to.
+
+ charcodes : list of int or unicode string
+ The character codes to include in the output font file.
+ """
+ fontfile = FontFile.read(input_fd)
+ fontfile.subset(charcodes)
+ fontfile.write(output_fd)
diff --git a/lib/freetypy/tests/test_subset.py b/lib/freetypy/tests/test_subset.py
new file mode 100644
index 0000000..37b0585
--- /dev/null
+++ b/lib/freetypy/tests/test_subset.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015, Michael Droettboom All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be interpreted
+# as representing official policies, either expressed or implied, of
+# the FreeBSD Project.
+
+from __future__ import print_function, unicode_literals, absolute_import
+
+import freetypy as ft
+from freetypy import subset
+from .util import *
+
+import io
+
+
+def test_subset():
+ face = ft.Face(vera_path())
+ face.set_char_size(12, 12, 300, 300)
+ glyph = face.load_char_unicode('B')
+ original_B = glyph.outline.to_string(' M ', ' L ', ' C ', ' Q ')
+
+ with open(vera_path(), 'rb') as input_fd:
+ output_fd = io.BytesIO()
+ subset.subset_font(input_fd, output_fd, 'ABCD')
+
+ output_fd.seek(0)
+
+ face = ft.Face(output_fd)
+ face.set_char_size(12, 12, 300, 300)
+ glyph = face.load_char_unicode('B')
+ s = glyph.outline.to_string(' M ', ' L ', ' C ', ' Q ')
+ assert original_B == s
+
+ # Not here
+ glyph = face.load_char_unicode('X')
+ s = glyph.outline.to_string(' M ', ' L ', ' C ', ' Q ')
+ assert len(s) == 0
diff --git a/lib/freetypy/util.py b/lib/freetypy/util.py
index 6a88627..362c59b 100644
--- a/lib/freetypy/util.py
+++ b/lib/freetypy/util.py
@@ -58,8 +58,10 @@ def bitmap_to_ascii(a):
lines = []
for row in a.to_list():
+ line = []
for col in row:
col = int(float(col) / 255. * 4.)
c = shades[col]
- lines.append(c)
+ line.append(c)
+ lines.append(''.join(line))
return '\n'.join(lines)
diff --git a/src/outline.c b/src/outline.c
index 78cd3d5..6a4d861 100644
--- a/src/outline.c
+++ b/src/outline.c
@@ -863,6 +863,7 @@ Py_Outline_to_string(Py_Outline* self, PyObject* args, PyObject* kwds)
"move_command", "line_command", "cubic_command", "conic_command",
"prefix", NULL};
+ data.prefix = 0;
data.conic_command = NULL;
if (!PyArg_ParseTupleAndKeywords(