stream_dock_linux/key_renderer.py

308 lines
10 KiB
Python
Raw Normal View History

import os
import math
from PIL import Image, ImageDraw, ImageFont
_FONT_CACHE = {}
_FONT_SEARCH_PATHS = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
"/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
]
def _get_font(size):
if size not in _FONT_CACHE:
for name in _FONT_SEARCH_PATHS:
if os.path.exists(name):
_FONT_CACHE[size] = ImageFont.truetype(name, size)
return _FONT_CACHE[size]
_FONT_CACHE[size] = ImageFont.load_default()
return _FONT_CACHE[size]
ICON_COLORS = {
"lightbulb": {"on": (255, 210, 50), "off": (80, 80, 80)},
"power": {"on": (50, 200, 50), "off": (80, 80, 80)},
"thermometer": {"on": (200, 60, 30), "off": (80, 80, 80)},
"thermostat": {"on": (50, 150, 220), "off": (80, 80, 80)},
"play": {"on": (50, 200, 50), "off": (80, 80, 80)},
"film": {"on": (160, 50, 200), "off": (80, 80, 80)},
"door": {"on": (50, 200, 50), "off": (200, 50, 50)},
"lock": {"on": (50, 200, 50), "off": (200, 50, 50)},
"speaker": {"on": (50, 150, 220), "off": (80, 80, 80)},
"fan": {"on": (50, 200, 220), "off": (80, 80, 80)},
"default": {"on": (50, 150, 220), "off": (80, 80, 80)},
}
def _is_on(state_str):
s = state_str.lower() if state_str else "off"
return s in ("on", "playing", "open", "unlocked", "home", "heat", "cool", "auto")
def _get_colors(icon, state_str):
is_on = _is_on(state_str)
icon_key = icon if icon in ICON_COLORS else "default"
key = "on" if is_on else "off"
return ICON_COLORS[icon_key][key], is_on
def _draw_icon(draw, icon, cx, cy, r, color):
if icon == "lightbulb":
_draw_lightbulb(draw, cx, cy, r, color)
elif icon == "power":
_draw_power(draw, cx, cy, r, color)
elif icon == "thermometer":
_draw_thermometer(draw, cx, cy, r, color)
elif icon == "play":
_draw_play(draw, cx, cy, r, color)
elif icon == "film":
_draw_film(draw, cx, cy, r, color)
elif icon == "door":
_draw_door(draw, cx, cy, r, color)
elif icon == "lock":
_draw_lock(draw, cx, cy, r, color)
elif icon == "fan":
_draw_fan(draw, cx, cy, r, color)
elif icon == "thermostat":
_draw_thermostat(draw, cx, cy, r, color)
elif icon == "speaker":
_draw_speaker(draw, cx, cy, r, color)
else:
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=color)
def _draw_lightbulb(draw, cx, cy, r, color):
bulb_r = int(r * 0.6)
bulb_cy = cy - int(r * 0.15)
draw.ellipse(
[cx - bulb_r, bulb_cy - bulb_r, cx + bulb_r, bulb_cy + bulb_r], fill=color
)
base_w = int(r * 0.35)
base_h = int(r * 0.3)
base_top = bulb_cy + bulb_r - 2
draw.rectangle(
[cx - base_w, base_top, cx + base_w, base_top + base_h], fill=color
)
for i in range(3):
yy = base_top + int(base_h * 0.2) + i * int(base_h * 0.3)
draw.line([cx - base_w, yy, cx + base_w, yy], fill=(30, 30, 30), width=1)
def _draw_power(draw, cx, cy, r, color):
gap = int(r * 0.2)
arc_r = int(r * 0.7)
draw.line([cx, cy - r, cx, cy - gap], fill=color, width=max(2, int(r * 0.15)))
bbox = [cx - arc_r, cy - arc_r, cx + arc_r, cy + arc_r]
draw.arc(bbox, start=225, end=315, fill=color, width=max(2, int(r * 0.12)))
draw.arc(bbox, start=45, end=135, fill=color, width=max(2, int(r * 0.12)))
def _draw_thermometer(draw, cx, cy, r, color):
bulb_r = int(r * 0.35)
bulb_cy = cy + int(r * 0.35)
draw.ellipse(
[cx - bulb_r, bulb_cy - bulb_r, cx + bulb_r, bulb_cy + bulb_r], fill=color
)
tube_w = int(r * 0.18)
tube_top = cy - int(r * 0.85)
draw.rectangle(
[cx - tube_w, tube_top, cx + tube_w, bulb_cy - bulb_r + 2], fill=color
)
fill_h = int(r * 0.5)
fill_top = bulb_cy - fill_h
draw.rectangle(
[cx - tube_w + 1, fill_top, cx + tube_w - 1, bulb_cy], fill=(220, 60, 30)
)
def _draw_play(draw, cx, cy, r, color):
pts = [
(cx - int(r * 0.4), cy - int(r * 0.6)),
(cx - int(r * 0.4), cy + int(r * 0.6)),
(cx + int(r * 0.6), cy),
]
draw.polygon(pts, fill=color)
def _draw_film(draw, cx, cy, r, color):
w, h = int(r * 1.2), int(r * 0.8)
draw.rectangle([cx - w, cy - h, cx + w, cy + h], outline=color, width=2)
for yy in [cy - h, cy + h]:
for xx_off in [-w, w]:
draw.rectangle(
[cx + xx_off - 3, yy - 3, cx + xx_off + 3, yy + 3], fill=color
)
draw.rectangle(
[cx - int(w * 0.6), cy - int(h * 0.5), cx + int(w * 0.6), cy + int(h * 0.5)],
fill=color,
)
def _draw_door(draw, cx, cy, r, color):
w, h = int(r * 0.6), int(r * 0.9)
draw.rectangle(
[cx - w, cy - h, cx + w, cy + h], outline=color, width=max(2, int(r * 0.08))
)
knob_r = max(2, int(r * 0.08))
draw.ellipse([cx + w - knob_r * 4, cy - knob_r, cx + w - knob_r * 2, cy + knob_r], fill=color)
def _draw_lock(draw, cx, cy, r, color):
body_w, body_h = int(r * 0.7), int(r * 0.5)
body_top = cy - int(r * 0.05)
draw.rectangle(
[cx - body_w, body_top, cx + body_w, body_top + body_h], fill=color
)
arc_r = int(r * 0.45)
arc_cy = body_top - 2
draw.arc(
[cx - arc_r, arc_cy - arc_r, cx + arc_r, arc_cy + arc_r],
start=180,
end=360,
fill=color,
width=max(2, int(r * 0.1)),
)
def _draw_fan(draw, cx, cy, r, color):
blade_r = int(r * 0.75)
for angle_offset in [0, 90, 180, 270]:
a = math.radians(angle_offset)
x2 = cx + int(blade_r * math.cos(a))
y2 = cy + int(blade_r * math.sin(a))
perp_x = int(blade_r * 0.3 * math.cos(a + math.pi / 2))
perp_y = int(blade_r * 0.3 * math.sin(a + math.pi / 2))
pts = [(cx, cy), (x2 + perp_x, y2 + perp_y), (x2 - perp_x, y2 - perp_y)]
draw.polygon(pts, fill=color)
draw.ellipse([cx - int(r * 0.15), cy - int(r * 0.15), cx + int(r * 0.15), cy + int(r * 0.15)], fill=(40, 40, 40))
def _draw_thermostat(draw, cx, cy, r, color):
draw.ellipse(
[cx - int(r * 0.7), cy - int(r * 0.7), cx + int(r * 0.7), cy + int(r * 0.7)],
outline=color,
width=max(2, int(r * 0.08)),
)
hand_len = int(r * 0.5)
angle = math.radians(-45)
hx = cx + int(hand_len * math.cos(angle))
hy = cy + int(hand_len * math.sin(angle))
draw.line([cx, cy, hx, hy], fill=color, width=max(2, int(r * 0.1)))
draw.ellipse([cx - 3, cy - 3, cx + 3, cy + 3], fill=color)
def _draw_speaker(draw, cx, cy, r, color):
draw.polygon(
[
(cx - int(r * 0.5), cy - int(r * 0.25)),
(cx - int(r * 0.15), cy - int(r * 0.25)),
(cx - int(r * 0.15), cy - int(r * 0.6)),
(cx + int(r * 0.2), cy - int(r * 0.6)),
],
fill=color,
)
draw.polygon(
[
(cx - int(r * 0.5), cy + int(r * 0.25)),
(cx - int(r * 0.15), cy + int(r * 0.25)),
(cx - int(r * 0.15), cy + int(r * 0.6)),
(cx + int(r * 0.2), cy + int(r * 0.6)),
],
fill=color,
)
draw.rectangle(
[cx - int(r * 0.5), cy - int(r * 0.25), cx - int(r * 0.15), cy + int(r * 0.25)],
fill=color,
)
def render_key_image(key_config, state_str, size=64):
icon = key_config.get("icon", "default")
label = key_config.get("label", "")
bg_color, is_on = _get_colors(icon, state_str)
img = Image.new("RGB", (size, size), (20, 20, 20))
draw = ImageDraw.Draw(img)
if is_on:
glow = Image.new("RGB", (size, size), (0, 0, 0))
gd = ImageDraw.Draw(glow)
glow_r = int(size * 0.45)
gx, gy = size // 2, size // 2 - int(size * 0.05)
for i in range(glow_r, 0, -1):
alpha = int(40 * (i / glow_r))
c = tuple(min(255, v * alpha // 100) for v in bg_color)
gd.ellipse([gx - i, gy - i, gx + i, gy + i], fill=c)
img = Image.blend(img, glow, 0.6)
draw = ImageDraw.Draw(img)
icon_r = int(size * 0.28)
icon_cy = size // 2 - int(size * 0.1)
_draw_icon(draw, icon, size // 2, icon_cy, icon_r, bg_color)
if label:
font_size = max(8, int(size * 0.19))
font = _get_font(font_size)
text = label[:8]
bbox = draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
tx = (size - tw) // 2
ty = size - int(size * 0.28)
draw.text((tx, ty), text, fill=(220, 220, 220), font=font)
state_display = _format_state(state_str, key_config)
if state_display:
font_size = max(7, int(size * 0.16))
font = _get_font(font_size)
bbox = draw.textbbox((0, 0), state_display, font=font)
tw = bbox[2] - bbox[0]
tx = (size - tw) // 2
ty = int(size * 0.06)
draw.text((tx, ty), state_display, fill=(180, 180, 180), font=font)
return img
def _format_state(state_str, key_config):
if not state_str:
return ""
s = state_str.lower()
if s in ("on", "off", "unavailable", "unknown"):
return s.capitalize()
if s in ("playing", "paused", "idle", "standby"):
return s.capitalize()
if s in ("open", "closed"):
return s.capitalize()
if s in ("home", "away", "not_home"):
return s.capitalize()
if any(unit in state_str for unit in ["°", "%", "V", "A", "W", "kW", "mph", "km/h", "Hz"]):
return state_str
return state_str[:7]
def render_background(pages, active_page, page_num=1, total_pages=1, size=(320, 240)):
img = Image.new("RGB", size, (25, 25, 30))
draw = ImageDraw.Draw(img)
font = _get_font(max(10, int(size[1] * 0.07)))
small_font = _get_font(max(8, int(size[1] * 0.05)))
draw.text((10, 10), active_page, fill=(255, 255, 255), font=font)
if total_pages > 1:
dot_r = 3
dot_spacing = 14
total_width = total_pages * dot_spacing
start_x = (size[0] - total_width) // 2
page_label_y = size[1] - 20
for i in range(total_pages):
cx = start_x + i * dot_spacing + dot_spacing // 2
if i == page_num - 1:
draw.ellipse([cx - dot_r, page_label_y - dot_r, cx + dot_r, page_label_y + dot_r], fill=(255, 255, 255))
else:
draw.ellipse([cx - dot_r, page_label_y - dot_r, cx + dot_r, page_label_y + dot_r], fill=(80, 80, 80))
return img