From c1ae0a275ffd89ca938c6f735d798b79cdebf21b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 30 Nov 2013 17:43:44 +0530 Subject: [PATCH] Implement marking of images as covers --- src/calibre/ebooks/oeb/polish/container.py | 4 ++ src/calibre/ebooks/oeb/polish/cover.py | 48 ++++++++++++++++++++++ src/calibre/gui2/tweak_book/boss.py | 14 +++++++ src/calibre/gui2/tweak_book/file_list.py | 13 +++++- 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index dca3150e1a..904c5e60ab 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -697,6 +697,10 @@ class Container(object): # {{{ mdata.remove(child) if len(mdata) > 0: mdata[-1].tail = '\n ' + # Ensure name comes before content, needed for Nooks + for meta in self.opf_xpath('//opf:meta[@name="cover"]'): + if 'content' in meta.attrib: + meta.set('content', meta.attrib.pop('content')) def serialize_item(self, name): data = self.parsed(name) diff --git a/src/calibre/ebooks/oeb/polish/cover.py b/src/calibre/ebooks/oeb/polish/cover.py index 3104cff38d..342c989dcd 100644 --- a/src/calibre/ebooks/oeb/polish/cover.py +++ b/src/calibre/ebooks/oeb/polish/cover.py @@ -38,6 +38,12 @@ def get_azw3_raster_cover_name(container): if items: return container.href_to_name(items[0].get('href')) +def mark_as_cover_azw3(container, name): + href = container.name_to_href(name, container.opf_name) + for item in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'): + item.set('href', href) + container.dirty(container.opf_name) + def get_raster_cover_name(container): if container.book_type == 'azw3': return get_azw3_raster_cover_name(container) @@ -54,6 +60,17 @@ def set_cover(container, cover_path, report): else: set_epub_cover(container, cover_path, report) +def mark_as_cover(container, name): + if name not in container.mime_map: + raise ValueError('Cannot mark %s as cover as it does not exist' % name) + mt = container.mime_map[name] + if not is_raster_image(mt): + raise ValueError('Cannot mark %s as the cover image as it is not a raster image' % name) + if container.book_type == 'azw3': + mark_as_cover_azw3(container, name) + else: + mark_as_cover_epub(container, name) + ############################################################################### # The delightful EPUB cover processing @@ -100,6 +117,37 @@ def find_cover_image(container, strict=False): if largest_cover[0]: return largest_cover[0] +def mark_as_cover_epub(container, name): + mmap = {v:k for k, v in container.manifest_id_map.iteritems()} + if name not in mmap: + raise ValueError('Cannot mark %s as cover as it is not in manifest' % name) + mid = mmap[name] + + # Remove all entries from the opf that identify a raster image as cover + for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'): + container.remove_from_xml(meta) + for ref in container.opf_xpath('//opf:guide/opf:reference[@href and @type]'): + if ref.get('type').lower() not in COVER_TYPES: + continue + name = container.href_to_name(ref.get('href'), container.opf_name) + mt = container.mime_map.get(name, None) + if is_raster_image(mt): + container.remove_from_xml(ref) + + # Add reference to image in + for metadata in container.opf_xpath('//opf:metadata'): + m = metadata.makeelement(OPF('meta'), name='cover', content=mid) + container.insert_into_xml(metadata, m) + + # If no entry for titlepage exists in guide, insert one that points to this + # image + if not container.opf_xpath('//opf:guide/opf:reference[@type="cover"]'): + for guide in container.opf_xpath('//opf:guide'): + container.insert_into_xml(guide, guide.makeelement( + OPF('reference', type='cover', href=container.name_to_href(name, container.opf_name)))) + + container.dirty(container.opf_name) + def find_cover_page(container): 'Find a document marked as a cover in the OPF' mm = container.mime_map diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index fd63d998b5..1b7072a958 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -19,6 +19,7 @@ from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ebooks.oeb.base import urlnormalize from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type +from calibre.ebooks.oeb.polish.cover import mark_as_cover from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.replace import rename_files from calibre.ebooks.oeb.polish.split import split, merge, AbortError @@ -57,6 +58,7 @@ class Boss(QObject): fl.rename_requested.connect(self.rename_requested) fl.edit_file.connect(self.edit_file_requested) fl.merge_requested.connect(self.merge_requested) + fl.mark_requested.connect(self.mark_requested) self.gui.central.current_editor_changed.connect(self.apply_current_editor_state) self.gui.central.close_requested.connect(self.editor_close_requested) self.gui.central.search_panel.search_triggered.connect(self.search) @@ -70,6 +72,18 @@ class Boss(QObject): for ed in editors.itervalues(): ed.apply_settings() + def mark_requested(self, name, action): + if not self.check_opf_dirtied(): + return + c = current_container() + if action == 'cover': + mark_as_cover(current_container(), name) + + if c.opf_name in editors: + editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) + self.gui.file_list.build(c) + self.set_modified() + def mkdtemp(self, prefix=''): self.container_count += 1 return tempfile.mkdtemp(prefix='%s%05d-' % (prefix, self.container_count), dir=self.tdir) diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 0f972d0a71..698761e52b 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -18,7 +18,8 @@ from PyQt4.Qt import ( from calibre import human_readable, sanitize_file_name_unicode from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS from calibre.ebooks.oeb.polish.container import guess_type, OEB_FONTS -from calibre.ebooks.oeb.polish.cover import get_cover_page_name, get_raster_cover_name +from calibre.ebooks.oeb.polish.cover import ( + get_cover_page_name, get_raster_cover_name, is_raster_image) from calibre.gui2 import error_dialog, choose_files from calibre.gui2.tweak_book import current_container, elided_text from calibre.gui2.tweak_book.editor import syntax_from_mime @@ -83,6 +84,7 @@ class FileList(QTreeWidget): rename_requested = pyqtSignal(object, object) edit_file = pyqtSignal(object, object, object) merge_requested = pyqtSignal(object, object, object) + mark_requested = pyqtSignal(object, object) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) @@ -306,7 +308,10 @@ class FileList(QTreeWidget): ci = self.currentItem() if ci is not None: cn = unicode(ci.data(0, NAME_ROLE).toString()) + mt = unicode(ci.data(0, MIME_ROLE).toString()) m.addAction(QIcon(I('modified.png')), _('&Rename %s') % (elided_text(self.font(), cn)), self.edit_current_item) + if is_raster_image(mt): + m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % elided_text(self.font(), cn), partial(self.mark_as_cover, cn)) selected_map = defaultdict(list) for item in sel: @@ -340,6 +345,9 @@ class FileList(QTreeWidget): if self.currentItem() is not None: self.editItem(self.currentItem()) + def mark_as_cover(self, name): + self.mark_requested.emit(name, 'cover') + def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace): ev.accept() @@ -544,6 +552,7 @@ class FileListWidget(QWidget): rename_requested = pyqtSignal(object, object) edit_file = pyqtSignal(object, object, object) merge_requested = pyqtSignal(object, object, object) + mark_requested = pyqtSignal(object, object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -551,7 +560,7 @@ class FileListWidget(QWidget): self.file_list = FileList(self) self.layout().addWidget(self.file_list) self.layout().setContentsMargins(0, 0, 0, 0) - for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file', 'merge_requested'): + for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file', 'merge_requested', 'mark_requested'): getattr(self.file_list, x).connect(getattr(self, x)) for x in ('delete_done', 'select_name'): setattr(self, x, getattr(self.file_list, x))