diff --git a/imgsrc/srv/print.svg b/imgsrc/srv/print.svg new file mode 100644 index 0000000000..daa9ee9e81 --- /dev/null +++ b/imgsrc/srv/print.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/calibre/gui2/viewer/printing.py b/src/calibre/gui2/viewer/printing.py new file mode 100644 index 0000000000..0b6c350e3d --- /dev/null +++ b/src/calibre/gui2/viewer/printing.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2015, Kovid Goyal +from __future__ import absolute_import, division, print_function, unicode_literals + +import os +import subprocess +import sys +from threading import Thread + +from PyQt5.Qt import ( + QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QHBoxLayout, QIcon, QLabel, + QLineEdit, QPageSize, QPrinter, QProgressDialog, QTimer, QToolButton +) + +from calibre import sanitize_file_name +from calibre.ebooks.conversion.plugins.pdf_output import PAPER_SIZES +from calibre.gui2 import ( + Application, choose_save_file, dynamic, elided_text, error_dialog, + open_local_file +) +from calibre.gui2.widgets2 import Dialog +from calibre.ptempfile import PersistentTemporaryFile +from calibre.utils.config import JSONConfig +from calibre.utils.filenames import expanduser +from calibre.utils.icu import numeric_sort_key +from calibre.utils.ipc.simple_worker import start_pipe_worker +from calibre.utils.serialize import msgpack_dumps, msgpack_loads + + +vprefs = JSONConfig('viewer') + + +class PrintDialog(Dialog): + + OUTPUT_NAME = 'print-to-pdf-choose-file' + + def __init__(self, book_title, parent=None, prefs=vprefs): + self.book_title = book_title + self.default_file_name = sanitize_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 setup_ui(self): + self.l = l = QFormLayout(self) + l.addRow(QLabel(_('Print %s to a PDF file') % elided_text(self.book_title))) + self.h = h = QHBoxLayout() + self.file_name = f = QLineEdit(self) + val = dynamic.get(self.OUTPUT_NAME, None) + if not val: + val = expanduser('~') + else: + val = os.path.dirname(val) + f.setText(os.path.abspath(os.path.join(val, 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) + + self.paper_size = ps = QComboBox(self) + 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) + + self.show_file = sf = QCheckBox(_('&Open PDF file after printing'), self) + sf.setChecked(vprefs.get('print-to-pdf-show-file', True)) + l.addRow(sf) + + l.addRow(self.bb) + + @property + def data(self): + fpath = self.file_name.text().strip() + head, tail = os.path.split(fpath) + tail = sanitize_file_name(tail) + fpath = tail + if head: + fpath = os.path.join(head, tail) + ans = { + 'output': fpath, + 'paper_size': self.paper_size.currentText().lower(), + 'page_numbers':self.pnum.isChecked(), + 'show_file':self.show_file.isChecked(), + } + for edge in 'left top right bottom'.split(): + ans['margin_' + edge] = getattr(self, '%s_margin' % edge).value() + return ans + + def choose_file(self): + ans = choose_save_file(self, self.OUTPUT_NAME, _('PDF file'), filters=[(_('PDF file'), ['pdf'])], + all_files=False, initial_filename=self.default_file_name) + if ans: + self.file_name.setText(ans) + + def save_used_values(self): + data = self.data + vprefs['print-to-pdf-page-size'] = data['paper_size'] + vprefs['print-to-pdf-page-numbers'] = data['page_numbers'] + vprefs['print-to-pdf-show-file'] = data['show_file'] + for edge in 'left top right bottom'.split(): + vprefs['print-to-pdf-%s-margin' % edge] = data['margin_' + edge] + + 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(msgpack_dumps(self.data)), 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(): + from calibre.customize.ui import plugin_for_input_format + stdin = getattr(sys.stdin, 'buffer', sys.stdin) + data = msgpack_loads(stdin.read()) + ext = data['input'].lower().rpartition('.')[-1] + input_plugin = plugin_for_input_format(ext) + if input_plugin is None: + raise ValueError('Not a supported file type: {}'.format(ext.upper())) + args = ['ebook-convert', data['input'], data['output'], '--paper-size', data['paper_size'], '--pdf-add-toc', + '--disable-remove-fake-margins', '--chapter-mark', 'none', '-vv'] + if input_plugin.is_image_collection: + args.append('--no-process') + else: + args.append('--disable-font-rescaling') + args.append('--page-breaks-before=/') + if data['page_numbers']: + args.append('--pdf-page-numbers') + for edge in 'left top right bottom'.split(): + args.append('--pdf-page-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, show_file, parent=None): + QProgressDialog.__init__(self, _('Printing, this will take a while, please wait...'), _('&Cancel'), 0, 0, parent) + self.show_file = show_file + 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: + if self.show_file: + 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, data['show_file'], parent).exec_() + + +if __name__ == '__main__': + app = Application([]) + print_book(sys.argv[-1]) + del app diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 3bd0a25883..d7496b1774 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -142,6 +142,7 @@ class EbookViewer(MainWindow): self.web_view.copy_image.connect(self.copy_image, type=Qt.QueuedConnection) self.web_view.show_loading_message.connect(self.show_loading_message) self.web_view.show_error.connect(self.show_error) + self.web_view.print_book.connect(self.print_book, type=Qt.QueuedConnection) self.setCentralWidget(self.web_view) self.loading_overlay = LoadingOverlay(self) self.restore_state() @@ -270,6 +271,10 @@ class EbookViewer(MainWindow): self.loading_overlay.hide() error_dialog(self, title, msg, det_msg=details or None, show=True) + def print_book(self): + from .printing import print_book + print_book(set_book_path.pathtoebook, book_title=self.current_book_data['metadata']['title'], parent=self) + def ask_for_open(self, path=None): if path is None: files = choose_files( diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 50c1a25a84..84db3d1bac 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -236,6 +236,7 @@ class ViewerBridge(Bridge): show_loading_message = from_js(object) show_error = from_js(object, object, object) export_shortcut_map = from_js(object) + print_book = from_js() create_view = to_js() start_book_load = to_js() @@ -376,6 +377,7 @@ class WebView(RestartingWebEngineView): overlay_visibility_changed = pyqtSignal(object) show_loading_message = pyqtSignal(object) show_error = pyqtSignal(object, object, object) + print_book = pyqtSignal() def __init__(self, parent=None): self._host_widget = None @@ -406,6 +408,7 @@ class WebView(RestartingWebEngineView): self.bridge.overlay_visibility_changed.connect(self.overlay_visibility_changed) self.bridge.show_loading_message.connect(self.show_loading_message) self.bridge.show_error.connect(self.show_error) + self.bridge.print_book.connect(self.print_book) self.bridge.export_shortcut_map.connect(self.set_shortcut_map) self.shortcut_map = {} self.bridge.report_cfi.connect(self.call_callback) diff --git a/src/pyj/read_book/overlay.pyj b/src/pyj/read_book/overlay.pyj index b500f9e57c..b1ab0d76e4 100644 --- a/src/pyj/read_book/overlay.pyj +++ b/src/pyj/read_book/overlay.pyj @@ -276,6 +276,8 @@ class MainOverlay: # {{{ text = _('Exit full screen') if runtime.viewer_in_full_screen else _('Enter full screen') full_screen_actions.push( ac(text, '', def(): self.overlay.hide(), ui_operations.toggle_full_screen();, 'full-screen')) + full_screen_actions.push( + ac(_('Print'), _('Print book to PDF'), def(): self.overlay.hide(), ui_operations.print_book();, 'print')) else: if not full_screen_element() and not is_ios: # No fullscreen on iOS, see http://caniuse.com/#search=fullscreen diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index b741fada5d..c7dafd6031 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -318,6 +318,8 @@ if window is window.top: to_python.show_loading_message(msg) ui_operations.export_shortcut_map = def(smap): to_python.export_shortcut_map(smap) + ui_operations.print_book = def(): + to_python.print_book() document.body.appendChild(E.div(id='view')) window.onerror = onerror