diff --git a/manual/edit.rst b/manual/edit.rst index 95e3a60de7..033ffb7b6d 100644 --- a/manual/edit.rst +++ b/manual/edit.rst @@ -327,6 +327,17 @@ Some of the checks performed are: * Various compatibility checks for known problems that can cause the book to malfunction on reader devices. +Add a cover +^^^^^^^^^^^^ + +You can easily add a cover to the book via :guilabel:`Tools->Add cover`. This +allows you to either choose an existing image in the book as the cover or +import a new image into the book and make it the cover. When editing EPUB +files, the HTML wrapper for the cover is automatically generated. If an +existing cover in the book is found, it is replaced. The tool also +automatically takes care of correctly marking the cover files as covers in the +OPF. + Embedding referenced fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/calibre/ebooks/oeb/polish/cover.py b/src/calibre/ebooks/oeb/polish/cover.py index 0f531385d9..1784c627f7 100644 --- a/src/calibre/ebooks/oeb/polish/cover.py +++ b/src/calibre/ebooks/oeb/polish/cover.py @@ -11,25 +11,31 @@ import shutil, re, os from calibre.ebooks.oeb.base import OPF, OEB_DOCS, XPath, XLINK, xml2text from calibre.ebooks.oeb.polish.replace import replace_links -from calibre.utils.magick.draw import identify +from calibre.utils.magick.draw import identify, identify_data -def set_azw3_cover(container, cover_path, report): +def set_azw3_cover(container, cover_path, report, options=None): + existing_image = options is not None and options.get('existing_image', False) name = None found = True for gi in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'): href = gi.get('href') name = container.href_to_name(href, container.opf_name) container.remove_from_xml(gi) - if name is None or not container.has_name(name): - item = container.generate_item(name='cover.jpeg', id_prefix='cover') - name = container.href_to_name(item.get('href'), container.opf_name) + if existing_image: + name = cover_path found = False + else: + if name is None or not container.has_name(name): + item = container.generate_item(name='cover.jpeg', id_prefix='cover') + name = container.href_to_name(item.get('href'), container.opf_name) + found = False href = container.name_to_href(name, container.opf_name) guide = container.opf_xpath('//opf:guide')[0] container.insert_into_xml(guide, guide.makeelement( OPF('reference'), href=href, type='cover')) - with open(cover_path, 'rb') as src, container.open(name, 'wb') as dest: - shutil.copyfileobj(src, dest) + if not existing_image: + with open(cover_path, 'rb') as src, container.open(name, 'wb') as dest: + shutil.copyfileobj(src, dest) container.dirty(container.opf_name) report('Cover updated' if found else 'Cover inserted') @@ -60,11 +66,11 @@ def get_cover_page_name(container): return return find_cover_page(container) -def set_cover(container, cover_path, report): +def set_cover(container, cover_path, report, options=None): if container.book_type == 'azw3': - set_azw3_cover(container, cover_path, report) + set_azw3_cover(container, cover_path, report, options=options) else: - set_epub_cover(container, cover_path, report) + set_epub_cover(container, cover_path, report, options=options) def mark_as_cover(container, name): if name not in container.mime_map: @@ -226,26 +232,38 @@ def clean_opf(container): container.dirty(container.opf_name) -def create_epub_cover(container, cover_path): +def create_epub_cover(container, cover_path, existing_image, options=None): from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.transforms.cover import CoverManager ext = cover_path.rpartition('.')[-1].lower() - raster_cover_item = container.generate_item('cover.'+ext, id_prefix='cover') - raster_cover = container.href_to_name(raster_cover_item.get('href'), - container.opf_name) - with open(cover_path, 'rb') as src, container.open(raster_cover, 'wb') as dest: - shutil.copyfileobj(src, dest) - opts = load_defaults('epub_output') - keep_aspect = opts.get('preserve_cover_aspect_ratio', False) - no_svg = opts.get('no_svg_cover', False) + if existing_image: + raster_cover = existing_image + manifest_id = {v:k for k, v in container.manifest_id_map.iteritems()}[existing_image] + raster_cover_item = container.opf_xpath('//opf:manifest/*[@id="%s"]' % manifest_id)[0] + else: + raster_cover_item = container.generate_item('cover.'+ext, id_prefix='cover') + raster_cover = container.href_to_name(raster_cover_item.get('href'), container.opf_name) + + with open(cover_path, 'rb') as src, container.open(raster_cover, 'wb') as dest: + shutil.copyfileobj(src, dest) + if options is None: + opts = load_defaults('epub_output') + keep_aspect = opts.get('preserve_cover_aspect_ratio', False) + no_svg = opts.get('no_svg_cover', False) + else: + keep_aspect = options.get('keep_aspect', False) + no_svg = options.get('no_svg', False) if no_svg: style = 'style="height: 100%%"' templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style) else: width, height = 600, 800 try: - width, height = identify(cover_path)[:2] + if existing_image: + width, height = identify_data(container.raw_data(existing_image, decode=False))[:2] + else: + width, height = identify(cover_path)[:2] except: container.log.exception("Failed to get width and height of cover") ar = 'xMidYMid meet' if keep_aspect else 'none' @@ -294,7 +312,10 @@ def remove_cover_image_in_page(container, page, cover_images): img.getparent().remove(img) break -def set_epub_cover(container, cover_path, report): +def set_epub_cover(container, cover_path, report, options=None): + existing_image = options is not None and options.get('existing_image', False) + if existing_image: + existing_image = cover_path cover_image = find_cover_image(container) cover_page = find_cover_page(container) wrapped_image = extra_cover_page = None @@ -340,15 +361,17 @@ def set_epub_cover(container, cover_path, report): # we can remove it safely. log('Existing cover page is a simple wrapper, removing it') container.remove_item(cover_page) - container.remove_item(wrapped_image) + if wrapped_image != existing_image: + container.remove_item(wrapped_image) updated = True if cover_image and cover_image != wrapped_image: # Remove the old cover image - container.remove_item(cover_image) + if cover_image != existing_image: + container.remove_item(cover_image) # Insert the new cover - raster_cover, titlepage = create_epub_cover(container, cover_path) + raster_cover, titlepage = create_epub_cover(container, cover_path, existing_image, options=options) report('Cover updated' if updated else 'Cover inserted') @@ -356,7 +379,7 @@ def set_epub_cover(container, cover_path, report): link_sub = {s:d for s, d in { cover_page:titlepage, wrapped_image:raster_cover, cover_image:raster_cover, extra_cover_page:titlepage}.iteritems() - if s is not None} + if s is not None and s != d} if link_sub: replace_links(container, link_sub, frag_map=lambda x, y:None) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 31984eb962..2265f268e7 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -18,7 +18,7 @@ from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory 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, OEB_FONTS, OEB_DOCS, OEB_STYLES -from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage +from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage, set_cover from calibre.ebooks.oeb.polish.css import filter_css from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_recommended_folders, rationalize_folders @@ -412,8 +412,13 @@ class Boss(QObject): d = AddCover(current_container(), self.gui) d.import_requested.connect(self.do_add_file) try: - if d.exec_() == d.Accepted: - pass + if d.exec_() == d.Accepted and d.file_name is not None: + report = [] + with BusyCursor(): + self.add_savepoint(_('Before: Add cover')) + set_cover(current_container(), d.file_name, report.append, options={ + 'existing_image':True, 'keep_aspect':tprefs['add_cover_preserve_aspect_ratio']}) + self.apply_container_update_to_gui() finally: d.import_requested.disconnect() diff --git a/src/calibre/gui2/tweak_book/widgets.py b/src/calibre/gui2/tweak_book/widgets.py index 85ca0c1002..1809dbbc88 100644 --- a/src/calibre/gui2/tweak_book/widgets.py +++ b/src/calibre/gui2/tweak_book/widgets.py @@ -16,7 +16,7 @@ from PyQt4.Qt import ( QPoint, QSizePolicy, QPainter, QStaticText, pyqtSignal, QTextOption, QAbstractListModel, QModelIndex, QVariant, QStyledItemDelegate, QStyle, QListView, QTextDocument, QSize, QComboBox, QFrame, QCursor, QCheckBox, - QSplitter, QPixmap, QRect) + QSplitter, QPixmap, QRect, QGroupBox) from calibre import prepare_string_for_xml, human_readable from calibre.ebooks.oeb.polish.utils import lead_text, guess_type @@ -1056,13 +1056,19 @@ class AddCover(Dialog): def setup_ui(self): self.l = l = QVBoxLayout(self) self.setLayout(l) - self.names, self.names_filter = create_filterable_names_list(sorted(self.image_names, key=sort_key), filter_text=_('Filter the list of images')) + self.gb = gb = QGroupBox(_('&Images in book'), self) + self.v = v = QVBoxLayout(gb) + gb.setLayout(v), gb.setFlat(True) + self.names, self.names_filter = create_filterable_names_list( + sorted(self.image_names, key=sort_key), filter_text=_('Filter the list of images'), parent=self) + self.names.doubleClicked.connect(self.double_clicked, type=Qt.QueuedConnection) self.cover_view = CoverView(self) l.addWidget(self.names_filter) + v.addWidget(self.names) self.splitter = s = QSplitter(self) l.addWidget(s) - s.addWidget(self.names) + s.addWidget(gb) s.addWidget(self.cover_view) self.h = h = QHBoxLayout() @@ -1087,9 +1093,16 @@ class AddCover(Dialog): self.names.setFocus(Qt.OtherFocusReason) self.names.selectionModel().currentChanged.connect(self.current_image_changed) + def double_clicked(self): + self.accept() + + @property + def file_name(self): + return self.names.model().name_for_index(self.names.currentIndex()) + def current_image_changed(self): self.info_label.setText('') - name = self.names.model().name_for_index(self.names.currentIndex()) + name = self.file_name if name is not None: data = self.container.raw_data(name, decode=False) self.cover_view.set_pixmap(data)