diff --git a/grawlix/output/epub.py b/grawlix/output/epub.py index a73a23e..4e79dd1 100644 --- a/grawlix/output/epub.py +++ b/grawlix/output/epub.py @@ -84,55 +84,91 @@ class Epub(OutputFormat): async def _download_epub_in_parts(self, data: EpubInParts, metadata: Metadata, location: str, update: Update) -> None: files = data.files - file_count = len(files) - progress = 1/(file_count) + progress = 1/len(files) temporary_file_location = f"{location}.tmp" - added_files: set[str] = set() - def get_new_files(zipfile: ZipFile): - """Returns files in zipfile not already added to file""" - for filename in zipfile.namelist(): - if filename in added_files or filename.endswith(".opf") or filename.endswith(".ncx"): - continue - yield filename + # First occurrence wins; parts arrive in reading order and share most + # files, so insertion order doubles as spine order. + collected: dict[str, bytes] = {} + try: + for file in files: + await self._download_and_write_file(file, temporary_file_location) + with ZipFile(temporary_file_location, "r") as zipfile: + for filepath in zipfile.namelist(): + if filepath.endswith("/") or filepath in collected or self._is_part_packaging(filepath): + continue + collected[filepath] = zipfile.read(filepath) + if update: + update(progress) + finally: + if os.path.exists(temporary_file_location): + os.remove(temporary_file_location) + self._build_epub_from_files(collected, data.files_in_toc, metadata, location) + + + @staticmethod + def _is_part_packaging(filename: str) -> bool: + """ + True for files that belong to a part's own epub packaging. Each part is + a complete epub, so copying these into the merged book would leave + stray mimetype/container/package files next to the generated ones. + """ + lowered = filename.lower() + return ( + lowered == "mimetype" + or lowered.startswith("meta-inf/") + or lowered.endswith((".opf", ".ncx")) + ) + + + @staticmethod + def _build_epub_from_files( + files: dict[str, bytes], + files_in_toc: dict[str, str], + metadata: Metadata, + location: str, + ) -> None: + """ + Assemble an epub from the raw files collected from its parts. Content + documents are stored byte-for-byte as delivered by the source, so their + stylesheet links, language attributes and markup survive; only the + packaging (opf, ncx, nav) is generated. + """ output = epub.EpubBook() - for file in files: - await self._download_and_write_file(file, temporary_file_location) - with ZipFile(temporary_file_location, "r") as zipfile: - for filepath in get_new_files(zipfile): - content = zipfile.read(filepath) - if filepath.endswith("html"): - filename = os.path.basename(filepath) - is_in_toc = False - title = None - for key, value in data.files_in_toc.items(): - toc_filename = key.split("#")[0] - if filename == toc_filename: - title = value - is_in_toc = True - break - epub_file = epub.EpubHtml( - title = title, - file_name = filepath, - content = content - ) - output.add_item(epub_file) - output.spine.append(epub_file) - if is_in_toc: - output.toc.append(epub_file) - else: - epub_file = epub.EpubItem( - file_name = filepath, - content = content - ) - output.add_item(epub_file) - added_files.add(filepath) - if update: - update(progress) - os.remove(temporary_file_location) + output.set_title(metadata.title) + if metadata.language: + output.set_language(metadata.language) + for author in metadata.authors: + output.add_author(author) + + # Toc keys may or may not carry directory prefixes and fragments + # ("Text/chap01.html#nav_1"); compare by basename like the spine files. + toc_titles = { + os.path.basename(key.split("#")[0]): title + for key, title in files_in_toc.items() + } + cover_flagged = False + for filepath, content in files.items(): + is_content_document = filepath.lower().endswith((".html", ".xhtml", ".htm")) + item = epub.EpubItem( + file_name = filepath, + # Empty media type makes ebooklib guess from the extension + media_type = "application/xhtml+xml" if is_content_document else "", + content = content, + ) + output.add_item(item) + if is_content_document: + output.spine.append(item) + title = toc_titles.get(os.path.basename(filepath)) + if title is not None: + output.toc.append(epub.Link(filepath, title, uid=item.get_id())) + elif not cover_flagged and os.path.basename(filepath).lower().startswith("cover") \ + and item.media_type.startswith("image/"): + item.properties = ["cover-image"] + output.add_metadata(None, "meta", "", {"name": "cover", "content": item.get_id()}) + cover_flagged = True output.add_item(epub.EpubNcx()) output.add_item(epub.EpubNav()) epub.write_epub(location, output) - exit()