161 lines
4.4 KiB
Python
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
|