mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Font embedding: Add support for the CSS 3 Fonts module, which means you can embed font families that have more that the usual four faces, with the full set of font-stretch and font-weight variations. Of course, whether the fonts actually show up on a reader will depend on the readers support for CSS 3.
This commit is contained in:
parent
6d355b82b8
commit
1af17f0c20
@ -690,29 +690,6 @@ def remove_bracketed_text(src,
|
|||||||
buf.append(char)
|
buf.append(char)
|
||||||
return u''.join(buf)
|
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):
|
def ipython(user_ns=None):
|
||||||
from calibre.utils.ipython import ipython
|
from calibre.utils.ipython import ipython
|
||||||
ipython(user_ns=user_ns)
|
ipython(user_ns=user_ns)
|
||||||
|
@ -254,7 +254,6 @@ def unit_convert(value, base, font, dpi):
|
|||||||
|
|
||||||
def generate_masthead(title, output_path=None, width=600, height=60):
|
def generate_masthead(title, output_path=None, width=600, height=60):
|
||||||
from calibre.ebooks.conversion.config import load_defaults
|
from calibre.ebooks.conversion.config import load_defaults
|
||||||
from calibre.utils.fonts import fontconfig
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
fp = tweaks['generate_cover_title_font']
|
fp = tweaks['generate_cover_title_font']
|
||||||
if not fp:
|
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')
|
masthead_font_family = recs.get('masthead_font', 'Default')
|
||||||
|
|
||||||
if masthead_font_family != 'Default':
|
if masthead_font_family != 'Default':
|
||||||
masthead_font = fontconfig.files_for_family(masthead_font_family)
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
# Assume 'normal' always in dict, else use default
|
faces = font_scanner.fonts_for_family(masthead_font_family)
|
||||||
# {'normal': (path_to_font, friendly name)}
|
if faces:
|
||||||
if 'normal' in masthead_font:
|
font_path = faces[0]['path']
|
||||||
font_path = masthead_font['normal'][0]
|
|
||||||
|
|
||||||
if not font_path or not os.access(font_path, os.R_OK):
|
if not font_path or not os.access(font_path, os.R_OK):
|
||||||
font_path = default_font
|
font_path = default_font
|
||||||
|
@ -34,24 +34,24 @@ class PRS500_PROFILE(object):
|
|||||||
name = 'prs500'
|
name = 'prs500'
|
||||||
|
|
||||||
def find_custom_fonts(options, logger):
|
def find_custom_fonts(options, logger):
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
files_for_family = fontconfig.files_for_family
|
|
||||||
fonts = {'serif' : None, 'sans' : None, 'mono' : None}
|
fonts = {'serif' : None, 'sans' : None, 'mono' : None}
|
||||||
def family(cmd):
|
def family(cmd):
|
||||||
return cmd.split(',')[-1].strip()
|
return cmd.split(',')[-1].strip()
|
||||||
if options.serif_family:
|
if options.serif_family:
|
||||||
f = family(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']:
|
if not fonts['serif']:
|
||||||
logger.warn('Unable to find serif family %s'%f)
|
logger.warn('Unable to find serif family %s'%f)
|
||||||
if options.sans_family:
|
if options.sans_family:
|
||||||
f = family(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']:
|
if not fonts['sans']:
|
||||||
logger.warn('Unable to find sans family %s'%f)
|
logger.warn('Unable to find sans family %s'%f)
|
||||||
if options.mono_family:
|
if options.mono_family:
|
||||||
f = family(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']:
|
if not fonts['mono']:
|
||||||
logger.warn('Unable to find mono family %s'%f)
|
logger.warn('Unable to find mono family %s'%f)
|
||||||
return fonts
|
return fonts
|
||||||
|
@ -178,49 +178,40 @@ class CSSFlattener(object):
|
|||||||
body_font_family = None
|
body_font_family = None
|
||||||
if not family:
|
if not family:
|
||||||
return body_font_family, efi
|
return body_font_family, efi
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
from calibre.utils.fonts.utils import (get_font_characteristics,
|
from calibre.utils.fonts.utils import panose_to_css_generic_family
|
||||||
panose_to_css_generic_family, get_font_names)
|
faces = font_scanner.fonts_for_family(family)
|
||||||
faces = fontconfig.fonts_for_family(family)
|
if not faces:
|
||||||
if not faces or not u'normal' in faces:
|
|
||||||
msg = (u'No embeddable fonts found for family: %r'%self.opts.embed_font_family)
|
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:
|
if failure_critical:
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
self.oeb.log.warn(msg)
|
self.oeb.log.warn(msg)
|
||||||
return body_font_family, efi
|
return body_font_family, efi
|
||||||
|
|
||||||
for k, v in faces.iteritems():
|
for i, font in enumerate(faces):
|
||||||
ext, data = v[0::2]
|
ext = 'otf' if font['is_otf'] else 'ttf'
|
||||||
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'
|
|
||||||
fid, href = self.oeb.manifest.generate(id=u'font',
|
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,
|
item = self.oeb.manifest.add(fid, href,
|
||||||
guess_type(full_name+'.'+ext)[0],
|
guess_type('dummy.'+ext)[0],
|
||||||
data=data)
|
data=font_scanner.get_font_data(font))
|
||||||
item.unload_data_from_memory()
|
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
|
rule = '@font-face { %s }'%('; '.join(u'%s:%s'%(k, v) for k, v in
|
||||||
font.iteritems()))
|
cfont.iteritems()))
|
||||||
rule = cssutils.parseString(rule)
|
rule = cssutils.parseString(rule)
|
||||||
efi.append(rule)
|
efi.append(rule)
|
||||||
|
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
""" The GUI """
|
""" The GUI """
|
||||||
import os, sys, Queue, threading
|
import os, sys, Queue, threading, glob
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt,
|
from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt,
|
||||||
QByteArray, QTranslator, QCoreApplication, QThread,
|
QByteArray, QTranslator, QCoreApplication, QThread,
|
||||||
QEvent, QTimer, pyqtSignal, QDateTime, QDesktopServices,
|
QEvent, QTimer, pyqtSignal, QDateTime, QDesktopServices,
|
||||||
QFileDialog, QFileIconProvider, QSettings, QColor,
|
QFileDialog, QFileIconProvider, QSettings, QColor,
|
||||||
QIcon, QApplication, QDialog, QUrl, QFont, QPalette)
|
QIcon, QApplication, QDialog, QUrl, QFont, QPalette,
|
||||||
|
QFontDatabase)
|
||||||
|
|
||||||
ORG_NAME = 'KovidsBrain'
|
ORG_NAME = 'KovidsBrain'
|
||||||
APP_UID = 'libprs500'
|
APP_UID = 'libprs500'
|
||||||
from calibre import prints, load_builtin_fonts
|
from calibre import prints
|
||||||
from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx,
|
from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx,
|
||||||
plugins, config_dir, filesystem_encoding, DEBUG)
|
plugins, config_dir, filesystem_encoding, DEBUG)
|
||||||
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||||
@ -779,7 +780,7 @@ class Application(QApplication):
|
|||||||
qt_app = self
|
qt_app = self
|
||||||
self._file_open_paths = []
|
self._file_open_paths = []
|
||||||
self._file_open_lock = RLock()
|
self._file_open_lock = RLock()
|
||||||
load_builtin_fonts()
|
self.load_builtin_fonts()
|
||||||
self.setup_styles(force_calibre_style)
|
self.setup_styles(force_calibre_style)
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -792,6 +793,28 @@ class Application(QApplication):
|
|||||||
self.redirect_notify = True
|
self.redirect_notify = True
|
||||||
return ret
|
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):
|
def load_calibre_style(self):
|
||||||
# On OS X QtCurve resets the palette, so we preserve it explicitly
|
# On OS X QtCurve resets the palette, so we preserve it explicitly
|
||||||
orig_pal = QPalette(self.palette())
|
orig_pal = QPalette(self.palette())
|
||||||
@ -964,22 +987,9 @@ def is_gui_thread():
|
|||||||
global gui_thread
|
global gui_thread
|
||||||
return gui_thread is QThread.currentThread()
|
return gui_thread is QThread.currentThread()
|
||||||
|
|
||||||
_rating_font = None
|
_rating_font = 'Arial Unicode MS' if iswindows else 'sans-serif'
|
||||||
def rating_font():
|
def rating_font():
|
||||||
global _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
|
return _rating_font
|
||||||
|
|
||||||
def find_forms(srcdir):
|
def find_forms(srcdir):
|
||||||
|
@ -29,38 +29,8 @@ class PluginWidget(Widget, Ui_Form):
|
|||||||
)
|
)
|
||||||
self.db, self.book_id = db, book_id
|
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.opt_mobi_file_type.addItems(['old', 'both', 'new'])
|
||||||
|
|
||||||
self.initialize_options(get_option, get_help, db, book_id)
|
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
|
|
||||||
'''
|
|
||||||
|
@ -13,9 +13,6 @@ from PyQt4.Qt import (QFontInfo, QFontMetrics, Qt, QFont, QFontDatabase, QPen,
|
|||||||
QToolButton, QGridLayout, QListView, QWidget, QDialogButtonBox, QIcon,
|
QToolButton, QGridLayout, QListView, QWidget, QDialogButtonBox, QIcon,
|
||||||
QHBoxLayout, QLabel, QModelIndex)
|
QHBoxLayout, QLabel, QModelIndex)
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog
|
|
||||||
from calibre.utils.icu import sort_key
|
|
||||||
|
|
||||||
def writing_system_for_font(font):
|
def writing_system_for_font(font):
|
||||||
has_latin = True
|
has_latin = True
|
||||||
systems = QFontDatabase().writingSystems(font.family())
|
systems = QFontDatabase().writingSystems(font.family())
|
||||||
@ -122,19 +119,14 @@ class FontFamilyDialog(QDialog):
|
|||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.setWindowTitle(_('Choose font family'))
|
self.setWindowTitle(_('Choose font family'))
|
||||||
self.setWindowIcon(QIcon(I('font.png')))
|
self.setWindowIcon(QIcon(I('font.png')))
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
try:
|
try:
|
||||||
self.families = fontconfig.find_font_families()
|
self.families = font_scanner.find_font_families()
|
||||||
except:
|
except:
|
||||||
self.families = []
|
self.families = []
|
||||||
print ('WARNING: Could not load fonts')
|
print ('WARNING: Could not load fonts')
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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.families.insert(0, _('None'))
|
||||||
|
|
||||||
self.l = l = QGridLayout()
|
self.l = l = QGridLayout()
|
||||||
@ -174,20 +166,6 @@ class FontFamilyDialog(QDialog):
|
|||||||
if idx == 0: return None
|
if idx == 0: return None
|
||||||
return self.families[idx]
|
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):
|
class FontFamilyChooser(QWidget):
|
||||||
|
|
||||||
family_changed = pyqtSignal(object)
|
family_changed = pyqtSignal(object)
|
||||||
|
@ -11,7 +11,7 @@ from PyQt4.Qt import (QIcon, QFont, QLabel, QListWidget, QAction,
|
|||||||
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, QRegExp, QSize,
|
QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, QRegExp, QSize,
|
||||||
QSplitter, QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, QMenu,
|
QSplitter, QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, QMenu,
|
||||||
QStringListModel, QCompleter, QStringList, QTimer, QRect,
|
QStringListModel, QCompleter, QStringList, QTimer, QRect,
|
||||||
QFontDatabase, QGraphicsView, QByteArray)
|
QGraphicsView, QByteArray)
|
||||||
|
|
||||||
from calibre.constants import iswindows
|
from calibre.constants import iswindows
|
||||||
from calibre.gui2 import (NONE, error_dialog, pixmap_to_data, gprefs,
|
from calibre.gui2 import (NONE, error_dialog, pixmap_to_data, gprefs,
|
||||||
@ -352,17 +352,14 @@ class FontFamilyModel(QAbstractListModel): # {{{
|
|||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QAbstractListModel.__init__(self, *args)
|
QAbstractListModel.__init__(self, *args)
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
try:
|
try:
|
||||||
self.families = fontconfig.find_font_families()
|
self.families = font_scanner.find_font_families()
|
||||||
except:
|
except:
|
||||||
self.families = []
|
self.families = []
|
||||||
print 'WARNING: Could not load fonts'
|
print 'WARNING: Could not load fonts'
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
# Restrict to Qt families as Qt tends to crash
|
# 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.families[:0] = [_('None')]
|
||||||
self.font = QFont('Arial' if iswindows else 'sansserif')
|
self.font = QFont('Arial' if iswindows else 'sansserif')
|
||||||
|
|
||||||
|
@ -2757,7 +2757,6 @@ class CatalogBuilder(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from calibre.ebooks.conversion.config import load_defaults
|
from calibre.ebooks.conversion.config import load_defaults
|
||||||
from calibre.utils.fonts import fontconfig
|
|
||||||
|
|
||||||
MI_WIDTH = 600
|
MI_WIDTH = 600
|
||||||
MI_HEIGHT = 60
|
MI_HEIGHT = 60
|
||||||
@ -2767,11 +2766,10 @@ class CatalogBuilder(object):
|
|||||||
masthead_font_family = recs.get('masthead_font', 'Default')
|
masthead_font_family = recs.get('masthead_font', 'Default')
|
||||||
|
|
||||||
if masthead_font_family != 'Default':
|
if masthead_font_family != 'Default':
|
||||||
masthead_font = fontconfig.files_for_family(masthead_font_family)
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
# Assume 'normal' always in dict, else use default
|
faces = font_scanner.fonts_for_family(masthead_font_family)
|
||||||
# {'normal': (path_to_font, friendly name)}
|
if faces:
|
||||||
if 'normal' in masthead_font:
|
font_path = faces[0]['path']
|
||||||
font_path = masthead_font['normal'][0]
|
|
||||||
|
|
||||||
if not font_path or not os.access(font_path, os.R_OK):
|
if not font_path or not os.access(font_path, os.R_OK):
|
||||||
font_path = default_font
|
font_path = default_font
|
||||||
|
@ -37,14 +37,6 @@ def test_freetype():
|
|||||||
test()
|
test()
|
||||||
print ('FreeType OK!')
|
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():
|
def test_winutil():
|
||||||
from calibre.devices.scanner import win_pnp_drives
|
from calibre.devices.scanner import win_pnp_drives
|
||||||
matches = win_pnp_drives.scanner()
|
matches = win_pnp_drives.scanner()
|
||||||
@ -123,7 +115,6 @@ def test():
|
|||||||
test_plugins()
|
test_plugins()
|
||||||
test_lxml()
|
test_lxml()
|
||||||
test_freetype()
|
test_freetype()
|
||||||
test_fontconfig()
|
|
||||||
test_sqlite()
|
test_sqlite()
|
||||||
test_qt()
|
test_qt()
|
||||||
test_imaging()
|
test_imaging()
|
||||||
|
@ -6,120 +6,3 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__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()
|
|
||||||
|
@ -83,11 +83,11 @@ def test():
|
|||||||
raise RuntimeError('Incorrectly claiming that text is supported')
|
raise RuntimeError('Incorrectly claiming that text is supported')
|
||||||
|
|
||||||
def test_find_font():
|
def test_find_font():
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
abcd = '诶比西迪'
|
abcd = '诶比西迪'
|
||||||
family = fontconfig.find_font_for_text(abcd)[0]
|
family = font_scanner.find_font_for_text(abcd)[0]
|
||||||
print ('Family for Chinese text:', family)
|
print ('Family for Chinese text:', family)
|
||||||
family = fontconfig.find_font_for_text(abcd)[0]
|
family = font_scanner.find_font_for_text(abcd)[0]
|
||||||
abcd = 'لوحة المفاتيح العربية'
|
abcd = 'لوحة المفاتيح العربية'
|
||||||
print ('Family for Arabic text:', family)
|
print ('Family for Arabic text:', family)
|
||||||
|
|
||||||
|
114
src/calibre/utils/fonts/metadata.py
Normal file
114
src/calibre/utils/fonts/metadata.py
Normal file
@ -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 <kovid at kovidgoyal.net>'
|
||||||
|
__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
|
||||||
|
|
||||||
|
|
294
src/calibre/utils/fonts/scanner.py
Normal file
294
src/calibre/utils/fonts/scanner.py
Normal file
@ -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 <kovid at kovidgoyal.net>'
|
||||||
|
__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()
|
||||||
|
|
||||||
|
|
@ -36,15 +36,19 @@ def get_table(raw, name):
|
|||||||
return table, table_index, table_offset, table_checksum
|
return table, table_index, table_offset, table_checksum
|
||||||
return None, None, None, None
|
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
|
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
|
||||||
'''
|
'''
|
||||||
os2_table = get_table(raw, 'os/2')[0]
|
if raw_is_table:
|
||||||
if os2_table is None:
|
os2_table = raw
|
||||||
raise UnsupportedFont('Not a supported font, has no OS/2 table')
|
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'
|
common_fields = b'>Hh3H11h'
|
||||||
(version, char_width, weight, width, fs_type, subscript_x_size,
|
(version, char_width, weight, width, fs_type, subscript_x_size,
|
||||||
@ -65,10 +69,12 @@ def get_font_characteristics(raw):
|
|||||||
offset += 4
|
offset += 4
|
||||||
selection, = struct.unpack_from(b'>H', os2_table, offset)
|
selection, = struct.unpack_from(b'>H', os2_table, offset)
|
||||||
|
|
||||||
is_italic = (selection & 0b1) != 0
|
is_italic = (selection & (1 << 0)) != 0
|
||||||
is_bold = (selection & 0b100000) != 0
|
is_bold = (selection & (1 << 5)) != 0
|
||||||
is_regular = (selection & 0b1000000) != 0
|
is_regular = (selection & (1 << 6)) != 0
|
||||||
return weight, is_italic, is_bold, is_regular, fs_type, panose
|
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):
|
def panose_to_css_generic_family(panose):
|
||||||
proportion = panose[3]
|
proportion = panose[3]
|
||||||
@ -142,10 +148,13 @@ def decode_name_record(recs):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_font_names(raw):
|
def _get_font_names(raw, raw_is_table=False):
|
||||||
table = get_table(raw, 'name')[0]
|
if raw_is_table:
|
||||||
if table is None:
|
table = raw
|
||||||
raise UnsupportedFont('Not a supported font, has no name table')
|
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)
|
table_type, count, string_offset = struct.unpack_from(b'>3H', table)
|
||||||
|
|
||||||
records = defaultdict(list)
|
records = defaultdict(list)
|
||||||
@ -161,12 +170,32 @@ def get_font_names(raw):
|
|||||||
records[name_id].append((platform_id, encoding_id, language_id,
|
records[name_id].append((platform_id, encoding_id, language_id,
|
||||||
src))
|
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])
|
family_name = decode_name_record(records[1])
|
||||||
subfamily_name = decode_name_record(records[2])
|
subfamily_name = decode_name_record(records[2])
|
||||||
full_name = decode_name_record(records[4])
|
full_name = decode_name_record(records[4])
|
||||||
|
|
||||||
return family_name, subfamily_name, full_name
|
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):
|
def checksum_of_block(raw):
|
||||||
extra = 4 - len(raw)%4
|
extra = 4 - len(raw)%4
|
||||||
raw += b'\0'*extra
|
raw += b'\0'*extra
|
||||||
@ -249,11 +278,11 @@ def get_font_for_text(text, candidate_font_data=None):
|
|||||||
except FreeTypeError:
|
except FreeTypeError:
|
||||||
ok = True
|
ok = True
|
||||||
if not ok:
|
if not ok:
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts.scanner import font_scanner
|
||||||
family, faces = fontconfig.find_font_for_text(text)
|
family, faces = font_scanner.find_font_for_text(text)
|
||||||
if family is not None:
|
if faces:
|
||||||
f = faces.get('bold', faces['normal'])
|
with lopen(faces[0]['path'], 'rb') as f:
|
||||||
candidate_font_data = f[2]
|
candidate_font_data = f.read()
|
||||||
return candidate_font_data
|
return candidate_font_data
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user