Allow adding fonts in WOFF formats

Switch to using fontools to read font metadata instead of calibre code
since it supports WOFF as well.
This commit is contained in:
Kovid Goyal 2024-03-08 16:50:51 +05:30
parent 82522d710f
commit bb4807cf99
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 85 additions and 58 deletions

View File

@ -66,7 +66,7 @@ def pretty_opf(root):
cat = 2 cat = 2
elif mt.startswith('image/'): elif mt.startswith('image/'):
cat = 3 cat = 3
elif ext in {'otf', 'ttf', 'woff'}: elif ext in {'otf', 'ttf', 'woff', 'woff2'}:
cat = 4 cat = 4
elif mt.startswith('audio/'): elif mt.startswith('audio/'):
cat = 5 cat = 5

View File

@ -32,7 +32,7 @@ def get_category(name, mt):
elif mt in OEB_DOCS: elif mt in OEB_DOCS:
category = 'text' category = 'text'
ext = name.rpartition('.')[-1].lower() ext = name.rpartition('.')[-1].lower()
if ext in {'ttf', 'otf', 'woff'}: if ext in {'ttf', 'otf', 'woff', 'woff2'}:
# Probably wrong mimetype in the OPF # Probably wrong mimetype in the OPF
category = 'font' category = 'font'
elif ext == 'opf': elif ext == 'opf':

View File

@ -10,8 +10,8 @@ import shutil
from qt.core import ( from qt.core import (
QAbstractItemView, QDialog, QDialogButtonBox, QFont, QFontComboBox, QFontDatabase, QAbstractItemView, QDialog, QDialogButtonBox, QFont, QFontComboBox, QFontDatabase,
QFontInfo, QFontMetrics, QGridLayout, QHBoxLayout, QIcon, QLabel, QLineEdit, QFontInfo, QFontMetrics, QGridLayout, QHBoxLayout, QIcon, QLabel, QLineEdit,
QListView, QPen, QPushButton, QSize, QSizePolicy, QStringListModel, QStyle, QListView, QPen, QPushButton, QRawFont, QSize, QSizePolicy, QStringListModel,
QStyledItemDelegate, Qt, QToolButton, QVBoxLayout, QWidget, pyqtSignal, QStyle, QStyledItemDelegate, Qt, QToolButton, QVBoxLayout, QWidget, pyqtSignal,
) )
from calibre.constants import config_dir from calibre.constants import config_dir
@ -20,24 +20,21 @@ from calibre.utils.icu import lower as icu_lower
def add_fonts(parent): def add_fonts(parent):
from calibre.utils.fonts.metadata import FontMetadata
files = choose_files(parent, 'add fonts to calibre', files = choose_files(parent, 'add fonts to calibre',
_('Select font files'), filters=[(_('TrueType/OpenType Fonts'), _('Select font files'), filters=[(_('TrueType/OpenType Fonts'),
['ttf', 'otf'])], all_files=False) ['ttf', 'otf', 'woff', 'woff2'])], all_files=False)
if not files: if not files:
return return
families = set() families = set()
for f in files: for f in files:
try: r = QRawFont()
with open(f, 'rb') as stream: r.loadFromFile(f, 11.0, QFont.HintingPreference.PreferDefaultHinting)
fm = FontMetadata(stream) if r.isValid():
except: families.add(r.familyName())
import traceback else:
error_dialog(parent, _('Corrupt font'), error_dialog(parent, _('Corrupt font'),
_('Failed to read metadata from the font file: %s')% _('Failed to load font from the file: {}').format(f), show=True)
f, det_msg=traceback.format_exc(), show=True)
return return
families.add(fm.font_family)
families = sorted(families) families = sorted(families)
dest = os.path.join(config_dir, 'fonts') dest = os.path.join(config_dir, 'fonts')

View File

@ -503,7 +503,7 @@ class FileList(QTreeWidget, OpenWithHandler):
elif mt in OEB_DOCS: elif mt in OEB_DOCS:
category = 'text' category = 'text'
ext = name.rpartition('.')[-1].lower() ext = name.rpartition('.')[-1].lower()
if ext in {'ttf', 'otf', 'woff'}: if ext in {'ttf', 'otf', 'woff', 'woff2'}:
# Probably wrong mimetype in the OPF # Probably wrong mimetype in the OPF
category = 'fonts' category = 'fonts'
return category return category

View File

@ -243,7 +243,7 @@ class ManageFonts(Dialog):
h.setContentsMargins(0, 0, 0, 0) h.setContentsMargins(0, 0, 0, 0)
self.install_fonts_button = b = QPushButton(_('&Install fonts'), self) self.install_fonts_button = b = QPushButton(_('&Install fonts'), self)
h.addWidget(b), b.setIcon(QIcon.ic('plus.png')) 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) b.clicked.connect(self.install_fonts)
l.addWidget(s), l.addLayout(h), h.addStretch(10), h.addWidget(self.bb) l.addWidget(s), l.addLayout(h), h.addStretch(10), h.addWidget(self.bb)

View File

@ -6,10 +6,9 @@ __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from io import BytesIO from io import BytesIO
from struct import calcsize, unpack, unpack_from
from collections import namedtuple 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): class UnsupportedFont(ValueError):
@ -25,18 +24,19 @@ FontNames = namedtuple('FontNames',
class FontMetadata: class FontMetadata:
def __init__(self, bytes_or_stream): def __init__(self, bytes_or_stream):
from fontTools.subset import load_font, Subsetter
if not hasattr(bytes_or_stream, 'read'): if not hasattr(bytes_or_stream, 'read'):
bytes_or_stream = BytesIO(bytes_or_stream) bytes_or_stream = BytesIO(bytes_or_stream)
f = bytes_or_stream f = bytes_or_stream
f.seek(0) f.seek(0)
header = f.read(4) s = Subsetter()
if header not in {b'\x00\x01\x00\x00', b'OTTO'}: try:
raise UnsupportedFont('Not a supported sfnt variant') font = load_font(f, s.options, dontLoadGlyphNames=True)
except Exception as e:
self.is_otf = header == b'OTTO' raise UnsupportedFont(str(e)) from e
self.read_table_metadata(f) self.is_otf = font.sfntVersion == 'OTTO'
self.read_names(f) self._read_names(font)
self.read_characteristics(f) self._read_characteristics(font)
f.seek(0) f.seek(0)
self.font_family = self.names.family_name self.font_family = self.names.family_name
@ -60,41 +60,20 @@ class FontMetadata:
else: else:
self.font_style = 'normal' self.font_style = 'normal'
def read_table_metadata(self, f): def _read_names(self, font):
f.seek(4) try:
num_tables = unpack(b'>H', f.read(2))[0] name_table = font['name']
# Start of table record entries except KeyError:
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:
raise UnsupportedFont('This font has no name table') raise UnsupportedFont('This font has no name table')
toff, tlen = self.tables[b'name'][:2] self.names = FontNames(*get_font_names_from_ttlib_names_table(name_table))
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): def _read_characteristics(self, font):
if b'os/2' not in self.tables: try:
os2_table = font['OS/2']
except KeyError:
raise UnsupportedFont('This font has no OS/2 table') raise UnsupportedFont('This font has no OS/2 table')
toff, tlen = self.tables[b'os/2'][:2]
f.seek(toff) vals = get_font_characteristics(os2_table, raw_is_table=True)
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)
self.characteristics = FontCharacteristics(*vals) self.characteristics = FontCharacteristics(*vals)
def to_dict(self): def to_dict(self):

View File

@ -48,6 +48,32 @@ def get_table(raw, name):
return None, None, None, None 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): def get_font_characteristics(raw, raw_is_table=False, return_all=False):
''' '''
Return (weight, is_italic, is_bold, is_regular, fs_type, panose, width, Return (weight, is_italic, is_bold, is_regular, fs_type, panose, width,
@ -55,6 +81,8 @@ def get_font_characteristics(raw, raw_is_table=False, return_all=False):
values are taken from the OS/2 table of the font. See values are taken from the OS/2 table of the font. See
http://www.microsoft.com/typography/otspec/os2.htm for details 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: if raw_is_table:
os2_table = raw os2_table = raw
else: else:
@ -196,6 +224,29 @@ def _get_font_names(raw, raw_is_table=False):
return records 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): def get_font_names(raw, raw_is_table=False):
records = _get_font_names(raw, raw_is_table) records = _get_font_names(raw, raw_is_table)
family_name = decode_name_record(records[1]) family_name = decode_name_record(records[1])