mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
ca08d4dec9
commit
b8005284f1
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
||||
# }}}
|
Loading…
x
Reference in New Issue
Block a user