From 95ea76222d50ed1697e231a868f3f5b917582f29 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Jan 2013 13:56:17 +0530 Subject: [PATCH 1/2] ... --- src/calibre/gui2/preferences/look_feel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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__': From 037efa42d8d1294781537e2706f556c6ecfbd2e7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Jan 2013 14:24:26 +0530 Subject: [PATCH 2/2] Allow choosing which page of a PDF to use as the cover. To access this functionality add the PDF to calibre then click the edit metadata button. In the top right area of the edit metadata dialog there is a button to get the cover from the ebook file, this will now allow you to choose which page (from the first ten pages) of the pdf to use as the cover. Fixes #1110019 ([Enhancement] Pick interior page of PDF for cover image) --- src/calibre/ebooks/metadata/pdf.py | 29 ++++-- src/calibre/gui2/metadata/basic_widgets.py | 21 ++++ src/calibre/gui2/metadata/pdf_covers.py | 108 +++++++++++++++++++++ src/calibre/gui2/metadata/single.py | 21 +++- 4 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 src/calibre/gui2/metadata/pdf_covers.py 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