Edit Book: new tool to easily add a cover to the book. It automatically generates the HTML wrapper and takes care of marking the covers files as covers in the OPF.

This commit is contained in:
Kovid Goyal 2014-06-26 09:28:20 +05:30
parent 00a368909a
commit 4521c70f5a
4 changed files with 84 additions and 32 deletions

View File

@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -11,15 +11,20 @@ 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 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)
@ -28,6 +33,7 @@ def set_azw3_cover(container, cover_path, report):
guide = container.opf_xpath('//opf:guide')[0]
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), href=href, type='cover'))
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)
@ -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,25 +232,37 @@ 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()
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)
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:
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")
@ -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)
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
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)

View File

@ -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()

View File

@ -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)