diff --git a/src/calibre/ebooks/oeb/polish/pretty.py b/src/calibre/ebooks/oeb/polish/pretty.py index 071c29728f..8b1dd2942a 100644 --- a/src/calibre/ebooks/oeb/polish/pretty.py +++ b/src/calibre/ebooks/oeb/polish/pretty.py @@ -66,7 +66,7 @@ def pretty_opf(root): 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 diff --git a/src/calibre/ebooks/oeb/polish/report.py b/src/calibre/ebooks/oeb/polish/report.py index 05939e464d..6cf8bda5bd 100644 --- a/src/calibre/ebooks/oeb/polish/report.py +++ b/src/calibre/ebooks/oeb/polish/report.py @@ -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': diff --git a/src/calibre/gui2/font_family_chooser.py b/src/calibre/gui2/font_family_chooser.py index 338828d9b7..0d2ee0ece1 100644 --- a/src/calibre/gui2/font_family_chooser.py +++ b/src/calibre/gui2/font_family_chooser.py @@ -10,8 +10,8 @@ import shutil 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 @@ -20,24 +20,21 @@ from calibre.utils.icu import lower as icu_lower 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') diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 78f6e187ec..6d2c95a184 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -503,7 +503,7 @@ class FileList(QTreeWidget, OpenWithHandler): 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 diff --git a/src/calibre/gui2/tweak_book/manage_fonts.py b/src/calibre/gui2/tweak_book/manage_fonts.py index 68b8930a86..ed9ce2eeb8 100644 --- a/src/calibre/gui2/tweak_book/manage_fonts.py +++ b/src/calibre/gui2/tweak_book/manage_fonts.py @@ -243,7 +243,7 @@ class ManageFonts(Dialog): 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) diff --git a/src/calibre/utils/fonts/metadata.py b/src/calibre/utils/fonts/metadata.py index 8fb513785a..8a9ce83d1c 100644 --- a/src/calibre/utils/fonts/metadata.py +++ b/src/calibre/utils/fonts/metadata.py @@ -6,10 +6,9 @@ __copyright__ = '2012, Kovid Goyal ' __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): @@ -25,18 +24,19 @@ FontNames = namedtuple('FontNames', 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 @@ -60,41 +60,20 @@ class FontMetadata: 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) + self.names = FontNames(*get_font_names_from_ttlib_names_table(name_table)) - def read_characteristics(self, f): - if b'os/2' not in self.tables: + 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): diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py index 9450a17699..b07a008627 100644 --- a/src/calibre/utils/fonts/utils.py +++ b/src/calibre/utils/fonts/utils.py @@ -48,6 +48,32 @@ 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, @@ -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 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: @@ -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])