mirror of
https://github.com/luongnv89/claude-howto.git
synced 2026-06-01 10:31:33 +02:00
fix(epub): embed SVG images instead of replacing with placeholders (#46)
The Foliate EPUB reader blocks <object> elements (not <img>), so SVG images can be served via <img> tags. Embed SVG files as EPUB resources, unwrap <picture>/<source> wrappers, and skip external badge URLs. Closes #44
This commit is contained in:
+72
-26
@@ -161,6 +161,9 @@ class BuildState:
|
|||||||
mermaid_cache: dict[str, tuple[bytes, str]] = field(default_factory=dict)
|
mermaid_cache: dict[str, tuple[bytes, str]] = field(default_factory=dict)
|
||||||
mermaid_counter: int = 0
|
mermaid_counter: int = 0
|
||||||
mermaid_added_to_book: set[str] = field(default_factory=set)
|
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)
|
path_to_chapter: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
@@ -168,6 +171,9 @@ class BuildState:
|
|||||||
self.mermaid_cache.clear()
|
self.mermaid_cache.clear()
|
||||||
self.mermaid_counter = 0
|
self.mermaid_counter = 0
|
||||||
self.mermaid_added_to_book.clear()
|
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()
|
self.path_to_chapter.clear()
|
||||||
|
|
||||||
|
|
||||||
@@ -679,25 +685,50 @@ def create_chapter_html(
|
|||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
def handle_svg_image(src: str, alt: str, logger: logging.Logger) -> str:
|
def handle_svg_image(
|
||||||
"""Handle SVG images with a styled placeholder."""
|
src: str,
|
||||||
placeholder = f"""
|
alt: str,
|
||||||
<div class="svg-placeholder" style="
|
book: epub.EpubBook,
|
||||||
border: 1px dashed #ccc;
|
state: BuildState,
|
||||||
padding: 1em;
|
chapter_dir: Path,
|
||||||
text-align: center;
|
root_path: Path,
|
||||||
background: #f9f9f9;
|
logger: logging.Logger,
|
||||||
border-radius: 4px;
|
) -> str:
|
||||||
margin: 1em 0;
|
"""Embed an SVG image in the EPUB as a proper image resource."""
|
||||||
">
|
# Resolve the SVG file path
|
||||||
<p><em>[SVG Image: {html.escape(alt)}]</em></p>
|
svg_path = (chapter_dir / src).resolve()
|
||||||
<p style="font-size: 0.8em; color: #666;">
|
if not svg_path.is_file():
|
||||||
Original: {html.escape(src)}
|
# Try relative to repo root
|
||||||
</p>
|
svg_path = (root_path / src).resolve()
|
||||||
</div>
|
if not svg_path.is_file():
|
||||||
"""
|
logger.warning(f"SVG file not found: {src}")
|
||||||
logger.debug(f"Replaced SVG image: {src}")
|
return f'<p><em>[SVG not found: {html.escape(src)}]</em></p>'
|
||||||
return placeholder
|
|
||||||
|
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'<img src="images/{img_name}" alt="{html.escape(alt)}"/>'
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -794,7 +825,7 @@ def md_to_html(
|
|||||||
|
|
||||||
Handles:
|
Handles:
|
||||||
- Mermaid diagrams (rendered as PNG images)
|
- 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)
|
- Internal links (converted to EPUB chapter references)
|
||||||
- Standard markdown features
|
- 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 <img> tags, not <object>)
|
||||||
soup = BeautifulSoup(html_content, "html.parser")
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
|
chapter_dir = current_file.parent
|
||||||
|
|
||||||
|
# Handle <picture> elements: extract <img> child, remove <picture>/<source> 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"):
|
for img in soup.find_all("img"):
|
||||||
src = img.get("src", "")
|
src = img.get("src", "")
|
||||||
if src.endswith(".svg"):
|
if not src.endswith(".svg"):
|
||||||
alt = img.get("alt", "Image")
|
continue
|
||||||
placeholder = handle_svg_image(src, alt, logger)
|
# Skip external URLs (badges, shields, etc.)
|
||||||
img.replace_with(BeautifulSoup(placeholder, "html.parser"))
|
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)
|
html_content = str(soup)
|
||||||
|
|
||||||
@@ -851,7 +898,6 @@ def create_stylesheet() -> epub.EpubItem:
|
|||||||
a { color: #e67e22; }
|
a { color: #e67e22; }
|
||||||
img { max-width: 100%; height: auto; display: block; margin: 1em auto; }
|
img { max-width: 100%; height: auto; display: block; margin: 1em auto; }
|
||||||
.diagram { text-align: center; margin: 1.5em 0; }
|
.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(
|
return epub.EpubItem(
|
||||||
uid="style_nav",
|
uid="style_nav",
|
||||||
|
|||||||
Reference in New Issue
Block a user