Improve EPUB restoration, book terminology and Calibre QA workflow
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""Patch the original container to preserve OPF, navigation, heads and assets."""
|
||||
import posixpath
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
from lxml import etree
|
||||
from src.common.html import parse_html
|
||||
|
||||
OPF = 'http://www.idpf.org/2007/opf'
|
||||
DC = 'http://purl.org/dc/elements/1.1/'
|
||||
|
||||
|
||||
def package_path(zf):
|
||||
container = etree.fromstring(zf.read('META-INF/container.xml'))
|
||||
return container.xpath('//*[local-name()="rootfile"]/@full-path')[0]
|
||||
|
||||
|
||||
def write_preserving_package(original, structure, output):
|
||||
output = Path(output)
|
||||
if output.resolve() == Path(original).resolve():
|
||||
raise ValueError('Output must not overwrite the source EPUB')
|
||||
replacements = {}
|
||||
with zipfile.ZipFile(original) as source:
|
||||
opf_path = package_path(source)
|
||||
base = posixpath.dirname(opf_path)
|
||||
opf = etree.fromstring(source.read(opf_path))
|
||||
manifest = opf.find(f'{{{OPF}}}manifest')
|
||||
items = {x.get('id'): x for x in manifest}
|
||||
# A collision-free stylesheet path and ID, independent of book layout.
|
||||
css_href = 'Styles/bilingual_style.css'
|
||||
css_id = 'bilingual_style_css'
|
||||
while posixpath.join(base, css_href) in source.namelist():
|
||||
css_href = 'Styles/' + Path(css_href).stem + '-new.css'
|
||||
while css_id in items:
|
||||
css_id += '_new'
|
||||
css_archive_path = posixpath.normpath(posixpath.join(base, css_href))
|
||||
etree.SubElement(manifest, f'{{{OPF}}}item', id=css_id, href=css_href, **{'media-type': 'text/css'})
|
||||
replacements[css_archive_path] = (Path(__file__).parent/'bilingual_style.css').read_bytes()
|
||||
for item_id, resource in structure.resources.items():
|
||||
if resource.media_type != 'application/xhtml+xml' or not resource.content:
|
||||
continue
|
||||
if 'nav' in (items.get(item_id).get('properties', '') if item_id in items else '').split() and 'translation-for' not in resource.content and 'class="translation"' not in resource.content:
|
||||
continue
|
||||
if item_id not in items:
|
||||
raise ValueError(f'Unknown package item: {item_id}')
|
||||
archive_path = posixpath.normpath(posixpath.join(base, unquote(items[item_id].get('href'))))
|
||||
if archive_path not in source.namelist():
|
||||
raise ValueError(f'Missing source resource: {archive_path}')
|
||||
soup = parse_html(resource.content)
|
||||
soup.html['xmlns'] = 'http://www.w3.org/1999/xhtml'
|
||||
soup.html['xmlns:epub'] = 'http://www.idpf.org/2007/ops'
|
||||
soup.html['lang'] = structure.metadata.language
|
||||
soup.html['xml:lang'] = structure.metadata.language
|
||||
if soup.find(class_='translation'):
|
||||
link = soup.new_tag('link', rel='stylesheet', type='text/css')
|
||||
link['href'] = posixpath.relpath(css_archive_path, posixpath.dirname(archive_path))
|
||||
soup.head.append(link)
|
||||
data = str(soup).encode('utf-8')
|
||||
# Never silently let an EPUB contain invalid XHTML.
|
||||
etree.fromstring(data, etree.XMLParser(resolve_entities=False, no_network=True))
|
||||
replacements[archive_path] = data
|
||||
for language in opf.findall(f'.//{{{DC}}}language'):
|
||||
language.text = structure.metadata.language
|
||||
replacements[opf_path] = etree.tostring(opf, encoding='utf-8', xml_declaration=True)
|
||||
temp = output.with_suffix(output.suffix + '.tmp')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
with zipfile.ZipFile(temp, 'w', compression=zipfile.ZIP_DEFLATED) as target:
|
||||
target.writestr('mimetype', b'application/epub+zip', compress_type=zipfile.ZIP_STORED)
|
||||
for info in source.infolist():
|
||||
if info.filename == 'mimetype':
|
||||
continue
|
||||
target.writestr(info, replacements.pop(info.filename, source.read(info.filename)))
|
||||
for name, data in replacements.items():
|
||||
target.writestr(name, data)
|
||||
temp.replace(output)
|
||||
finally:
|
||||
temp.unlink(missing_ok=True)
|
||||
return output
|
||||
Reference in New Issue
Block a user