mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add jacket related actions to ebook-polish
This commit is contained in:
parent
c6c8cb439f
commit
3e1090d48f
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
70
src/calibre/ebooks/oeb/polish/jacket.py
Normal file
70
src/calibre/ebooks/oeb/polish/jacket.py
Normal 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
|
||||||
|
|
@ -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.'))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user