"""PNG-рендер валидных SVG. Бэкенды (по убыванию предпочтения): 1. resvg-py — self-contained Rust-движок, не требует нативных зависимостей. Это дефолт на Windows, где cairo.dll часто отсутствует в глобальном Python. 2. cairosvg — fallback, если cairo.dll уже установлена (типично для conda). Если ничего не работает, `render_png()` возвращает `None` и пишет WARNING. """ from __future__ import annotations import logging from pathlib import Path from typing import Tuple log = logging.getLogger(__name__) # Ленивая инициализация бэкендов. _resvg_module = None _resvg_error: str | None = None _cairosvg_module = None _cairosvg_error: str | None = None def _get_resvg(): global _resvg_module, _resvg_error if _resvg_module is not None: return _resvg_module if _resvg_error is not None: return None try: import resvg_py # type: ignore _resvg_module = resvg_py log.info("renderer: using resvg-py") return _resvg_module except Exception as exc: # noqa: BLE001 _resvg_error = f"{type(exc).__name__}: {exc}" log.warning("resvg-py недоступен: %s", _resvg_error) return None def _get_cairosvg(): global _cairosvg_module, _cairosvg_error if _cairosvg_module is not None: return _cairosvg_module if _cairosvg_error is not None: return None try: import cairosvg # type: ignore _cairosvg_module = cairosvg log.info("renderer: using cairosvg (fallback)") return _cairosvg_module except Exception as exc: # noqa: BLE001 _cairosvg_error = f"{type(exc).__name__}: {exc}" log.warning("cairosvg недоступен: %s", _cairosvg_error) return None def _ensure_xmlns(svg_text: str) -> str: """Добавляет xmlns, если модель его не поставила (рендереры без него падают).""" head_end = svg_text.find(">") if head_end < 0: return svg_text head = svg_text[:head_end] if "xmlns=" in head: return svg_text return svg_text.replace(" bytes | None: """Рендерит SVG в PNG-байты. Возвращает None если ни один бэкенд не сработал. Args: svg_text: валидный SVG-текст. size: целевой размер. resvg/cairopng сохраняют пропорции по viewBox. Returns: PNG-байты или None. """ if not svg_text or not svg_text.strip(): log.warning("render_png: пустой svg_text") return None output_width = size[0] svg_str = _ensure_xmlns(svg_text) # Бэкенд 1: resvg-py resvg = _get_resvg() if resvg is not None: try: return resvg.svg_to_bytes(svg_string=svg_str) except Exception as exc: # noqa: BLE001 log.warning("resvg упал (%s); пробую cairosvg", exc) # Бэкенд 2: cairosvg cairosvg = _get_cairosvg() if cairosvg is not None: try: return cairosvg.svg2png( bytestring=svg_str.encode("utf-8"), output_width=output_width, background_color="white", ) except Exception as exc: # noqa: BLE001 log.warning("cairosvg упал (%s); превью пропущено", exc) log.warning("render_png: ни один бэкенд не сработал") return None def save_png( png_bytes: bytes, *, previews_dir: str | Path, record_id: int, candidate_index: int, ) -> Path: previews_dir = Path(previews_dir) previews_dir.mkdir(parents=True, exist_ok=True) path = previews_dir / f"{record_id}_{candidate_index}.png" path.write_bytes(png_bytes) return path __all__ = ["render_png", "save_png"]