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