Files
epub_bilingual_translator/src/assembly/package_writer.py
T

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