From 3e1090d48f280023284a5ac8c1979a78f3366d10 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 14 Feb 2013 11:49:21 +0530
Subject: [PATCH] Add jacket related actions to ebook-polish
---
src/calibre/ebooks/oeb/polish/container.py | 20 ++++++-
src/calibre/ebooks/oeb/polish/cover.py | 4 ++
src/calibre/ebooks/oeb/polish/jacket.py | 70 ++++++++++++++++++++++
src/calibre/ebooks/oeb/polish/main.py | 51 ++++++++++++++--
src/calibre/gui2/actions/polish.py | 10 +++-
5 files changed, 144 insertions(+), 11 deletions(-)
create mode 100644 src/calibre/ebooks/oeb/polish/jacket.py
diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py
index 9b84ff82e1..187669cc13 100644
--- a/src/calibre/ebooks/oeb/polish/container.py
+++ b/src/calibre/ebooks/oeb/polish/container.py
@@ -8,6 +8,7 @@ __copyright__ = '2013, Kovid Goyal '
__docformat__ = 'restructuredtext en'
import os, logging, sys, hashlib, uuid, re
+from io import BytesIO
from urllib import unquote as urlunquote, quote as urlquote
from urlparse import urlparse
@@ -214,6 +215,13 @@ class Container(object):
def opf(self):
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
def manifest_id_map(self):
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:
mdata[-1].tail = '\n '
- def commit_item(self, name):
+ def serialize_item(self, name):
+ data = self.parsed(name)
if name == self.opf_name:
self.format_opf()
- self.dirtied.remove(name)
- data = self.parsed_cache.pop(name)
data = serialize(data, self.mime_map[name])
if name == self.opf_name:
# Needed as I can't get lxml to output opf:role and
# not output as well
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:
f.write(data)
diff --git a/src/calibre/ebooks/oeb/polish/cover.py b/src/calibre/ebooks/oeb/polish/cover.py
index fafd1de4f1..e246476d0a 100644
--- a/src/calibre/ebooks/oeb/polish/cover.py
+++ b/src/calibre/ebooks/oeb/polish/cover.py
@@ -195,6 +195,7 @@ def set_epub_cover(container, cover_path, report):
cover_page = find_cover_page(container)
wrapped_image = extra_cover_page = None
updated = False
+ log = container.log
possible_removals = set(clean_opf(container))
possible_removals
@@ -209,6 +210,7 @@ def set_epub_cover(container, cover_path, report):
cover_page = candidate
if cover_page is not None:
+ log('Found existing cover page')
wrapped_image = find_cover_image_in_page(container, cover_page)
if len(spine_items) > 1:
@@ -217,6 +219,7 @@ def set_epub_cover(container, cover_path, report):
if c != cover_page:
candidate = find_cover_image_in_page(container, c)
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
# cover image, remove it.
container.remove_item(c)
@@ -231,6 +234,7 @@ def set_epub_cover(container, cover_path, report):
if wrapped_image is not None:
# The cover page is a simple wrapper around a single cover image,
# 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)
updated = True
diff --git a/src/calibre/ebooks/oeb/polish/jacket.py b/src/calibre/ebooks/oeb/polish/jacket.py
new file mode 100644
index 0000000000..b6286830a4
--- /dev/null
+++ b/src/calibre/ebooks/oeb/polish/jacket.py
@@ -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 '
+__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
+
diff --git a/src/calibre/ebooks/oeb/polish/main.py b/src/calibre/ebooks/oeb/polish/main.py
index 2c227cef1f..f2f5287a6b 100644
--- a/src/calibre/ebooks/oeb/polish/main.py
+++ b/src/calibre/ebooks/oeb/polish/main.py
@@ -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.subset import subset_all_fonts
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
ALL_OPTS = {
'subset': False,
'opf': None,
'cover': None,
+ 'jacket': False,
+ 'remove_jacket':False,
}
SUPPORTED = {'EPUB', 'AZW3'}
@@ -59,6 +63,15 @@ characters or completely removed.
date you decide to add more text to your books, the newly added
text might not be covered by the subset font.
'''),
+
+'jacket': _('''\
+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.
'''),
+
+'remove_jacket': _('''\
+Remove a previous inserted book jacket page.
+'''),
}
def hfix(name, raw):
@@ -92,30 +105,54 @@ def polish(file_map, opts, log, report):
rt = lambda x: report('\n### ' + x)
st = time.time()
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)
+ jacket = None
if opts.subset:
stats = StatsCollector(ebook)
if opts.opf:
- rt('Updating metadata')
+ rt(_('Updating metadata'))
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:
- rt('Subsetting embedded fonts')
+ rt(_('Subsetting embedded fonts'))
subset_all_fonts(ebook, stats.font_stats, report)
report('')
if opts.cover:
- rt('Setting cover')
+ rt(_('Setting cover'))
set_cover(ebook, opts.cover, 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)
report('-'*70)
- report('Polishing took: %.1f seconds'%(time.time()-st))
+ report(_('Polishing took: %.1f seconds')%(time.time()-st))
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.'))
a('--opf', '-o', help=_(
'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.'))
diff --git a/src/calibre/gui2/actions/polish.py b/src/calibre/gui2/actions/polish.py
index d35bc21886..6b03bfb9f4 100644
--- a/src/calibre/gui2/actions/polish.py
+++ b/src/calibre/gui2/actions/polish.py
@@ -28,8 +28,10 @@ class Polish(QDialog): # {{{
QDialog.__init__(self, parent)
self.db, self.book_id_map = weakref.ref(db), book_id_map
self.setWindowIcon(QIcon(I('polish.png')))
- self.setWindowTitle(ngettext(
- 'Polish book', _('Polish %d books')%len(book_id_map), len(book_id_map)))
+ title = _('Polish book')
+ if len(book_id_map) > 1:
+ title = _('Polish %d books')%len(book_id_map)
+ self.setWindowTitle(title)
self.help_text = {
'polish': _('About Polishing books
%s')%HELP['about'],
@@ -44,6 +46,8 @@ class Polish(QDialog): # {{{
' Note that most ebook'
' formats are not capable of supporting all the'
' metadata in calibre.
'),
+ 'jacket':_('Book Jacket
%s')%HELP['jacket'],
+ 'remove_jacket':_('Remove Book Jacket
%s')%HELP['jacket'],
}
self.l = l = QGridLayout()
@@ -56,6 +60,8 @@ class Polish(QDialog): # {{{
self.all_actions = OrderedDict([
('subset', _('Subset all embedded fonts')),
('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():
count += 1