85 lines
4.5 KiB
Python
85 lines
4.5 KiB
Python
"""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 not in {'application/xhtml+xml','application/x-dtbncx+xml'} or not resource.content:
|
|
continue
|
|
if 'nav' in (items.get(item_id).get('properties', '') if item_id in items else '').split() and not resource.translation_modified 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}')
|
|
if resource.media_type=='application/x-dtbncx+xml':
|
|
if resource.translation_modified:
|
|
etree.fromstring(resource.content.encode(),etree.XMLParser(resolve_entities=False,no_network=True))
|
|
replacements[archive_path]=resource.content.encode()
|
|
continue
|
|
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
|