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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,19 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" 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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,120 +6,3 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__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')
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)

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