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:
Kovid Goyal 2012-10-28 17:24:00 +05:30
parent 6d355b82b8
commit 1af17f0c20
15 changed files with 529 additions and 299 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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
'''

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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)

View 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

View 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()

View File

@ -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():