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