mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-01-03 09:14:11 +01:00
168 lines
5.9 KiB
Python
168 lines
5.9 KiB
Python
from collections import OrderedDict
|
|
from io import BytesIO
|
|
from struct import calcsize, pack
|
|
|
|
from ebook_converter.utils.fonts.sfnt import UnknownTable, align_block, max_power_of_two
|
|
from ebook_converter.utils.fonts.sfnt.cff.table import CFFTable
|
|
from ebook_converter.utils.fonts.sfnt.cmap import CmapTable
|
|
from ebook_converter.utils.fonts.sfnt.errors import UnsupportedFont
|
|
from ebook_converter.utils.fonts.sfnt.glyf import GlyfTable
|
|
from ebook_converter.utils.fonts.sfnt.gsub import GSUBTable
|
|
from ebook_converter.utils.fonts.sfnt.head import (
|
|
HeadTable, HorizontalHeader, OS2Table, PostTable, VerticalHeader
|
|
)
|
|
from ebook_converter.utils.fonts.sfnt.kern import KernTable
|
|
from ebook_converter.utils.fonts.sfnt.loca import LocaTable
|
|
from ebook_converter.utils.fonts.sfnt.maxp import MaxpTable
|
|
from ebook_converter.utils.fonts.utils import checksum_of_block, get_tables, verify_checksums
|
|
|
|
|
|
# OpenType spec: http://www.microsoft.com/typography/otspec/otff.htm
|
|
|
|
|
|
class Sfnt(object):
|
|
|
|
TABLE_MAP = {
|
|
b'head' : HeadTable,
|
|
b'hhea' : HorizontalHeader,
|
|
b'vhea' : VerticalHeader,
|
|
b'maxp' : MaxpTable,
|
|
b'loca' : LocaTable,
|
|
b'glyf' : GlyfTable,
|
|
b'cmap' : CmapTable,
|
|
b'CFF ' : CFFTable,
|
|
b'kern' : KernTable,
|
|
b'GSUB' : GSUBTable,
|
|
b'OS/2' : OS2Table,
|
|
b'post' : PostTable,
|
|
}
|
|
|
|
def __init__(self, raw_or_get_table):
|
|
self.tables = {}
|
|
if isinstance(raw_or_get_table, bytes):
|
|
raw = raw_or_get_table
|
|
self.sfnt_version = raw[:4]
|
|
if self.sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO', b'true',
|
|
b'type1'}:
|
|
raise UnsupportedFont('Font has unknown sfnt version: %r'%self.sfnt_version)
|
|
for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw):
|
|
self.tables[table_tag] = self.TABLE_MAP.get(
|
|
table_tag, UnknownTable)(table)
|
|
else:
|
|
for table_tag in {
|
|
b'cmap', b'hhea', b'head', b'hmtx', b'maxp', b'name', b'OS/2',
|
|
b'post', b'cvt ', b'fpgm', b'glyf', b'loca', b'prep', b'CFF ',
|
|
b'VORG', b'EBDT', b'EBLC', b'EBSC', b'BASE', b'GSUB', b'GPOS',
|
|
b'GDEF', b'JSTF', b'gasp', b'hdmx', b'kern', b'LTSH', b'PCLT',
|
|
b'VDMX', b'vhea', b'vmtx', b'MATH'}:
|
|
table = bytes(raw_or_get_table(table_tag))
|
|
if table:
|
|
self.tables[table_tag] = self.TABLE_MAP.get(
|
|
table_tag, UnknownTable)(table)
|
|
if not self.tables:
|
|
raise UnsupportedFont('This font has no tables')
|
|
self.sfnt_version = (b'\0\x01\0\0' if b'glyf' in self.tables
|
|
else b'OTTO')
|
|
|
|
def __getitem__(self, key):
|
|
return self.tables[key]
|
|
|
|
def __contains__(self, key):
|
|
return key in self.tables
|
|
|
|
def __delitem__(self, key):
|
|
del self.tables[key]
|
|
|
|
def __iter__(self):
|
|
'''Iterate over the table tags in order.'''
|
|
for x in sorted(self.tables):
|
|
yield x
|
|
# Although the optimal order is not alphabetical, the OTF spec says
|
|
# they should be alphabetical, so we stick with that. See
|
|
# http://partners.adobe.com/public/developer/opentype/index_recs.html
|
|
# for optimal order.
|
|
# keys = list(self.tables)
|
|
# order = {x:i for i, x in enumerate((b'head', b'hhea', b'maxp', b'OS/2',
|
|
# b'hmtx', b'LTSH', b'VDMX', b'hdmx', b'cmap', b'fpgm', b'prep',
|
|
# b'cvt ', b'loca', b'glyf', b'CFF ', b'kern', b'name', b'post',
|
|
# b'gasp', b'PCLT', b'DSIG'))}
|
|
# keys.sort(key=lambda x:order.get(x, 1000))
|
|
# for x in keys:
|
|
# yield x
|
|
|
|
def pop(self, key, default=None):
|
|
return self.tables.pop(key, default)
|
|
|
|
def get(self, key, default=None):
|
|
return self.tables.get(key, default)
|
|
|
|
def sizes(self):
|
|
ans = OrderedDict()
|
|
for tag in self:
|
|
ans[tag] = len(self[tag])
|
|
return ans
|
|
|
|
def __call__(self, stream=None):
|
|
stream = BytesIO() if stream is None else stream
|
|
|
|
def spack(*args):
|
|
stream.write(pack(*args))
|
|
|
|
stream.seek(0)
|
|
|
|
# Write header
|
|
num_tables = len(self.tables)
|
|
ln2 = max_power_of_two(num_tables)
|
|
srange = (2**ln2) * 16
|
|
spack(b'>4s4H',
|
|
self.sfnt_version, num_tables, srange, ln2, num_tables * 16 - srange)
|
|
|
|
# Write tables
|
|
head_offset = None
|
|
table_data = []
|
|
offset = stream.tell() + (calcsize(b'>4s3L') * num_tables)
|
|
sizes = OrderedDict()
|
|
for tag in self:
|
|
table = self.tables[tag]
|
|
raw = table()
|
|
table_len = len(raw)
|
|
if tag == b'head':
|
|
head_offset = offset
|
|
raw = raw[:8] + b'\0\0\0\0' + raw[12:]
|
|
raw = align_block(raw)
|
|
checksum = checksum_of_block(raw)
|
|
spack(b'>4s3L', tag, checksum, offset, table_len)
|
|
offset += len(raw)
|
|
table_data.append(raw)
|
|
sizes[tag] = table_len
|
|
|
|
for x in table_data:
|
|
stream.write(x)
|
|
|
|
checksum = checksum_of_block(stream.getvalue())
|
|
q = (0xB1B0AFBA - checksum) & 0xffffffff
|
|
stream.seek(head_offset + 8)
|
|
spack(b'>L', q)
|
|
|
|
return stream.getvalue(), sizes
|
|
|
|
|
|
def test_roundtrip(ff=None):
|
|
if ff is None:
|
|
data = P('fonts/liberation/LiberationSerif-Regular.ttf', data=True)
|
|
else:
|
|
with open(ff, 'rb') as f:
|
|
data = f.read()
|
|
rd = Sfnt(data)()[0]
|
|
verify_checksums(rd)
|
|
if data[:12] != rd[:12]:
|
|
raise ValueError('Roundtripping failed, font header not the same')
|
|
if len(data) != len(rd):
|
|
raise ValueError('Roundtripping failed, size different (%d vs. %d)'%
|
|
(len(data), len(rd)))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
test_roundtrip(sys.argv[-1])
|