kindle manager
This commit is contained in:
828
mobiparse/mobi/mobi_opf.py
Executable file
828
mobiparse/mobi/mobi_opf.py
Executable file
@@ -0,0 +1,828 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
from __future__ import unicode_literals, division, absolute_import, print_function
|
||||
|
||||
from .compatibility_utils import unicode_str, unescapeit
|
||||
from .compatibility_utils import lzip
|
||||
from loguru import logger
|
||||
|
||||
from .unipath import pathof
|
||||
|
||||
from xml.sax.saxutils import escape as xmlescape
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
# In EPUB3, NCX and <guide> MAY exist in OPF, although the NCX is superseded
|
||||
# by the Navigation Document and the <guide> is deprecated. Currently, EPUB3_WITH_NCX
|
||||
# and EPUB3_WITH_GUIDE are set to True due to compatibility with epub2 reading systems.
|
||||
# They might be change to set to False in the future.
|
||||
|
||||
EPUB3_WITH_NCX = True # Do not set to False except for debug.
|
||||
""" Set to True to create a toc.ncx when converting to epub3. """
|
||||
|
||||
EPUB3_WITH_GUIDE = True # Do not set to False except for debug.
|
||||
""" Set to True to create a guide element in an opf when converting to epub3. """
|
||||
|
||||
EPUB_OPF = "content.opf"
|
||||
""" The name for the OPF of EPUB. """
|
||||
|
||||
TOC_NCX = "toc.ncx"
|
||||
""" The name for the TOC of EPUB2. """
|
||||
|
||||
NAVIGATION_DOCUMENT = "nav.xhtml"
|
||||
""" The name for the navigation document of EPUB3. """
|
||||
|
||||
BEGIN_INFO_ONLY = "<!-- BEGIN INFORMATION ONLY "
|
||||
""" The comment to indicate the beginning of metadata which will be ignored by kindlegen. """
|
||||
|
||||
END_INFO_ONLY = "END INFORMATION ONLY -->"
|
||||
""" The comment to indicate the end of metadata which will be ignored by kindlegen. """
|
||||
|
||||
EXTH_TITLE_FURIGANA = "Title-Pronunciation"
|
||||
""" The name for Title Furigana(similar to file-as) set by KDP. """
|
||||
|
||||
EXTH_CREATOR_FURIGANA = "Author-Pronunciation"
|
||||
""" The name for Creator Furigana(similar to file-as) set by KDP. """
|
||||
|
||||
EXTH_PUBLISHER_FURIGANA = "Publisher-Pronunciation"
|
||||
""" The name for Publisher Furigana(similar to file-as) set by KDP. """
|
||||
|
||||
EXTRA_ENTITIES = {'"': """, "'": "'"}
|
||||
|
||||
|
||||
class OPFProcessor(object):
|
||||
def __init__(
|
||||
self,
|
||||
files,
|
||||
metadata,
|
||||
fileinfo,
|
||||
rscnames,
|
||||
hasNCX,
|
||||
mh,
|
||||
usedmap,
|
||||
pagemapxml="",
|
||||
guidetext="",
|
||||
k8resc=None,
|
||||
epubver="2",
|
||||
):
|
||||
self.files = files
|
||||
self.metadata = metadata
|
||||
self.fileinfo = fileinfo
|
||||
self.rscnames = rscnames
|
||||
self.has_ncx = hasNCX
|
||||
self.codec = mh.codec
|
||||
self.isK8 = mh.isK8()
|
||||
self.printReplica = mh.isPrintReplica()
|
||||
self.guidetext = unicode_str(guidetext)
|
||||
self.used = usedmap
|
||||
self.k8resc = k8resc
|
||||
self.covername = None
|
||||
self.cover_id = "cover_img"
|
||||
if self.k8resc is not None and self.k8resc.cover_name is not None:
|
||||
# update cover id info from RESC if available
|
||||
self.cover_id = self.k8resc.cover_name
|
||||
# Create a unique urn uuid
|
||||
self.BookId = unicode_str(str(uuid.uuid4()))
|
||||
self.pagemap = pagemapxml
|
||||
|
||||
self.ncxname = None
|
||||
self.navname = None
|
||||
|
||||
# page-progression-direction is only set in spine
|
||||
self.page_progression_direction = metadata.pop(
|
||||
"page-progression-direction", [None]
|
||||
)[0]
|
||||
if "rl" in metadata.get("primary-writing-mode", [""])[0]:
|
||||
self.page_progression_direction = "rtl"
|
||||
self.epubver = epubver # the epub version set by user
|
||||
self.target_epubver = (
|
||||
epubver # the epub vertion set by user or detected automatically
|
||||
)
|
||||
if self.epubver == "A":
|
||||
self.target_epubver = self.autodetectEPUBVersion()
|
||||
elif self.epubver == "F":
|
||||
self.target_epubver = "2"
|
||||
elif self.epubver != "2" and self.epubver != "3":
|
||||
self.target_epubver = "2"
|
||||
|
||||
# id for rifine attributes
|
||||
self.title_id = {}
|
||||
self.creator_id = {}
|
||||
self.publisher_id = {}
|
||||
# extra attributes
|
||||
self.title_attrib = {}
|
||||
self.creator_attrib = {}
|
||||
self.publisher_attrib = {}
|
||||
self.extra_attributes = [] # for force epub2 option
|
||||
# Create epub3 metadata from EXTH.
|
||||
self.exth_solved_refines_metadata = []
|
||||
self.exth_refines_metadata = []
|
||||
self.exth_fixedlayout_metadata = []
|
||||
|
||||
self.defineRefinesID()
|
||||
self.processRefinesMetadata()
|
||||
if self.k8resc is not None:
|
||||
# Create metadata in RESC section.
|
||||
self.k8resc.createMetadata(epubver)
|
||||
if self.target_epubver == "3":
|
||||
self.createMetadataForFixedlayout()
|
||||
|
||||
def escapeit(self, sval, EXTRAS=None):
|
||||
# note, xmlescape and unescape do not work with utf-8 bytestrings
|
||||
sval = unicode_str(sval)
|
||||
if EXTRAS:
|
||||
res = xmlescape(unescapeit(sval), EXTRAS)
|
||||
else:
|
||||
res = xmlescape(unescapeit(sval))
|
||||
return res
|
||||
|
||||
def createMetaTag(self, data, property, content, refid=""):
|
||||
refines = ""
|
||||
if refid:
|
||||
refines = ' refines="#%s"' % refid
|
||||
data.append('<meta property="%s"%s>%s</meta>\n' % (property, refines, content))
|
||||
|
||||
def buildOPFMetadata(self, start_tag, has_obfuscated_fonts=False):
|
||||
# convert from EXTH metadata format to target epub version metadata
|
||||
# epub 3 will ignore <meta name="xxxx" content="yyyy" /> style metatags
|
||||
# but allows them to be present for backwards compatibility
|
||||
# instead the new format is
|
||||
# <meta property="xxxx" id="iiii" ... > property_value</meta>
|
||||
# and DCMES elements such as:
|
||||
# <dc:blah id="iiii">value</dc:blah>
|
||||
|
||||
metadata = self.metadata
|
||||
k8resc = self.k8resc
|
||||
|
||||
META_TAGS = [
|
||||
"Drm Server Id",
|
||||
"Drm Commerce Id",
|
||||
"Drm Ebookbase Book Id",
|
||||
"ASIN",
|
||||
"ThumbOffset",
|
||||
"Fake Cover",
|
||||
"Creator Software",
|
||||
"Creator Major Version",
|
||||
"Creator Minor Version",
|
||||
"Creator Build Number",
|
||||
"Watermark",
|
||||
"Clipping Limit",
|
||||
"Publisher Limit",
|
||||
"Text to Speech Disabled",
|
||||
"CDE Type",
|
||||
"Updated Title",
|
||||
"Font Signature (hex)",
|
||||
"Tamper Proof Keys (hex)",
|
||||
]
|
||||
|
||||
# def handleTag(data, metadata, key, tag, ids={}):
|
||||
def handleTag(data, metadata, key, tag, attrib={}):
|
||||
"""Format metadata values.
|
||||
|
||||
@param data: List of formatted metadata entries.
|
||||
@param metadata: The metadata dictionary.
|
||||
@param key: The key of the metadata value to handle.
|
||||
@param tag: The opf tag corresponds to the metadata value.
|
||||
###@param ids: The ids in tags for refines property of epub3.
|
||||
@param attrib: The extra attibute for refines or opf prefixs.
|
||||
"""
|
||||
if key in metadata:
|
||||
for i, value in enumerate(metadata[key]):
|
||||
closingTag = tag.split(" ")[0]
|
||||
res = "<%s%s>%s</%s>\n" % (
|
||||
tag,
|
||||
attrib.get(i, ""),
|
||||
self.escapeit(value),
|
||||
closingTag,
|
||||
)
|
||||
data.append(res)
|
||||
del metadata[key]
|
||||
|
||||
# these are allowed but ignored by epub3
|
||||
def handleMetaPairs(data, metadata, key, name):
|
||||
if key in metadata:
|
||||
for value in metadata[key]:
|
||||
res = '<meta name="%s" content="%s" />\n' % (
|
||||
name,
|
||||
self.escapeit(value, EXTRA_ENTITIES),
|
||||
)
|
||||
data.append(res)
|
||||
del metadata[key]
|
||||
|
||||
data = []
|
||||
data.append(start_tag + "\n")
|
||||
# Handle standard metadata
|
||||
if "Title" in metadata:
|
||||
handleTag(data, metadata, "Title", "dc:title", self.title_attrib)
|
||||
else:
|
||||
data.append("<dc:title>Untitled</dc:title>\n")
|
||||
handleTag(data, metadata, "Language", "dc:language")
|
||||
if "UniqueID" in metadata:
|
||||
handleTag(data, metadata, "UniqueID", 'dc:identifier id="uid"')
|
||||
else:
|
||||
# No unique ID in original, give it a generic one.
|
||||
data.append('<dc:identifier id="uid">0</dc:identifier>\n')
|
||||
|
||||
if self.target_epubver == "3":
|
||||
# epub version 3 minimal metadata requires a dcterms:modifed date tag
|
||||
self.createMetaTag(
|
||||
data,
|
||||
"dcterms:modified",
|
||||
datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
)
|
||||
|
||||
if self.isK8 and has_obfuscated_fonts:
|
||||
# Use the random generated urn:uuid so obuscated fonts work.
|
||||
# It doesn't need to be _THE_ unique identifier to work as a key
|
||||
# for obfuscated fonts in Sigil, ADE and calibre. Its just has
|
||||
# to use the opf:scheme="UUID" and have the urn:uuid: prefix.
|
||||
if self.target_epubver == "3":
|
||||
data.append(
|
||||
"<dc:identifier>urn:uuid:" + self.BookId + "</dc:identifier>\n"
|
||||
)
|
||||
else:
|
||||
data.append(
|
||||
'<dc:identifier opf:scheme="UUID">urn:uuid:'
|
||||
+ self.BookId
|
||||
+ "</dc:identifier>\n"
|
||||
)
|
||||
|
||||
handleTag(data, metadata, "Creator", "dc:creator", self.creator_attrib)
|
||||
handleTag(data, metadata, "Contributor", "dc:contributor")
|
||||
handleTag(data, metadata, "Publisher", "dc:publisher", self.publisher_attrib)
|
||||
handleTag(data, metadata, "Source", "dc:source")
|
||||
handleTag(data, metadata, "Type", "dc:type")
|
||||
if self.target_epubver == "3":
|
||||
if "ISBN" in metadata:
|
||||
for i, value in enumerate(metadata["ISBN"]):
|
||||
res = (
|
||||
"<dc:identifier>urn:isbn:%s</dc:identifier>\n"
|
||||
% self.escapeit(value)
|
||||
)
|
||||
data.append(res)
|
||||
else:
|
||||
handleTag(data, metadata, "ISBN", 'dc:identifier opf:scheme="ISBN"')
|
||||
if "Subject" in metadata:
|
||||
if "SubjectCode" in metadata:
|
||||
codeList = metadata["SubjectCode"]
|
||||
del metadata["SubjectCode"]
|
||||
else:
|
||||
codeList = None
|
||||
for i in range(len(metadata["Subject"])):
|
||||
if codeList and i < len(codeList):
|
||||
data.append('<dc:subject BASICCode="' + codeList[i] + '">')
|
||||
else:
|
||||
data.append("<dc:subject>")
|
||||
data.append(self.escapeit(metadata["Subject"][i]) + "</dc:subject>\n")
|
||||
del metadata["Subject"]
|
||||
handleTag(data, metadata, "Description", "dc:description")
|
||||
if self.target_epubver == "3":
|
||||
if "Published" in metadata:
|
||||
for i, value in enumerate(metadata["Published"]):
|
||||
res = "<dc:date>%s</dc:date>\n" % self.escapeit(value)
|
||||
data.append(res)
|
||||
else:
|
||||
handleTag(data, metadata, "Published", 'dc:date opf:event="publication"')
|
||||
handleTag(data, metadata, "Rights", "dc:rights")
|
||||
|
||||
if self.epubver == "F":
|
||||
if self.extra_attributes or k8resc is not None and k8resc.extra_attributes:
|
||||
data.append(
|
||||
"<!-- THE FOLLOWINGS ARE REQUIRED TO INSERT INTO <dc:xxx> MANUALLY\n"
|
||||
)
|
||||
if self.extra_attributes:
|
||||
data += self.extra_attributes
|
||||
if k8resc is not None and k8resc.extra_attributes:
|
||||
data += k8resc.extra_attributes
|
||||
data.append("-->\n")
|
||||
else:
|
||||
# Append refines metadata.
|
||||
if self.exth_solved_refines_metadata:
|
||||
data.append("<!-- Refines MetaData from EXTH -->\n")
|
||||
data += self.exth_solved_refines_metadata
|
||||
if (
|
||||
self.exth_refines_metadata
|
||||
or k8resc is not None
|
||||
and k8resc.refines_metadata
|
||||
):
|
||||
data.append("<!-- THE FOLLOWINGS ARE REQUIRED TO EDIT IDS MANUALLY\n")
|
||||
if self.exth_refines_metadata:
|
||||
data += self.exth_refines_metadata
|
||||
if k8resc is not None and k8resc.refines_metadata:
|
||||
data += k8resc.refines_metadata
|
||||
data.append("-->\n")
|
||||
|
||||
# Append metadata in RESC section.
|
||||
if k8resc is not None and k8resc.extra_metadata:
|
||||
data.append("<!-- Extra MetaData from RESC\n")
|
||||
data += k8resc.extra_metadata
|
||||
data.append("-->\n")
|
||||
|
||||
if "CoverOffset" in metadata:
|
||||
imageNumber = int(metadata["CoverOffset"][0])
|
||||
self.covername = self.rscnames[imageNumber]
|
||||
if self.covername is None:
|
||||
logger.debug(
|
||||
"Error: Cover image %s was not recognized as a valid image"
|
||||
% imageNumber
|
||||
)
|
||||
else:
|
||||
# <meta name="cover"> is obsoleted in EPUB3, but kindlegen v2.9 requires it.
|
||||
data.append('<meta name="cover" content="' + self.cover_id + '" />\n')
|
||||
self.used[self.covername] = "used"
|
||||
del metadata["CoverOffset"]
|
||||
|
||||
handleMetaPairs(data, metadata, "Codec", "output encoding")
|
||||
# handle kindlegen specifc tags
|
||||
handleTag(data, metadata, "DictInLanguage", "DictionaryInLanguage")
|
||||
handleTag(data, metadata, "DictOutLanguage", "DictionaryOutLanguage")
|
||||
handleMetaPairs(data, metadata, "RegionMagnification", "RegionMagnification")
|
||||
handleMetaPairs(data, metadata, "book-type", "book-type")
|
||||
handleMetaPairs(data, metadata, "zero-gutter", "zero-gutter")
|
||||
handleMetaPairs(data, metadata, "zero-margin", "zero-margin")
|
||||
handleMetaPairs(data, metadata, "primary-writing-mode", "primary-writing-mode")
|
||||
handleMetaPairs(data, metadata, "fixed-layout", "fixed-layout")
|
||||
handleMetaPairs(data, metadata, "orientation-lock", "orientation-lock")
|
||||
handleMetaPairs(data, metadata, "original-resolution", "original-resolution")
|
||||
|
||||
# these are not allowed in epub2 or 3 so convert them to meta name content pairs
|
||||
# perhaps these could better be mapped into the dcterms namespace instead
|
||||
handleMetaPairs(data, metadata, "Review", "review")
|
||||
handleMetaPairs(data, metadata, "Imprint", "imprint")
|
||||
handleMetaPairs(data, metadata, "Adult", "adult")
|
||||
handleMetaPairs(data, metadata, "DictShortName", "DictionaryVeryShortName")
|
||||
|
||||
# these are needed by kobo books upon submission but not sure if legal metadata in epub2 or epub3
|
||||
if "Price" in metadata and "Currency" in metadata:
|
||||
priceList = metadata["Price"]
|
||||
currencyList = metadata["Currency"]
|
||||
if len(priceList) != len(currencyList):
|
||||
logger.debug("Error: found %s price entries, but %s currency entries.")
|
||||
else:
|
||||
for i in range(len(priceList)):
|
||||
data.append(
|
||||
'<SRP Currency="'
|
||||
+ currencyList[i]
|
||||
+ '">'
|
||||
+ priceList[i]
|
||||
+ "</SRP>\n"
|
||||
)
|
||||
del metadata["Price"]
|
||||
del metadata["Currency"]
|
||||
|
||||
if self.target_epubver == "3":
|
||||
# Append metadata for EPUB3.
|
||||
if self.exth_fixedlayout_metadata:
|
||||
data.append("<!-- EPUB3 MedaData converted from EXTH -->\n")
|
||||
data += self.exth_fixedlayout_metadata
|
||||
|
||||
# all that remains is extra EXTH info we will store inside a comment inside meta name/content pairs
|
||||
# so it can not impact anything and will be automatically stripped out if found again in a RESC section
|
||||
data.append(BEGIN_INFO_ONLY + "\n")
|
||||
if "ThumbOffset" in metadata:
|
||||
imageNumber = int(metadata["ThumbOffset"][0])
|
||||
imageName = self.rscnames[imageNumber]
|
||||
if imageName is None:
|
||||
logger.debug(
|
||||
"Error: Cover Thumbnail image %s was not recognized as a valid image"
|
||||
% imageNumber
|
||||
)
|
||||
else:
|
||||
data.append(
|
||||
'<meta name="Cover ThumbNail Image" content="'
|
||||
+ "Images/"
|
||||
+ imageName
|
||||
+ '" />\n'
|
||||
)
|
||||
# self.used[imageName] = 'used' # thumbnail image is always generated by Kindlegen, so don't include in manifest
|
||||
self.used[imageName] = "not used"
|
||||
del metadata["ThumbOffset"]
|
||||
for metaName in META_TAGS:
|
||||
if metaName in metadata:
|
||||
for value in metadata[metaName]:
|
||||
data.append(
|
||||
'<meta name="'
|
||||
+ metaName
|
||||
+ '" content="'
|
||||
+ self.escapeit(value, EXTRA_ENTITIES)
|
||||
+ '" />\n'
|
||||
)
|
||||
del metadata[metaName]
|
||||
for key in list(metadata.keys()):
|
||||
for value in metadata[key]:
|
||||
data.append(
|
||||
'<meta name="'
|
||||
+ key
|
||||
+ '" content="'
|
||||
+ self.escapeit(value, EXTRA_ENTITIES)
|
||||
+ '" />\n'
|
||||
)
|
||||
del metadata[key]
|
||||
data.append(END_INFO_ONLY + "\n")
|
||||
data.append("</metadata>\n")
|
||||
return data
|
||||
|
||||
def buildOPFManifest(self, ncxname, navname=None):
|
||||
# buildManifest for mobi7, azw4, epub2 and epub3.
|
||||
k8resc = self.k8resc
|
||||
cover_id = self.cover_id
|
||||
hasK8RescSpine = k8resc is not None and k8resc.hasSpine()
|
||||
self.ncxname = ncxname
|
||||
self.navname = navname
|
||||
|
||||
data = []
|
||||
data.append("<manifest>\n")
|
||||
media_map = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".xhtml": "application/xhtml+xml",
|
||||
".html": "text/html", # for mobi7
|
||||
".pdf": "application/pdf", # for azw4(print replica textbook)
|
||||
".ttf": "application/x-font-ttf",
|
||||
".otf": "application/x-font-opentype", # replaced?
|
||||
".css": "text/css",
|
||||
# '.html' : 'text/x-oeb1-document', # for mobi7
|
||||
# '.otf' : 'application/vnd.ms-opentype', # [OpenType] OpenType fonts
|
||||
# '.woff' : 'application/font-woff', # [WOFF] WOFF fonts
|
||||
# '.smil' : 'application/smil+xml', # [MediaOverlays301] EPUB Media Overlay documents
|
||||
# '.pls' : 'application/pls+xml', # [PLS] Text-to-Speech (TTS) Pronunciation lexicons
|
||||
# '.mp3' : 'audio/mpeg',
|
||||
# '.mp4' : 'video/mp4',
|
||||
# '.js' : 'text/javascript', # not supported in K8
|
||||
}
|
||||
spinerefs = []
|
||||
|
||||
idcnt = 0
|
||||
for [key, dir, fname] in self.fileinfo:
|
||||
name, ext = os.path.splitext(fname)
|
||||
ext = ext.lower()
|
||||
media = media_map.get(ext)
|
||||
ref = "item%d" % idcnt
|
||||
if hasK8RescSpine:
|
||||
if key is not None and key in k8resc.spine_idrefs:
|
||||
ref = k8resc.spine_idrefs[key]
|
||||
properties = ""
|
||||
if dir != "":
|
||||
fpath = dir + "/" + fname
|
||||
else:
|
||||
fpath = fname
|
||||
data.append(
|
||||
'<item id="{0:}" media-type="{1:}" href="{2:}" {3:}/>\n'.format(
|
||||
ref, media, fpath, properties
|
||||
)
|
||||
)
|
||||
|
||||
if ext in [".xhtml", ".html"]:
|
||||
spinerefs.append(ref)
|
||||
idcnt += 1
|
||||
|
||||
for fname in self.rscnames:
|
||||
if fname is not None:
|
||||
if self.used.get(fname, "not used") == "not used":
|
||||
continue
|
||||
name, ext = os.path.splitext(fname)
|
||||
ext = ext.lower()
|
||||
media = media_map.get(ext, ext[1:])
|
||||
properties = ""
|
||||
if fname == self.covername:
|
||||
ref = cover_id
|
||||
if self.target_epubver == "3":
|
||||
properties = 'properties="cover-image"'
|
||||
else:
|
||||
ref = "item%d" % idcnt
|
||||
if ext == ".ttf" or ext == ".otf":
|
||||
if self.isK8: # fonts are only used in Mobi 8
|
||||
fpath = "Fonts/" + fname
|
||||
data.append(
|
||||
'<item id="{0:}" media-type="{1:}" href="{2:}" {3:}/>\n'.format(
|
||||
ref, media, fpath, properties
|
||||
)
|
||||
)
|
||||
else:
|
||||
fpath = "Images/" + fname
|
||||
data.append(
|
||||
'<item id="{0:}" media-type="{1:}" href="{2:}" {3:}/>\n'.format(
|
||||
ref, media, fpath, properties
|
||||
)
|
||||
)
|
||||
idcnt += 1
|
||||
|
||||
if self.target_epubver == "3" and navname is not None:
|
||||
data.append(
|
||||
'<item id="nav" media-type="application/xhtml+xml" href="Text/'
|
||||
+ navname
|
||||
+ '" properties="nav"/>\n'
|
||||
)
|
||||
if self.has_ncx and ncxname is not None:
|
||||
data.append(
|
||||
'<item id="ncx" media-type="application/x-dtbncx+xml" href="'
|
||||
+ ncxname
|
||||
+ '" />\n'
|
||||
)
|
||||
if self.pagemap != "":
|
||||
data.append(
|
||||
'<item id="map" media-type="application/oebs-page-map+xml" href="page-map.xml" />\n'
|
||||
)
|
||||
data.append("</manifest>\n")
|
||||
return [data, spinerefs]
|
||||
|
||||
def buildOPFSpine(self, spinerefs, isNCX):
|
||||
# build spine
|
||||
k8resc = self.k8resc
|
||||
hasK8RescSpine = k8resc is not None and k8resc.hasSpine()
|
||||
data = []
|
||||
ppd = ""
|
||||
if self.isK8 and self.page_progression_direction is not None:
|
||||
ppd = ' page-progression-direction="{:s}"'.format(
|
||||
self.page_progression_direction
|
||||
)
|
||||
ncx = ""
|
||||
if isNCX:
|
||||
ncx = ' toc="ncx"'
|
||||
map = ""
|
||||
if self.pagemap != "":
|
||||
map = ' page-map="map"'
|
||||
if self.epubver == "F":
|
||||
if ppd:
|
||||
ppd = "<!--" + ppd + " -->"
|
||||
spine_start_tag = "<spine{1:s}{2:s}>{0:s}\n".format(ppd, map, ncx)
|
||||
else:
|
||||
spine_start_tag = "<spine{0:s}{1:s}{2:s}>\n".format(ppd, map, ncx)
|
||||
data.append(spine_start_tag)
|
||||
|
||||
if hasK8RescSpine:
|
||||
for key in k8resc.spine_order:
|
||||
idref = k8resc.spine_idrefs[key]
|
||||
attribs = k8resc.spine_pageattributes[key]
|
||||
tag = '<itemref idref="%s"' % idref
|
||||
for aname, val in list(attribs.items()):
|
||||
if self.epubver == "F" and aname == "properties":
|
||||
continue
|
||||
if val is not None:
|
||||
tag += ' %s="%s"' % (aname, val)
|
||||
tag += "/>"
|
||||
if self.epubver == "F" and "properties" in attribs:
|
||||
val = attribs["properties"]
|
||||
if val is not None:
|
||||
tag += '<!-- properties="%s" -->' % val
|
||||
tag += "\n"
|
||||
data.append(tag)
|
||||
else:
|
||||
start = 0
|
||||
# special case the created coverpage if need be
|
||||
[key, dir, fname] = self.fileinfo[0]
|
||||
if key is not None and key == "coverpage":
|
||||
entry = spinerefs[start]
|
||||
data.append('<itemref idref="%s" linear="no"/>\n' % entry)
|
||||
start += 1
|
||||
for entry in spinerefs[start:]:
|
||||
data.append('<itemref idref="' + entry + '"/>\n')
|
||||
data.append("</spine>\n")
|
||||
return data
|
||||
|
||||
def buildMobi7OPF(self):
|
||||
# Build an OPF for mobi7 and azw4.
|
||||
logger.debug("Building an opf for mobi7/azw4.")
|
||||
data = []
|
||||
data.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
data.append(
|
||||
'<package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="uid">\n'
|
||||
)
|
||||
metadata_tag = '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">'
|
||||
opf_metadata = self.buildOPFMetadata(metadata_tag)
|
||||
data += opf_metadata
|
||||
if self.has_ncx:
|
||||
# ncxname = self.files.getInputFileBasename() + '.ncx'
|
||||
ncxname = "toc.ncx"
|
||||
else:
|
||||
ncxname = None
|
||||
[opf_manifest, spinerefs] = self.buildOPFManifest(ncxname)
|
||||
data += opf_manifest
|
||||
opf_spine = self.buildOPFSpine(spinerefs, self.has_ncx)
|
||||
data += opf_spine
|
||||
data.append("<tours>\n</tours>\n")
|
||||
if not self.printReplica:
|
||||
guide = "<guide>\n" + self.guidetext + "</guide>\n"
|
||||
data.append(guide)
|
||||
data.append("</package>\n")
|
||||
return "".join(data)
|
||||
|
||||
def buildEPUBOPF(self, has_obfuscated_fonts=False):
|
||||
logger.debug(
|
||||
"Building an opf for mobi8 using epub version: %s" % self.target_epubver
|
||||
)
|
||||
if self.target_epubver == "2":
|
||||
has_ncx = self.has_ncx
|
||||
has_guide = True
|
||||
ncxname = None
|
||||
ncxname = TOC_NCX
|
||||
navname = None
|
||||
package = '<package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="uid">\n'
|
||||
tours = "<tours>\n</tours>\n"
|
||||
metadata_tag = '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">'
|
||||
else:
|
||||
has_ncx = EPUB3_WITH_NCX
|
||||
has_guide = EPUB3_WITH_GUIDE
|
||||
ncxname = None
|
||||
if has_ncx:
|
||||
ncxname = TOC_NCX
|
||||
navname = NAVIGATION_DOCUMENT
|
||||
package = '<package version="3.0" xmlns="http://www.idpf.org/2007/opf" prefix="rendition: http://www.idpf.org/vocab/rendition/#" unique-identifier="uid">\n'
|
||||
tours = ""
|
||||
metadata_tag = '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">'
|
||||
|
||||
data = []
|
||||
data.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
data.append(package)
|
||||
opf_metadata = self.buildOPFMetadata(metadata_tag, has_obfuscated_fonts)
|
||||
data += opf_metadata
|
||||
[opf_manifest, spinerefs] = self.buildOPFManifest(ncxname, navname)
|
||||
data += opf_manifest
|
||||
opf_spine = self.buildOPFSpine(spinerefs, has_ncx)
|
||||
data += opf_spine
|
||||
data.append(tours)
|
||||
if has_guide:
|
||||
guide = "<guide>\n" + self.guidetext + "</guide>\n"
|
||||
data.append(guide)
|
||||
data.append("</package>\n")
|
||||
return "".join(data)
|
||||
|
||||
def writeOPF(self, has_obfuscated_fonts=False):
|
||||
if self.isK8:
|
||||
data = self.buildEPUBOPF(has_obfuscated_fonts)
|
||||
outopf = os.path.join(self.files.k8oebps, EPUB_OPF)
|
||||
with open(pathof(outopf), "wb") as f:
|
||||
f.write(data.encode("utf-8"))
|
||||
return self.BookId
|
||||
else:
|
||||
data = self.buildMobi7OPF()
|
||||
outopf = os.path.join(self.files.mobi7dir, "content.opf")
|
||||
with open(pathof(outopf), "wb") as f:
|
||||
f.write(data.encode("utf-8"))
|
||||
return 0
|
||||
|
||||
def getBookId(self):
|
||||
return self.BookId
|
||||
|
||||
def getNCXName(self):
|
||||
return self.ncxname
|
||||
|
||||
def getNAVName(self):
|
||||
return self.navname
|
||||
|
||||
def getEPUBVersion(self):
|
||||
return self.target_epubver
|
||||
|
||||
def hasNCX(self):
|
||||
return self.ncxname is not None and self.has_ncx
|
||||
|
||||
def hasNAV(self):
|
||||
return self.navname is not None
|
||||
|
||||
def autodetectEPUBVersion(self):
|
||||
# Determine EPUB version from metadata and RESC.
|
||||
metadata = self.metadata
|
||||
k8resc = self.k8resc
|
||||
epubver = "2"
|
||||
if "true" == metadata.get("fixed-layout", [""])[0].lower():
|
||||
epubver = "3"
|
||||
elif metadata.get("orientation-lock", [""])[0].lower() in [
|
||||
"portrait",
|
||||
"landscape",
|
||||
]:
|
||||
epubver = "3"
|
||||
elif self.page_progression_direction == "rtl":
|
||||
epubver = "3"
|
||||
elif EXTH_TITLE_FURIGANA in metadata:
|
||||
epubver = "3"
|
||||
elif EXTH_CREATOR_FURIGANA in metadata:
|
||||
epubver = "3"
|
||||
elif EXTH_PUBLISHER_FURIGANA in metadata:
|
||||
epubver = "3"
|
||||
elif k8resc is not None and k8resc.needEPUB3():
|
||||
epubver = "3"
|
||||
return epubver
|
||||
|
||||
def defineRefinesID(self):
|
||||
# the following EXTH are set by KDP.
|
||||
# 'Title_Furigana_(508)'
|
||||
# 'Creator_Furigana_(517)',
|
||||
# 'Publisher_Furigana_(522)'
|
||||
# It is difficult to find correspondence between Title, Creator, Publisher
|
||||
# and EXTH 508,512, 522 if they have more than two values since KDP seems not preserve the oders of EXTH 508,512 and 522.
|
||||
# It is also difficult to find correspondence between them and tags which have refine attributes in RESC.
|
||||
# So editing manually is required.
|
||||
metadata = self.metadata
|
||||
|
||||
needRefinesId = False
|
||||
if self.k8resc is not None:
|
||||
needRefinesId = self.k8resc.hasRefines()
|
||||
# Create id for rifine attributes
|
||||
if (needRefinesId or EXTH_TITLE_FURIGANA in metadata) and "Title" in metadata:
|
||||
for i in range(len(metadata.get("Title"))):
|
||||
self.title_id[i] = "title%02d" % (i + 1)
|
||||
|
||||
if (
|
||||
needRefinesId or EXTH_CREATOR_FURIGANA in metadata
|
||||
) and "Creator" in metadata:
|
||||
for i in range(len(metadata.get("Creator"))):
|
||||
self.creator_id[i] = "creator%02d" % (i + 1)
|
||||
|
||||
if (
|
||||
needRefinesId or EXTH_PUBLISHER_FURIGANA in metadata
|
||||
) and "Publisher" in metadata:
|
||||
for i in range(len(metadata.get("Publisher"))):
|
||||
self.publisher_id[i] = "publisher%02d" % (i + 1)
|
||||
|
||||
def processRefinesMetadata(self):
|
||||
# create refines metadata defined in epub3 or convert refines property to opf: attribues for epub2.
|
||||
metadata = self.metadata
|
||||
|
||||
refines_list = [
|
||||
[EXTH_TITLE_FURIGANA, self.title_id, self.title_attrib, "title00"],
|
||||
[EXTH_CREATOR_FURIGANA, self.creator_id, self.creator_attrib, "creator00"],
|
||||
[
|
||||
EXTH_PUBLISHER_FURIGANA,
|
||||
self.publisher_id,
|
||||
self.publisher_attrib,
|
||||
"publisher00",
|
||||
],
|
||||
]
|
||||
|
||||
create_refines_metadata = False
|
||||
for EXTH in lzip(*refines_list)[0]:
|
||||
if EXTH in metadata:
|
||||
create_refines_metadata = True
|
||||
break
|
||||
if create_refines_metadata:
|
||||
for [EXTH, id, attrib, defaultid] in refines_list:
|
||||
if self.target_epubver == "3":
|
||||
for i, value in list(id.items()):
|
||||
attrib[i] = ' id="%s"' % value
|
||||
|
||||
if EXTH in metadata:
|
||||
if len(metadata[EXTH]) == 1 and len(id) == 1:
|
||||
self.createMetaTag(
|
||||
self.exth_solved_refines_metadata,
|
||||
"file-as",
|
||||
metadata[EXTH][0],
|
||||
id[0],
|
||||
)
|
||||
else:
|
||||
for i, value in enumerate(metadata[EXTH]):
|
||||
self.createMetaTag(
|
||||
self.exth_refines_metadata,
|
||||
"file-as",
|
||||
value,
|
||||
id.get(i, defaultid),
|
||||
)
|
||||
else:
|
||||
if EXTH in metadata:
|
||||
if len(metadata[EXTH]) == 1 and len(id) == 1:
|
||||
attr = ' opf:file-as="%s"' % metadata[EXTH][0]
|
||||
attrib[0] = attr
|
||||
else:
|
||||
for i, value in enumerate(metadata[EXTH]):
|
||||
attr = ' id="#%s" opf:file-as="%s"\n' % (
|
||||
id.get(i, defaultid),
|
||||
value,
|
||||
)
|
||||
self.extra_attributes.append(attr)
|
||||
|
||||
def createMetadataForFixedlayout(self):
|
||||
# convert fixed layout to epub3 format if needed.
|
||||
metadata = self.metadata
|
||||
|
||||
if "fixed-layout" in metadata:
|
||||
fixedlayout = metadata["fixed-layout"][0]
|
||||
content = {"true": "pre-paginated"}.get(fixedlayout.lower(), "reflowable")
|
||||
self.createMetaTag(
|
||||
self.exth_fixedlayout_metadata, "rendition:layout", content
|
||||
)
|
||||
|
||||
if "orientation-lock" in metadata:
|
||||
content = metadata["orientation-lock"][0].lower()
|
||||
if content == "portrait" or content == "landscape":
|
||||
self.createMetaTag(
|
||||
self.exth_fixedlayout_metadata, "rendition:orientation", content
|
||||
)
|
||||
|
||||
# according to epub3 spec about correspondence with Amazon
|
||||
# if 'original-resolution' is provided it needs to be converted to
|
||||
# meta viewport property tag stored in the <head></head> of **each**
|
||||
# xhtml page - so this tag would need to be handled by editing each part
|
||||
# before reaching this routine
|
||||
# we need to add support for this to the k8html routine
|
||||
# if 'original-resolution' in metadata.keys():
|
||||
# resolution = metadata['original-resolution'][0].lower()
|
||||
# width, height = resolution.split('x')
|
||||
# if width.isdigit() and int(width) > 0 and height.isdigit() and int(height) > 0:
|
||||
# viewport = 'width=%s, height=%s' % (width, height)
|
||||
# self.createMetaTag(self.exth_fixedlayout_metadata, 'rendition:viewport', viewport)
|
||||
Reference in New Issue
Block a user