Use same code path for generating image based PDFs as for text based PDFs

Allows getting rid of a bunch of old code. Also removes dependency on
the highly finicky Qt PDF printer
This commit is contained in:
Kovid Goyal 2017-03-21 11:43:35 +05:30
parent ca08d4dec9
commit b8005284f1
3 changed files with 50 additions and 401 deletions

View File

@ -156,7 +156,7 @@ class PDFOutput(OutputFormatPlugin):
os.environ.pop('CALIBRE_WEBKIT_NO_HINTING', None)
def convert_images(self, images):
from calibre.ebooks.pdf.writer import ImagePDFWriter
from calibre.ebooks.pdf.render.from_html import ImagePDFWriter
self.write(ImagePDFWriter, images, None)
def get_cover_data(self):

View File

@ -420,3 +420,52 @@ class PDFWriter(QObject):
if not self.doc.errors_occurred and self.doc.current_page_num > 1:
self.doc.add_links(self.current_item, start_page, amap['links'],
amap['anchors'])
class ImagePDFWriter(object):
def __init__(self, opts, log, cover_data=None, toc=None):
from calibre.gui2 import must_use_qt
must_use_qt()
self.logger = self.log = log
self.opts = opts
self.cover_data = cover_data
self.toc = toc
def dump(self, items, out_stream, pdf_metadata):
opts = self.opts
page_size = get_page_size(self.opts)
ml, mr = opts.margin_left, opts.margin_right
self.doc = PdfDevice(
out_stream, page_size=page_size, left_margin=ml,
top_margin=opts.margin_top, right_margin=mr,
bottom_margin=opts.margin_bottom,
errors=self.log.error, debug=self.log.debug, compress=not
opts.uncompressed_pdf, opts=opts, mark_links=opts.pdf_mark_links)
self.painter = QPainter(self.doc)
self.doc.set_metadata(title=pdf_metadata.title,
author=pdf_metadata.author,
tags=pdf_metadata.tags, mi=pdf_metadata.mi)
self.doc_title = pdf_metadata.title
self.doc_author = pdf_metadata.author
page_rect = QRect(*self.doc.full_page_rect)
for imgpath in items:
self.log.debug('Processing %s...' % imgpath)
self.doc.init_page()
p = QPixmap()
with lopen(imgpath, 'rb') as f:
if not p.loadFromData(f.read()):
raise ValueError('Could not read image from: {}'.format(imgpath))
draw_image_page(page_rect,
self.painter, p,
preserve_aspect_ratio=True)
self.doc.end_page()
if self.toc is not None and len(self.toc) > 0:
self.doc.add_outline(self.toc)
self.painter.end()
if self.doc.errors_occurred:
raise Exception('PDF Output failed, see log for details')

View File

@ -1,400 +0,0 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
'''
Write content to PDF.
'''
import os, shutil, json
from PyQt5.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter,
QPixmap, QTimer, pyqtProperty, QSize)
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.pdf.pageoptions import (unit, paper_size)
from calibre.ebooks.pdf.outline_writer import Outline
from calibre.ptempfile import PersistentTemporaryFile
from calibre import (__appname__, __version__, fit_image, isosx)
from calibre.ebooks.oeb.display.webview import load_html
def get_custom_size(opts):
custom_size = None
if opts.custom_size is not None:
width, sep, height = opts.custom_size.partition('x')
if height:
try:
width = float(width)
height = float(height)
custom_size = (width, height)
except:
custom_size = None
return custom_size
def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{
from calibre.gui2 import must_use_qt
must_use_qt()
printer = QPrinter(QPrinter.HighResolution)
custom_size = get_custom_size(opts)
if isosx and not for_comic:
# On OSX, the native engine can only produce a single page size
# (usually A4). The Qt engine on the other hand produces image based
# PDFs. If we set a custom page size using QSizeF the native engine
# produces unreadable output, so we just ignore the custom size
# settings.
printer.setPaperSize(paper_size(opts.paper_size))
else:
if opts.output_profile.short_name == 'default' or \
opts.output_profile.width > 9999 or opts.override_profile_size:
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:
w = opts.output_profile.comic_screen_size[0] if for_comic else \
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)
if for_comic:
# Comic pages typically have their own margins, or their background
# color is not white, in which case the margin looks bad
printer.setPageMargins(0, 0, 0, 0, QPrinter.Point)
else:
printer.setPageMargins(opts.margin_left, opts.margin_top,
opts.margin_right, opts.margin_bottom, QPrinter.Point)
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setFullPage(for_comic)
if output_file_name:
printer.setOutputFileName(output_file_name)
if isosx and not for_comic:
# Ensure we are not generating enormous image based PDFs
printer.setOutputFormat(QPrinter.NativeFormat)
return printer
# }}}
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())
class Page(QWebPage): # {{{
def __init__(self, opts, log):
from calibre.gui2 import secure_web_page
self.log = log
QWebPage.__init__(self)
settings = self.settings()
secure_web_page(settings)
settings.setFontSize(QWebSettings.DefaultFontSize,
opts.pdf_default_font_size)
settings.setFontSize(QWebSettings.DefaultFixedFontSize,
opts.pdf_mono_font_size)
settings.setFontSize(QWebSettings.MinimumLogicalFontSize, 8)
settings.setFontSize(QWebSettings.MinimumFontSize, 8)
std = {'serif':opts.pdf_serif_family, 'sans':opts.pdf_sans_family,
'mono':opts.pdf_mono_family}.get(opts.pdf_standard_font,
opts.pdf_serif_family)
if std:
settings.setFontFamily(QWebSettings.StandardFont, std)
if opts.pdf_serif_family:
settings.setFontFamily(QWebSettings.SerifFont, opts.pdf_serif_family)
if opts.pdf_sans_family:
settings.setFontFamily(QWebSettings.SansSerifFont,
opts.pdf_sans_family)
if opts.pdf_mono_family:
settings.setFontFamily(QWebSettings.FixedFont, opts.pdf_mono_family)
def javaScriptConsoleMessage(self, msg, lineno, msgid):
self.log.debug(u'JS:', unicode(msg))
def javaScriptAlert(self, frame, msg):
self.log(unicode(msg))
# }}}
class PDFWriter(QObject): # {{{
def __init__(self, opts, log, cover_data=None, toc=None):
from calibre.gui2 import must_use_qt
from calibre.utils.podofo import get_podofo
must_use_qt()
QObject.__init__(self)
self.logger = self.log = log
self.podofo = get_podofo()
self.doc = self.podofo.PDFDoc()
self.loop = QEventLoop()
self.view = QWebView()
self.page = Page(opts, self.log)
self.view.setPage(self.page)
self.view.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform)
self.view.loadFinished.connect(self._render_html,
type=Qt.QueuedConnection)
for x in (Qt.Horizontal, Qt.Vertical):
self.view.page().mainFrame().setScrollBarPolicy(x,
Qt.ScrollBarAlwaysOff)
self.render_queue = []
self.combine_queue = []
self.tmp_path = PersistentTemporaryDirectory(u'_pdf_output_parts')
self.opts = opts
self.cover_data = cover_data
self.paged_js = None
self.toc = toc
def dump(self, items, out_stream, pdf_metadata):
self.metadata = pdf_metadata
self._delete_tmpdir()
self.outline = Outline(self.toc, items)
self.render_queue = items
self.combine_queue = []
self.out_stream = out_stream
self.insert_cover()
self.render_succeeded = False
self.current_page_num = self.doc.page_count()
self.combine_queue.append(os.path.join(self.tmp_path,
'qprinter_out.pdf'))
self.first_page = True
self.setup_printer(self.combine_queue[-1])
QTimer.singleShot(0, self._render_book)
self.loop.exec_()
if self.painter is not None:
self.painter.end()
if self.printer is not None:
self.printer.abort()
if not self.render_succeeded:
raise Exception('Rendering HTML to PDF failed')
def _render_book(self):
try:
if len(self.render_queue) == 0:
self._write()
else:
self._render_next()
except:
self.logger.exception('Rendering failed')
self.loop.exit(1)
def _render_next(self):
item = unicode(self.render_queue.pop(0))
self.logger.debug('Processing %s...' % item)
self.current_item = item
load_html(item, self.view)
def _render_html(self, ok):
if ok:
self.do_paged_render()
else:
# The document is so corrupt that we can't render the page.
self.logger.error('Document cannot be rendered.')
self.loop.exit(0)
return
self._render_book()
def _pass_json_value_getter(self):
val = json.dumps(self.bridge_value)
return val
def _pass_json_value_setter(self, value):
self.bridge_value = json.loads(unicode(value))
_pass_json_value = pyqtProperty(str, fget=_pass_json_value_getter,
fset=_pass_json_value_setter)
def setup_printer(self, outpath):
self.printer = self.painter = None
printer = get_pdf_printer(self.opts, output_file_name=outpath)
painter = QPainter(printer)
zoomx = printer.logicalDpiX()/self.view.logicalDpiX()
zoomy = printer.logicalDpiY()/self.view.logicalDpiY()
painter.scale(zoomx, zoomy)
pr = printer.pageRect()
self.printer, self.painter = printer, painter
self.viewport_size = QSize(pr.width()/zoomx, pr.height()/zoomy)
self.page.setViewportSize(self.viewport_size)
def do_paged_render(self):
if self.paged_js is None:
from calibre.utils.resources import compiled_coffeescript
self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils')
self.paged_js += compiled_coffeescript('ebooks.oeb.display.indexing')
self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged')
self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self)
evaljs = self.view.page().mainFrame().evaluateJavaScript
evaljs(self.paged_js)
evaljs('''
Object.defineProperty(py_bridge, 'value', {
get : function() { return JSON.parse(this._pass_json_value); },
set : function(val) { this._pass_json_value = JSON.stringify(val); }
});
document.body.style.backgroundColor = "white";
paged_display.set_geometry(1, 0, 0, 0);
paged_display.layout();
paged_display.fit_images();
''')
mf = self.view.page().mainFrame()
start_page = self.current_page_num
if not self.first_page:
start_page += 1
while True:
if not self.first_page:
if self.printer.newPage():
self.current_page_num += 1
self.first_page = False
mf.render(self.painter)
try:
nsl = int(evaljs('paged_display.next_screen_location()'))
except (TypeError, ValueError):
break
if nsl <= 0:
break
evaljs('window.scrollTo(%d, 0)'%nsl)
self.bridge_value = tuple(self.outline.anchor_map[self.current_item])
evaljs('py_bridge.value = book_indexing.anchor_positions(py_bridge.value)')
amap = self.bridge_value
if not isinstance(amap, dict):
amap = {} # Some javascript error occurred
self.outline.set_pos(self.current_item, None, start_page, 0)
for anchor, x in amap.iteritems():
pagenum, ypos = x
self.outline.set_pos(self.current_item, anchor, start_page + pagenum, ypos)
def append_doc(self, outpath):
doc = self.podofo.PDFDoc()
with open(outpath, 'rb') as f:
raw = f.read()
doc.load(raw)
self.doc.append(doc)
def _delete_tmpdir(self):
if os.path.exists(self.tmp_path):
shutil.rmtree(self.tmp_path, True)
self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts')
def insert_cover(self):
if not isinstance(self.cover_data, bytes):
return
item_path = os.path.join(self.tmp_path, 'cover.pdf')
printer = get_pdf_printer(self.opts, output_file_name=item_path,
for_comic=True)
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()
self.append_doc(item_path)
printer.abort()
def _write(self):
self.painter.end()
self.printer.abort()
self.painter = self.printer = None
self.append_doc(self.combine_queue[-1])
try:
self.doc.creator = u'%s %s [https://calibre-ebook.com]'%(
__appname__, __version__)
self.doc.title = self.metadata.title
self.doc.author = self.metadata.author
if self.metadata.tags:
self.doc.keywords = self.metadata.tags
self.outline(self.doc)
self.doc.save_to_fileobj(self.out_stream)
self.render_succeeded = True
finally:
self._delete_tmpdir()
self.loop.exit(0)
# }}}
class ImagePDFWriter(object): # {{{
def __init__(self, opts, log, cover_data=None, toc=None):
self.opts = opts
self.log = log
def dump(self, items, out_stream, pdf_metadata):
from calibre.utils.podofo import get_podofo
f = PersistentTemporaryFile('_comic2pdf.pdf')
f.close()
self.metadata = pdf_metadata
try:
self.render_images(f.name, pdf_metadata, items)
with open(f.name, 'rb') as x:
raw = x.read()
doc = get_podofo().PDFDoc()
doc.load(raw)
doc.creator = u'%s %s [https://calibre-ebook.com]'%(
__appname__, __version__)
doc.title = self.metadata.title
doc.author = self.metadata.author
if self.metadata.tags:
doc.keywords = self.metadata.tags
raw = doc.write()
out_stream.write(raw)
finally:
try:
os.remove(f.name)
except:
pass
def render_images(self, outpath, mi, items):
printer = get_pdf_printer(self.opts, for_comic=True,
output_file_name=outpath)
printer.setDocName(mi.title)
painter = QPainter(printer)
painter.setRenderHints(QPainter.Antialiasing|QPainter.SmoothPixmapTransform)
for i, imgpath in enumerate(items):
self.log('Rendering image:', i)
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()
# }}}