From b8371b94703bc129d67140fb27dbe80cc22d5a23 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 29 May 2010 17:54:46 -0600 Subject: [PATCH] Nicer default cover --- src/calibre/ebooks/oeb/transforms/cover.py | 50 +---- src/calibre/utils/PythonMagickWand.py | 27 ++- src/calibre/utils/magick_draw.py | 211 +++++++++++++++++++++ src/calibre/utils/pil_draw.py | 33 ---- src/calibre/web/feeds/news.py | 44 +---- 5 files changed, 242 insertions(+), 123 deletions(-) create mode 100644 src/calibre/utils/magick_draw.py delete mode 100644 src/calibre/utils/pil_draw.py diff --git a/src/calibre/ebooks/oeb/transforms/cover.py b/src/calibre/ebooks/oeb/transforms/cover.py index bd11a92af8..ecdc1294ad 100644 --- a/src/calibre/ebooks/oeb/transforms/cover.py +++ b/src/calibre/ebooks/oeb/transforms/cover.py @@ -15,7 +15,7 @@ try: except ImportError: import Image as PILImage -from calibre import __appname__, __version__, guess_type +from calibre import guess_type class CoverManager(object): @@ -89,7 +89,6 @@ class CoverManager(object): ''' Create a generic cover for books that dont have a cover ''' - from calibre.utils.pil_draw import draw_centered_text from calibre.ebooks.metadata import authors_to_string if self.no_default_cover: return None @@ -98,46 +97,15 @@ class CoverManager(object): title = unicode(m.title[0]) authors = [unicode(x) for x in m.creator if x.role == 'aut'] - cover_file = cStringIO.StringIO() try: - try: - from PIL import Image, ImageDraw, ImageFont - Image, ImageDraw, ImageFont - except ImportError: - import Image, ImageDraw, ImageFont - font_path = P('fonts/liberation/LiberationSerif-Bold.ttf') - app = '['+__appname__ +' '+__version__+']' - - COVER_WIDTH, COVER_HEIGHT = 590, 750 - img = Image.new('RGB', (COVER_WIDTH, COVER_HEIGHT), 'white') - draw = ImageDraw.Draw(img) - # Title - font = ImageFont.truetype(font_path, 44) - bottom = draw_centered_text(img, draw, font, title, 15, ysep=9) - # Authors - bottom += 14 - font = ImageFont.truetype(font_path, 32) - authors = authors_to_string(authors) - bottom = draw_centered_text(img, draw, font, authors, bottom, ysep=7) - # Vanity - font = ImageFont.truetype(font_path, 28) - width, height = draw.textsize(app, font=font) - left = max(int((COVER_WIDTH - width)/2.), 0) - top = COVER_HEIGHT - height - 15 - draw.text((left, top), app, fill=(0,0,0), font=font) - # Logo - logo = Image.open(I('library.png'), 'r') - width, height = logo.size - left = max(int((COVER_WIDTH - width)/2.), 0) - top = max(int((COVER_HEIGHT - height)/2.), 0) - img.paste(logo, (left, max(bottom, top))) - img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE) - - img.convert('RGB').save(cover_file, 'JPEG') - cover_file.flush() - id, href = self.oeb.manifest.generate('cover_image', 'cover_image.jpg') - item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0], - data=cover_file.getvalue()) + from calibre.utils.magick_draw import create_cover_page, TextLine + lines = [TextLine(title, 44), TextLine(authors_to_string(authors), + 32)] + img_data = create_cover_page(lines, I('library.png')) + id, href = self.oeb.manifest.generate('cover_image', + 'cover_image.png') + item = self.oeb.manifest.add(id, href, guess_type('t.png')[0], + data=img_data) m.clear('cover') m.add('cover', item.id) diff --git a/src/calibre/utils/PythonMagickWand.py b/src/calibre/utils/PythonMagickWand.py index 20f503bc22..cf9b5d167f 100644 --- a/src/calibre/utils/PythonMagickWand.py +++ b/src/calibre/utils/PythonMagickWand.py @@ -596,15 +596,22 @@ IndexChannel = ChannelType(32) AllChannels = ChannelType(255) DefaultChannels = ChannelType(247) -class DistortImageMethod(ctypes.c_int): pass -UndefinedDistortion = DistortImageMethod(0) -AffineDistortion = DistortImageMethod(1) -AffineProjectionDistortion = DistortImageMethod(2) -ArcDistortion = DistortImageMethod(3) -BilinearDistortion = DistortImageMethod(4) -PerspectiveDistortion = DistortImageMethod(5) -PerspectiveProjectionDistortion = DistortImageMethod(6) -ScaleRotateTranslateDistortion = DistortImageMethod(7) +UndefinedDistortion = 0 +AffineDistortion = 1 +AffineProjectionDistortion = 2 +ScaleRotateTranslateDistortion = 3 +PerspectiveDistortion = 4 +BilinearForwardDistortion = 5 +BilinearDistortion = 6 +BilinearReverseDistortion = 7 +PolynomialDistortion = 8 +ArcDistortion = 9 +PolarDistortion = 10 +DePolarDistortion = 11 +BarrelDistortion = 12 +BarrelInverseDistortion = 13 +ShepardsDistortion = 14 +SentinelDistortion = 15 class FillRule(ctypes.c_int): pass UndefinedRule = FillRule(0) @@ -2254,7 +2261,7 @@ else: # MagickDistortImage try: _magick.MagickDistortImage.restype = MagickBooleanType - _magick.MagickDistortImage.argtypes = (MagickWand,DistortImageMethod,ctypes.c_ulong,ctypes.POINTER(ctypes.c_double),MagickBooleanType) + _magick.MagickDistortImage.argtypes = (MagickWand,ctypes.c_int,ctypes.c_ulong,ctypes.POINTER(ctypes.c_double),MagickBooleanType) except AttributeError,e: pass else: diff --git a/src/calibre/utils/magick_draw.py b/src/calibre/utils/magick_draw.py new file mode 100644 index 0000000000..0288107b45 --- /dev/null +++ b/src/calibre/utils/magick_draw.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from ctypes import byref, c_double + +import calibre.utils.PythonMagickWand as p +from calibre.ptempfile import TemporaryFile +from calibre.constants import filesystem_encoding, __appname__, __version__ + +# Font metrics {{{ +class Rect(object): + + def __init__(self, left, top, right, bottom): + self.left, self.top, self.right, self.bottom = left, top, right, bottom + + def __str__(self): + return '(%s, %s) -- (%s, %s)'%(self.left, self.top, self.right, + self.bottom) + +class FontMetrics(object): + + def __init__(self, ret): + self._attrs = [] + for i, x in enumerate(('char_width', 'char_height', 'ascender', + 'descender', 'text_width', 'text_height', + 'max_horizontal_advance')): + setattr(self, x, ret[i]) + self._attrs.append(x) + self.bounding_box = Rect(ret[7], ret[8], ret[9], ret[10]) + self.x, self.y = ret[11], ret[12] + self._attrs.extend(['bounding_box', 'x', 'y']) + self._attrs = tuple(self._attrs) + + def __str__(self): + return '''FontMetrics: + char_width: %s + char_height: %s + ascender: %s + descender: %s + text_width: %s + text_height: %s + max_horizontal_advance: %s + bounding_box: %s + x: %s + y: %s + '''%tuple([getattr(self, x) for x in self._attrs]) + + +def get_font_metrics(image, d_wand, text): + ret = p.MagickQueryFontMetrics(image, d_wand, text) + return FontMetrics(ret) + +# }}} + +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 + self.font_path = font_path + +def alloc_wand(name): + ans = getattr(p, name)() + if ans < 0: + raise RuntimeError('Cannot create wand') + return ans + +def create_text_wand(font_size, font_path=None): + if font_path is None: + font_path = P('fonts/liberation/LiberationSerif-Bold.ttf') + if isinstance(font_path, unicode): + font_path = font_path.encode(filesystem_encoding) + ans = alloc_wand('NewDrawingWand') + if not p.DrawSetFont(ans, font_path): + raise ValueError('Failed to set font to: '+font_path) + p.DrawSetFontSize(ans, font_size) + p.DrawSetGravity(ans, p.CenterGravity) + p.DrawSetTextAntialias(ans, p.MagickTrue) + return ans + + +def _get_line(img, dw, tokens, line_width): + line, rest = tokens, [] + while True: + m = get_font_metrics(img, dw, ' '.join(line)) + width, height = m.text_width, m.text_height + if width < line_width: + return line, rest + rest = line[-1:] + rest + line = line[:-1] + +def annotate_img(img, dw, left, top, rotate, text, + translate_from_top_left=True): + if isinstance(text, unicode): + text = text.encode('utf-8') + if translate_from_top_left: + m = get_font_metrics(img, dw, text) + img_width = p.MagickGetImageWidth(img) + img_height = p.MagickGetImageHeight(img) + left = left - img_width/2. + m.text_width/2. + top = top - img_height/2. + m.text_height/2. + p.MagickAnnotateImage(img, dw, left, top, rotate, text) + +def draw_centered_line(img, dw, line, top): + m = get_font_metrics(img, dw, line) + width, height = m.text_width, m.text_height + img_width = p.MagickGetImageWidth(img) + left = max(int((img_width - width)/2.), 0) + annotate_img(img, dw, left, top, 0, line) + return top + height + +def draw_centered_text(img, dw, text, top, margin=10): + img_width = p.MagickGetImageWidth(img) + tokens = text.split(' ') + while tokens: + line, tokens = _get_line(img, dw, tokens, img_width-2*margin) + bottom = draw_centered_line(img, dw, ' '.join(line), top) + top = bottom + return top + +def create_canvas(width, height, bgcolor): + canvas = alloc_wand('NewMagickWand') + p_wand = alloc_wand('NewPixelWand') + p.PixelSetColor(p_wand, bgcolor) + p.MagickNewImage(canvas, width, height, p_wand) + p.DestroyPixelWand(p_wand) + return canvas + +def compose_image(canvas, image, left, top): + p.MagickCompositeImage(canvas, image, p.OverCompositeOp, int(left), + int(top)) + +def load_image(path): + img = alloc_wand('NewMagickWand') + if not p.MagickReadImage(img, path): + severity = p.ExceptionType(0) + msg = p.MagickGetException(img, byref(severity)) + raise IOError('Failed to read image from: %s: %s' + %(path, msg)) + return img + +def create_text_arc(text, font_size, font=None, bgcolor='white'): + if isinstance(text, unicode): + text = text.encode('utf-8') + + canvas = create_canvas(300, 300, bgcolor) + tw = create_text_wand(font_size, font_path=font) + m = get_font_metrics(canvas, tw, text) + p.DestroyMagickWand(canvas) + canvas = create_canvas(int(m.text_width)+20, int(m.text_height*3.5), bgcolor) + p.MagickAnnotateImage(canvas, tw, 0, 0, 0, text) + angle = c_double(120.) + p.MagickDistortImage(canvas, 9, 1, byref(angle), + p.MagickTrue) + p.MagickTrimImage(canvas, 0) + return canvas + + +def create_cover_page(top_lines, logo_path, width=590, height=750, + bgcolor='white', output_format='png'): + ans = None + with p.ImageMagick(): + canvas = create_canvas(width, height, bgcolor) + + bottom = 10 + for line in top_lines: + twand = create_text_wand(line.font_size, font_path=line.font_path) + bottom = draw_centered_text(canvas, twand, line.text, bottom) + bottom += line.bottom_margin + p.DestroyDrawingWand(twand) + bottom -= top_lines[-1].bottom_margin + + vanity = create_text_arc(__appname__ + ' ' + __version__, 24, + font=P('fonts/liberation/LiberationMono-Regular.ttf')) + lwidth = p.MagickGetImageWidth(vanity) + lheight = p.MagickGetImageHeight(vanity) + left = int(max(0, (width - lwidth)/2.)) + top = height - lheight - 10 + compose_image(canvas, vanity, left, top) + + logo = load_image(logo_path) + lwidth = p.MagickGetImageWidth(logo) + lheight = p.MagickGetImageHeight(logo) + left = int(max(0, (width - lwidth)/2.)) + top = max(int((height - lheight)/2.), bottom+20) + compose_image(canvas, logo, left, top) + p.DestroyMagickWand(logo) + + with TemporaryFile('.'+output_format) as f: + p.MagickWriteImage(canvas, f) + with open(f, 'rb') as f: + ans = f.read() + p.DestroyMagickWand(canvas) + return ans + +def test(): + import subprocess + with TemporaryFile('.png') as f: + data = create_cover_page( + [TextLine('A very long title indeed, don\'t you agree?', 42), + TextLine('Mad Max & Mixy poo', 32)], I('library.png')) + with open(f, 'wb') as g: + g.write(data) + subprocess.check_call(['gwenview', f]) + +if __name__ == '__main__': + test() diff --git a/src/calibre/utils/pil_draw.py b/src/calibre/utils/pil_draw.py deleted file mode 100644 index 66a483e75c..0000000000 --- a/src/calibre/utils/pil_draw.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -from __future__ import with_statement - -__license__ = 'GPL v3' -__copyright__ = '2010, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -def _get_line(draw, font, tokens, line_width): - line, rest = tokens, [] - while True: - width, height = draw.textsize(' '.join(line), font=font) - if width < line_width: - return line, rest - rest = line[-1:] + rest - line = line[:-1] - -def draw_centered_line(img, draw, font, line, top): - width, height = draw.textsize(line, font=font) - left = max(int((img.size[0] - width)/2.), 0) - draw.text((left, top), line, fill=(0,0,0), font=font) - return top + height - -def draw_centered_text(img, draw, font, text, top, margin=10, ysep=5): - img_width, img_height = img.size - tokens = text.split(' ') - while tokens: - line, tokens = _get_line(draw, font, tokens, img_width-2*margin) - bottom = draw_centered_line(img, draw, font, ' '.join(line), top) - top = bottom + ysep - return top - ysep - - diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index db4ce3fda3..26b3ad0593 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -14,7 +14,7 @@ from contextlib import nested, closing from calibre import browser, __appname__, iswindows, \ - strftime, __version__, preferred_encoding + strftime, preferred_encoding from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag from calibre.ebooks.metadata.opf2 import OPFCreator from calibre import entity_to_unicode @@ -949,47 +949,13 @@ class BasicNewsRecipe(Recipe): Create a generic cover for recipes that dont have a cover ''' try: - try: - from PIL import Image, ImageDraw, ImageFont - Image, ImageDraw, ImageFont - except ImportError: - import Image, ImageDraw, ImageFont - font_path = P('fonts/liberation/LiberationSerif-Bold.ttf') + from calibre.utils.magick_draw import create_cover_page, TextLine title = self.title if isinstance(self.title, unicode) else \ self.title.decode(preferred_encoding, 'replace') date = strftime(self.timefmt) - app = '['+__appname__ +' '+__version__+']' - - COVER_WIDTH, COVER_HEIGHT = 590, 750 - img = Image.new('RGB', (COVER_WIDTH, COVER_HEIGHT), 'white') - draw = ImageDraw.Draw(img) - # Title - font = ImageFont.truetype(font_path, 44) - width, height = draw.textsize(title, font=font) - left = max(int((COVER_WIDTH - width)/2.), 0) - top = 15 - draw.text((left, top), title, fill=(0,0,0), font=font) - bottom = top + height - # Date - font = ImageFont.truetype(font_path, 32) - width, height = draw.textsize(date, font=font) - left = max(int((COVER_WIDTH - width)/2.), 0) - draw.text((left, bottom+15), date, fill=(0,0,0), font=font) - # Vanity - font = ImageFont.truetype(font_path, 28) - width, height = draw.textsize(app, font=font) - left = max(int((COVER_WIDTH - width)/2.), 0) - top = COVER_HEIGHT - height - 15 - draw.text((left, top), app, fill=(0,0,0), font=font) - # Logo - logo = Image.open(I('library.png'), 'r') - width, height = logo.size - left = max(int((COVER_WIDTH - width)/2.), 0) - top = max(int((COVER_HEIGHT - height)/2.), 0) - img.paste(logo, (left, top)) - img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE) - - img.convert('RGB').save(cover_file, 'JPEG') + lines = [TextLine(title, 44), TextLine(date, 32)] + img_data = create_cover_page(lines, I('library.png'), output_format='jpg') + cover_file.write(img_data) cover_file.flush() except: self.log.exception('Failed to generate default cover')