mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
00a368909a
commit
4521c70f5a
@ -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
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user