Files
Text-Texture-Generator-for-…/src/utils/overlay_assets.py
2025-10-13 12:56:15 +02:00

117 lines
3.7 KiB
Python

"""
Utilities for managing image overlay assets, including SVG rasterization.
"""
from __future__ import annotations
import hashlib
import math
import os
import sys
import tempfile
from dataclasses import dataclass
class SVGConversionError(RuntimeError):
"""Raised when an SVG overlay cannot be rasterized."""
pass
@dataclass(frozen=True)
class RasterizationResult:
"""Metadata about a rasterized overlay image."""
path: str
applied_scale: float
def _get_bpy_module():
"""Return bpy module if available, otherwise None."""
return sys.modules.get("bpy")
def is_svg_path(path: str) -> bool:
"""Return True if the provided path looks like an SVG file."""
if not path:
return False
return str(path).lower().endswith(".svg")
def get_overlay_cache_dir() -> str:
"""
Return a writable directory for cached overlay images.
Prefers Blender's CONFIG directory when available, otherwise falls back
to the system temporary directory.
"""
base_dir = None
bpy_module = _get_bpy_module()
if bpy_module is not None:
bpy_utils = getattr(bpy_module, "utils", None)
user_resource = getattr(bpy_utils, "user_resource", None)
if callable(user_resource):
try:
base_dir = user_resource('CONFIG')
except Exception:
base_dir = None
if not base_dir:
bpy_app = getattr(bpy_module, "app", None)
base_dir = getattr(bpy_app, "tempdir", None)
if not base_dir:
base_dir = tempfile.gettempdir()
cache_dir = os.path.join(os.path.abspath(base_dir), "text_texture_generator", "overlay_cache")
try:
os.makedirs(cache_dir, exist_ok=True)
except OSError:
# Fall back to a temporary directory specific to this process.
fallback_dir = os.path.join(tempfile.gettempdir(), "text_texture_generator", "overlay_cache")
os.makedirs(fallback_dir, exist_ok=True)
cache_dir = fallback_dir
return cache_dir
def ensure_svg_raster(image_path: str, scale: float) -> RasterizationResult:
"""
Ensure a rasterized PNG exists for the given SVG and return its path with applied scale.
The rasterization scale is applied during conversion so the caller should not
reapply the same scale.
"""
if not image_path or not os.path.exists(image_path):
raise SVGConversionError(f"SVG overlay path does not exist: {image_path}")
try:
import cairosvg # type: ignore
except ImportError as import_error:
raise SVGConversionError(
"SVG overlay support requires the 'cairosvg' package. "
"Install it via pip (pip install cairosvg) inside Blender's Python environment."
) from import_error
# Guard against invalid scales (NaN, infinity, zero, negative)
if not isinstance(scale, (int, float)) or not math.isfinite(scale):
raise SVGConversionError(f"Invalid overlay scale for SVG: {scale!r}")
if scale <= 0:
raise SVGConversionError("Overlay scale must be greater than zero for SVG rasterization.")
normalized_scale = max(float(scale), 0.01)
source_mtime = os.path.getmtime(image_path)
cache_dir = get_overlay_cache_dir()
cache_key = f"{os.path.abspath(image_path)}|{source_mtime:.6f}|{normalized_scale:.6f}"
cache_name = hashlib.sha256(cache_key.encode("utf-8")).hexdigest()[:32] + ".png"
cache_path = os.path.join(cache_dir, cache_name)
if not os.path.exists(cache_path):
# Perform rasterization and write to cache path
with open(cache_path, "wb") as cache_file:
cairosvg.svg2png(url=image_path, scale=normalized_scale, write_to=cache_file) # type: ignore[arg-type]
return RasterizationResult(path=cache_path, applied_scale=1.0)