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
|
||||
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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user