Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user