zcbot/skills/ppt/scripts/svg_to_pptx/pptx_media.py

161 lines
4.4 KiB
Python

"""SVG to PNG conversion for Office compatibility mode."""
from __future__ import annotations
import hashlib
import shutil
import tempfile
from pathlib import Path
# SVG to PNG library detection
# Prefer CairoSVG (better quality), fall back to svglib
PNG_RENDERER: str | None = None
try:
import cairosvg
PNG_RENDERER = 'cairosvg'
except (ImportError, OSError):
try:
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM
PNG_RENDERER = 'svglib'
except (ImportError, OSError):
pass
def get_png_renderer_info() -> tuple[str | None, str, str | None]:
"""Get PNG renderer status information.
Returns:
(renderer_name, status_text, install_hint) tuple.
"""
if PNG_RENDERER == 'cairosvg':
return ('cairosvg', '(full gradient/filter support)', None)
elif PNG_RENDERER == 'svglib':
return ('svglib', '(some gradients may be lost)',
'Install cairosvg for better results: pip install cairosvg')
else:
return (None, '(not installed)',
'Install via: pip install cairosvg or pip install svglib reportlab')
def convert_svg_to_png(
svg_path: Path,
png_path: Path,
width: int | None = None,
height: int | None = None,
) -> bool:
"""Convert SVG to PNG using the available renderer.
Args:
svg_path: SVG file path.
png_path: Output PNG file path.
width: Output width in pixels.
height: Output height in pixels.
Returns:
Whether the conversion was successful.
"""
if PNG_RENDERER is None:
return False
try:
if PNG_RENDERER == 'cairosvg':
cairosvg.svg2png(
url=str(svg_path),
write_to=str(png_path),
output_width=width,
output_height=height,
)
return True
elif PNG_RENDERER == 'svglib':
drawing = svg2rlg(str(svg_path))
if drawing is None:
print(f" Warning: Unable to parse SVG ({svg_path.name})")
return False
renderPM.drawToFile(
drawing,
str(png_path),
fmt="PNG",
configPIL={'quality': 95},
)
return True
except Exception as e:
print(f" Warning: SVG to PNG conversion failed ({svg_path.name}): {e}")
return False
return False
def _cache_key(svg_path: Path, width: int | None, height: int | None) -> str:
h = hashlib.sha256()
with open(svg_path, 'rb') as f:
for chunk in iter(lambda: f.read(65536), b''):
h.update(chunk)
return f"{h.hexdigest()}_{width or 0}x{height or 0}_{PNG_RENDERER or 'none'}"
def convert_svg_to_png_cached(
svg_path: Path,
png_path: Path,
width: int | None = None,
height: int | None = None,
cache_dir: Path | None = None,
) -> bool:
"""Cache-aware SVG→PNG conversion.
Returns True on success (cache hit or fresh render). Cache key bakes in
SVG content hash + size + renderer name; switching renderers invalidates
naturally. Failures are never cached.
"""
if cache_dir is None:
return convert_svg_to_png(svg_path, png_path, width, height)
if PNG_RENDERER is None:
return False
try:
key = _cache_key(svg_path, width, height)
except OSError as e:
print(f" Warning: Failed to hash SVG ({svg_path.name}): {e}")
return convert_svg_to_png(svg_path, png_path, width, height)
cached = cache_dir / f"{key}.png"
if cached.is_file():
try:
shutil.copy(cached, png_path)
return True
except OSError as e:
print(f" Warning: Cache copy failed, re-rendering ({svg_path.name}): {e}")
cache_dir.mkdir(parents=True, exist_ok=True)
tmp_fd, tmp_name = tempfile.mkstemp(suffix='.png', dir=str(cache_dir))
tmp_path = Path(tmp_name)
import os
os.close(tmp_fd)
ok = convert_svg_to_png(svg_path, tmp_path, width, height)
if not ok:
try:
tmp_path.unlink()
except OSError:
pass
return False
try:
os.replace(tmp_path, cached)
except OSError:
try:
tmp_path.unlink()
except OSError:
pass
try:
shutil.copy(cached, png_path)
return True
except OSError as e:
print(f" Warning: Cache copy failed ({svg_path.name}): {e}")
return False