workbuddy.xpcool.com/.workbuddy/tmp/make-bark-assets.py

127 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
生成 Bark 推送用的图标与告警配图2026-09-13v2 修正版)
产物:
icon.png 512x512 通知图标
down.png 1200x630 服务异常告警配图(平线 = 心跳停止)
up.png 1200x630 服务恢复配图(正常心电)
要点:所有坐标必须落在画布内;波形与文字分区,互不重叠。
"""
import os
from PIL import Image, ImageDraw, ImageFont, ImageFilter
OUT = r"E:\Project\workbuddy.xpcool.com\.workbuddy\tmp\bark-assets"
os.makedirs(OUT, exist_ok=True)
FONT_BOLD = r"C:\Windows\Fonts\msyhbd.ttc"
FONT_REG = r"C:\Windows\Fonts\msyh.ttc"
def vgrad(size, top, bottom):
"""竖向线性渐变"""
w, h = size
base = Image.new("RGB", (1, h))
d = ImageDraw.Draw(base)
for y in range(h):
t = y / max(h - 1, 1)
d.point((0, y), tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3)))
return base.resize((w, h), Image.BILINEAR)
def rounded_mask(size, radius):
m = Image.new("L", size, 0)
ImageDraw.Draw(m).rounded_rectangle([0, 0, size[0] - 1, size[1] - 1], radius=radius, fill=255)
return m
# 归一化波形v 为相对基线的倍数,正=向上);幅度一律控制在 [-1, 1] 内
SHAPE_NORMAL = [
(0.00, 0.00), (0.12, 0.00), (0.16, 0.12), (0.20, 0.00),
(0.30, 0.00), (0.33, 0.18), (0.36, 1.00), (0.40, -0.42),
(0.44, 0.00), (0.58, 0.00), (0.64, 0.16), (0.70, 0.00), (1.00, 0.00),
]
SHAPE_FLAT = [ # 心跳停止:几乎一条直线,末尾一点微弱扰动
(0.00, 0.00), (0.62, 0.00), (0.68, -0.10), (0.74, 0.06), (1.00, 0.00),
]
def ecg_points(x0, x1, ybase, amp, shape, cycles=1):
pts, span = [], x1 - x0
for c in range(cycles):
for t, v in shape:
pts.append((x0 + (c + t) * span / cycles, ybase - v * amp))
ymin = min(p[1] for p in pts)
ymax = max(p[1] for p in pts)
assert amp + 1 < ybase < 630 - amp, f"基线 {ybase} 超出安全范围"
return pts, ymin, ymax
def draw_glow_line(img, pts, color, width, glow=16):
"""带柔和外发光的折线"""
layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
ImageDraw.Draw(layer).line(pts, fill=color + (105,), width=width + glow * 2, joint="curve")
img.alpha_composite(layer.filter(ImageFilter.GaussianBlur(glow)))
ImageDraw.Draw(img).line(pts, fill=color + (255,), width=width, joint="curve")
def center_text(d, cx, y, text, font, fill):
l, t, r, b = d.textbbox((0, 0), text, font=font)
d.text((cx - (r - l) / 2 - l, y), text, font=font, fill=fill)
# ---------------- 1. 通知图标 512x512 ----------------
def make_icon():
S = 512
img = vgrad((S, S), (30, 27, 75), (15, 23, 42)).convert("RGBA")
img.putalpha(rounded_mask((S, S), 116))
d = ImageDraw.Draw(img)
d.rounded_rectangle([14, 14, S - 15, S - 15], radius=102, outline=(99, 102, 241, 95), width=3)
pts, _, _ = ecg_points(80, S - 80, 272, 128, SHAPE_NORMAL, cycles=1)
draw_glow_line(img, pts, (34, 211, 238), 20, glow=15)
d.ellipse([65, 257, 95, 287], fill=(165, 243, 252, 255))
img.convert("RGB").save(os.path.join(OUT, "icon.png"), optimize=True)
print("icon.png OK")
# ---------------- 2. 告警配图 1200x630 ----------------
def make_banner(name, top, bottom, line, dot, title, subtitle, flat):
W, H = 1200, 630
img = vgrad((W, H), top, bottom).convert("RGBA")
halo = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ImageDraw.Draw(halo).ellipse([700, -260, 1460, 500], fill=dot + (52,))
img.alpha_composite(halo.filter(ImageFilter.GaussianBlur(95)))
# 波形放在上半区,基线 175振幅 62 → 范围 113~237与下方文字完全分离
shape = SHAPE_FLAT if flat else SHAPE_NORMAL
pts, ymin, ymax = ecg_points(110, W - 110, 175, 62, shape, cycles=1 if flat else 3)
draw_glow_line(img, pts, line, 11, glow=13)
# 基线参考 + 底部细分隔线
d = ImageDraw.Draw(img)
d.line([(110, 175), (W - 110, 175)], fill=line + (55,), width=2)
d.line([(110, 300), (W - 110, 300)], fill=line + (70,), width=2)
center_text(d, W / 2, 350, title, ImageFont.truetype(FONT_BOLD, 96), (255, 255, 255, 255))
center_text(d, W / 2, 480, subtitle, ImageFont.truetype(FONT_REG, 32), line + (230,))
# 左下角状态点
d.ellipse([112, 515, 148, 551], fill=dot + (255,))
img.convert("RGB").save(os.path.join(OUT, f"{name}.png"), optimize=True)
print(f"{name}.png OK (波形 y: {ymin:.0f}~{ymax:.0f})")
make_icon()
make_banner("down", (127, 29, 29), (43, 6, 6), (252, 165, 165), (239, 68, 68),
"服务异常", "UPTIME KUMA · SERVICE DOWN", flat=True)
make_banner("up", (6, 78, 59), (2, 34, 25), (110, 231, 183), (16, 185, 129),
"服务已恢复", "UPTIME KUMA · SERVICE RECOVERED", flat=False)
print("---")
for f in sorted(os.listdir(OUT)):
print(f"{f} {os.path.getsize(os.path.join(OUT, f))} bytes")