diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 4b0ba5cf68..b64fd08d0a 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -690,29 +690,6 @@ def remove_bracketed_text(src, buf.append(char) return u''.join(buf) -def load_builtin_fonts(): - # On linux these are loaded by fontconfig which means that - # they are available to Qt as well, since Qt uses fontconfig - from calibre.utils.fonts import fontconfig - fontconfig - - families = {u'Liberation Serif', u'Liberation Sans', u'Liberation Mono'} - - if iswindows or isosx: - import glob - from PyQt4.Qt import QFontDatabase - families = set() - for f in glob.glob(P('fonts/liberation/*.ttf')): - with open(f, 'rb') as s: - # Windows requires font files to be executable for them to be - # loaded successfully, so we use the in memory loader - fid = QFontDatabase.addApplicationFontFromData(s.read()) - if fid > -1: - families |= set(map(unicode, - QFontDatabase.applicationFontFamilies(fid))) - - return families - def ipython(user_ns=None): from calibre.utils.ipython import ipython ipython(user_ns=user_ns) diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 9cf0e51e7e..a5417be220 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -254,7 +254,6 @@ def unit_convert(value, base, font, dpi): def generate_masthead(title, output_path=None, width=600, height=60): from calibre.ebooks.conversion.config import load_defaults - from calibre.utils.fonts import fontconfig from calibre.utils.config import tweaks fp = tweaks['generate_cover_title_font'] if not fp: @@ -264,11 +263,10 @@ def generate_masthead(title, output_path=None, width=600, height=60): masthead_font_family = recs.get('masthead_font', 'Default') if masthead_font_family != 'Default': - masthead_font = fontconfig.files_for_family(masthead_font_family) - # Assume 'normal' always in dict, else use default - # {'normal': (path_to_font, friendly name)} - if 'normal' in masthead_font: - font_path = masthead_font['normal'][0] + from calibre.utils.fonts.scanner import font_scanner + faces = font_scanner.fonts_for_family(masthead_font_family) + if faces: + font_path = faces[0]['path'] if not font_path or not os.access(font_path, os.R_OK): font_path = default_font diff --git a/src/calibre/ebooks/lrf/__init__.py b/src/calibre/ebooks/lrf/__init__.py index b12c0d6b34..725338ead8 100644 --- a/src/calibre/ebooks/lrf/__init__.py +++ b/src/calibre/ebooks/lrf/__init__.py @@ -34,24 +34,24 @@ class PRS500_PROFILE(object): name = 'prs500' def find_custom_fonts(options, logger): - from calibre.utils.fonts import fontconfig - files_for_family = fontconfig.files_for_family + from calibre.utils.fonts.scanner import font_scanner fonts = {'serif' : None, 'sans' : None, 'mono' : None} def family(cmd): return cmd.split(',')[-1].strip() if options.serif_family: f = family(options.serif_family) - fonts['serif'] = files_for_family(f) + fonts['serif'] = font_scanner.legacy_fonts_for_family(f) + print (111111, fonts['serif']) if not fonts['serif']: logger.warn('Unable to find serif family %s'%f) if options.sans_family: f = family(options.sans_family) - fonts['sans'] = files_for_family(f) + fonts['sans'] = font_scanner.legacy_fonts_for_family(f) if not fonts['sans']: logger.warn('Unable to find sans family %s'%f) if options.mono_family: f = family(options.mono_family) - fonts['mono'] = files_for_family(f) + fonts['mono'] = font_scanner.legacy_fonts_for_family(f) if not fonts['mono']: logger.warn('Unable to find mono family %s'%f) return fonts diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 6aae85d8d1..f963f468aa 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -178,49 +178,40 @@ class CSSFlattener(object): body_font_family = None if not family: return body_font_family, efi - from calibre.utils.fonts import fontconfig - from calibre.utils.fonts.utils import (get_font_characteristics, - panose_to_css_generic_family, get_font_names) - faces = fontconfig.fonts_for_family(family) - if not faces or not u'normal' in faces: + from calibre.utils.fonts.scanner import font_scanner + from calibre.utils.fonts.utils import panose_to_css_generic_family + faces = font_scanner.fonts_for_family(family) + if not faces: msg = (u'No embeddable fonts found for family: %r'%self.opts.embed_font_family) - if faces: - msg = (u'The selected font %s has no Regular typeface, only' - ' %s faces, it cannot be used.')%( - self.opts.embed_font_family, - ', '.join(faces.iterkeys())) if failure_critical: raise ValueError(msg) self.oeb.log.warn(msg) return body_font_family, efi - for k, v in faces.iteritems(): - ext, data = v[0::2] - weight, is_italic, is_bold, is_regular, fs_type, panose = \ - get_font_characteristics(data) - generic_family = panose_to_css_generic_family(panose) - family_name, subfamily_name, full_name = get_font_names(data) - if k == u'normal': - body_font_family = u"'%s',%s"%(family_name, generic_family) - if family_name.lower() != family.lower(): - self.oeb.log.warn(u'Failed to find an exact match for font:' - u' %r, using %r instead'%(family, family_name)) - else: - self.oeb.log(u'Embedding font: %s'%family_name) - font = {u'font-family':u'"%s"'%family_name} - if is_italic: - font[u'font-style'] = u'italic' - if is_bold: - font[u'font-weight'] = u'bold' + for i, font in enumerate(faces): + ext = 'otf' if font['is_otf'] else 'ttf' fid, href = self.oeb.manifest.generate(id=u'font', - href=u'%s.%s'%(ascii_filename(full_name).replace(u' ', u'-'), ext)) + href=u'%s.%s'%(ascii_filename(font['full_name']).replace(u' ', u'-'), ext)) item = self.oeb.manifest.add(fid, href, - guess_type(full_name+'.'+ext)[0], - data=data) + guess_type('dummy.'+ext)[0], + data=font_scanner.get_font_data(font)) item.unload_data_from_memory() - font[u'src'] = u'url(%s)'%item.href + + cfont = { + u'font-family':u'"%s"'%font['font-family'], + u'panose-1': u' '.join(map(unicode, font['panose'])), + u'src': u'url(%s)'%item.href, + } + + if i == 0: + generic_family = panose_to_css_generic_family(font['panose']) + body_font_family = u"'%s',%s"%(font['font-family'], generic_family) + self.oeb.log(u'Embedding font: %s'%font['font-family']) + for k in (u'font-weight', u'font-style', u'font-stretch'): + if font[k] != u'normal': + cfont[k] = font[k] rule = '@font-face { %s }'%('; '.join(u'%s:%s'%(k, v) for k, v in - font.iteritems())) + cfont.iteritems())) rule = cssutils.parseString(rule) efi.append(rule) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 688b134f0e..c1a088bcac 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -1,18 +1,19 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' """ The GUI """ -import os, sys, Queue, threading +import os, sys, Queue, threading, glob from threading import RLock from urllib import unquote from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QByteArray, QTranslator, QCoreApplication, QThread, QEvent, QTimer, pyqtSignal, QDateTime, QDesktopServices, QFileDialog, QFileIconProvider, QSettings, QColor, - QIcon, QApplication, QDialog, QUrl, QFont, QPalette) + QIcon, QApplication, QDialog, QUrl, QFont, QPalette, + QFontDatabase) ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' -from calibre import prints, load_builtin_fonts +from calibre import prints from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx, plugins, config_dir, filesystem_encoding, DEBUG) from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig @@ -779,7 +780,7 @@ class Application(QApplication): qt_app = self self._file_open_paths = [] self._file_open_lock = RLock() - load_builtin_fonts() + self.load_builtin_fonts() self.setup_styles(force_calibre_style) if DEBUG: @@ -792,6 +793,28 @@ class Application(QApplication): self.redirect_notify = True return ret + def load_builtin_fonts(self): + global _rating_font + from calibre.utils.fonts.scanner import font_scanner + # Start scanning the users computer for fonts + font_scanner + + # Load the builtin fonts and any fonts added to calibre by the user to + # Qt + for ff in glob.glob(P('fonts/liberation/*.?tf')) + \ + [P('fonts/calibreSymbols.otf')] + \ + glob.glob(os.path.join(config_dir, 'fonts', '*.?tf')): + if ff.rpartition('.')[-1].lower() in {'ttf', 'otf'}: + with open(ff, 'rb') as s: + # Windows requires font files to be executable for them to be + # loaded successfully, so we use the in memory loader + fid = QFontDatabase.addApplicationFontFromData(s.read()) + if fid > -1: + fam = QFontDatabase.applicationFontFamilies(fid) + fam = set(map(unicode, fam)) + if u'calibre Symbols' in fam: + _rating_font = u'calibre Symbols' + def load_calibre_style(self): # On OS X QtCurve resets the palette, so we preserve it explicitly orig_pal = QPalette(self.palette()) @@ -964,22 +987,9 @@ def is_gui_thread(): global gui_thread return gui_thread is QThread.currentThread() -_rating_font = None +_rating_font = 'Arial Unicode MS' if iswindows else 'sans-serif' def rating_font(): global _rating_font - if _rating_font is None: - from PyQt4.Qt import QFontDatabase - _rating_font = 'Arial Unicode MS' if iswindows else 'sans-serif' - fontid = QFontDatabase.addApplicationFont( - #P('fonts/liberation/LiberationSerif-Regular.ttf') - P('fonts/calibreSymbols.otf') - ) - if fontid > -1: - try: - _rating_font = unicode(list( - QFontDatabase.applicationFontFamilies(fontid))[0]) - except: - pass return _rating_font def find_forms(srcdir): diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py index ac2bf15164..6a5c4120a0 100644 --- a/src/calibre/gui2/convert/mobi_output.py +++ b/src/calibre/gui2/convert/mobi_output.py @@ -29,38 +29,8 @@ class PluginWidget(Widget, Ui_Form): ) self.db, self.book_id = db, book_id - ''' - from calibre.utils.fonts import fontconfig - - global font_family_model - if font_family_model is None: - font_family_model = FontFamilyModel() - try: - font_family_model.families = fontconfig.find_font_families(allowed_extensions=['ttf']) - except: - import traceback - font_family_model.families = [] - print 'WARNING: Could not load fonts' - traceback.print_exc() - font_family_model.families.sort() - font_family_model.families[:0] = [_('Default')] - - self.font_family_model = font_family_model - self.opt_masthead_font.setModel(self.font_family_model) - ''' self.opt_mobi_file_type.addItems(['old', 'both', 'new']) self.initialize_options(get_option, get_help, db, book_id) - ''' - def set_value_handler(self, g, val): - if unicode(g.objectName()) in 'opt_masthead_font': - idx = -1 - if val: - idx = g.findText(val, Qt.MatchFixedString) - if idx < 0: - idx = 0 - g.setCurrentIndex(idx) - return True - return False - ''' + diff --git a/src/calibre/gui2/font_family_chooser.py b/src/calibre/gui2/font_family_chooser.py index 5110e92d24..6c3aa4594a 100644 --- a/src/calibre/gui2/font_family_chooser.py +++ b/src/calibre/gui2/font_family_chooser.py @@ -13,9 +13,6 @@ from PyQt4.Qt import (QFontInfo, QFontMetrics, Qt, QFont, QFontDatabase, QPen, QToolButton, QGridLayout, QListView, QWidget, QDialogButtonBox, QIcon, QHBoxLayout, QLabel, QModelIndex) -from calibre.gui2 import error_dialog -from calibre.utils.icu import sort_key - def writing_system_for_font(font): has_latin = True systems = QFontDatabase().writingSystems(font.family()) @@ -122,19 +119,14 @@ class FontFamilyDialog(QDialog): QDialog.__init__(self, parent) self.setWindowTitle(_('Choose font family')) self.setWindowIcon(QIcon(I('font.png'))) - from calibre.utils.fonts import fontconfig + from calibre.utils.fonts.scanner import font_scanner try: - self.families = fontconfig.find_font_families() + self.families = font_scanner.find_font_families() except: self.families = [] print ('WARNING: Could not load fonts') import traceback traceback.print_exc() - # Restrict to Qt families as we need the font to be available in - # QFontDatabase - qt_families = set([unicode(x) for x in QFontDatabase().families()]) - self.families = list(qt_families.intersection(set(self.families))) - self.families.sort(key=sort_key) self.families.insert(0, _('None')) self.l = l = QGridLayout() @@ -174,20 +166,6 @@ class FontFamilyDialog(QDialog): if idx == 0: return None return self.families[idx] - def accept(self): - ff = self.font_family - if ff: - from calibre.utils.fonts import fontconfig - faces = fontconfig.fonts_for_family(ff) or {} - faces = frozenset(faces.iterkeys()) - if 'normal' not in faces: - error_dialog(self, _('Not a useable font'), - _('The %s font family does not have a Regular typeface, so it' - ' cannot be used. It has only the "%s" face(s).')%( - ff, ', '.join(faces)), show=True) - return - QDialog.accept(self) - class FontFamilyChooser(QWidget): family_changed = pyqtSignal(object) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index a990baaa1e..dfbcbdcbf0 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -11,7 +11,7 @@ from PyQt4.Qt import (QIcon, QFont, QLabel, QListWidget, QAction, QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, QRegExp, QSize, QSplitter, QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, QMenu, QStringListModel, QCompleter, QStringList, QTimer, QRect, - QFontDatabase, QGraphicsView, QByteArray) + QGraphicsView, QByteArray) from calibre.constants import iswindows from calibre.gui2 import (NONE, error_dialog, pixmap_to_data, gprefs, @@ -352,17 +352,14 @@ class FontFamilyModel(QAbstractListModel): # {{{ def __init__(self, *args): QAbstractListModel.__init__(self, *args) - from calibre.utils.fonts import fontconfig + from calibre.utils.fonts.scanner import font_scanner try: - self.families = fontconfig.find_font_families() + self.families = font_scanner.find_font_families() except: self.families = [] print 'WARNING: Could not load fonts' traceback.print_exc() # Restrict to Qt families as Qt tends to crash - qt_families = set([unicode(x) for x in QFontDatabase().families()]) - self.families = list(qt_families.intersection(set(self.families))) - self.families.sort() self.families[:0] = [_('None')] self.font = QFont('Arial' if iswindows else 'sansserif') diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 48bb634fd6..fb7bda13cf 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -2757,7 +2757,6 @@ class CatalogBuilder(object): """ from calibre.ebooks.conversion.config import load_defaults - from calibre.utils.fonts import fontconfig MI_WIDTH = 600 MI_HEIGHT = 60 @@ -2767,11 +2766,10 @@ class CatalogBuilder(object): masthead_font_family = recs.get('masthead_font', 'Default') if masthead_font_family != 'Default': - masthead_font = fontconfig.files_for_family(masthead_font_family) - # Assume 'normal' always in dict, else use default - # {'normal': (path_to_font, friendly name)} - if 'normal' in masthead_font: - font_path = masthead_font['normal'][0] + from calibre.utils.fonts.scanner import font_scanner + faces = font_scanner.fonts_for_family(masthead_font_family) + if faces: + font_path = faces[0]['path'] if not font_path or not os.access(font_path, os.R_OK): font_path = default_font diff --git a/src/calibre/test_build.py b/src/calibre/test_build.py index 8ca0a36528..d6b3c9a400 100644 --- a/src/calibre/test_build.py +++ b/src/calibre/test_build.py @@ -37,14 +37,6 @@ def test_freetype(): test() print ('FreeType OK!') -def test_fontconfig(): - from calibre.utils.fonts import fontconfig - families = fontconfig.find_font_families() - num = len(families) - if num < 10: - raise RuntimeError('Fontconfig found only %d font families'%num) - print ('Fontconfig OK! (%d families)'%num) - def test_winutil(): from calibre.devices.scanner import win_pnp_drives matches = win_pnp_drives.scanner() @@ -123,7 +115,6 @@ def test(): test_plugins() test_lxml() test_freetype() - test_fontconfig() test_sqlite() test_qt() test_imaging() diff --git a/src/calibre/utils/fonts/__init__.py b/src/calibre/utils/fonts/__init__.py index f3fee56033..3af92bd6a0 100644 --- a/src/calibre/utils/fonts/__init__.py +++ b/src/calibre/utils/fonts/__init__.py @@ -6,120 +6,3 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from calibre.constants import iswindows, isosx - -class Fonts(object): - - def __init__(self): - if iswindows: - from calibre.utils.fonts.win_fonts import load_winfonts - self.backend = load_winfonts() - else: - from calibre.utils.fonts.fc import fontconfig - self.backend = fontconfig - - def find_font_families(self, allowed_extensions={'ttf', 'otf'}): - if iswindows: - return self.backend.font_families() - return self.backend.find_font_families(allowed_extensions=allowed_extensions) - - def find_font_families_no_delay(self, allowed_extensions={'ttf', 'otf'}): - if isosx: - if self.backend.is_scanning(): - return False, [] - return True, self.find_font_families(allowed_extensions=allowed_extensions) - - def files_for_family(self, family, normalize=True): - ''' - Find all the variants in the font family `family`. - Returns a dictionary of tuples. Each tuple is of the form (path to font - file, Full font name). - The keys of the dictionary depend on `normalize`. If `normalize` is `False`, - they are a tuple (slant, weight) otherwise they are strings from the set - `('normal', 'bold', 'italic', 'bi', 'light', 'li')` - ''' - if iswindows: - from calibre.ptempfile import PersistentTemporaryFile - fonts = self.backend.fonts_for_family(family, normalize=normalize) - ans = {} - for ft, val in fonts.iteritems(): - ext, name, data = val - pt = PersistentTemporaryFile('.'+ext) - pt.write(data) - pt.close() - ans[ft] = (pt.name, name) - return ans - return self.backend.files_for_family(family, normalize=normalize) - - def fonts_for_family(self, family, normalize=True): - ''' - Just like files for family, except that it returns 3-tuples of the form - (extension, full name, font data). - ''' - if iswindows: - return self.backend.fonts_for_family(family, normalize=normalize) - files = self.backend.files_for_family(family, normalize=normalize) - ans = {} - for ft, val in files.iteritems(): - f, name = val - ext = f.rpartition('.')[-1].lower() - ans[ft] = (ext, name, open(f, 'rb').read()) - return ans - - def find_font_for_text(self, text, allowed_families={'serif', 'sans-serif'}, - preferred_families=('serif', 'sans-serif', 'monospace', 'cursive', 'fantasy')): - ''' - Find a font on the system capable of rendering the given text. - - Returns a font family (as given by fonts_for_family()) that has a - "normal" font and that can render the supplied text. If no such font - exists, returns None. - - :return: (family name, faces) or None, None - ''' - from calibre.utils.fonts.free_type import FreeType, get_printable_characters, FreeTypeError - from calibre.utils.fonts.utils import panose_to_css_generic_family, get_font_characteristics - ft = FreeType() - found = {} - if not isinstance(text, unicode): - raise TypeError(u'%r is not unicode'%text) - text = get_printable_characters(text) - - def filter_faces(faces): - ans = {} - for k, v in faces.iteritems(): - try: - font = ft.load_font(v[2]) - except FreeTypeError: - continue - if font.supports_text(text, has_non_printable_chars=False): - ans[k] = v - return ans - - for family in sorted(self.find_font_families()): - faces = filter_faces(self.fonts_for_family(family)) - if 'normal' not in faces: - continue - panose = get_font_characteristics(faces['normal'][2])[5] - generic_family = panose_to_css_generic_family(panose) - if generic_family in allowed_families or generic_family == preferred_families[0]: - return (family, faces) - elif generic_family not in found: - found[generic_family] = (family, faces) - - for f in preferred_families: - if f in found: - return found[f] - return None, None - -fontconfig = Fonts() - -def test(): - import os - print(fontconfig.find_font_families()) - m = 'Liberation Serif' - for ft, val in fontconfig.files_for_family(m).iteritems(): - print val[0], ft, val[1], os.path.getsize(val[0]) - -if __name__ == '__main__': - test() diff --git a/src/calibre/utils/fonts/free_type.py b/src/calibre/utils/fonts/free_type.py index a2e8eca213..6be782dc79 100644 --- a/src/calibre/utils/fonts/free_type.py +++ b/src/calibre/utils/fonts/free_type.py @@ -83,11 +83,11 @@ def test(): raise RuntimeError('Incorrectly claiming that text is supported') def test_find_font(): - from calibre.utils.fonts import fontconfig + from calibre.utils.fonts.scanner import font_scanner abcd = '诶比西迪' - family = fontconfig.find_font_for_text(abcd)[0] + family = font_scanner.find_font_for_text(abcd)[0] print ('Family for Chinese text:', family) - family = fontconfig.find_font_for_text(abcd)[0] + family = font_scanner.find_font_for_text(abcd)[0] abcd = 'لوحة المفاتيح العربية' print ('Family for Arabic text:', family) diff --git a/src/calibre/utils/fonts/metadata.py b/src/calibre/utils/fonts/metadata.py new file mode 100644 index 0000000000..4907678c21 --- /dev/null +++ b/src/calibre/utils/fonts/metadata.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__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 + +class UnsupportedFont(ValueError): + pass + +FontCharacteristics = namedtuple('FontCharacteristics', + 'weight, is_italic, is_bold, is_regular, fs_type, panose, width, is_oblique, is_wws, os2_version') +FontNames = namedtuple('FontNames', + 'family_name, subfamily_name, full_name, preferred_family_name, preferred_subfamily_name, wws_family_name, wws_subfamily_name') + +class FontMetadata(object): + + def __init__(self, bytes_or_stream): + 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) + + f.seek(0) + self.font_family = (self.names.wws_family_name or + self.names.preferred_family_name or self.names.family_name) + wt = self.characteristics.weight + if wt == 400: + wt = 'normal' + elif wt == 700: + wt = 'bold' + else: + wt = type(u'')(wt) + self.font_weight = wt + + self.font_stretch = ('ultra-condensed', 'extra-condensed', + 'condensed', 'semi-condensed', 'normal', 'semi-expanded', + 'expanded', 'extra-expanded', 'ultra-expanded')[ + self.characteristics.width-1] + if self.characteristics.is_oblique: + self.font_style = 'oblique' + elif self.characteristics.is_italic: + self.font_style = 'italic' + 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 xrange(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') + 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: + 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) + self.characteristics = FontCharacteristics(*vals) + + def to_dict(self): + ans = { + 'is_otf':self.is_otf, + 'font-family':self.font_family, + 'font-weight':self.font_weight, + 'font-style':self.font_style, + 'font-stretch':self.font_stretch + } + for f in self.names._fields: + ans[f] = getattr(self.names, f) + for f in self.characteristics._fields: + ans[f] = getattr(self.characteristics, f) + return ans + + diff --git a/src/calibre/utils/fonts/scanner.py b/src/calibre/utils/fonts/scanner.py new file mode 100644 index 0000000000..b26525e690 --- /dev/null +++ b/src/calibre/utils/fonts/scanner.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os +from collections import defaultdict +from threading import Thread + +from calibre import walk, prints, as_unicode +from calibre.constants import config_dir, iswindows, isosx, plugins, DEBUG +from calibre.utils.fonts.metadata import FontMetadata +from calibre.utils.fonts.utils import panose_to_css_generic_family +from calibre.utils.icu import sort_key + +class NoFonts(ValueError): + pass + +def font_dirs(): + if iswindows: + winutil, err = plugins['winutil'] + if err: + raise RuntimeError('Failed to load winutil: %s'%err) + return winutil.special_folder_path(winutil.CSIDL_FONTS) + if isosx: + return [ + '/Library/Fonts', + '/System/Library/Fonts', + '/usr/share/fonts', + '/var/root/Library/Fonts', + os.path.expanduser('~/.fonts'), + os.path.expanduser('~/Library/Fonts'), + ] + return [ + '/opt/share/fonts', + '/usr/share/fonts', + '/usr/local/share/fonts', + os.path.expanduser('~/.fonts') + ] + +class Scanner(Thread): + + CACHE_VERSION = 1 + + def __init__(self, folders=[], allowed_extensions={'ttf', 'otf'}): + Thread.__init__(self) + self.folders = folders + font_dirs() + [os.path.join(config_dir, 'fonts'), + P('fonts/liberation')] + self.folders = [os.path.normcase(os.path.abspath(f)) for f in + self.folders] + self.font_families = () + self.allowed_extensions = allowed_extensions + + def find_font_families(self): + self.join() + return self.font_families + + def fonts_for_family(self, family): + ''' + Return a list of the faces belonging to the specified family. The first + face is the "Regular" face of family. Each face is a dictionary with + many keys, the most important of which are: path, font-family, + font-weight, font-style, font-stretch. The font-* properties follow the + CSS 3 Fonts specification. + ''' + self.join() + try: + return self.font_family_map[icu_lower(family)] + except KeyError: + raise NoFonts('No fonts found for the family: %r'%family) + + def legacy_fonts_for_family(self, family): + ''' + Return a simple set of regular, bold, italic and bold-italic faces for + the specified family. Returns a dictionary with each element being a + 2-tuple of (path to font, full font name) and the keys being: normal, + bold, italic, bi. + ''' + ans = {} + try: + faces = self.fonts_for_family(family) + except NoFonts: + return ans + for i, face in enumerate(faces): + if i == 0: + key = 'normal' + elif face['font-style'] in {'italic', 'oblique'}: + key = 'bi' if face['font-weight'] == 'bold' else 'italic' + elif face['font-weight'] == 'bold': + key = 'bold' + else: + continue + ans[key] = (face['path'], face['full_name']) + return ans + + def get_font_data(self, font_or_path): + path = font_or_path + if isinstance(font_or_path, dict): + path = font_or_path['path'] + with lopen(path, 'rb') as f: + return f.read() + + def find_font_for_text(self, text, allowed_families={'serif', 'sans-serif'}, + preferred_families=('serif', 'sans-serif', 'monospace', 'cursive', 'fantasy')): + ''' + Find a font on the system capable of rendering the given text. + + Returns a font family (as given by fonts_for_family()) that has a + "normal" font and that can render the supplied text. If no such font + exists, returns None. + + :return: (family name, faces) or None, None + ''' + from calibre.utils.fonts.free_type import FreeType, get_printable_characters + ft = FreeType() + found = {} + if not isinstance(text, unicode): + raise TypeError(u'%r is not unicode'%text) + text = get_printable_characters(text) + + def filter_faces(font): + try: + ftface = ft.load_font(self.get_font_data(font)) + return ftface.supports_text(text, has_non_printable_chars=False) + except: + pass + return False + + for family in self.find_font_families(): + faces = filter(filter_faces, self.fonts_for_family(family)) + if not faces: continue + generic_family = panose_to_css_generic_family(faces[0]['panose']) + if generic_family in allowed_families or generic_family == preferred_families[0]: + return (family, faces) + elif generic_family not in found: + found[generic_family] = (family, faces) + + for f in preferred_families: + if f in found: + return found[f] + return None, None + + def reload_cache(self): + if not hasattr(self, 'cache'): + from calibre.utils.config import JSONConfig + self.cache = JSONConfig('fonts/scanner_cache') + self.cache.refresh() + if self.cache.get('version', None) != self.CACHE_VERSION: + self.cache.clear() + self.cached_fonts = self.cache.get('fonts', {}) + + def run(self): + self.do_scan() + + def do_scan(self): + self.reload_cache() + num = 0 + for folder in self.folders: + if not os.path.isdir(folder): + continue + try: + files = tuple(walk(folder)) + except EnvironmentError as e: + if DEBUG: + prints('Failed to walk font folder:', folder, + as_unicode(e)) + continue + for candidate in files: + if (candidate.rpartition('.')[-1].lower() not in self.allowed_extensions + or not os.path.isfile(candidate)): + continue + candidate = os.path.normcase(os.path.abspath(candidate)) + try: + s = os.stat(candidate) + except EnvironmentError: + continue + fileid = '{0}||{1}:{2}'.format(candidate, s.st_size, s.st_mtime) + if fileid in self.cached_fonts: + continue + try: + self.read_font_metadata(candidate, fileid) + except Exception as e: + if DEBUG: + prints('Failed to read metadata from font file:', + candidate, as_unicode(e)) + continue + num += 1 + if num >= 10: + num = 0 + self.write_cache() + if num > 0: + self.write_cache() + self.build_families() + + def font_priority(self, font): + ''' + Try to ensure that the "Regular" face is the first font for a given + family. + ''' + style_normal = font['font-style'] == 'normal' + width_normal = font['font-stretch'] == 'normal' + weight_normal = font['font-weight'] == 'normal' + num_normal = sum(filter(None, (style_normal, width_normal, + weight_normal))) + subfamily_name = (font['wws_subfamily_name'] or + font['preferred_subfamily_name'] or font['subfamily_name']) + if num_normal == 3 and subfamily_name == 'Regular': + return 0 + if num_normal == 3: + return 1 + if subfamily_name == 'Regular': + return 2 + return 3 + (3 - num_normal) + + def build_families(self): + families = defaultdict(list) + for f in self.cached_fonts.itervalues(): + lf = icu_lower(f['font-family'] or '') + if lf: + families[lf].append(f) + + for fonts in families.itervalues(): + # Look for duplicate font files and choose the copy that is from a + # more significant font directory (prefer user directories over + # system directories). + fmap = {} + remove = [] + for f in fonts: + fingerprint = (icu_lower(f['font-family']), f['font-weight'], + f['font-stretch'], f['font-style']) + if fingerprint in fmap: + opath = fmap[fingerprint]['path'] + npath = f['path'] + if self.path_significance(npath) >= self.path_significance(opath): + remove.append(fmap[fingerprint]) + fmap[fingerprint] = f + else: + remove.append(f) + else: + fmap[fingerprint] = f + for font in remove: + fonts.remove(font) + fonts.sort(key=self.font_priority) + + self.font_family_map = dict.copy(families) + self.font_families = tuple(sorted((f[0]['font-family'] for f in + self.font_family_map.itervalues()), key=sort_key)) + + def path_significance(self, path): + path = os.path.normcase(os.path.abspath(path)) + for i, q in enumerate(self.folders): + if path.startswith(q): + return i + return -1 + + def write_cache(self): + with self.cache: + self.cache['version'] = self.CACHE_VERSION + self.cache['fonts'] = self.cached_fonts + + def read_font_metadata(self, path, fileid): + with lopen(path, 'rb') as f: + fm = FontMetadata(f) + data = fm.to_dict() + data['path'] = path + self.cached_fonts[fileid] = data + + def dump_fonts(self): + self.join() + for family in self.font_families: + prints(family) + for font in self.fonts_for_family(family): + prints('\t%s: %s'%(font['full_name'], font['path'])) + prints(end='\t') + for key in ('font-stretch', 'font-weight', 'font-style'): + prints('%s: %s'%(key, font[key]), end=' ') + prints() + prints('\tSub-family:', font['wws_subfamily_name'] or + font['preferred_subfamily_name'] or + font['subfamily_name']) + prints() + prints() + +font_scanner = Scanner() +font_scanner.start() + +if __name__ == '__main__': + font_scanner.dump_fonts() + + diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py index 4fcaa20c44..29b39bbefd 100644 --- a/src/calibre/utils/fonts/utils.py +++ b/src/calibre/utils/fonts/utils.py @@ -36,15 +36,19 @@ def get_table(raw, name): return table, table_index, table_offset, table_checksum return None, None, None, None -def get_font_characteristics(raw): +def get_font_characteristics(raw, raw_is_table=False): ''' - Return (weight, is_italic, is_bold, is_regular, fs_type, panose). These + 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 ''' - os2_table = get_table(raw, 'os/2')[0] - if os2_table is None: - raise UnsupportedFont('Not a supported font, has no OS/2 table') + if raw_is_table: + os2_table = raw + else: + os2_table = get_table(raw, 'os/2')[0] + if os2_table is None: + raise UnsupportedFont('Not a supported font, has no OS/2 table') common_fields = b'>Hh3H11h' (version, char_width, weight, width, fs_type, subscript_x_size, @@ -65,10 +69,12 @@ def get_font_characteristics(raw): offset += 4 selection, = struct.unpack_from(b'>H', os2_table, offset) - is_italic = (selection & 0b1) != 0 - is_bold = (selection & 0b100000) != 0 - is_regular = (selection & 0b1000000) != 0 - return weight, is_italic, is_bold, is_regular, fs_type, panose + 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 + return weight, is_italic, is_bold, is_regular, fs_type, panose, width, is_oblique, is_wws, version def panose_to_css_generic_family(panose): proportion = panose[3] @@ -142,10 +148,13 @@ def decode_name_record(recs): return None -def get_font_names(raw): - table = get_table(raw, 'name')[0] - if table is None: - raise UnsupportedFont('Not a supported font, has no name table') +def _get_font_names(raw, raw_is_table=False): + if raw_is_table: + table = raw + else: + table = get_table(raw, 'name')[0] + if table is None: + raise UnsupportedFont('Not a supported font, has no name table') table_type, count, string_offset = struct.unpack_from(b'>3H', table) records = defaultdict(list) @@ -161,12 +170,32 @@ def get_font_names(raw): records[name_id].append((platform_id, encoding_id, language_id, src)) + return records + +def get_font_names(raw, raw_is_table=False): + records = _get_font_names(raw, raw_is_table) family_name = decode_name_record(records[1]) subfamily_name = decode_name_record(records[2]) full_name = decode_name_record(records[4]) return family_name, subfamily_name, full_name +def get_font_names2(raw, raw_is_table=False): + records = _get_font_names(raw, raw_is_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 checksum_of_block(raw): extra = 4 - len(raw)%4 raw += b'\0'*extra @@ -249,11 +278,11 @@ def get_font_for_text(text, candidate_font_data=None): except FreeTypeError: ok = True if not ok: - from calibre.utils.fonts import fontconfig - family, faces = fontconfig.find_font_for_text(text) - if family is not None: - f = faces.get('bold', faces['normal']) - candidate_font_data = f[2] + from calibre.utils.fonts.scanner import font_scanner + family, faces = font_scanner.find_font_for_text(text) + if faces: + with lopen(faces[0]['path'], 'rb') as f: + candidate_font_data = f.read() return candidate_font_data def test():