mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Viewer: Restore print to PDF functionality
This commit is contained in:
parent
5ea542b13c
commit
74303fe6d8
1
imgsrc/srv/print.svg
Normal file
1
imgsrc/srv/print.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M448 1536h896v-256h-896v256zm0-640h896v-384h-160q-40 0-68-28t-28-68v-160h-640v640zm1152 64q0-26-19-45t-45-19-45 19-19 45 19 45 45 19 45-19 19-45zm128 0v416q0 13-9.5 22.5t-22.5 9.5h-224v160q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-224q-13 0-22.5-9.5t-9.5-22.5v-416q0-79 56.5-135.5t135.5-56.5h64v-544q0-40 28-68t68-28h672q40 0 88 20t76 48l152 152q28 28 48 76t20 88v256h64q79 0 135.5 56.5t56.5 135.5z"/></svg>
|
After Width: | Height: | Size: 513 B |
240
src/calibre/gui2/viewer/printing.py
Normal file
240
src/calibre/gui2/viewer/printing.py
Normal file
@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
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
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user