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 * Various compatibility checks for known problems that can cause the book
to malfunction on reader devices. 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 Embedding referenced fonts
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -11,25 +11,31 @@ import shutil, re, os
from calibre.ebooks.oeb.base import OPF, OEB_DOCS, XPath, XLINK, xml2text from calibre.ebooks.oeb.base import OPF, OEB_DOCS, XPath, XLINK, xml2text
from calibre.ebooks.oeb.polish.replace import replace_links 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 name = None
found = True found = True
for gi in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'): for gi in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'):
href = gi.get('href') href = gi.get('href')
name = container.href_to_name(href, container.opf_name) name = container.href_to_name(href, container.opf_name)
container.remove_from_xml(gi) container.remove_from_xml(gi)
if name is None or not container.has_name(name): if existing_image:
item = container.generate_item(name='cover.jpeg', id_prefix='cover') name = cover_path
name = container.href_to_name(item.get('href'), container.opf_name)
found = False 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) href = container.name_to_href(name, container.opf_name)
guide = container.opf_xpath('//opf:guide')[0] guide = container.opf_xpath('//opf:guide')[0]
container.insert_into_xml(guide, guide.makeelement( container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), href=href, type='cover')) OPF('reference'), href=href, type='cover'))
with open(cover_path, 'rb') as src, container.open(name, 'wb') as dest: if not existing_image:
shutil.copyfileobj(src, dest) with open(cover_path, 'rb') as src, container.open(name, 'wb') as dest:
shutil.copyfileobj(src, dest)
container.dirty(container.opf_name) container.dirty(container.opf_name)
report('Cover updated' if found else 'Cover inserted') report('Cover updated' if found else 'Cover inserted')
@ -60,11 +66,11 @@ def get_cover_page_name(container):
return return
return find_cover_page(container) 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': if container.book_type == 'azw3':
set_azw3_cover(container, cover_path, report) set_azw3_cover(container, cover_path, report, options=options)
else: else:
set_epub_cover(container, cover_path, report) set_epub_cover(container, cover_path, report, options=options)
def mark_as_cover(container, name): def mark_as_cover(container, name):
if name not in container.mime_map: if name not in container.mime_map:
@ -226,26 +232,38 @@ def clean_opf(container):
container.dirty(container.opf_name) 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.conversion.config import load_defaults
from calibre.ebooks.oeb.transforms.cover import CoverManager from calibre.ebooks.oeb.transforms.cover import CoverManager
ext = cover_path.rpartition('.')[-1].lower() ext = cover_path.rpartition('.')[-1].lower()
raster_cover_item = container.generate_item('cover.'+ext, id_prefix='cover') if existing_image:
raster_cover = container.href_to_name(raster_cover_item.get('href'), raster_cover = existing_image
container.opf_name) manifest_id = {v:k for k, v in container.manifest_id_map.iteritems()}[existing_image]
with open(cover_path, 'rb') as src, container.open(raster_cover, 'wb') as dest: raster_cover_item = container.opf_xpath('//opf:manifest/*[@id="%s"]' % manifest_id)[0]
shutil.copyfileobj(src, dest) else:
opts = load_defaults('epub_output') raster_cover_item = container.generate_item('cover.'+ext, id_prefix='cover')
keep_aspect = opts.get('preserve_cover_aspect_ratio', False) raster_cover = container.href_to_name(raster_cover_item.get('href'), container.opf_name)
no_svg = opts.get('no_svg_cover', False)
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: if no_svg:
style = 'style="height: 100%%"' style = 'style="height: 100%%"'
templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style) templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style)
else: else:
width, height = 600, 800 width, height = 600, 800
try: 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: except:
container.log.exception("Failed to get width and height of cover") container.log.exception("Failed to get width and height of cover")
ar = 'xMidYMid meet' if keep_aspect else 'none' 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) img.getparent().remove(img)
break 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_image = find_cover_image(container)
cover_page = find_cover_page(container) cover_page = find_cover_page(container)
wrapped_image = extra_cover_page = None wrapped_image = extra_cover_page = None
@ -340,15 +361,17 @@ def set_epub_cover(container, cover_path, report):
# we can remove it safely. # we can remove it safely.
log('Existing cover page is a simple wrapper, removing it') log('Existing cover page is a simple wrapper, removing it')
container.remove_item(cover_page) container.remove_item(cover_page)
container.remove_item(wrapped_image) if wrapped_image != existing_image:
container.remove_item(wrapped_image)
updated = True updated = True
if cover_image and cover_image != wrapped_image: if cover_image and cover_image != wrapped_image:
# Remove the old cover 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 # 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') 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 { link_sub = {s:d for s, d in {
cover_page:titlepage, wrapped_image:raster_cover, cover_page:titlepage, wrapped_image:raster_cover,
cover_image:raster_cover, extra_cover_page:titlepage}.iteritems() 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: if link_sub:
replace_links(container, link_sub, frag_map=lambda x, y:None) 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.base import urlnormalize
from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish 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.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.css import filter_css
from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all 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 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 = AddCover(current_container(), self.gui)
d.import_requested.connect(self.do_add_file) d.import_requested.connect(self.do_add_file)
try: try:
if d.exec_() == d.Accepted: if d.exec_() == d.Accepted and d.file_name is not None:
pass 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: finally:
d.import_requested.disconnect() d.import_requested.disconnect()

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import (
QPoint, QSizePolicy, QPainter, QStaticText, pyqtSignal, QTextOption, QPoint, QSizePolicy, QPainter, QStaticText, pyqtSignal, QTextOption,
QAbstractListModel, QModelIndex, QVariant, QStyledItemDelegate, QStyle, QAbstractListModel, QModelIndex, QVariant, QStyledItemDelegate, QStyle,
QListView, QTextDocument, QSize, QComboBox, QFrame, QCursor, QCheckBox, 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 import prepare_string_for_xml, human_readable
from calibre.ebooks.oeb.polish.utils import lead_text, guess_type from calibre.ebooks.oeb.polish.utils import lead_text, guess_type
@ -1056,13 +1056,19 @@ class AddCover(Dialog):
def setup_ui(self): def setup_ui(self):
self.l = l = QVBoxLayout(self) self.l = l = QVBoxLayout(self)
self.setLayout(l) 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) self.cover_view = CoverView(self)
l.addWidget(self.names_filter) l.addWidget(self.names_filter)
v.addWidget(self.names)
self.splitter = s = QSplitter(self) self.splitter = s = QSplitter(self)
l.addWidget(s) l.addWidget(s)
s.addWidget(self.names) s.addWidget(gb)
s.addWidget(self.cover_view) s.addWidget(self.cover_view)
self.h = h = QHBoxLayout() self.h = h = QHBoxLayout()
@ -1087,9 +1093,16 @@ class AddCover(Dialog):
self.names.setFocus(Qt.OtherFocusReason) self.names.setFocus(Qt.OtherFocusReason)
self.names.selectionModel().currentChanged.connect(self.current_image_changed) 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): def current_image_changed(self):
self.info_label.setText('') self.info_label.setText('')
name = self.names.model().name_for_index(self.names.currentIndex()) name = self.file_name
if name is not None: if name is not None:
data = self.container.raw_data(name, decode=False) data = self.container.raw_data(name, decode=False)
self.cover_view.set_pixmap(data) self.cover_view.set_pixmap(data)