Generate cover: If the default font cannot render characters in the metadata (for example for east asian languages) try to automatically find a font on the system that is capable of rendering the characters

This commit is contained in:
Kovid Goyal 2012-10-20 17:14:17 +05:30
parent 407cd60dda
commit 135123ad46
5 changed files with 148 additions and 16 deletions

View File

@ -178,18 +178,41 @@ def normalize(x):
def calibre_cover(title, author_string, series_string=None, def calibre_cover(title, author_string, series_string=None,
output_format='jpg', title_size=46, author_size=36, logo_path=None): output_format='jpg', title_size=46, author_size=36, logo_path=None):
from calibre.utils.config_base import tweaks
title = normalize(title) title = normalize(title)
author_string = normalize(author_string) author_string = normalize(author_string)
series_string = normalize(series_string) series_string = normalize(series_string)
from calibre.utils.magick.draw import create_cover_page, TextLine from calibre.utils.magick.draw import create_cover_page, TextLine
lines = [TextLine(title, title_size), TextLine(author_string, author_size)] text = title + author_string + (series_string or u'')
font_path = tweaks['generate_cover_title_font']
if font_path is None:
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
from calibre.utils.fonts.utils import get_font_for_text
font = open(font_path, 'rb').read()
c = get_font_for_text(text, font)
cleanup = False
if c is not None and c != font:
from calibre.ptempfile import PersistentTemporaryFile
pt = PersistentTemporaryFile('.ttf')
pt.write(c)
pt.close()
font_path = pt.name
cleanup = True
lines = [TextLine(title, title_size, font_path=font_path),
TextLine(author_string, author_size, font_path=font_path)]
if series_string: if series_string:
lines.append(TextLine(series_string, author_size)) lines.append(TextLine(series_string, author_size, font_path=font_path))
if logo_path is None: if logo_path is None:
logo_path = I('library.png') logo_path = I('library.png')
return create_cover_page(lines, logo_path, output_format='jpg', try:
return create_cover_page(lines, logo_path, output_format='jpg',
texture_opacity=0.3, texture_data=I('cover_texture.png', texture_opacity=0.3, texture_data=I('cover_texture.png',
data=True)) data=True))
finally:
if cleanup:
os.remove(font_path)
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$') UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$')

View File

@ -60,6 +60,52 @@ class Fonts(object):
ans[ft] = (ext, name, open(f, 'rb').read()) ans[ft] = (ext, name, open(f, 'rb').read())
return ans 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() fontconfig = Fonts()
def test(): def test():

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import threading import threading, unicodedata
from functools import wraps from functools import wraps
from future_builtins import map from future_builtins import map
@ -20,6 +20,10 @@ class ThreadingViolation(Exception):
'You cannot use the MTP driver from a thread other than the ' 'You cannot use the MTP driver from a thread other than the '
' thread in which startup() was called') ' thread in which startup() was called')
def get_printable_characters(text):
return u''.join(x for x in unicodedata.normalize('NFC', text)
if unicodedata.category(x)[0] not in {'C', 'Z', 'M'})
def same_thread(func): def same_thread(func):
@wraps(func) @wraps(func)
def check_thread(self, *args, **kwargs): def check_thread(self, *args, **kwargs):
@ -28,6 +32,8 @@ def same_thread(func):
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return check_thread return check_thread
FreeTypeError = getattr(plugins['freetype'][0], 'FreeTypeError', Exception)
class Face(object): class Face(object):
def __init__(self, face): def __init__(self, face):
@ -42,9 +48,14 @@ class Face(object):
setattr(self, x, val) setattr(self, x, val)
@same_thread @same_thread
def supports_text(self, text): def supports_text(self, text, has_non_printable_chars=True):
'''
Returns True if all the characters in text have glyphs in this font.
'''
if not isinstance(text, unicode): if not isinstance(text, unicode):
raise TypeError('%r is not a unicode object'%text) raise TypeError('%r is not a unicode object'%text)
if has_non_printable_chars:
text = get_printable_characters(text)
chars = tuple(frozenset(map(ord, text))) chars = tuple(frozenset(map(ord, text)))
return self.face.supports_text(chars) return self.face.supports_text(chars)
@ -71,6 +82,17 @@ def test():
if font.supports_text('abc'): if font.supports_text('abc'):
raise RuntimeError('Incorrectly claiming that text is supported') raise RuntimeError('Incorrectly claiming that text is supported')
def test_find_font():
from calibre.utils.fonts import fontconfig
abcd = '诶比西迪'
family = fontconfig.find_font_for_text(abcd)[0]
print ('Family for Chinese text:', family)
family = fontconfig.find_font_for_text(abcd)[0]
abcd = 'لوحة المفاتيح العربية'
print ('Family for Arabic text:', family)
if __name__ == '__main__': if __name__ == '__main__':
test() test()
test_find_font()

View File

@ -38,8 +38,8 @@ def get_table(raw, name):
def get_font_characteristics(raw): def get_font_characteristics(raw):
''' '''
Return (weight, is_italic, is_bold, is_regular, fs_type). These values are taken Return (weight, is_italic, is_bold, is_regular, fs_type, panose). These
from the OS/2 table of the font. See values are taken from the OS/2 table of the font. See
http://www.microsoft.com/typography/otspec/os2.htm for details http://www.microsoft.com/typography/otspec/os2.htm for details
''' '''
os2_table = get_table(raw, 'os/2')[0] os2_table = get_table(raw, 'os/2')[0]
@ -54,7 +54,6 @@ def get_font_characteristics(raw):
family_class) = struct.unpack_from(common_fields, os2_table) family_class) = struct.unpack_from(common_fields, os2_table)
offset = struct.calcsize(common_fields) offset = struct.calcsize(common_fields)
panose = struct.unpack_from(b'>10B', os2_table, offset) panose = struct.unpack_from(b'>10B', os2_table, offset)
panose
offset += 10 offset += 10
(range1,) = struct.unpack_from(b'>L', os2_table, offset) (range1,) = struct.unpack_from(b'>L', os2_table, offset)
offset += struct.calcsize(b'>L') offset += struct.calcsize(b'>L')
@ -69,7 +68,21 @@ def get_font_characteristics(raw):
is_italic = (selection & 0b1) != 0 is_italic = (selection & 0b1) != 0
is_bold = (selection & 0b100000) != 0 is_bold = (selection & 0b100000) != 0
is_regular = (selection & 0b1000000) != 0 is_regular = (selection & 0b1000000) != 0
return weight, is_italic, is_bold, is_regular, fs_type return weight, is_italic, is_bold, is_regular, fs_type, panose
def panose_to_css_generic_family(panose):
proportion = panose[3]
if proportion == 9:
return 'monospace'
family_type = panose[0]
if family_type == 3:
return 'cursive'
if family_type == 4:
return 'fantasy'
serif_style = panose[1]
if serif_style in (11, 12, 13):
return 'sans-serif'
return 'serif'
def decode_name_record(recs): def decode_name_record(recs):
''' '''
@ -225,13 +238,33 @@ def remove_embed_restriction(raw):
verify_checksums(raw) verify_checksums(raw)
return raw return raw
def get_font_for_text(text, candidate_font_data=None):
ok = False
if candidate_font_data is not None:
from calibre.utils.fonts.free_type import FreeType, FreeTypeError
ft = FreeType()
try:
font = ft.load_font(candidate_font_data)
ok = font.supports_text(text)
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]
return candidate_font_data
def test(): def test():
import sys, os import sys, os
for f in sys.argv[1:]: for f in sys.argv[1:]:
print (os.path.basename(f)) print (os.path.basename(f))
raw = open(f, 'rb').read() raw = open(f, 'rb').read()
print (get_font_names(raw)) print (get_font_names(raw))
print (get_font_characteristics(raw)) characs = get_font_characteristics(raw)
print (characs)
print (panose_to_css_generic_family(characs[5]))
verify_checksums(raw) verify_checksums(raw)
remove_embed_restriction(raw) remove_embed_restriction(raw)

View File

@ -10,7 +10,7 @@ import os
from calibre.utils.magick import Image, DrawingWand, create_canvas from calibre.utils.magick import Image, DrawingWand, create_canvas
from calibre.constants import __appname__, __version__ from calibre.constants import __appname__, __version__
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre import fit_image from calibre import fit_image, force_unicode
def _data_to_image(data): def _data_to_image(data):
if isinstance(data, Image): if isinstance(data, Image):
@ -166,12 +166,9 @@ def add_borders_to_image(img_data, left=0, top=0, right=0, bottom=0,
return canvas.export(fmt) return canvas.export(fmt)
def create_text_wand(font_size, font_path=None): def create_text_wand(font_size, font_path=None):
if font_path is None:
font_path = tweaks['generate_cover_title_font']
if font_path is None:
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
ans = DrawingWand() ans = DrawingWand()
ans.font = font_path if font_path is not None:
ans.font = font_path
ans.font_size = font_size ans.font_size = font_size
ans.gravity = 'CenterGravity' ans.gravity = 'CenterGravity'
ans.text_alias = True ans.text_alias = True
@ -238,6 +235,17 @@ class TextLine(object):
def __init__(self, text, font_size, bottom_margin=30, font_path=None): def __init__(self, text, font_size, bottom_margin=30, font_path=None):
self.text, self.font_size, = text, font_size self.text, self.font_size, = text, font_size
self.bottom_margin = bottom_margin self.bottom_margin = bottom_margin
if font_path is None:
if not isinstance(text, unicode):
text = force_unicode(text)
from calibre.utils.fonts.utils import get_font_for_text
fd = get_font_for_text(text)
if fd is not None:
from calibre.ptempfile import PersistentTemporaryFile
pt = PersistentTemporaryFile('.ttf')
pt.write(fd)
pt.close()
font_path = pt.name
self.font_path = font_path self.font_path = font_path
def __repr__(self): def __repr__(self):