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)
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
'''
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
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 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():
|
||||
|
Loading…
x
Reference in New Issue
Block a user