PDF Output: Improved cover and comic handling

This commit is contained in:
Kovid Goyal 2010-05-22 17:35:32 -06:00
parent 16fd984fdc
commit bfef38f5bf
4 changed files with 144 additions and 91 deletions

View File

@ -15,42 +15,14 @@ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory 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, \ from calibre.ebooks.pdf.pageoptions import UNITS, PAPER_SIZES, \
ORIENTATIONS ORIENTATIONS
from calibre.ebooks.epub.output import CoverManager
class CoverManagerPDF(CoverManager): class PDFOutput(OutputFormatPlugin):
def setup_cover(self, opts):
width, height = get_pdf_page_size(opts)
factor = opts.output_profile.dpi
self.NONSVG_TITLEPAGE_COVER = '''\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="calibre:cover" content="true" />
<title>Cover</title>
<style type="text/css" title="override_css">
@page {padding: 0pt; margin:0pt}
body { text-align: center; padding:0pt; margin: 0pt; }
div { padding:0pt; margin: 0pt; }
</style>
</head>
<body>
<div>
<img src="%%s" alt="cover" width="%d" height="%d" />
</div>
</body>
</html>
'''%(int(width*factor), int(height*factor)-5)
class PDFOutput(OutputFormatPlugin, CoverManagerPDF):
name = 'PDF Output' name = 'PDF Output'
author = 'John Schember' author = 'John Schember and Kovid Goyal'
file_type = 'pdf' file_type = 'pdf'
options = set([ options = set([
@ -72,6 +44,12 @@ class PDFOutput(OutputFormatPlugin, CoverManagerPDF):
level=OptionRecommendation.LOW, choices=ORIENTATIONS.keys(), level=OptionRecommendation.LOW, choices=ORIENTATIONS.keys(),
help=_('The orientation of the page. Default is portrait. Choices ' help=_('The orientation of the page. Default is portrait. Choices '
'are %s') % ORIENTATIONS.keys()), 'are %s') % ORIENTATIONS.keys()),
OptionRecommendation(name='preserve_cover_aspect_ratio',
recommended_value=False,
help=_('Preserve the aspect ratio of the cover, instead'
' of stretching it to fill the ull first page of the'
' generated pdf.')
),
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):
@ -79,6 +57,7 @@ class PDFOutput(OutputFormatPlugin, CoverManagerPDF):
self.input_plugin, self.opts, self.log = input_plugin, opts, log self.input_plugin, self.opts, self.log = input_plugin, opts, log
self.output_path = output_path self.output_path = output_path
self.metadata = oeb_book.metadata self.metadata = oeb_book.metadata
self.cover_data = None
if input_plugin.is_image_collection: if input_plugin.is_image_collection:
log.debug('Converting input as an image collection...') log.debug('Converting input as an image collection...')
@ -90,13 +69,20 @@ class PDFOutput(OutputFormatPlugin, CoverManagerPDF):
def convert_images(self, images): def convert_images(self, images):
self.write(ImagePDFWriter, images) self.write(ImagePDFWriter, images)
def get_cover_data(self):
g, m = self.oeb.guide, self.oeb.manifest
if 'titlepage' not in g:
if 'cover' in g:
href = g['cover'].href
from calibre.ebooks.oeb.base import urlnormalize
for item in m:
if item.href == urlnormalize(href):
self.cover_data = item.data
def convert_text(self, oeb_book): def convert_text(self, oeb_book):
self.log.debug('Serializing oeb input to disk for processing...') self.log.debug('Serializing oeb input to disk for processing...')
self.opts.no_svg_cover = True self.get_cover_data()
self.opts.no_default_epub_cover = True
self.opts.preserve_cover_aspect_ratio = False
self.setup_cover(self.opts)
self.insert_cover()
with TemporaryDirectory('_pdf_out') as oeb_dir: with TemporaryDirectory('_pdf_out') as oeb_dir:
from calibre.customize.ui import plugin_for_output_format from calibre.customize.ui import plugin_for_output_format
oeb_output = plugin_for_output_format('oeb') oeb_output = plugin_for_output_format('oeb')
@ -108,7 +94,7 @@ class PDFOutput(OutputFormatPlugin, CoverManagerPDF):
self.write(PDFWriter, [s.path for s in opf.spine]) self.write(PDFWriter, [s.path for s in opf.spine])
def write(self, Writer, items): def write(self, Writer, items):
writer = Writer(self.opts, self.log) writer = Writer(self.opts, self.log, cover_data=self.cover_data)
close = False close = False
if not hasattr(self.output_path, 'write'): if not hasattr(self.output_path, 'write'):

View File

@ -15,14 +15,20 @@ from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.pdf.pageoptions import unit, paper_size, \ from calibre.ebooks.pdf.pageoptions import unit, paper_size, \
orientation orientation
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.ptempfile import PersistentTemporaryFile
from calibre import __appname__, __version__, fit_image
from PyQt4 import QtCore from PyQt4 import QtCore
from PyQt4.Qt import QUrl, QEventLoop, SIGNAL, QObject, \ from PyQt4.Qt import QUrl, QEventLoop, QObject, \
QPrinter, QMetaObject, QSizeF, Qt, QPainter QPrinter, QMetaObject, QSizeF, Qt, QPainter, QPixmap
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from pyPdf import PdfFileWriter, PdfFileReader from pyPdf import PdfFileWriter, PdfFileReader
def get_pdf_printer():
return QPrinter(QPrinter.HighResolution)
def get_custom_size(opts): def get_custom_size(opts):
custom_size = None custom_size = None
if opts.custom_size != None: if opts.custom_size != None:
@ -36,12 +42,12 @@ def get_custom_size(opts):
custom_size = None custom_size = None
return custom_size return custom_size
def get_pdf_page_size(opts): def setup_printer(opts, for_comic=False):
from calibre.gui2 import is_ok_to_use_qt from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt(): if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt') raise Exception('Not OK to use Qt')
printer = QPrinter(QPrinter.HighResolution) printer = get_pdf_printer()
custom_size = get_custom_size(opts) custom_size = get_custom_size(opts)
if opts.output_profile.short_name == 'default': if opts.output_profile.short_name == 'default':
@ -50,37 +56,41 @@ def get_pdf_page_size(opts):
else: else:
printer.setPaperSize(QSizeF(custom_size[0], custom_size[1]), unit(opts.unit)) printer.setPaperSize(QSizeF(custom_size[0], custom_size[1]), unit(opts.unit))
else: else:
printer.setPaperSize(QSizeF(opts.output_profile.width / opts.output_profile.dpi, w = opts.output_profile.comic_screen_size[0] if for_comic else \
opts.output_profile.height / opts.output_profile.dpi), QPrinter.Inch) opts.output_profile.width
h = opts.output_profile.comic_screen_size[1] if for_comic else \
opts.output_profile.height
dpi = opts.output_profile.dpi
printer.setPaperSize(QSizeF(float(w) / dpi, float(h)/dpi), QPrinter.Inch)
printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) printer.setPageMargins(0, 0, 0, 0, QPrinter.Point)
printer.setOrientation(orientation(opts.orientation)) printer.setOrientation(orientation(opts.orientation))
printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFormat(QPrinter.PdfFormat)
return printer
def get_printer_page_size(opts, for_comic=False):
printer = setup_printer(opts, for_comic=for_comic)
size = printer.paperSize(QPrinter.Millimeter) size = printer.paperSize(QPrinter.Millimeter)
return size.width() / 10., size.height() / 10.
return size.width() / 10, size.height() / 10 def draw_image_page(printer, painter, p, preserve_aspect_ratio=True):
page_rect = printer.pageRect()
if preserve_aspect_ratio:
aspect_ratio = float(p.width())/p.height()
nw, nh = page_rect.width(), page_rect.height()
if aspect_ratio > 1:
nh = int(page_rect.width()/aspect_ratio)
else: # Width is smaller than height
nw = page_rect.height()*aspect_ratio
__, nnw, nnh = fit_image(nw, nh, page_rect.width(),
page_rect.height())
dx = int((page_rect.width() - nnw)/2.)
dy = int((page_rect.height() - nnh)/2.)
page_rect.moveTo(dx, dy)
page_rect.setHeight(nnh)
page_rect.setWidth(nnw)
painter.drawPixmap(page_rect, p, p.rect())
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): class PDFMetadata(object):
def __init__(self, oeb_metadata=None): def __init__(self, oeb_metadata=None):
@ -94,9 +104,9 @@ class PDFMetadata(object):
self.author = authors_to_string([x.value for x in oeb_metadata.creator]) self.author = authors_to_string([x.value for x in oeb_metadata.creator])
class PDFWriter(QObject): class PDFWriter(QObject): # {{{
def __init__(self, opts, log): def __init__(self, opts, log, cover_data=None):
from calibre.gui2 import is_ok_to_use_qt from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt(): if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt') raise Exception('Not OK to use Qt')
@ -107,14 +117,15 @@ class PDFWriter(QObject):
self.loop = QEventLoop() self.loop = QEventLoop()
self.view = QWebView() self.view = QWebView()
self.view.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform) self.view.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform)
self.connect(self.view, SIGNAL('loadFinished(bool)'), self._render_html) self.view.loadFinished.connect(self._render_html,
type=Qt.QueuedConnection)
self.render_queue = [] self.render_queue = []
self.combine_queue = [] self.combine_queue = []
self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts') self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts')
self.opts = opts self.opts = opts
self.size = get_printer_page_size(opts)
self.size = get_pdf_page_size(opts) self.cover_data = cover_data
def dump(self, items, out_stream, pdf_metadata): def dump(self, items, out_stream, pdf_metadata):
self.metadata = pdf_metadata self.metadata = pdf_metadata
@ -143,17 +154,20 @@ class PDFWriter(QObject):
self.view.load(QUrl.fromLocalFile(item)) self.view.load(QUrl.fromLocalFile(item))
def _render_html(self, ok): def get_printer(self):
if ok: printer = get_pdf_printer()
item_path = os.path.join(self.tmp_path, '%i.pdf' % len(self.combine_queue))
self.logger.debug('\tRendering item %s as %i' % (os.path.basename(str(self.view.url().toLocalFile())), len(self.combine_queue)))
printer = QPrinter(QPrinter.HighResolution)
printer.setPaperSize(QSizeF(self.size[0] * 10, self.size[1] * 10), QPrinter.Millimeter) printer.setPaperSize(QSizeF(self.size[0] * 10, self.size[1] * 10), QPrinter.Millimeter)
printer.setPageMargins(0, 0, 0, 0, QPrinter.Point) printer.setPageMargins(0, 0, 0, 0, QPrinter.Point)
printer.setOrientation(orientation(self.opts.orientation)) printer.setOrientation(orientation(self.opts.orientation))
printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFormat(QPrinter.PdfFormat)
printer.setFullPage(True)
return printer
def _render_html(self, ok):
if ok:
item_path = os.path.join(self.tmp_path, '%i.pdf' % len(self.combine_queue))
self.logger.debug('\tRendering item %s as %i' % (os.path.basename(str(self.view.url().toLocalFile())), len(self.combine_queue)))
printer = self.get_printer()
printer.setOutputFileName(item_path) printer.setOutputFileName(item_path)
self.view.print_(printer) self.view.print_(printer)
self._render_book() self._render_book()
@ -163,9 +177,27 @@ class PDFWriter(QObject):
shutil.rmtree(self.tmp_path, True) shutil.rmtree(self.tmp_path, True)
self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts') self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts')
def insert_cover(self):
if self.cover_data is None:
return
item_path = os.path.join(self.tmp_path, 'cover.pdf')
printer = self.get_printer()
printer.setOutputFileName(item_path)
self.combine_queue.insert(0, item_path)
p = QPixmap()
p.loadFromData(self.cover_data)
if not p.isNull():
painter = QPainter(printer)
draw_image_page(printer, painter, p,
preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio)
painter.end()
def _write(self): def _write(self):
self.logger.debug('Combining individual PDF parts...') self.logger.debug('Combining individual PDF parts...')
self.insert_cover()
try: try:
outPDF = PdfFileWriter(title=self.metadata.title, author=self.metadata.author) outPDF = PdfFileWriter(title=self.metadata.title, author=self.metadata.author)
for item in self.combine_queue: for item in self.combine_queue:
@ -177,23 +209,50 @@ class PDFWriter(QObject):
self._delete_tmpdir() self._delete_tmpdir()
self.loop.exit(0) self.loop.exit(0)
# }}}
class ImagePDFWriter(PDFWriter): class ImagePDFWriter(object):
def __init__(self, opts, log): def __init__(self, opts, log, cover_data=None):
PDFWriter.__init__(self, opts, log) self.opts = opts
self.size = get_imagepdf_page_size(opts) self.log = log
self.size = get_printer_page_size(opts, for_comic=True)
def _render_next(self): def dump(self, items, out_stream, pdf_metadata):
item = str(self.render_queue.pop(0)) f = PersistentTemporaryFile('_comic2pdf.pdf')
self.combine_queue.append(os.path.join(self.tmp_path, '%i.pdf' % (len(self.combine_queue) + 1))) f.close()
try:
self.render_images(f.name, pdf_metadata, items)
with open(f.name, 'rb') as x:
shutil.copyfileobj(x, out_stream)
finally:
os.remove(f.name)
self.logger.debug('Processing %s...' % item) def render_images(self, outpath, mi, items):
printer = get_pdf_printer()
printer.setPaperSize(QSizeF(self.size[0] * 10, self.size[1] * 10), QPrinter.Millimeter)
printer.setPageMargins(0, 0, 0, 0, QPrinter.Point)
printer.setOrientation(orientation(self.opts.orientation))
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setOutputFileName(outpath)
printer.setDocName(mi.title)
printer.setCreator(u'%s [%s]'%(__appname__, __version__))
# Seems to be no way to set author
printer.setFullPage(True)
height = 'height: %fcm;' % (self.size[1] * 1.3) painter = QPainter(printer)
painter.setRenderHints(QPainter.Antialiasing|QPainter.SmoothPixmapTransform)
html = '<html><body style="margin: 0;"><img src="%s" style="%s display: block; margin-left: auto; margin-right: auto; padding: 0px;" /></body></html>' % (item, height) for i, imgpath in enumerate(items):
self.log('Rendering image:', i)
self.view.setHtml(html) p = QPixmap()
p.load(imgpath)
if not p.isNull():
if i > 0:
printer.newPage()
draw_image_page(printer, painter, p)
else:
self.log.warn('Failed to load image', i)
painter.end()

View File

@ -18,7 +18,8 @@ class PluginWidget(Widget, Ui_Form):
HELP = _('Options specific to')+' PDF '+_('output') HELP = _('Options specific to')+' PDF '+_('output')
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, 'pdf_output', ['paper_size', 'orientation']) Widget.__init__(self, parent, 'pdf_output', ['paper_size',
'orientation', 'preserve_cover_aspect_ratio'])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)

View File

@ -40,7 +40,7 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="opt_orientation"/> <widget class="QComboBox" name="opt_orientation"/>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -53,6 +53,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
<property name="text">
<string>Preserve &amp;aspect ratio of cover</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>