Skip to content

Commit

Permalink
Allow adding fonts in WOFF formats
Browse files Browse the repository at this point in the history
Switch to using fontools to read font metadata instead of calibre code
since it supports WOFF as well.
  • Loading branch information
kovidgoyal committed Mar 8, 2024
1 parent 82522d7 commit bb4807c
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/calibre/ebooks/oeb/polish/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def manifest_key(x):
cat = 2
elif mt.startswith('image/'):
cat = 3
elif ext in {'otf', 'ttf', 'woff'}:
elif ext in {'otf', 'ttf', 'woff', 'woff2'}:
cat = 4
elif mt.startswith('audio/'):
cat = 5
Expand Down
2 changes: 1 addition & 1 deletion src/calibre/ebooks/oeb/polish/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_category(name, mt):
elif mt in OEB_DOCS:
category = 'text'
ext = name.rpartition('.')[-1].lower()
if ext in {'ttf', 'otf', 'woff'}:
if ext in {'ttf', 'otf', 'woff', 'woff2'}:
# Probably wrong mimetype in the OPF
category = 'font'
elif ext == 'opf':
Expand Down
21 changes: 9 additions & 12 deletions src/calibre/gui2/font_family_chooser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from qt.core import (
QAbstractItemView, QDialog, QDialogButtonBox, QFont, QFontComboBox, QFontDatabase,
QFontInfo, QFontMetrics, QGridLayout, QHBoxLayout, QIcon, QLabel, QLineEdit,
QListView, QPen, QPushButton, QSize, QSizePolicy, QStringListModel, QStyle,
QStyledItemDelegate, Qt, QToolButton, QVBoxLayout, QWidget, pyqtSignal,
QListView, QPen, QPushButton, QRawFont, QSize, QSizePolicy, QStringListModel,
QStyle, QStyledItemDelegate, Qt, QToolButton, QVBoxLayout, QWidget, pyqtSignal,
)

from calibre.constants import config_dir
Expand All @@ -20,24 +20,21 @@


def add_fonts(parent):
from calibre.utils.fonts.metadata import FontMetadata
files = choose_files(parent, 'add fonts to calibre',
_('Select font files'), filters=[(_('TrueType/OpenType Fonts'),
['ttf', 'otf'])], all_files=False)
['ttf', 'otf', 'woff', 'woff2'])], all_files=False)
if not files:
return
families = set()
for f in files:
try:
with open(f, 'rb') as stream:
fm = FontMetadata(stream)
except:
import traceback
r = QRawFont()
r.loadFromFile(f, 11.0, QFont.HintingPreference.PreferDefaultHinting)
if r.isValid():
families.add(r.familyName())
else:
error_dialog(parent, _('Corrupt font'),
_('Failed to read metadata from the font file: %s')%
f, det_msg=traceback.format_exc(), show=True)
_('Failed to load font from the file: {}').format(f), show=True)
return
families.add(fm.font_family)
families = sorted(families)

dest = os.path.join(config_dir, 'fonts')
Expand Down
2 changes: 1 addition & 1 deletion src/calibre/gui2/tweak_book/file_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ def get_category(name, mt):
elif mt in OEB_DOCS:
category = 'text'
ext = name.rpartition('.')[-1].lower()
if ext in {'ttf', 'otf', 'woff'}:
if ext in {'ttf', 'otf', 'woff', 'woff2'}:
# Probably wrong mimetype in the OPF
category = 'fonts'
return category
Expand Down
2 changes: 1 addition & 1 deletion src/calibre/gui2/tweak_book/manage_fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def setup_ui(self):
h.setContentsMargins(0, 0, 0, 0)
self.install_fonts_button = b = QPushButton(_('&Install fonts'), self)
h.addWidget(b), b.setIcon(QIcon.ic('plus.png'))
b.setToolTip(textwrap.fill(_('Install fonts from .ttf/.otf files to make them available for embedding')))
b.setToolTip(textwrap.fill(_('Install fonts from font files to make them available for embedding')))
b.clicked.connect(self.install_fonts)
l.addWidget(s), l.addLayout(h), h.addStretch(10), h.addWidget(self.bb)

Expand Down
65 changes: 22 additions & 43 deletions src/calibre/utils/fonts/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
__docformat__ = 'restructuredtext en'

from io import BytesIO
from struct import calcsize, unpack, unpack_from
from collections import namedtuple

from calibre.utils.fonts.utils import get_font_names2, get_font_characteristics
from calibre.utils.fonts.utils import get_font_names_from_ttlib_names_table, get_font_characteristics


class UnsupportedFont(ValueError):
Expand All @@ -25,18 +24,19 @@ class UnsupportedFont(ValueError):
class FontMetadata:

def __init__(self, bytes_or_stream):
from fontTools.subset import load_font, Subsetter
if not hasattr(bytes_or_stream, 'read'):
bytes_or_stream = BytesIO(bytes_or_stream)
f = bytes_or_stream
f.seek(0)
header = f.read(4)
if header not in {b'\x00\x01\x00\x00', b'OTTO'}:
raise UnsupportedFont('Not a supported sfnt variant')

self.is_otf = header == b'OTTO'
self.read_table_metadata(f)
self.read_names(f)
self.read_characteristics(f)
s = Subsetter()
try:
font = load_font(f, s.options, dontLoadGlyphNames=True)
except Exception as e:
raise UnsupportedFont(str(e)) from e
self.is_otf = font.sfntVersion == 'OTTO'
self._read_names(font)
self._read_characteristics(font)

f.seek(0)
self.font_family = self.names.family_name
Expand All @@ -60,41 +60,20 @@ def __init__(self, bytes_or_stream):
else:
self.font_style = 'normal'

def read_table_metadata(self, f):
f.seek(4)
num_tables = unpack(b'>H', f.read(2))[0]
# Start of table record entries
f.seek(4 + 4*2)
table_record = b'>4s3L'
sz = calcsize(table_record)
self.tables = {}
block = f.read(sz * num_tables)
for i in range(num_tables):
table_tag, table_checksum, table_offset, table_length = \
unpack_from(table_record, block, i*sz)
self.tables[table_tag.lower()] = (table_offset, table_length,
table_checksum)

def read_names(self, f):
if b'name' not in self.tables:
def _read_names(self, font):
try:
name_table = font['name']
except KeyError:
raise UnsupportedFont('This font has no name table')
toff, tlen = self.tables[b'name'][:2]
f.seek(toff)
table = f.read(tlen)
if len(table) != tlen:
raise UnsupportedFont('This font has a name table of incorrect length')
vals = get_font_names2(table, raw_is_table=True)
self.names = FontNames(*vals)

def read_characteristics(self, f):
if b'os/2' not in self.tables:
self.names = FontNames(*get_font_names_from_ttlib_names_table(name_table))

def _read_characteristics(self, font):
try:
os2_table = font['OS/2']
except KeyError:
raise UnsupportedFont('This font has no OS/2 table')
toff, tlen = self.tables[b'os/2'][:2]
f.seek(toff)
table = f.read(tlen)
if len(table) != tlen:
raise UnsupportedFont('This font has an OS/2 table of incorrect length')
vals = get_font_characteristics(table, raw_is_table=True)

vals = get_font_characteristics(os2_table, raw_is_table=True)
self.characteristics = FontCharacteristics(*vals)

def to_dict(self):
Expand Down
51 changes: 51 additions & 0 deletions src/calibre/utils/fonts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,41 @@ def get_table(raw, name):
return None, None, None, None


def get_font_characteristics_from_ttlib_os2_table(t, return_all=False):
(char_width, weight, width, fs_type, subscript_x_size, subscript_y_size, subscript_x_offset, subscript_y_offset,
superscript_x_size, superscript_y_size, superscript_x_offset, superscript_y_offset, strikeout_size,
strikeout_position, family_class, selection, version) = (
t.xAvgCharWidth, t.usWeightClass, t.usWidthClass, t.fsType,
t.ySubscriptXSize, t.ySubscriptYSize, t.ySubscriptXOffset, t.ySubscriptYOffset,
t.ySuperscriptXSize, t.ySuperscriptYSize, t.ySuperscriptXOffset, t.ySuperscriptYOffset,
t.yStrikeoutSize, t.yStrikeoutPosition, t.sFamilyClass, t.fsSelection, t.version)
is_italic = (selection & (1 << 0)) != 0
is_bold = (selection & (1 << 5)) != 0
is_regular = (selection & (1 << 6)) != 0
is_wws = (selection & (1 << 8)) != 0
is_oblique = (selection & (1 << 9)) != 0
p = t.panose
panose = (p.bFamilyType, p.bSerifStyle, p.bWeight, p.bProportion, p.bContrast, p.bStrokeVariation, p.bArmStyle, p.bLetterForm, p.bMidline, p.bXHeight)

if return_all:
return (version, char_width, weight, width, fs_type, subscript_x_size,
subscript_y_size, subscript_x_offset, subscript_y_offset,
superscript_x_size, superscript_y_size, superscript_x_offset,
superscript_y_offset, strikeout_size, strikeout_position,
family_class, panose, selection, is_italic, is_bold, is_regular)

return weight, is_italic, is_bold, is_regular, fs_type, panose, width, is_oblique, is_wws, version


def get_font_characteristics(raw, raw_is_table=False, return_all=False):
'''
Return (weight, is_italic, is_bold, is_regular, fs_type, panose, width,
is_oblique, is_wws). These
values are taken from the OS/2 table of the font. See
http://www.microsoft.com/typography/otspec/os2.htm for details
'''
if hasattr(raw, 'getUnicodeRanges'):
return get_font_characteristics_from_ttlib_os2_table(raw, return_all)
if raw_is_table:
os2_table = raw
else:
Expand Down Expand Up @@ -196,6 +224,29 @@ def _get_font_names(raw, raw_is_table=False):
return records


def get_font_name_records_from_ttlib_names_table(names_table):
records = defaultdict(list)
for rec in names_table.names:
records[rec.nameID].append((rec.platformID, rec.platEncID, rec.langID, rec.string))
return records


def get_font_names_from_ttlib_names_table(names_table):
records = get_font_name_records_from_ttlib_names_table(names_table)
family_name = decode_name_record(records[1])
subfamily_name = decode_name_record(records[2])
full_name = decode_name_record(records[4])

preferred_family_name = decode_name_record(records[16])
preferred_subfamily_name = decode_name_record(records[17])

wws_family_name = decode_name_record(records[21])
wws_subfamily_name = decode_name_record(records[22])

return (family_name, subfamily_name, full_name, preferred_family_name,
preferred_subfamily_name, wws_family_name, wws_subfamily_name)


def get_font_names(raw, raw_is_table=False):
records = _get_font_names(raw, raw_is_table)
family_name = decode_name_record(records[1])
Expand Down

0 comments on commit bb4807c

Please sign in to comment.