Add jacket related actions to ebook-polish

This commit is contained in:
Kovid Goyal 2013-02-14 11:49:21 +05:30
parent c6c8cb439f
commit 3e1090d48f
5 changed files with 144 additions and 11 deletions

View File

@ -8,6 +8,7 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, logging, sys, hashlib, uuid, re import os, logging, sys, hashlib, uuid, re
from io import BytesIO
from urllib import unquote as urlunquote, quote as urlquote from urllib import unquote as urlunquote, quote as urlquote
from urlparse import urlparse from urlparse import urlparse
@ -214,6 +215,13 @@ class Container(object):
def opf(self): def opf(self):
return self.parsed(self.opf_name) return self.parsed(self.opf_name)
@property
def mi(self):
from calibre.ebooks.metadata.opf2 import OPF as O
mi = self.serialize_item(self.opf_name)
return O(BytesIO(mi), basedir=self.opf_dir, unquote_urls=False,
populate_spine=False).to_book_metadata()
@property @property
def manifest_id_map(self): def manifest_id_map(self):
return {item.get('id'):self.href_to_name(item.get('href'), self.opf_name) return {item.get('id'):self.href_to_name(item.get('href'), self.opf_name)
@ -376,17 +384,23 @@ class Container(object):
if len(mdata) > 0: if len(mdata) > 0:
mdata[-1].tail = '\n ' mdata[-1].tail = '\n '
def commit_item(self, name): def serialize_item(self, name):
data = self.parsed(name)
if name == self.opf_name: if name == self.opf_name:
self.format_opf() self.format_opf()
self.dirtied.remove(name)
data = self.parsed_cache.pop(name)
data = serialize(data, self.mime_map[name]) data = serialize(data, self.mime_map[name])
if name == self.opf_name: if name == self.opf_name:
# Needed as I can't get lxml to output opf:role and # Needed as I can't get lxml to output opf:role and
# not output <opf:metadata> as well # not output <opf:metadata> as well
data = re.sub(br'(<[/]{0,1})opf:', r'\1', data) data = re.sub(br'(<[/]{0,1})opf:', r'\1', data)
return data
def commit_item(self, name):
if name not in self.parsed_cache:
return
data = self.serialize_item(name)
self.dirtied.remove(name)
self.parsed_cache.pop(name)
with open(self.name_path_map[name], 'wb') as f: with open(self.name_path_map[name], 'wb') as f:
f.write(data) f.write(data)

View File

@ -195,6 +195,7 @@ def set_epub_cover(container, cover_path, report):
cover_page = find_cover_page(container) cover_page = find_cover_page(container)
wrapped_image = extra_cover_page = None wrapped_image = extra_cover_page = None
updated = False updated = False
log = container.log
possible_removals = set(clean_opf(container)) possible_removals = set(clean_opf(container))
possible_removals possible_removals
@ -209,6 +210,7 @@ def set_epub_cover(container, cover_path, report):
cover_page = candidate cover_page = candidate
if cover_page is not None: if cover_page is not None:
log('Found existing cover page')
wrapped_image = find_cover_image_in_page(container, cover_page) wrapped_image = find_cover_image_in_page(container, cover_page)
if len(spine_items) > 1: if len(spine_items) > 1:
@ -217,6 +219,7 @@ def set_epub_cover(container, cover_path, report):
if c != cover_page: if c != cover_page:
candidate = find_cover_image_in_page(container, c) candidate = find_cover_image_in_page(container, c)
if candidate and candidate in {wrapped_image, cover_image}: if candidate and candidate in {wrapped_image, cover_image}:
log('Found an extra cover page that is a simple wrapper, removing it')
# This page has only a single image and that image is the # This page has only a single image and that image is the
# cover image, remove it. # cover image, remove it.
container.remove_item(c) container.remove_item(c)
@ -231,6 +234,7 @@ def set_epub_cover(container, cover_path, report):
if wrapped_image is not None: if wrapped_image is not None:
# The cover page is a simple wrapper around a single cover image, # The cover page is a simple wrapper around a single cover image,
# we can remove it safely. # we can remove it safely.
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) container.remove_item(wrapped_image)
updated = True updated = True

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.customize.ui import output_profiles
from calibre.ebooks.conversion.config import load_defaults
from calibre.ebooks.oeb.base import XPath, OPF
from calibre.ebooks.oeb.polish.cover import find_cover_page
from calibre.ebooks.oeb.transforms.jacket import render_jacket as render
def render_jacket(mi):
ps = load_defaults('page_setup')
op = ps.get('output_profile', 'default')
opmap = {x.short_name:x for x in output_profiles()}
output_profile = opmap.get(op, opmap['default'])
return render(mi, output_profile)
def is_legacy_jacket(root):
return len(root.xpath(
'//*[starts-with(@class,"calibrerescale") and (local-name()="h1" or local-name()="h2")]')) > 0
def is_current_jacket(root):
return len(XPath(
'//h:meta[@name="calibre-content" and @content="jacket"]')(root)) > 0
def find_existing_jacket(container):
for item in container.spine_items:
name = container.abspath_to_name(item)
if name.rpartition('/')[-1].startswith('jacket') and name.endswith('.xhtml'):
root = container.parsed(name)
if is_current_jacket(root) or is_legacy_jacket(root):
return name
def replace_jacket(container, name):
root = render_jacket(container.mi)
container.parsed_cache[name] = root
container.dirty(name)
def remove_jacket(container):
name = find_existing_jacket(container)
if name is not None:
container.remove_item(name)
return True
return False
def add_or_replace_jacket(container):
name = find_existing_jacket(container)
found = True
if name is None:
jacket_item = container.generate_item('jacket.xhtml', id_prefix='jacket')
name = container.href_to_name(jacket_item.get('href'), container.opf_name)
found = False
replace_jacket(container, name)
if not found:
# Insert new jacket into spine
index = 0
sp = container.abspath_to_name(container.spine_items.next())
if sp == find_cover_page(container):
index = 1
itemref = container.opf.makeelement(OPF('itemref'),
idref=jacket_item.get('id'))
container.insert_into_xml(container.opf_xpath('//opf:spine')[0], itemref,
index=index)
return found

View File

@ -15,12 +15,16 @@ from calibre.ebooks.oeb.polish.container import get_container
from calibre.ebooks.oeb.polish.stats import StatsCollector from calibre.ebooks.oeb.polish.stats import StatsCollector
from calibre.ebooks.oeb.polish.subset import subset_all_fonts from calibre.ebooks.oeb.polish.subset import subset_all_fonts
from calibre.ebooks.oeb.polish.cover import set_cover from calibre.ebooks.oeb.polish.cover import set_cover
from calibre.ebooks.oeb.polish.jacket import (
replace_jacket, add_or_replace_jacket, find_existing_jacket, remove_jacket)
from calibre.utils.logging import Log from calibre.utils.logging import Log
ALL_OPTS = { ALL_OPTS = {
'subset': False, 'subset': False,
'opf': None, 'opf': None,
'cover': None, 'cover': None,
'jacket': False,
'remove_jacket':False,
} }
SUPPORTED = {'EPUB', 'AZW3'} SUPPORTED = {'EPUB', 'AZW3'}
@ -59,6 +63,15 @@ characters or completely removed.</p>
date you decide to add more text to your books, the newly added date you decide to add more text to your books, the newly added
text might not be covered by the subset font.</p> text might not be covered by the subset font.</p>
'''), '''),
'jacket': _('''\
<p>Insert a "book jacket" page at the start of the book that contains
all the book metadata such as title, tags, authors, series, commets,
etc.</p>'''),
'remove_jacket': _('''\
<p>Remove a previous inserted book jacket page.</p>
'''),
} }
def hfix(name, raw): def hfix(name, raw):
@ -92,30 +105,54 @@ def polish(file_map, opts, log, report):
rt = lambda x: report('\n### ' + x) rt = lambda x: report('\n### ' + x)
st = time.time() st = time.time()
for inbook, outbook in file_map.iteritems(): for inbook, outbook in file_map.iteritems():
report('## Polishing: %s'%(inbook.rpartition('.')[-1].upper())) report(_('## Polishing: %s')%(inbook.rpartition('.')[-1].upper()))
ebook = get_container(inbook, log) ebook = get_container(inbook, log)
jacket = None
if opts.subset: if opts.subset:
stats = StatsCollector(ebook) stats = StatsCollector(ebook)
if opts.opf: if opts.opf:
rt('Updating metadata') rt(_('Updating metadata'))
update_metadata(ebook, opts.opf) update_metadata(ebook, opts.opf)
report('Metadata updated\n') jacket = find_existing_jacket(ebook)
if jacket is not None:
replace_jacket(ebook, jacket)
report(_('Updated metadata jacket'))
report(_('Metadata updated\n'))
if opts.subset: if opts.subset:
rt('Subsetting embedded fonts') rt(_('Subsetting embedded fonts'))
subset_all_fonts(ebook, stats.font_stats, report) subset_all_fonts(ebook, stats.font_stats, report)
report('') report('')
if opts.cover: if opts.cover:
rt('Setting cover') rt(_('Setting cover'))
set_cover(ebook, opts.cover, report) set_cover(ebook, opts.cover, report)
report('') report('')
if opts.jacket:
rt(_('Inserting metadata jacket'))
if jacket is None:
if add_or_replace_jacket(ebook):
report(_('Existing metadata jacket replaced'))
else:
report(_('Metadata jacket inserted'))
else:
report(_('Existing metadata jacket replaced'))
report('')
if opts.remove_jacket:
rt(_('Removing metadata jacket'))
if remove_jacket(ebook):
report(_('Metadata jacket removed'))
else:
report(_('No metadata jacket found'))
report('')
ebook.commit(outbook) ebook.commit(outbook)
report('-'*70) report('-'*70)
report('Polishing took: %.1f seconds'%(time.time()-st)) report(_('Polishing took: %.1f seconds')%(time.time()-st))
REPORT = '{0} REPORT {0}'.format('-'*30) REPORT = '{0} REPORT {0}'.format('-'*30)
@ -151,6 +188,8 @@ def option_parser():
'If no cover is present, or the cover is not properly identified, inserts a new cover.')) 'If no cover is present, or the cover is not properly identified, inserts a new cover.'))
a('--opf', '-o', help=_( a('--opf', '-o', help=_(
'Path to an OPF file. The metadata in the book is updated from the OPF file.')) 'Path to an OPF file. The metadata in the book is updated from the OPF file.'))
o('--jacket', '-j', help=CLI_HELP['jacket'])
o('--remove-jacket', help=CLI_HELP['remove_jacket'])
o('--verbose', help=_('Produce more verbose output, useful for debugging.')) o('--verbose', help=_('Produce more verbose output, useful for debugging.'))

View File

@ -28,8 +28,10 @@ class Polish(QDialog): # {{{
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.db, self.book_id_map = weakref.ref(db), book_id_map self.db, self.book_id_map = weakref.ref(db), book_id_map
self.setWindowIcon(QIcon(I('polish.png'))) self.setWindowIcon(QIcon(I('polish.png')))
self.setWindowTitle(ngettext( title = _('Polish book')
'Polish book', _('Polish %d books')%len(book_id_map), len(book_id_map))) if len(book_id_map) > 1:
title = _('Polish %d books')%len(book_id_map)
self.setWindowTitle(title)
self.help_text = { self.help_text = {
'polish': _('<h3>About Polishing books</h3>%s')%HELP['about'], 'polish': _('<h3>About Polishing books</h3>%s')%HELP['about'],
@ -44,6 +46,8 @@ class Polish(QDialog): # {{{
' <p>Note that most ebook' ' <p>Note that most ebook'
' formats are not capable of supporting all the' ' formats are not capable of supporting all the'
' metadata in calibre.</p>'), ' metadata in calibre.</p>'),
'jacket':_('<h3>Book Jacket</h3>%s')%HELP['jacket'],
'remove_jacket':_('<h3>Remove Book Jacket</h3>%s')%HELP['jacket'],
} }
self.l = l = QGridLayout() self.l = l = QGridLayout()
@ -56,6 +60,8 @@ class Polish(QDialog): # {{{
self.all_actions = OrderedDict([ self.all_actions = OrderedDict([
('subset', _('Subset all embedded fonts')), ('subset', _('Subset all embedded fonts')),
('metadata', _('Update metadata in book files')), ('metadata', _('Update metadata in book files')),
('jacket', _('Add metadata as a "book jacket" page')),
('remove_jacket', _('Remove a previously inserted book jacket')),
]) ])
for name, text in self.all_actions.iteritems(): for name, text in self.all_actions.iteritems():
count += 1 count += 1