mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
407cd60dda
commit
135123ad46
@ -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')
|
||||
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)$')
|
||||
|
||||
|
@ -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():
|
||||
|
@ -7,7 +7,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,11 +166,8 @@ 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()
|
||||
if font_path is not None:
|
||||
ans.font = font_path
|
||||
ans.font_size = font_size
|
||||
ans.gravity = 'CenterGravity'
|
||||
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user