From 1b1670909d771a0a0036720eb13fb028e335dfba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thi=C3=AAn=20To=C3=A1n?= Date: Tue, 7 Apr 2026 03:04:44 +0700 Subject: [PATCH] fix(epub): embed SVG images instead of replacing with placeholders (#46) The Foliate EPUB reader blocks elements (not ), so SVG images can be served via tags. Embed SVG files as EPUB resources, unwrap / wrappers, and skip external badge URLs. Closes #44 --- scripts/build_epub.py | 98 +++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/scripts/build_epub.py b/scripts/build_epub.py index 1fb4a79..ae33f4c 100755 --- a/scripts/build_epub.py +++ b/scripts/build_epub.py @@ -161,6 +161,9 @@ class BuildState: mermaid_cache: dict[str, tuple[bytes, str]] = field(default_factory=dict) mermaid_counter: int = 0 mermaid_added_to_book: set[str] = field(default_factory=set) + svg_cache: dict[str, tuple[bytes, str]] = field(default_factory=dict) + svg_counter: int = 0 + svg_added_to_book: set[str] = field(default_factory=set) path_to_chapter: dict[str, str] = field(default_factory=dict) def reset(self) -> None: @@ -168,6 +171,9 @@ class BuildState: self.mermaid_cache.clear() self.mermaid_counter = 0 self.mermaid_added_to_book.clear() + self.svg_cache.clear() + self.svg_counter = 0 + self.svg_added_to_book.clear() self.path_to_chapter.clear() @@ -679,25 +685,50 @@ def create_chapter_html( """ -def handle_svg_image(src: str, alt: str, logger: logging.Logger) -> str: - """Handle SVG images with a styled placeholder.""" - placeholder = f""" -
-

[SVG Image: {html.escape(alt)}]

-

- Original: {html.escape(src)} -

-
- """ - logger.debug(f"Replaced SVG image: {src}") - return placeholder +def handle_svg_image( + src: str, + alt: str, + book: epub.EpubBook, + state: BuildState, + chapter_dir: Path, + root_path: Path, + logger: logging.Logger, +) -> str: + """Embed an SVG image in the EPUB as a proper image resource.""" + # Resolve the SVG file path + svg_path = (chapter_dir / src).resolve() + if not svg_path.is_file(): + # Try relative to repo root + svg_path = (root_path / src).resolve() + if not svg_path.is_file(): + logger.warning(f"SVG file not found: {src}") + return f'

[SVG not found: {html.escape(src)}]

' + + svg_key = str(svg_path) + + # Check cache for the assigned image name + if svg_key in state.svg_cache: + _, img_name = state.svg_cache[svg_key] + else: + svg_data = svg_path.read_bytes() + state.svg_counter += 1 + img_name = f"svg_{state.svg_counter}.svg" + state.svg_cache[svg_key] = (svg_data, img_name) + + # Add image to book once + if img_name not in state.svg_added_to_book: + svg_data, _ = state.svg_cache[svg_key] + img_item = epub.EpubItem( + uid=img_name.replace(".", "_"), + file_name=f"images/{img_name}", + media_type="image/svg+xml", + content=svg_data, + ) + book.add_item(img_item) + state.svg_added_to_book.add(img_name) + + logger.debug(f"Embedded SVG image: {src} -> {img_name}") + return f'{html.escape(alt)}' # ============================================================================= @@ -794,7 +825,7 @@ def md_to_html( Handles: - Mermaid diagrams (rendered as PNG images) - - SVG images (replaced with styled placeholders) + - SVG images (embedded as EPUB image resources) - Internal links (converted to EPUB chapter references) - Standard markdown features """ @@ -812,14 +843,30 @@ def md_to_html( ], ) - # Clean up any SVG references (they won't work in EPUB) + # Embed SVG images as EPUB resources (using tags, not ) soup = BeautifulSoup(html_content, "html.parser") + chapter_dir = current_file.parent + + # Handle elements: extract child, remove / wrapper + for picture in soup.find_all("picture"): + img = picture.find("img") + if img: + picture.replace_with(img) + else: + picture.decompose() + for img in soup.find_all("img"): src = img.get("src", "") - if src.endswith(".svg"): - alt = img.get("alt", "Image") - placeholder = handle_svg_image(src, alt, logger) - img.replace_with(BeautifulSoup(placeholder, "html.parser")) + if not src.endswith(".svg"): + continue + # Skip external URLs (badges, shields, etc.) + if src.startswith(("http://", "https://")): + continue + alt = img.get("alt", "Image") + converted = handle_svg_image( + src, alt, book, state, chapter_dir, root_path, logger + ) + img.replace_with(BeautifulSoup(converted, "html.parser")) html_content = str(soup) @@ -851,7 +898,6 @@ def create_stylesheet() -> epub.EpubItem: a { color: #e67e22; } img { max-width: 100%; height: auto; display: block; margin: 1em auto; } .diagram { text-align: center; margin: 1.5em 0; } - .svg-placeholder { border: 1px dashed #ccc; padding: 1em; text-align: center; background: #f9f9f9; border-radius: 4px; margin: 1em 0; } """ return epub.EpubItem( uid="style_nav",