From 135123ad4662b990971da861481e5fb7e02861f0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 20 Oct 2012 17:14:17 +0530 Subject: [PATCH] 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 --- src/calibre/ebooks/__init__.py | 29 ++++++++++-- src/calibre/utils/fonts/__init__.py | 46 +++++++++++++++++++ .../utils/fonts/{freetype.py => free_type.py} | 26 ++++++++++- src/calibre/utils/fonts/utils.py | 43 +++++++++++++++-- src/calibre/utils/magick/draw.py | 20 +++++--- 5 files changed, 148 insertions(+), 16 deletions(-) rename src/calibre/utils/fonts/{freetype.py => free_type.py} (72%) diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index ee880000f0..9cf0e51e7e 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -178,18 +178,41 @@ def normalize(x): def calibre_cover(title, author_string, series_string=None, output_format='jpg', title_size=46, author_size=36, logo_path=None): + from calibre.utils.config_base import tweaks title = normalize(title) author_string = normalize(author_string) series_string = normalize(series_string) 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: - lines.append(TextLine(series_string, author_size)) + lines.append(TextLine(series_string, author_size, font_path=font_path)) if logo_path is None: 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', 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)$') diff --git a/src/calibre/utils/fonts/__init__.py b/src/calibre/utils/fonts/__init__.py index 45a665b75a..e245b1536c 100644 --- a/src/calibre/utils/fonts/__init__.py +++ b/src/calibre/utils/fonts/__init__.py @@ -60,6 +60,52 @@ class Fonts(object): 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(): diff --git a/src/calibre/utils/fonts/freetype.py b/src/calibre/utils/fonts/free_type.py similarity index 72% rename from src/calibre/utils/fonts/freetype.py rename to src/calibre/utils/fonts/free_type.py index ac5385ea98..a2e8eca213 100644 --- a/src/calibre/utils/fonts/freetype.py +++ b/src/calibre/utils/fonts/free_type.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import threading +import threading, unicodedata from functools import wraps from future_builtins import map @@ -20,6 +20,10 @@ class ThreadingViolation(Exception): 'You cannot use the MTP driver from a thread other than the ' ' 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): @wraps(func) def check_thread(self, *args, **kwargs): @@ -28,6 +32,8 @@ def same_thread(func): return func(self, *args, **kwargs) return check_thread +FreeTypeError = getattr(plugins['freetype'][0], 'FreeTypeError', Exception) + class Face(object): def __init__(self, face): @@ -42,9 +48,14 @@ class Face(object): setattr(self, x, val) @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): 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))) return self.face.supports_text(chars) @@ -71,6 +82,17 @@ def test(): if font.supports_text('abc'): 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__': test() + test_find_font() diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py index f20f238481..4fcaa20c44 100644 --- a/src/calibre/utils/fonts/utils.py +++ b/src/calibre/utils/fonts/utils.py @@ -38,8 +38,8 @@ def get_table(raw, name): def get_font_characteristics(raw): ''' - Return (weight, is_italic, is_bold, is_regular, fs_type). These values are taken - from the OS/2 table of the font. See + Return (weight, is_italic, is_bold, is_regular, fs_type, panose). 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] @@ -54,7 +54,6 @@ def get_font_characteristics(raw): family_class) = struct.unpack_from(common_fields, os2_table) offset = struct.calcsize(common_fields) panose = struct.unpack_from(b'>10B', os2_table, offset) - panose offset += 10 (range1,) = struct.unpack_from(b'>L', os2_table, offset) offset += struct.calcsize(b'>L') @@ -69,7 +68,21 @@ def get_font_characteristics(raw): 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 + 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): ''' @@ -225,13 +238,33 @@ def remove_embed_restriction(raw): verify_checksums(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(): import sys, os for f in sys.argv[1:]: print (os.path.basename(f)) raw = open(f, 'rb').read() 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) remove_embed_restriction(raw) diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index 046d0d5224..9d8cfdfcbf 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -10,7 +10,7 @@ import os from calibre.utils.magick import Image, DrawingWand, create_canvas from calibre.constants import __appname__, __version__ from calibre.utils.config import tweaks -from calibre import fit_image +from calibre import fit_image, force_unicode def _data_to_image(data): 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) 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.font = font_path + if font_path is not None: + ans.font = font_path ans.font_size = font_size ans.gravity = 'CenterGravity' ans.text_alias = True @@ -238,6 +235,17 @@ class TextLine(object): def __init__(self, text, font_size, bottom_margin=30, font_path=None): self.text, self.font_size, = text, font_size 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 def __repr__(self):