Use fontconfig to manage fonts

This commit is contained in:
Kovid Goyal 2008-06-09 16:12:13 -07:00
parent 261bf8021a
commit 94736825ca
11 changed files with 364 additions and 105 deletions

View File

@ -240,6 +240,9 @@ _check_symlinks_prescript()
os.link(os.path.expanduser('~/pdftohtml'), os.path.join(frameworks_dir, 'pdftohtml'))
print 'Adding plugins'
module_dir = os.path.join(resource_dir, 'lib', 'python2.5', 'lib-dynload')
print 'Adding fontconfig'
for f in glob.glob(os.path.expanduser('~/fontconfig/*')):
os.link(f, os.path.join(frameworks_dir, os.path.basename(f)))
for src, dest in plugin_files:
if 'dylib' in dest:
os.link(src, os.path.join(frameworks_dir, dest))

View File

@ -15,15 +15,14 @@ from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter
from logging import Formatter
from ttfquery import findsystem, describe
from PyQt4.QtCore import QSettings, QVariant, QUrl
from PyQt4.QtGui import QDesktopServices
from calibre.translations.msgfmt import make
from calibre.ebooks.chardet import detect
from calibre.terminfo import TerminalController
terminal_controller = TerminalController(sys.stdout)
terminal_controller = TerminalController(sys.stdout)
iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower()
isosx = 'darwin' in sys.platform.lower()
islinux = not(iswindows or isosx)
@ -306,44 +305,6 @@ def set_translator():
set_translator()
font_families = {}
def get_font_families(cached=None):
global font_families
if cached is not None:
font_families = cached
if not font_families:
try:
ffiles = findsystem.findFonts()
except Exception, err:
print 'WARNING: Could not find fonts on your system.'
print err
else:
zlist = []
for ff in ffiles:
try:
if 'Optane' in str(ff):
font = describe.openFont(ff)
wt, italic = describe.modifiers(font)
except:
pass
try:
font = describe.openFont(ff)
except: # Some font files cause ttfquery to raise an exception, in which case they are ignored
continue
try:
wt, italic = describe.modifiers(font)
except:
wt, italic = 0, 0
if wt == 400 and italic == 0:
try:
family = describe.shortName(font)[1].strip()
except: # Windows strikes again!
continue
zlist.append((family, ff))
font_families = dict(zlist)
return font_families
def sanitize_file_name(name):
'''
Remove characters that are illegal in filenames from name.
@ -596,3 +557,12 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
except KeyError:
return '&'+ent+';'
if isosx:
fdir = os.path.expanduser('~/.fonts')
if not os.path.exists(fdir):
os.makedirs(fdir)
if not os.path.exists(os.path.join(fdir, 'LiberationSans_Regular.ttf')):
from calibre.ebooks.lrf.fonts.liberation import __all__ as fonts
for font in fonts:
exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data'
open(os.path.join(fdir, font+'.ttf'), 'wb').write(font_data)

View File

@ -9,7 +9,6 @@ from optparse import OptionValueError
from htmlentitydefs import name2codepoint
from uuid import uuid4
from ttfquery import describe, findsystem
from fontTools.ttLib import TTLibError
from calibre.ebooks.lrf.pylrs.pylrs import Book as _Book
@ -67,20 +66,6 @@ def profile_from_string(option, opt_str, value, parser):
except KeyError:
raise OptionValueError('Profile: '+value+' is not implemented. Implemented profiles: %s'%(profile_map.keys()))
def font_family(option, opt_str, value, parser):
if value:
value = value.split(',')
if len(value) != 2:
raise OptionValueError('Font family specification must be of the form'+\
' "path to font directory, font family"')
path, family = tuple(value)
if not os.path.isdir(path) or not os.access(path, os.R_OK|os.X_OK):
raise OptionValueError('Cannot read from ' + path)
setattr(parser.values, option.dest, (path, family))
else:
setattr(parser.values, option.dest, tuple())
def option_parser(usage, gui_mode=False):
parser = OptionParser(usage=usage, gui_mode=gui_mode)
metadata = parser.add_option_group('METADATA OPTIONS')
@ -203,18 +188,17 @@ def option_parser(usage, gui_mode=False):
fonts = parser.add_option_group('FONT FAMILIES',
_('''Specify trutype font families for serif, sans-serif and monospace fonts. '''
'''These fonts will be embedded in the LRF file. Note that custom fonts lead to '''
'''slower page turns. Each family specification is of the form: '''
'''"path to fonts directory, family" '''
'''slower page turns. '''
'''For example: '''
'''--serif-family "%s, Times New Roman"
''') % ('C:\Windows\Fonts' if iswindows else '/usr/share/fonts/corefonts'))
fonts.add_option('--serif-family', action='callback', callback=font_family,
'''--serif-family "Times New Roman"
'''))
fonts.add_option('--serif-family',
default=None, dest='serif_family', type='string',
help=_('The serif family of fonts to embed'))
fonts.add_option('--sans-family', action='callback', callback=font_family,
fonts.add_option('--sans-family',
default=None, dest='sans_family', type='string',
help=_('The sans-serif family of fonts to embed'))
fonts.add_option('--mono-family', action='callback', callback=font_family,
fonts.add_option('--mono-family',
default=None, dest='mono_family', type='string',
help=_('The monospace family of fonts to embed'))
@ -231,45 +215,25 @@ def option_parser(usage, gui_mode=False):
return parser
def find_custom_fonts(options, logger):
from calibre.utils.fontconfig import files_for_family
fonts = {'serif' : None, 'sans' : None, 'mono' : None}
def find_family(option):
path, family = option
paths = findsystem.findFonts([path])
results = {}
for path in paths:
if len(results.keys()) == 4:
break
f = describe.openFont(path)
name, cfamily = describe.shortName(f)
if cfamily.lower().strip() != family.lower().strip():
continue
try:
wt, italic = describe.modifiers(f)
except TTLibError:
logger.exception('Could not process fonts in %s', path)
wt, italic = 0, 0
result = (path, name)
if wt == 400 and italic == 0:
results['normal'] = result
elif wt == 400 and italic > 0:
results['italic'] = result
elif wt >= 700 and italic == 0:
results['bold'] = result
elif wt >= 700 and italic > 0:
results['bi'] = result
return results
def family(cmd):
return cmd.split(',')[-1].strip()
if options.serif_family:
fonts['serif'] = find_family(options.serif_family)
f = family(options.serif_family)
fonts['serif'] = files_for_family(f)
if not fonts['serif']:
logger.warn('Unable to find serif family %s in %s'%(options.serif_family[1].strip(), options.serif_family[0]))
logger.warn('Unable to find serif family %s'%f)
if options.sans_family:
fonts['sans'] = find_family(options.sans_family)
f = family(options.sans_family)
fonts['sans'] = files_for_family(f)
if not fonts['sans']:
logger.warn('Unable to find sans family %s in %s'%(options.sans_family[1].strip(), options.sans_family[0]))
logger.warn('Unable to find sans family %s'%f)
if options.mono_family:
fonts['mono'] = find_family(options.mono_family)
f = family(options.mono_family)
fonts['mono'] = files_for_family(f)
if not fonts['mono']:
logger.warn('Unable to find mono family %s in %s'%(options.mono_family[1].strip(), options.mono_family[0]))
logger.warn('Unable to find mono family %s'%f)
return fonts
@ -324,4 +288,4 @@ def Book(options, logger, font_delta=0, header=None,
raise ConversionError, 'Could not find the normal version of the ' + family + ' font'
return book, fonts
from calibre import entity_to_unicode
from calibre import entity_to_unicode

View File

@ -0,0 +1,5 @@
__all__ = ['LiberationMono_Bold', 'LiberationMono_Regular', 'LiberationSans_Bold',
'LiberationSans_Regular', 'LiberationSerif_Bold', 'LiberationSerif_Regular',
'LiberationMono_BoldItalic', 'LiberationMono_Italic',
'LiberationSans_BoldItalic', 'LiberationSans_Italic',
'LiberationSerif_BoldItalic', 'LiberationSerif_Italic']

View File

@ -143,7 +143,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
for opt in ('--serif-family', '--sans-family', '--mono-family'):
if opt in cmdline:
print 'in'
family = cmdline[cmdline.index(opt)+1].split(',')[1].strip()
family = cmdline[cmdline.index(opt)+1].split(',')[-1].strip()
obj = getattr(self, 'gui_'+opt[2:].replace('-', '_'))
try:
obj.setCurrentIndex(self.font_family_model.index_of(family))
@ -332,12 +332,8 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
for opt in ('--serif-family', '--sans-family', '--mono-family'):
obj = getattr(self, 'gui_'+opt[2:].replace('-', '_'))
family = qstring_to_unicode(obj.itemText(obj.currentIndex())).strip()
try:
path = self.font_family_model.path_of(family)
except KeyError:
continue
if path:
cmd.extend([opt, os.path.dirname(path)+', '+family])
if family != 'None':
cmd.extend([opt, family])
return cmd

View File

@ -48,6 +48,7 @@ entry_points = {
'lrf2html = calibre.ebooks.lrf.html.convert_to:main',
'calibre-debug = calibre.debug:main',
'calibredb = calibre.library.cli:main',
'calibre-fontconfig = calibre.utils.fontconfig:main',
],
'gui_scripts' : [
__appname__+' = calibre.gui2.main:main',

View File

@ -31,7 +31,6 @@ class Distribution(object):
('libusb', '0.1.12', None, None, None),
('Qt', '4.4.0', 'qt', 'libqt4-core libqt4-gui', 'qt4'),
('PyQt', '4.4.2', 'PyQt4', 'python-qt4', 'PyQt4'),
('fonttools', '2.0-beta1', 'fonttools', 'fonttools', 'fonttools'),
('mechanize for python', '0.1.7b', 'dev-python/mechanize', 'python-mechanize', 'python-mechanize'),
('ImageMagick', '6.3.5', 'imagemagick', 'imagemagick', 'ImageMagick'),
('xdg-utils', '1.0.2', 'xdg-utils', 'xdg-utils', 'xdg-utils'),

View File

@ -34,7 +34,7 @@
<div py:if="distro.is_generic">
<ol>
<li>Make sure that your system has <code>python &gt;= 2.5</code></li>
<li>Install the various dependencies listed below: Make sure that any python packages are installed into python2.5 (e.g. setuptools, python-imaging, PyQt4, fonttools, etc)</li>
<li>Install the various dependencies listed below: Make sure that any python packages are installed into python2.5 (e.g. setuptools, python-imaging, PyQt4, etc)</li>
<li>As root run the command <pre class="wiki">easy_install-2.5 -U TTFQuery calibre &amp;&amp; calibre_postinstall</pre></li>
</ol>
<h2>Dependencies</h2>

View File

@ -85,7 +85,6 @@ def main(args=sys.argv):
if __name__ == '__main__':
cwd = os.getcwd()
sys.path.insert(0, os.path.dirname(os.path.dirname(cwd)))
print sys.path[0]
sys.exit(main())

View File

@ -0,0 +1,320 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
:mod:`fontconfig` -- Query system fonts
=============================================
.. module:: fontconfig
:platform: Unix, Windows, OS X
:synopsis: Query system fonts
.. moduleauthor:: Kovid Goyal <kovid@kovidgoyal.net>
A ctypes based wrapper around the `fontconfig <http://fontconfig.org>`_ library.
It can be used to find all fonts available on the system as well as the closest
match to a given font specification. The main functions in this module are:
.. autofunction:: find_font_families
.. autofunction:: files_for_family
.. autofunction:: match
'''
import sys, os, locale, codecs
from ctypes import cdll, c_void_p, Structure, c_int, POINTER, c_ubyte, c_char, \
pointer, byref, create_string_buffer, Union, c_char_p, c_double
try:
preferred_encoding = locale.getpreferredencoding()
codecs.lookup(preferred_encoding)
except:
preferred_encoding = 'utf-8'
iswindows = 'win32' in sys.platform or 'win64' in sys.platform
isosx = 'darwin' in sys.platform
def load_library():
if isosx:
lib = 'libfontconfig.1.dylib'
if hasattr(sys, 'frameworks_dir'):
lib = os.path.join(getattr(sys, 'frameworks_dir'), lib)
return cdll.LoadLibrary(lib)
elif iswindows:
return cdll.LoadLibrary('libfontconfig-1')
else:
return cdll.LoadLibrary('libfontconfig.so')
class FcPattern(Structure):
_fields_ = [
('num', c_int),
('size', c_int),
('elts_offset', c_void_p),
('ref', c_int)
]
class FcFontSet(Structure):
_fields_ = [
('nfont', c_int),
('sfont', c_int),
('fonts', POINTER(POINTER(FcPattern)))
]
(
FcTypeVoid,
FcTypeInteger,
FcTypeDouble,
FcTypeString,
FcTypeBool,
FcTypeMatrix,
FcTypeCharSet,
FcTypeFTFace,
FcTypeLangSet
) = map(c_int, range(9))
(FcMatchPattern, FcMatchFont, FcMatchScan) = map(c_int, range(3))
(
FcResultMatch, FcResultNoMatch, FcResultTypeMismatch, FcResultNoId,
FcResultOutOfMemory
) = map(c_int, range(5))
FcFalse, FcTrue = c_int(0), c_int(1)
class _FcValue(Union):
_fields_ = [
('s', c_char_p),
('i', c_int),
('b', c_int),
('d', c_double),
]
class FcValue(Structure):
_fields_ = [
('type', c_int),
('u', _FcValue)
]
lib = load_library()
lib.FcPatternCreate.restype = c_void_p
lib.FcObjectSetCreate.restype = c_void_p
lib.FcFontSetDestroy.argtypes = [POINTER(FcFontSet)]
lib.FcFontList.restype = POINTER(FcFontSet)
lib.FcNameUnparse.argtypes = [POINTER(FcPattern)]
lib.FcNameUnparse.restype = POINTER(c_ubyte)
lib.FcPatternGetString.argtypes = [POINTER(FcPattern), POINTER(c_char), c_int, c_void_p]
lib.FcPatternGetString.restype = c_int
lib.FcPatternAdd.argtypes = [c_void_p, POINTER(c_char), FcValue, c_int]
lib.FcPatternGetInteger.argtypes = [POINTER(FcPattern), POINTER(c_char), c_int, POINTER(c_int)]
lib.FcPatternGetInteger.restype = c_int
lib.FcNameParse.argtypes = [c_char_p]
lib.FcNameParse.restype = POINTER(FcPattern)
lib.FcDefaultSubstitute.argtypes = [POINTER(FcPattern)]
lib.FcConfigSubstitute.argtypes = [c_void_p, POINTER(FcPattern), c_int]
lib.FcFontSetCreate.restype = POINTER(FcFontSet)
lib.FcFontMatch.argtypes = [c_void_p, POINTER(FcPattern), POINTER(c_int)]
lib.FcFontMatch.restype = POINTER(FcPattern)
lib.FcFontSetAdd.argtypes = [POINTER(FcFontSet), POINTER(FcPattern)]
lib.FcFontSort.argtypes = [c_void_p, POINTER(FcPattern), c_int, c_void_p, POINTER(c_int)]
lib.FcFontSort.restype = POINTER(FcFontSet)
lib.FcFontRenderPrepare.argtypes = [c_void_p, POINTER(FcPattern), POINTER(FcPattern)]
lib.FcFontRenderPrepare.restype = POINTER(FcPattern)
if not lib.FcInit():
raise RuntimeError(_('Could not initialize the fontconfig library'))
def find_font_families(allowed_extensions=['ttf']):
'''
Return an alphabetically sorted list of font families available on the system.
`allowed_extensions`: A list of allowed extensions for font file types. Defaults to
`['ttf']`. If it is empty, it is ignored.
'''
allowed_extensions = [i.lower() for i in allowed_extensions]
empty_pattern = lib.FcPatternCreate()
oset = lib.FcObjectSetCreate()
if not lib.FcObjectSetAdd(oset, 'file'):
raise RuntimeError('Allocation failure')
if not lib.FcObjectSetAdd(oset, 'family'):
raise RuntimeError('Allocation failure')
fs = lib.FcFontList(0, empty_pattern, oset)
font_set = fs.contents
file = pointer(create_string_buffer(chr(0), 5000))
family = pointer(create_string_buffer(chr(0), 200))
font_families = []
for i in range(font_set.nfont):
pat = font_set.fonts[i]
if lib.FcPatternGetString(pat, 'file', 0, byref(file)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
path = str(file.contents.value)
ext = os.path.splitext(path)[1]
if ext:
ext = ext[1:].lower()
if allowed_extensions and ext in allowed_extensions:
if lib.FcPatternGetString(pat, 'family', 0, byref(family)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
font_families.append(str(family.contents.value))
lib.FcObjectSetDestroy(oset)
lib.FcPatternDestroy(empty_pattern)
lib.FcFontSetDestroy(fs)
font_families = list(set(font_families))
font_families.sort()
return font_families
def files_for_family(family, normalize=True):
'''
Find all the variants in the font family `family`.
Returns a dictionary of tuples. Each tuple is of the form (Full font name, path to font file).
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 isinstance(family, unicode):
family = family.encode(preferred_encoding)
family_pattern = lib.FcPatternBuild(0, 'family', FcTypeString, family, 0)
if not family_pattern:
raise RuntimeError('Allocation failure')
#lib.FcPatternPrint(family_pattern)
oset = lib.FcObjectSetCreate()
if not lib.FcObjectSetAdd(oset, 'file'):
raise RuntimeError('Allocation failure')
if not lib.FcObjectSetAdd(oset, 'weight'):
raise RuntimeError('Allocation failure')
if not lib.FcObjectSetAdd(oset, 'fullname'):
raise RuntimeError('Allocation failure')
if not lib.FcObjectSetAdd(oset, 'slant'):
raise RuntimeError('Allocation failure')
if not lib.FcObjectSetAdd(oset, 'style'):
raise RuntimeError('Allocation failure')
fonts = {}
fs = lib.FcFontList(0, family_pattern, oset)
font_set = fs.contents
file = pointer(create_string_buffer(chr(0), 5000))
full_name = pointer(create_string_buffer(chr(0), 200))
weight = c_int(0)
slant = c_int(0)
fname = ''
for i in range(font_set.nfont):
pat = font_set.fonts[i]
#lib.FcPatternPrint(pat)
pat = font_set.fonts[i]
if lib.FcPatternGetString(pat, 'file', 0, byref(file)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
if lib.FcPatternGetInteger(pat, 'weight', 0, byref(weight)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
if lib.FcPatternGetString(pat, 'fullname', 0, byref(full_name)) != FcResultMatch.value:
if lib.FcPatternGetString(pat, 'fullname', 0, byref(full_name)) == FcResultNoMatch.value:
if lib.FcPatternGetString(pat, 'style', 0, byref(full_name)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
fname = family + ' ' + full_name.contents.value
else:
raise RuntimeError('Error processing pattern')
else:
fname = full_name.contents.value
if lib.FcPatternGetInteger(pat, 'slant', 0, byref(slant)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
style = (slant.value, weight.value)
if normalize:
italic = slant.value > 0
normal = weight.value == 80
bold = weight.value > 80
if italic:
style = 'italic' if normal else 'bi' if bold else 'li'
else:
style = 'normal' if normal else 'bold' if bold else 'light'
fonts[style] = (file.contents.value, fname)
lib.FcObjectSetDestroy(oset)
lib.FcPatternDestroy(family_pattern)
if not iswindows:
lib.FcFontSetDestroy(fs)
return fonts
def match(name, sort=False, verbose=False):
'''
Find the system font that most closely matches `name`, where `name` is a specification
of the form::
familyname-<pointsize>:<property1=value1>:<property2=value2>...
For example, `verdana:weight=bold:slant=italic`
Returns a list of dictionaries. Each dictionary has the keys: 'weight', 'slant', 'family', 'file'
`sort`: If `True` return a sorted list of matching fonts, where the sort id in order of
decreasing closeness of matching.
`verbose`: If `True` print debugging information to stdout
'''
if isinstance(name, unicode):
name = name.encode(preferred_encoding)
pat = lib.FcNameParse(name)
if not pat:
raise ValueError('Could not parse font name')
if verbose:
print 'Searching for pattern'
lib.FcPatternPrint(pat)
if not lib.FcConfigSubstitute(0, pat, FcMatchPattern):
raise RuntimeError('Allocation failure')
lib.FcDefaultSubstitute(pat)
fs = lib.FcFontSetCreate()
result = c_int(0)
matches = []
if sort:
font_patterns = lib.FcFontSort(0, pat, FcFalse, 0, byref(result))
if not font_patterns:
raise RuntimeError('Allocation failed')
fps = font_patterns.contents
for j in range(fps.nfont):
fpat = fps.fonts[j]
fp = lib.FcFontRenderPrepare(0, pat, fpat)
if fp:
lib.FcFontSetAdd(fs, fp)
lib.FcFontSetDestroy(font_patterns)
else:
match_pat = lib.FcFontMatch(0, pat, byref(result))
if pat:
lib.FcFontSetAdd(fs, match_pat)
if result.value != FcResultMatch.value:
lib.FcPatternDestroy(pat)
return matches
font_set = fs.contents
file = pointer(create_string_buffer(chr(0), 5000))
family = pointer(create_string_buffer(chr(0), 200))
weight = c_int(0)
slant = c_int(0)
for j in range(font_set.nfont):
fpat = font_set.fonts[j]
#lib.FcPatternPrint(fpat)
if lib.FcPatternGetString(fpat, 'file', 0, byref(file)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
if lib.FcPatternGetString(fpat, 'family', 0, byref(family)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
if lib.FcPatternGetInteger(fpat, 'weight', 0, byref(weight)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
if lib.FcPatternGetInteger(fpat, 'slant', 0, byref(slant)) != FcResultMatch.value:
raise RuntimeError('Error processing pattern')
matches.append({
'file' : file.contents.value,
'family' : family.contents.value,
'weight' : weight.value,
'slant' : slant.value,
}
)
lib.FcPatternDestroy(pat)
lib.FcFontSetDestroy(fs)
return matches
def main(args=sys.argv):
print find_font_families()
if len(args) > 1:
print
print files_for_family(args[1])
print
print match(args[1], verbose=True)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -46,6 +46,7 @@ BrandingText "${PRODUCT_NAME} created by Kovid Goyal"
!define CLIT "C:\clit\clit.exe"
!define PDFTOHTML "C:\pdftohtml\pdftohtml.exe"
!define IMAGEMAGICK "C:\ImageMagick"
!DEFINE FONTCONFIG "C:\fontconfig"
; ---------------PATH manipulation -----------------------------------------------------------------
@ -283,6 +284,7 @@ Section "Main" "secmain"
File /r "${PY2EXE_DIR}\*"
File "${CLIT}"
File "${PDFTOHTML}"
File /r "${FONTCONFIG}\*"
SetOutPath "$INSTDIR\ImageMagick"
File /r "${IMAGEMAGICK}\*"