"""Placeholder PNG generator — рендерит per-cell offline placeholder ("CAMERA LABEL — НЕТ СИГНАЛА") при controller startup. Filter cuda_grid looks up "_.png" first (per-cell), fallback к ".png" generic. Этот module генерирует per-cell PNG с camera label из FrigateBridge config — user меняет label в yaml, controller restarts, PNGs регенерируются. Поддерживает UTF-8 (Russian text) через PIL freetype — недоступно в filter's basic FT_Get_Char_Index path (single-byte only). """ from __future__ import annotations from pathlib import Path import structlog from PIL import Image, ImageDraw, ImageFont log = structlog.get_logger() _FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: try: return ImageFont.truetype(_FONT_PATH, size=size) except OSError: return ImageFont.load_default() def render_placeholder( label: str, width: int = 480, height: int = 120, ) -> Image.Image: """2-line transparent PNG: camera label (top, yellow) + 'НЕТ СИГНАЛА' (bottom, gray). Transparent bg — filter заполняет cell black, потом blit'ит этот PNG поверх.""" img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) label_font = _font(36) sub_font = _font(28) bbox_l = draw.textbbox((0, 0), label, font=label_font) lw, lh = bbox_l[2] - bbox_l[0], bbox_l[3] - bbox_l[1] label_y = height // 2 - lh - 4 draw.text(((width - lw) // 2, label_y), label, fill=(255, 230, 100, 255), font=label_font) sub_text = "НЕТ СИГНАЛА" bbox_s = draw.textbbox((0, 0), sub_text, font=sub_font) sw, sh = bbox_s[2] - bbox_s[0], bbox_s[3] - bbox_s[1] sub_y = height // 2 + 4 draw.text(((width - sw) // 2, sub_y), sub_text, fill=(220, 220, 220, 230), font=sub_font) return img def generate_placeholders(icon_dir: Path, labels: dict[int, str], base_name: str = "offline") -> None: """Generates _.png для each (pad → label) entry. Also generic .png (fallback когда pad index без custom label).""" icon_dir.mkdir(parents=True, exist_ok=True) for pad, label in labels.items(): img = render_placeholder(label) path = icon_dir / f"{base_name}_{pad}.png" tmp = path.with_suffix(".png.tmp") img.save(tmp, "PNG") tmp.replace(path) log.info("placeholder.rendered", pad=pad, label=label, path=str(path)) img = render_placeholder("СИГНАЛ ПОТЕРЯН") path = icon_dir / f"{base_name}.png" tmp = path.with_suffix(".png.tmp") img.save(tmp, "PNG") tmp.replace(path) log.info("placeholder.rendered_generic", path=str(path))