mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
E-book viewer: When printing, print to a PDF file instead of directly to the printer. This fixes printing not working on some systems. Fixes #1448330 [Ebook Viewer Print not working](https://bugs.launchpad.net/calibre/+bug/1448330)
This commit is contained in:
parent
328f51c7ce
commit
13c5fdb2be
14298
imgsrc/print-preview.svg
14298
imgsrc/print-preview.svg
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 354 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.7 KiB |
@ -11,7 +11,6 @@ from PyQt5.Qt import (
|
|||||||
QPropertyAnimation, QUrl, QInputDialog, QAction, QModelIndex)
|
QPropertyAnimation, QUrl, QInputDialog, QAction, QModelIndex)
|
||||||
|
|
||||||
from calibre.gui2.viewer.ui import Main as MainWindow
|
from calibre.gui2.viewer.ui import Main as MainWindow
|
||||||
from calibre.gui2.viewer.printing import Printing
|
|
||||||
from calibre.gui2.viewer.toc import TOC
|
from calibre.gui2.viewer.toc import TOC
|
||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre.gui2 import (
|
from calibre.gui2 import (
|
||||||
@ -156,7 +155,6 @@ class EbookViewer(MainWindow):
|
|||||||
self.clock_timer.timeout.connect(self.update_clock)
|
self.clock_timer.timeout.connect(self.update_clock)
|
||||||
|
|
||||||
self.action_print.triggered.connect(self.print_book)
|
self.action_print.triggered.connect(self.print_book)
|
||||||
self.print_menu.actions()[0].triggered.connect(self.print_preview)
|
|
||||||
self.clear_recent_history_action = QAction(
|
self.clear_recent_history_action = QAction(
|
||||||
_('Clear list of recently opened books'), self)
|
_('Clear list of recently opened books'), self)
|
||||||
self.clear_recent_history_action.triggered.connect(self.clear_recent_history)
|
self.clear_recent_history_action.triggered.connect(self.clear_recent_history)
|
||||||
@ -310,12 +308,8 @@ class EbookViewer(MainWindow):
|
|||||||
open_url(url)
|
open_url(url)
|
||||||
|
|
||||||
def print_book(self):
|
def print_book(self):
|
||||||
p = Printing(self.iterator, self)
|
from calibre.gui2.viewer.printing import print_book
|
||||||
p.start_print()
|
print_book(self.iterator.pathtoebook, self, self.current_title)
|
||||||
|
|
||||||
def print_preview(self):
|
|
||||||
p = Printing(self.iterator, self)
|
|
||||||
p.start_preview()
|
|
||||||
|
|
||||||
def toggle_fullscreen(self):
|
def toggle_fullscreen(self):
|
||||||
if self.isFullScreen():
|
if self.isFullScreen():
|
||||||
|
@ -1,113 +1,193 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python2
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
# vim:fileencoding=utf-8
|
||||||
from __future__ import (unicode_literals, division, absolute_import,
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
print_function)
|
print_function)
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
from PyQt5.Qt import (QObject, QEventLoop, Qt, QPrintDialog, QPainter, QSize,
|
import os, subprocess, cPickle, sys
|
||||||
QPrintPreviewDialog)
|
from threading import Thread
|
||||||
from PyQt5.QtWebKitWidgets import QWebView
|
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog
|
from PyQt5.Qt import (
|
||||||
from calibre.ebooks.oeb.display.webview import load_html
|
QFormLayout, QLineEdit, QToolButton, QHBoxLayout, QLabel, QIcon, QPrinter,
|
||||||
from calibre.utils.resources import compiled_coffeescript
|
QPageSize, QComboBox, QDoubleSpinBox, QCheckBox, QProgressDialog, QTimer)
|
||||||
|
|
||||||
class Printing(QObject):
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.ebooks.conversion.plugins.pdf_output import PAPER_SIZES
|
||||||
|
from calibre.gui2 import elided_text, error_dialog, choose_save_file, Application, open_local_file
|
||||||
|
from calibre.gui2.widgets2 import Dialog
|
||||||
|
from calibre.gui2.viewer.main import vprefs
|
||||||
|
from calibre.utils.icu import numeric_sort_key
|
||||||
|
from calibre.utils.ipc.simple_worker import start_pipe_worker
|
||||||
|
|
||||||
def __init__(self, iterator, parent):
|
class PrintDialog(Dialog):
|
||||||
QObject.__init__(self, parent)
|
|
||||||
self.current_index = 0
|
|
||||||
self.iterator = iterator
|
|
||||||
self.view = QWebView(self.parent())
|
|
||||||
self.mf = mf = self.view.page().mainFrame()
|
|
||||||
for x in (Qt.Horizontal, Qt.Vertical):
|
|
||||||
mf.setScrollBarPolicy(x, Qt.ScrollBarAlwaysOff)
|
|
||||||
self.view.loadFinished.connect(self.load_finished)
|
|
||||||
self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils')
|
|
||||||
self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged')
|
|
||||||
|
|
||||||
def load_finished(self, ok):
|
def __init__(self, book_title, parent=None, prefs=vprefs):
|
||||||
self.loaded_ok = ok
|
self.book_title = book_title
|
||||||
|
self.default_file_name = book_title[:75] + '.pdf'
|
||||||
|
self.paper_size_map = {a:getattr(QPageSize, a.capitalize()) for a in PAPER_SIZES}
|
||||||
|
Dialog.__init__(self, _('Print to PDF'), 'print-to-pdf', prefs=prefs, parent=parent)
|
||||||
|
|
||||||
def start_print(self):
|
def setup_ui(self):
|
||||||
self.pd = QPrintDialog(self.parent())
|
self.l = l = QFormLayout(self)
|
||||||
self.pd.open(self._start_print)
|
l.addRow(QLabel(_('Print %s to a PDF file') % elided_text(self.book_title)))
|
||||||
|
self.h = h = QHBoxLayout()
|
||||||
|
self.file_name = f = QLineEdit(self)
|
||||||
|
f.setText(os.path.abspath(os.path.join(os.path.expanduser('~'), self.default_file_name)))
|
||||||
|
self.browse_button = b = QToolButton(self)
|
||||||
|
b.setIcon(QIcon(I('document_open.png'))), b.setToolTip(_('Choose location for PDF file'))
|
||||||
|
b.clicked.connect(self.choose_file)
|
||||||
|
h.addWidget(f), h.addWidget(b)
|
||||||
|
f.setMinimumWidth(350)
|
||||||
|
w = QLabel(_('&File:'))
|
||||||
|
l.addRow(w, h), w.setBuddy(f)
|
||||||
|
|
||||||
def _start_print(self):
|
self.paper_size = ps = QComboBox(self)
|
||||||
self.do_print(self.pd.printer())
|
ps.addItems([a.upper() for a in sorted(self.paper_size_map, key=numeric_sort_key)])
|
||||||
|
previous_size = vprefs.get('print-to-pdf-page-size', None)
|
||||||
|
if previous_size not in self.paper_size_map:
|
||||||
|
previous_size = (QPrinter().pageLayout().pageSize().name() or '').lower()
|
||||||
|
if previous_size not in self.paper_size_map:
|
||||||
|
previous_size = 'a4'
|
||||||
|
ps.setCurrentIndex(ps.findText(previous_size.upper()))
|
||||||
|
l.addRow(_('Paper &size:'), ps)
|
||||||
|
tmap = {
|
||||||
|
'left':_('&Left margin:'),
|
||||||
|
'top':_('&Top margin:'),
|
||||||
|
'right':_('&Right margin:'),
|
||||||
|
'bottom':_('&Bottom margin:'),
|
||||||
|
}
|
||||||
|
for edge in 'left top right bottom'.split():
|
||||||
|
m = QDoubleSpinBox(self)
|
||||||
|
m.setSuffix(' ' + _('inches'))
|
||||||
|
m.setMinimum(0), m.setMaximum(3), m.setSingleStep(0.1)
|
||||||
|
val = vprefs.get('print-to-pdf-%s-margin' % edge, 1)
|
||||||
|
m.setValue(val)
|
||||||
|
setattr(self, '%s_margin' % edge, m)
|
||||||
|
l.addRow(tmap[edge], m)
|
||||||
|
self.pnum = pnum = QCheckBox(_('Add page &number to printed pages'), self)
|
||||||
|
pnum.setChecked(vprefs.get('print-to-pdf-page-numbers', True))
|
||||||
|
l.addRow(pnum)
|
||||||
|
|
||||||
def start_preview(self):
|
l.addRow(self.bb)
|
||||||
self.pd = QPrintPreviewDialog(self.parent())
|
|
||||||
self.pd.paintRequested.connect(self.do_print)
|
|
||||||
self.pd.exec_()
|
|
||||||
|
|
||||||
def do_print(self, printer):
|
@property
|
||||||
painter = QPainter(printer)
|
def data(self):
|
||||||
zoomx = printer.logicalDpiX()/self.view.logicalDpiX()
|
ans = {
|
||||||
zoomy = printer.logicalDpiY()/self.view.logicalDpiY()
|
'output': self.file_name.text().strip(),
|
||||||
painter.scale(zoomx, zoomy)
|
'paper_size': self.paper_size.currentText().lower(),
|
||||||
pr = printer.pageRect()
|
'page_numbers':self.pnum.isChecked(),
|
||||||
self.view.page().setViewportSize(QSize(pr.width()/zoomx,
|
}
|
||||||
pr.height()/zoomy))
|
for edge in 'left top right bottom'.split():
|
||||||
evaljs = self.mf.evaluateJavaScript
|
ans['margin_' + edge] = getattr(self, '%s_margin' % edge).value()
|
||||||
loop = QEventLoop(self)
|
return ans
|
||||||
pagenum = 0
|
|
||||||
from_, to = printer.fromPage(), printer.toPage()
|
|
||||||
first = True
|
|
||||||
|
|
||||||
for path in self.iterator.spine:
|
def choose_file(self):
|
||||||
self.loaded_ok = None
|
ans = choose_save_file(self, 'print-to-pdf-choose-file', _('PDF file'), filters=[(_('PDF file'), 'pdf')],
|
||||||
load_html(path, self.view, codec=getattr(path, 'encoding', 'utf-8'),
|
all_files=False, initial_filename=self.default_file_name)
|
||||||
mime_type=getattr(path, 'mime_type', None))
|
if ans:
|
||||||
while self.loaded_ok is None:
|
self.file_name.setText(ans)
|
||||||
loop.processEvents(loop.ExcludeUserInputEvents)
|
|
||||||
if not self.loaded_ok:
|
|
||||||
return error_dialog(self.parent(), _('Failed to render'),
|
|
||||||
_('Failed to render document %s')%path, show=True)
|
|
||||||
evaljs(self.paged_js)
|
|
||||||
evaljs('''
|
|
||||||
document.body.style.backgroundColor = "white";
|
|
||||||
paged_display.set_geometry(1, 0, 0, 0);
|
|
||||||
paged_display.layout();
|
|
||||||
paged_display.fit_images();
|
|
||||||
''')
|
|
||||||
|
|
||||||
while True:
|
def save_used_values(self):
|
||||||
pagenum += 1
|
data = self.data
|
||||||
if (pagenum >= from_ and (to == 0 or pagenum <= to)):
|
vprefs['print-to-pdf-page-size'] = data['paper_size']
|
||||||
if not first:
|
vprefs['print-to-pdf-page-numbers'] = data['page_numbers']
|
||||||
printer.newPage()
|
for edge in 'left top right bottom'.split():
|
||||||
first = False
|
vprefs['print-to-pdf-%s-margin' % edge] = data['margin_' + edge]
|
||||||
self.mf.render(painter)
|
|
||||||
try:
|
|
||||||
nsl = int(evaljs('paged_display.next_screen_location()'))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
break
|
|
||||||
if nsl <= 0:
|
|
||||||
break
|
|
||||||
evaljs('window.scrollTo(%d, 0)'%nsl)
|
|
||||||
|
|
||||||
painter.end()
|
def accept(self):
|
||||||
|
fname = self.file_name.text().strip()
|
||||||
|
if not fname:
|
||||||
|
return error_dialog(self, _('No filename specified'), _(
|
||||||
|
'You must specify a filename for the PDF file to generate'), show=True)
|
||||||
|
if not fname.lower().endswith('.pdf'):
|
||||||
|
return error_dialog(self, _('Incorrect filename specified'), _(
|
||||||
|
'The filename for the PDF file must end with .pdf'), show=True)
|
||||||
|
self.save_used_values()
|
||||||
|
return Dialog.accept(self)
|
||||||
|
|
||||||
|
class DoPrint(Thread):
|
||||||
|
|
||||||
|
daemon = True
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
Thread.__init__(self, name='DoPrint')
|
||||||
|
self.data = data
|
||||||
|
self.tb = self.log = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
with PersistentTemporaryFile('print-to-pdf-log.txt') as f:
|
||||||
|
p = self.worker = start_pipe_worker('from calibre.gui2.viewer.printing import do_print; do_print()', stdout=f, stderr=subprocess.STDOUT)
|
||||||
|
p.stdin.write(cPickle.dumps(self.data, -1)), p.stdin.flush(), p.stdin.close()
|
||||||
|
rc = p.wait()
|
||||||
|
if rc != 0:
|
||||||
|
f.seek(0)
|
||||||
|
self.log = f.read().decode('utf-8', 'replace')
|
||||||
|
try:
|
||||||
|
os.remove(f.name)
|
||||||
|
except EnvironmentError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
self.tb = traceback.format_exc()
|
||||||
|
|
||||||
|
def do_print():
|
||||||
|
data = cPickle.loads(sys.stdin.read())
|
||||||
|
args = ['ebook-convert', data['input'], data['output'], '--override-profile-size', '--paper-size', data['paper_size'], '--pdf-add-toc',
|
||||||
|
'--disable-remove-fake-margins', '--disable-font-rescaling', '--page-breaks-before', '/', '--chapter-mark', 'none', '-vv']
|
||||||
|
if data['page_numbers']:
|
||||||
|
args.append('--pdf-page-numbers')
|
||||||
|
for edge in 'left top right bottom'.split():
|
||||||
|
args.append('--margin-' + edge), args.append('%.1f' % (data['margin_' + edge] * 72))
|
||||||
|
from calibre.ebooks.conversion.cli import main
|
||||||
|
main(args)
|
||||||
|
|
||||||
|
class Printing(QProgressDialog):
|
||||||
|
|
||||||
|
def __init__(self, thread, parent=None):
|
||||||
|
QProgressDialog.__init__(self, _('Printing, this will take a while, please wait...'), _('&Cancel'), 0, 0, parent)
|
||||||
|
self.setWindowTitle(_('Printing...'))
|
||||||
|
self.setWindowIcon(QIcon(I('print.png')))
|
||||||
|
self.thread = thread
|
||||||
|
self.timer = t = QTimer(self)
|
||||||
|
t.timeout.connect(self.check)
|
||||||
|
self.canceled.connect(self.do_cancel)
|
||||||
|
t.start(100)
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
if self.thread.is_alive():
|
||||||
|
return
|
||||||
|
if self.thread.tb or self.thread.log:
|
||||||
|
error_dialog(self, _('Failed to convert to PDF'), _(
|
||||||
|
'Failed to generate PDF file, click "Show details" for more information.'), det_msg=self.thread.tb or self.thread.log, show=True)
|
||||||
|
else:
|
||||||
|
open_local_file(self.thread.data['output'])
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def do_cancel(self):
|
||||||
|
if hasattr(self.thread, 'worker'):
|
||||||
|
try:
|
||||||
|
if self.thread.worker.poll() is None:
|
||||||
|
self.thread.worker.kill()
|
||||||
|
except EnvironmentError:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
self.timer.stop()
|
||||||
|
self.reject()
|
||||||
|
|
||||||
|
def print_book(path_to_book, parent=None, book_title=None):
|
||||||
|
book_title = book_title or os.path.splitext(os.path.basename(path_to_book))[0]
|
||||||
|
d = PrintDialog(book_title, parent)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
data = d.data
|
||||||
|
data['input'] = path_to_book
|
||||||
|
t = DoPrint(data)
|
||||||
|
t.start()
|
||||||
|
Printing(t, parent).exec_()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from calibre.gui2 import Application
|
|
||||||
from calibre.ebooks.oeb.iterator.book import EbookIterator
|
|
||||||
from PyQt5.Qt import QPrinter, QTimer
|
|
||||||
import sys
|
|
||||||
app = Application([])
|
app = Application([])
|
||||||
|
print_book(sys.argv[-1])
|
||||||
def doit():
|
del app
|
||||||
with EbookIterator(sys.argv[-1]) as it:
|
|
||||||
p = Printing(it, None)
|
|
||||||
printer = QPrinter()
|
|
||||||
of = sys.argv[-1]+'.pdf'
|
|
||||||
printer.setOutputFileName(of)
|
|
||||||
p.do_print(printer)
|
|
||||||
print ('Printed to:', of)
|
|
||||||
app.exit()
|
|
||||||
QTimer.singleShot(0, doit)
|
|
||||||
app.exec_()
|
|
||||||
|
|
||||||
|
@ -394,8 +394,7 @@ class Main(MainWindow):
|
|||||||
a('load_theme', _('Load a theme'), 'wizard.png', menu_name='themes', popup_mode=QToolButton.InstantPopup)
|
a('load_theme', _('Load a theme'), 'wizard.png', menu_name='themes', popup_mode=QToolButton.InstantPopup)
|
||||||
self.tool_bar.addSeparator()
|
self.tool_bar.addSeparator()
|
||||||
|
|
||||||
a('print', _('Print'), 'print.png', menu_name='print')
|
a('print', _('Print to PDF file'), 'print.png')
|
||||||
self.print_menu.addAction(QIcon(I('print-preview.png')), _('Print Preview'))
|
|
||||||
|
|
||||||
a('find_next', _('Find next occurrence'), 'arrow-down.png', tb=self.tool_bar2)
|
a('find_next', _('Find next occurrence'), 'arrow-down.png', tb=self.tool_bar2)
|
||||||
a('find_previous', _('Find previous occurrence'), 'arrow-up.png', tb=self.tool_bar2)
|
a('find_previous', _('Find previous occurrence'), 'arrow-up.png', tb=self.tool_bar2)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user