From 78be5af05a164d1463c24ec0dc40b3e1c7daac26 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 16 Jun 2016 12:18:19 +0530 Subject: [PATCH] EPUB metadata: When setting a cover image for an EPUB file that has no cover image defined, add the new cover image instead of aborting. Note that this is not a full EPUB cover, for that you still have to convert/use book polishing. However, this limited support should at least allow the cover image to work with those applications that support the EPUB raster cover metadata standard. --- src/calibre/ebooks/metadata/epub.py | 6 +++--- src/calibre/ebooks/metadata/opf.py | 28 ++++++++++++++++++++++------ src/calibre/ebooks/metadata/opf2.py | 3 ++- src/calibre/ebooks/metadata/utils.py | 25 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/metadata/epub.py b/src/calibre/ebooks/metadata/epub.py index 8e66ab38a7..ae0d9197a7 100644 --- a/src/calibre/ebooks/metadata/epub.py +++ b/src/calibre/ebooks/metadata/epub.py @@ -276,7 +276,7 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False, force_ide pass opfbytes, ver, raster_cover = set_metadata_opf( - reader.read_bytes(reader.opf_path), mi, + reader.read_bytes(reader.opf_path), posixpath.dirname(reader.opf_path), mi, cover_data=new_cdata, apply_null=apply_null, update_timestamp=update_timestamp, force_identifiers=force_identifiers) cpath = None replacements = {} @@ -294,10 +294,10 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False, force_ide if isinstance(reader.archive, LocalZipFile): reader.archive.safe_replace(reader.container[OPF.MIMETYPE], opfbytes, - extra_replacements=replacements) + extra_replacements=replacements, add_missing=True) else: safe_replace(stream, reader.container[OPF.MIMETYPE], opfbytes, - extra_replacements=replacements) + extra_replacements=replacements, add_missing=True) try: if cpath is not None: replacements[cpath].close() diff --git a/src/calibre/ebooks/metadata/opf.py b/src/calibre/ebooks/metadata/opf.py index 038e3fef89..e713d4c120 100644 --- a/src/calibre/ebooks/metadata/opf.py +++ b/src/calibre/ebooks/metadata/opf.py @@ -6,8 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) from calibre.ebooks.metadata import parse_opf_version -from calibre.ebooks.metadata.opf2 import OPF -from calibre.ebooks.metadata.utils import parse_opf, normalize_languages +from calibre.ebooks.metadata.opf2 import OPF, pretty_print +from calibre.ebooks.metadata.utils import parse_opf, normalize_languages, create_manifest_item from calibre.ebooks.metadata import MetaInformation class DummyFile(object): @@ -26,7 +26,7 @@ def get_metadata(stream): opf = OPF(None, preparsed_opf=root, read_toc=False) return opf.to_book_metadata(), ver, opf.raster_cover, opf.first_spine_item() -def set_metadata_opf2(root, mi, cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False): +def set_metadata_opf2(root, cover_prefix, mi, opf_version, cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False): mi = MetaInformation(mi) for x in ('guide', 'toc', 'manifest', 'spine'): setattr(mi, x, None) @@ -45,13 +45,29 @@ def set_metadata_opf2(root, mi, cover_data=None, apply_null=False, update_timest opf.set_identifiers({k:v for k, v in orig.iteritems() if k and v}) if update_timestamp and mi.timestamp is not None: opf.timestamp = mi.timestamp - return opf.render(), opf.raster_cover + raster_cover = opf.raster_cover + if raster_cover is None and cover_data is not None: + if cover_prefix and not cover_prefix.endswith('/'): + cover_prefix += '/' + name = cover_prefix + 'cover.jpg' + i = create_manifest_item(opf.root, name, 'cover') + if i is not None: + if opf_version.major < 3: + [x.getparent().remove(x) for x in opf.root.xpath('//*[local-name()="meta" and @name="cover"]')] + m = opf.create_metadata_element('meta', is_dc=False) + m.set('name', 'cover'), m.set('content', i.get('id')) + else: + i.set('properties', 'cover-image') + raster_cover = name -def set_metadata(stream, mi, cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False): + with pretty_print: + return opf.render(), raster_cover + +def set_metadata(stream, cover_prefix, mi, cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False): if isinstance(stream, bytes): stream = DummyFile(stream) root = parse_opf(stream) ver = parse_opf_version(root.get('version')) opfbytes, raster_cover = set_metadata_opf2( - root, mi, cover_data=cover_data, apply_null=apply_null, update_timestamp=update_timestamp, force_identifiers=force_identifiers) + root, cover_prefix, mi, ver, cover_data=cover_data, apply_null=apply_null, update_timestamp=update_timestamp, force_identifiers=force_identifiers) return opfbytes, ver, raster_cover diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 7ee43a5cc6..c51d7be5e8 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -1172,7 +1172,8 @@ class OPF(object): # {{{ return item.get('href', None) elif self.package_version >= 3.0: for item in self.itermanifest(): - if item.get('properties') == 'cover-image': + props = set((item.get('properties') or '').lower().split()) + if 'cover-image' in props: mt = item.get('media-type', '') if mt and 'xml' not in mt and 'html' not in mt: return item.get('href', None) diff --git a/src/calibre/ebooks/metadata/utils.py b/src/calibre/ebooks/metadata/utils.py index 238b9088d2..fdad83e6d5 100644 --- a/src/calibre/ebooks/metadata/utils.py +++ b/src/calibre/ebooks/metadata/utils.py @@ -9,6 +9,8 @@ from future_builtins import map from lxml import etree from calibre.ebooks.chardet import xml_to_unicode +from calibre.ebooks.oeb.base import OPF +from calibre.ebooks.oeb.polish.utils import guess_type from calibre.spell import parse_lang_code from calibre.utils.localization import lang_as_iso639_1 @@ -48,3 +50,26 @@ def normalize_languages(opf_languages, mi_languages): return lc return list(map(norm, mi_languages)) +def ensure_unique(template, existing): + b, e = template.rpartition('.')[::2] + if e: + e = '.' + e + q = template + c = 0 + while q in existing: + c += 1 + q = '%s-%d%s' % (b, c, e) + return q + +def create_manifest_item(root, href_template, id_template, media_type=None): + all_ids = frozenset(root.xpath('//*/@id')) + all_hrefs = frozenset(root.xpath('//*/@href')) + href = ensure_unique(href_template, all_hrefs) + item_id = ensure_unique(id_template, all_ids) + manifest = root.find(OPF('manifest')) + if manifest is not None: + i = manifest.makeelement(OPF('item')) + i.set('href', href), i.set('id', item_id) + i.set('media-type', media_type or guess_type(href_template)) + manifest.append(i) + return i