"""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