diff --git a/src/calibre/ebooks/metadata/pdf.py b/src/calibre/ebooks/metadata/pdf.py index 8eb12f86c5..5ec41cdef1 100644 --- a/src/calibre/ebooks/metadata/pdf.py +++ b/src/calibre/ebooks/metadata/pdf.py @@ -15,6 +15,14 @@ from calibre.utils.ipc.simple_worker import fork_job, WorkerError #_isbn_pat = re.compile(r'ISBN[: ]*([-0-9Xx]+)') +def get_tools(): + from calibre.ebooks.pdf.pdftohtml import PDFTOHTML + base = os.path.dirname(PDFTOHTML) + suffix = '.exe' if iswindows else '' + pdfinfo = os.path.join(base, 'pdfinfo') + suffix + pdftoppm = os.path.join(base, 'pdftoppm') + suffix + return pdfinfo, pdftoppm + def read_info(outputdir, get_cover): ''' Read info dict and cover from a pdf file named src.pdf in outputdir. Note that this function changes the cwd to outputdir and is therefore not @@ -22,13 +30,8 @@ def read_info(outputdir, get_cover): way to pass unicode paths via command line arguments. This also ensures that if poppler crashes, no stale file handles are left for the original file, only for src.pdf.''' - - from calibre.ebooks.pdf.pdftohtml import PDFTOHTML os.chdir(outputdir) - base = os.path.dirname(PDFTOHTML) - suffix = '.exe' if iswindows else '' - pdfinfo = os.path.join(base, 'pdfinfo') + suffix - pdftoppm = os.path.join(base, 'pdftoppm') + suffix + pdfinfo, pdftoppm = get_tools() try: raw = subprocess.check_output([pdfinfo, '-enc', 'UTF-8', 'src.pdf']) @@ -58,6 +61,20 @@ def read_info(outputdir, get_cover): return ans +def page_images(pdfpath, outputdir, first=1, last=1): + pdftoppm = get_tools()[1] + outputdir = os.path.abspath(outputdir) + args = {} + if iswindows: + import win32process as w + args['creationflags'] = w.HIGH_PRIORITY_CLASS | w.CREATE_NO_WINDOW + try: + subprocess.check_call([pdftoppm, '-jpeg', '-f', unicode(first), + '-l', unicode(last), pdfpath, + os.path.join(outputdir, 'page-images')], **args) + except subprocess.CalledProcessError as e: + raise ValueError('Failed to render PDF, pdftoppm errorcode: %s'%e.returncode) + def get_metadata(stream, cover=True): with TemporaryDirectory('_pdf_metadata_read') as pdfpath: stream.seek(0) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 2d1e1fe7c3..5e031a4b2c 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -819,6 +819,27 @@ class FormatsManager(QWidget): def show_format(self, item, *args): self.dialog.do_view_format(item.path, item.ext) + def get_selected_format(self): + row = self.formats.currentRow() + fmt = self.formats.item(row) + if fmt is None: + if self.formats.count() == 1: + fmt = self.formats.item(0) + if fmt is None: + error_dialog(self, _('No format selected'), + _('No format selected')).exec_() + return None + return fmt.ext.lower() + + def get_format_path(self, db, id_, fmt): + for i in xrange(self.formats.count()): + f = self.formats.item(i) + ext = f.ext.lower() + if ext == fmt: + if f.path is None: + return db.format(id_, ext, as_path=True, index_is_id=True) + return f.path + def get_selected_format_metadata(self, db, id_): old = prefs['read_file_metadata'] if not old: diff --git a/src/calibre/gui2/metadata/pdf_covers.py b/src/calibre/gui2/metadata/pdf_covers.py new file mode 100644 index 0000000000..7e493a2188 --- /dev/null +++ b/src/calibre/gui2/metadata/pdf_covers.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, shutil, os +from threading import Thread +from glob import glob + +from PyQt4.Qt import (QDialog, QApplication, QLabel, QGridLayout, + QDialogButtonBox, Qt, pyqtSignal, QListWidget, + QListWidgetItem, QSize, QIcon) + +from calibre import as_unicode +from calibre.ebooks.metadata.pdf import page_images +from calibre.gui2 import error_dialog, file_icon_provider +from calibre.ptempfile import PersistentTemporaryDirectory + +class PDFCovers(QDialog): + + rendering_done = pyqtSignal() + + def __init__(self, pdfpath, parent=None): + QDialog.__init__(self, parent) + self.pdfpath = pdfpath + self.l = l = QGridLayout() + self.setLayout(l) + + self.la = la = QLabel(_('Choose a cover from the list of PDF pages below')) + l.addWidget(la) + self.loading = la = QLabel(''+_('Rendering PDF pages, please wait...')) + l.addWidget(la) + + self.covers = c = QListWidget(self) + l.addWidget(c) + c.setIconSize(QSize(120, 160)) + c.setSelectionMode(c.SingleSelection) + c.setViewMode(c.IconMode) + c.setUniformItemSizes(True) + c.setResizeMode(c.Adjust) + c.itemDoubleClicked.connect(self.accept) + + self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + l.addWidget(bb) + self.rendering_done.connect(self.show_pages, type=Qt.QueuedConnection) + self.tdir = PersistentTemporaryDirectory('_pdf_covers') + self.thread = Thread(target=self.render) + self.thread.daemon = True + self.thread.start() + self.setWindowTitle(_('Choose cover from PDF')) + self.setWindowIcon(file_icon_provider().icon_from_ext('pdf')) + self.resize(QSize(800, 600)) + + @property + def cover_path(self): + for item in self.covers.selectedItems(): + return unicode(item.data(Qt.UserRole).toString()) + if self.covers.count() > 0: + return unicode(self.covers.item(0).data(Qt.UserRole).toString()) + + def cleanup(self): + try: + shutil.rmtree(self.tdir) + except: + pass + + def render(self): + self.error = None + try: + page_images(self.pdfpath, self.tdir, last=10) + except Exception as e: + self.error = as_unicode(e) + if self.isVisible(): + self.rendering_done.emit() + + def show_pages(self): + self.loading.setVisible(False) + if self.error is not None: + error_dialog(self, _('Failed to render'), + _('Could not render this PDF file'), show=True) + self.reject() + return + files = (glob(os.path.join(self.tdir, '*.jpg')) + + glob(os.path.join(self.tdir, '*.jpeg'))) + if not files: + error_dialog(self, _('Failed to render'), + _('This PDF has no pages'), show=True) + self.reject() + return + + for f in sorted(files): + i = QListWidgetItem(QIcon(f), '') + i.setData(Qt.UserRole, f) + self.covers.addItem(i) + +if __name__ == '__main__': + app = QApplication([]) + app + d = PDFCovers(sys.argv[-1]) + d.exec_() + print (d.cover_path) + diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 654a5a474e..f39ae5668f 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -318,7 +318,23 @@ class MetadataSingleDialogBase(ResizableDialog): if mi is not None: self.update_from_mi(mi) + def get_pdf_cover(self): + pdfpath = self.formats_manager.get_format_path(self.db, self.book_id, + 'pdf') + from calibre.gui2.metadata.pdf_covers import PDFCovers + d = self.__pdf_covers = PDFCovers(pdfpath, parent=self) + if d.exec_() == d.Accepted: + cpath = d.cover_path + if cpath: + with open(cpath, 'rb') as f: + self.update_cover(f.read(), 'PDF') + d.cleanup() + def cover_from_format(self, *args): + ext = self.formats_manager.get_selected_format() + if ext is None: return + if ext == 'pdf': + return self.get_pdf_cover() try: mi, ext = self.formats_manager.get_selected_format_metadata(self.db, self.book_id) @@ -343,12 +359,15 @@ class MetadataSingleDialogBase(ResizableDialog): error_dialog(self, _('Could not read cover'), _('Could not read cover from %s format')%ext).exec_() return + self.update_cover(cdata, ext) + + def update_cover(self, cdata, fmt): orig = self.cover.current_val self.cover.current_val = cdata if self.cover.current_val is None: self.cover.current_val = orig return error_dialog(self, _('Could not read cover'), - _('The cover in the %s format is invalid')%ext, + _('The cover in the %s format is invalid')%fmt, show=True) return diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 54b5e3a625..8305470624 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -290,7 +290,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.update_font_display() gui.tags_view.reread_collapse_parameters() gui.library_view.refresh_book_details() - if gui.cover_flow is not None: + if hasattr(gui.cover_flow, 'setShowReflections'): gui.cover_flow.setShowReflections(gprefs['cover_browser_reflections']) if __name__ == '__main__':