117 lines
3.7 KiB
Python
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)
|