From 2ddd2c1c76f173d27f34d73f00aa659af539fa95 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 21 May 2010 14:34:12 -0600 Subject: [PATCH] EPUB Output: Add option to toggle preserving the aspect ratio of the cover. The default has changed to not preserving it. PDF Output: Set the first page to the cover. Fixes #5581 (Economist (payed Edition) title image) --- src/calibre/ebooks/epub/output.py | 292 +++++++++++++----------- src/calibre/ebooks/pdf/output.py | 37 ++- src/calibre/ebooks/pdf/writer.py | 119 +++++----- src/calibre/gui2/convert/epub_output.py | 5 +- src/calibre/gui2/convert/epub_output.ui | 56 +++-- 5 files changed, 305 insertions(+), 204 deletions(-) diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py index 129a63ef3c..71d9d8b423 100644 --- a/src/calibre/ebooks/epub/output.py +++ b/src/calibre/ebooks/epub/output.py @@ -46,8 +46,155 @@ block_level_tags = ( 'ul', ) +class CoverManager(object): -class EPUBOutput(OutputFormatPlugin): + ''' + Manage the cover in the output document. Requires the opts object to have + the attributes: + + no_svg_cover + no_default_epub_cover + preserve_cover_aspect_ratio + ''' + + NONSVG_TITLEPAGE_COVER = '''\ + + + + + Cover + + + +
+ cover +
+ + + ''' + + TITLEPAGE_COVER = '''\ + + + + + Cover + + + + + + + + +''' + + def default_cover(self): + ''' + 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.opts.no_default_epub_cover: + return None + self.log('Generating default cover') + m = self.oeb.metadata + title = unicode(m.title[0]) + authors = [unicode(x) for x in m.creator if x.role == 'aut'] + + import cStringIO + 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()) + m.clear('cover') + m.add('cover', item.id) + + return item.href + except: + self.log.exception('Failed to generate default cover') + return None + + + def insert_cover(self): + from calibre.ebooks.oeb.base import urldefrag + from calibre import guess_type + g, m = self.oeb.guide, self.oeb.manifest + item = None + ar = 'xMidYMid meet' if self.opts.preserve_cover_aspect_ratio else \ + 'none' + svg_template = self.TITLEPAGE_COVER.replace('__ar__', ar) + if 'titlepage' not in g: + if 'cover' in g: + href = g['cover'].href + else: + href = self.default_cover() + if href is not None: + templ = self.NONSVG_TITLEPAGE_COVER if self.opts.no_svg_cover \ + else svg_template + tp = templ%unquote(href) + id, href = m.generate('titlepage', 'titlepage.xhtml') + item = m.add(id, href, guess_type('t.xhtml')[0], + data=etree.fromstring(tp)) + else: + item = self.oeb.manifest.hrefs[ + urldefrag(self.oeb.guide['titlepage'].href)[0]] + if item is not None: + self.oeb.spine.insert(0, item, True) + if 'cover' not in self.oeb.guide.refs: + self.oeb.guide.add('cover', 'Title Page', 'a') + self.oeb.guide.refs['cover'].href = item.href + if 'titlepage' in self.oeb.guide.refs: + self.oeb.guide.refs['titlepage'].href = item.href + + +class EPUBOutput(OutputFormatPlugin, CoverManager): name = 'EPUB Output' author = 'Kovid Goyal' @@ -92,51 +239,21 @@ class EPUBOutput(OutputFormatPlugin): 'as a blank page.') ), + OptionRecommendation(name='preserve_cover_aspect_ratio', + recommended_value=False, help=_( + 'When using an SVG cover, this option will cause the cover to scale ' + 'to cover the available screen area, but still preserve its aspect ratio ' + '(ratio of width to height). That means there may be white borders ' + 'at the sides or top and bottom of the image, but the image will ' + 'never be distorted. Without this option the image may be slightly ' + 'distorted, but there will be no borders.' + ) + ), + ]) recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)]) - NONSVG_TITLEPAGE_COVER = '''\ - - - - - Cover - - - -
- cover -
- - - ''' - - TITLEPAGE_COVER = '''\ - - - - - Cover - - - - - - - - -''' def workaround_webkit_quirks(self): from calibre.ebooks.oeb.base import XPath @@ -259,97 +376,6 @@ class EPUBOutput(OutputFormatPlugin): ans += '\n' return ans - def default_cover(self): - ''' - 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.opts.no_default_epub_cover: - return None - self.log('Generating default cover') - m = self.oeb.metadata - title = unicode(m.title[0]) - authors = [unicode(x) for x in m.creator if x.role == 'aut'] - - import cStringIO - 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()) - m.clear('cover') - m.add('cover', item.id) - - return item.href - except: - self.log.exception('Failed to generate default cover') - return None - - - def insert_cover(self): - from calibre.ebooks.oeb.base import urldefrag - from calibre import guess_type - g, m = self.oeb.guide, self.oeb.manifest - item = None - if 'titlepage' not in g: - if 'cover' in g: - href = g['cover'].href - else: - href = self.default_cover() - if href is not None: - templ = self.NONSVG_TITLEPAGE_COVER if self.opts.no_svg_cover \ - else self.TITLEPAGE_COVER - tp = templ%unquote(href) - id, href = m.generate('titlepage', 'titlepage.xhtml') - item = m.add(id, href, guess_type('t.xhtml')[0], - data=etree.fromstring(tp)) - else: - item = self.oeb.manifest.hrefs[ - urldefrag(self.oeb.guide['titlepage'].href)[0]] - if item is not None: - self.oeb.spine.insert(0, item, True) - if 'cover' not in self.oeb.guide.refs: - self.oeb.guide.add('cover', 'Title Page', 'a') - self.oeb.guide.refs['cover'].href = item.href - if 'titlepage' in self.oeb.guide.refs: - self.oeb.guide.refs['titlepage'].href = item.href - def condense_ncx(self, ncx_path): if not self.opts.pretty_print: tree = etree.parse(ncx_path) diff --git a/src/calibre/ebooks/pdf/output.py b/src/calibre/ebooks/pdf/output.py index b2d649c2cf..e302f67441 100644 --- a/src/calibre/ebooks/pdf/output.py +++ b/src/calibre/ebooks/pdf/output.py @@ -15,11 +15,39 @@ from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation from calibre.ebooks.metadata.opf2 import OPF from calibre.ptempfile import TemporaryDirectory -from calibre.ebooks.pdf.writer import PDFWriter, ImagePDFWriter, PDFMetadata +from calibre.ebooks.pdf.writer import PDFWriter, ImagePDFWriter, PDFMetadata, \ + get_pdf_page_size from calibre.ebooks.pdf.pageoptions import UNITS, PAPER_SIZES, \ ORIENTATIONS +from calibre.ebooks.epub.output import CoverManager -class PDFOutput(OutputFormatPlugin): +class CoverManagerPDF(CoverManager): + + def setup_cover(self, opts): + width, height = get_pdf_page_size(opts) + factor = opts.output_profile.dpi + self.NONSVG_TITLEPAGE_COVER = '''\ + + + + + Cover + + + +
+ cover +
+ + + '''%(int(width*factor), int(height*factor)-5) + + +class PDFOutput(OutputFormatPlugin, CoverManagerPDF): name = 'PDF Output' author = 'John Schember' @@ -47,6 +75,7 @@ class PDFOutput(OutputFormatPlugin): ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): + self.oeb = oeb_book self.input_plugin, self.opts, self.log = input_plugin, opts, log self.output_path = output_path self.metadata = oeb_book.metadata @@ -63,6 +92,10 @@ class PDFOutput(OutputFormatPlugin): def convert_text(self, oeb_book): self.log.debug('Serializing oeb input to disk for processing...') + self.opts.no_svg_cover = True + self.opts.no_default_epub_cover = True + self.setup_cover(self.opts) + self.insert_cover() with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format oeb_output = plugin_for_output_format('oeb') diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index 9b5094ac95..22e653f275 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -18,11 +18,70 @@ from calibre.ebooks.metadata import authors_to_string from PyQt4 import QtCore from PyQt4.Qt import QUrl, QEventLoop, SIGNAL, QObject, \ - QPrinter, QMetaObject, QSizeF, Qt + QPrinter, QMetaObject, QSizeF, Qt, QPainter from PyQt4.QtWebKit import QWebView from pyPdf import PdfFileWriter, PdfFileReader +def get_custom_size(opts): + custom_size = None + if opts.custom_size != None: + width, sep, height = opts.custom_size.partition('x') + if height != '': + try: + width = int(width) + height = int(height) + custom_size = (width, height) + except: + custom_size = None + return custom_size + +def get_pdf_page_size(opts): + from calibre.gui2 import is_ok_to_use_qt + if not is_ok_to_use_qt(): + raise Exception('Not OK to use Qt') + + printer = QPrinter(QPrinter.HighResolution) + custom_size = get_custom_size(opts) + + if opts.output_profile.short_name == 'default': + if custom_size is None: + printer.setPaperSize(paper_size(opts.paper_size)) + else: + printer.setPaperSize(QSizeF(custom_size[0], custom_size[1]), unit(opts.unit)) + else: + printer.setPaperSize(QSizeF(opts.output_profile.width / opts.output_profile.dpi, + opts.output_profile.height / opts.output_profile.dpi), QPrinter.Inch) + + printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) + printer.setOrientation(orientation(opts.orientation)) + printer.setOutputFormat(QPrinter.PdfFormat) + + size = printer.paperSize(QPrinter.Millimeter) + + return size.width() / 10, size.height() / 10 + +def get_imagepdf_page_size(opts): + printer = QPrinter(QPrinter.HighResolution) + custom_size = get_custom_size(opts) + + if opts.output_profile.short_name == 'default': + if custom_size == None: + printer.setPaperSize(paper_size(opts.paper_size)) + else: + printer.setPaperSize(QSizeF(custom_size[0], custom_size[1]), unit(opts.unit)) + else: + printer.setPaperSize(QSizeF(opts.output_profile.comic_screen_size[0] / opts.output_profile.dpi, + opts.output_profile.comic_screen_size[1] / opts.output_profile.dpi), QPrinter.Inch) + + printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) + printer.setOrientation(orientation(opts.orientation)) + printer.setOutputFormat(QPrinter.PdfFormat) + + size = printer.paperSize(QPrinter.Millimeter) + + return size.width() / 10, size.height() / 10 + class PDFMetadata(object): def __init__(self, oeb_metadata=None): self.title = _('Unknown') @@ -36,6 +95,7 @@ class PDFMetadata(object): class PDFWriter(QObject): + def __init__(self, opts, log): from calibre.gui2 import is_ok_to_use_qt if not is_ok_to_use_qt(): @@ -46,25 +106,15 @@ class PDFWriter(QObject): self.loop = QEventLoop() self.view = QWebView() + self.view.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform) self.connect(self.view, SIGNAL('loadFinished(bool)'), self._render_html) self.render_queue = [] self.combine_queue = [] self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts') - self.custom_size = None - if opts.custom_size != None: - width, sep, height = opts.custom_size.partition('x') - if height != '': - try: - width = int(width) - height = int(height) - self.custom_size = (width, height) - except: - self.custom_size = None - self.opts = opts - self.size = self._size() + self.size = get_pdf_page_size(opts) def dump(self, items, out_stream, pdf_metadata): self.metadata = pdf_metadata @@ -77,27 +127,6 @@ class PDFWriter(QObject): QMetaObject.invokeMethod(self, "_render_book", Qt.QueuedConnection) self.loop.exec_() - def _size(self): - ''' - The size of a pdf page in cm. - ''' - printer = QPrinter(QPrinter.HighResolution) - - if self.opts.output_profile.short_name == 'default': - if self.custom_size == None: - printer.setPaperSize(paper_size(self.opts.paper_size)) - else: - printer.setPaperSize(QSizeF(self.custom_size[0], self.custom_size[1]), unit(self.opts.unit)) - else: - printer.setPaperSize(QSizeF(self.opts.output_profile.width / self.opts.output_profile.dpi, self.opts.output_profile.height / self.opts.output_profile.dpi), QPrinter.Inch) - - printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) - printer.setOrientation(orientation(self.opts.orientation)) - printer.setOutputFormat(QPrinter.PdfFormat) - - size = printer.paperSize(QPrinter.Millimeter) - - return size.width() / 10, size.height() / 10 @QtCore.pyqtSignature('_render_book()') def _render_book(self): @@ -151,6 +180,10 @@ class PDFWriter(QObject): class ImagePDFWriter(PDFWriter): + def __init__(self, opts, log): + PDFWriter.__init__(self, opts, log) + self.size = get_imagepdf_page_size(opts) + def _render_next(self): item = str(self.render_queue.pop(0)) self.combine_queue.append(os.path.join(self.tmp_path, '%i.pdf' % (len(self.combine_queue) + 1))) @@ -163,22 +196,4 @@ class ImagePDFWriter(PDFWriter): self.view.setHtml(html) - def _size(self): - printer = QPrinter(QPrinter.HighResolution) - - if self.opts.output_profile.short_name == 'default': - if self.custom_size == None: - printer.setPaperSize(paper_size(self.opts.paper_size)) - else: - printer.setPaperSize(QSizeF(self.custom_size[0], self.custom_size[1]), unit(self.opts.unit)) - else: - printer.setPaperSize(QSizeF(self.opts.output_profile.comic_screen_size[0] / self.opts.output_profile.dpi, self.opts.output_profile.comic_screen_size[1] / self.opts.output_profile.dpi), QPrinter.Inch) - - printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) - printer.setOrientation(orientation(self.opts.orientation)) - printer.setOutputFormat(QPrinter.PdfFormat) - - size = printer.paperSize(QPrinter.Millimeter) - - return size.width() / 10, size.height() / 10 diff --git a/src/calibre/gui2/convert/epub_output.py b/src/calibre/gui2/convert/epub_output.py index 57027d9315..8130b00273 100644 --- a/src/calibre/gui2/convert/epub_output.py +++ b/src/calibre/gui2/convert/epub_output.py @@ -18,8 +18,11 @@ class PluginWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, 'epub_output', ['dont_split_on_page_breaks', 'flow_size', - 'no_default_epub_cover', 'no_svg_cover'] + 'no_default_epub_cover', 'no_svg_cover', + 'preserve_cover_aspect_ratio',] ) + for i in range(2): + self.opt_no_svg_cover.toggle() self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/epub_output.ui b/src/calibre/gui2/convert/epub_output.ui index 7f92ec3087..abca2405e8 100644 --- a/src/calibre/gui2/convert/epub_output.ui +++ b/src/calibre/gui2/convert/epub_output.ui @@ -14,13 +14,34 @@ Form - + Do not &split on page breaks + + + + No default &cover + + + + + + + No &SVG cover + + + + + + + Preserve cover &aspect ratio + + + @@ -60,22 +81,25 @@ - - - - No default &cover - - - - - - - No &SVG cover - - - - + + + opt_no_svg_cover + toggled(bool) + opt_preserve_cover_aspect_ratio + setDisabled(bool) + + + 81 + 73 + + + 237 + 68 + + + +