""" 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)