f5ea2e3005
При startup controller iterate'т FrigateBridge mappings и рендерит:
- offline_<cell>.png с camera label из placeholder_label (или frigate_camera
как fallback) — для каждого pad
- offline.png generic — для cells без mapping
Filter cuda_grid looks up "offline_<pad>.png" first per-cell, fallback к
"offline.png". User меняет yaml placeholder_label → restart controller →
PNGs регенерируются. Никаких ручных PNG manipulations.
Russian text supported (PIL + freetype, vs single-byte path в filter).
Pattern:
frigate.mappings:
- frigate_camera: parking_overview
placeholder_label: Парковка
cell: 0
...
При stale input на cell 0 → filter blits offline_0.png с "Парковка — НЕТ
СИГНАЛА" (текст желтый, transparent bg — filter сам fill'ит black за иконкой).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""Placeholder PNG generator — рендерит per-cell offline placeholder
|
||
("CAMERA LABEL — НЕТ СИГНАЛА") при controller startup.
|
||
|
||
Filter cuda_grid looks up "<placeholder_icon>_<pad>.png" first (per-cell),
|
||
fallback к "<placeholder_icon>.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 <base>_<pad>.png для each (pad → label) entry.
|
||
Also generic <base>.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))
|