#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 生成文章编辑器「添加内容」面板的图标。 原实现引用 qnycdn.mymoyu.top 上的图标,该域名不在本仓库管理范围内, 且项目里其余同类地址早已因签名过期而 403。这里改为本地生成, 产物落在 static/editor-icons/ 下,任何环境都能直接渲染。 用法: python tools/gen-editor-icons.py """ import os import math from PIL import Image, ImageDraw ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT_DIR = os.path.join(ROOT, "static", "editor-icons") SIZE = 96 # 逻辑尺寸 SCALE = 3 # 超采样倍数,边缘更干净 S = SIZE * SCALE INK = (58, 50, 44, 255) # 主体线条 ACCENT = (193, 28, 31, 255) # 军旅红点缀 MUTED = (176, 168, 160, 255) def blank(): return Image.new("RGBA", (S, S), (0, 0, 0, 0)) def stroke(width): return max(1, int(width * SCALE)) def draw_text_icon(): """文字:两行横线,长度不同""" img = blank() d = ImageDraw.Draw(img) w = stroke(7) d.rounded_rectangle([14 * SCALE, 20 * SCALE, 82 * SCALE, 76 * SCALE], radius=8 * SCALE, outline=INK, width=w) d.line([26 * SCALE, 38 * SCALE, 70 * SCALE, 38 * SCALE], fill=INK, width=w) d.line([26 * SCALE, 54 * SCALE, 56 * SCALE, 54 * SCALE], fill=ACCENT, width=w) return img def draw_image_icon(): """图片:方框 + 山峰 + 太阳""" img = blank() d = ImageDraw.Draw(img) w = stroke(7) d.rounded_rectangle([12 * SCALE, 20 * SCALE, 84 * SCALE, 76 * SCALE], radius=8 * SCALE, outline=INK, width=w) d.ellipse([26 * SCALE, 32 * SCALE, 40 * SCALE, 46 * SCALE], fill=ACCENT) d.polygon([(20 * SCALE, 70 * SCALE), (44 * SCALE, 46 * SCALE), (64 * SCALE, 70 * SCALE)], fill=INK) d.polygon([(52 * SCALE, 70 * SCALE), (66 * SCALE, 56 * SCALE), (82 * SCALE, 70 * SCALE)], fill=MUTED) return img def draw_video_icon(): """视频:圆角矩形 + 播放三角""" img = blank() d = ImageDraw.Draw(img) w = stroke(7) d.rounded_rectangle([10 * SCALE, 22 * SCALE, 86 * SCALE, 74 * SCALE], radius=10 * SCALE, outline=INK, width=w) d.polygon([(40 * SCALE, 36 * SCALE), (40 * SCALE, 60 * SCALE), (62 * SCALE, 48 * SCALE)], fill=ACCENT) return img def draw_vote_icon(): """投票:三条横条,第一条已勾选""" img = blank() d = ImageDraw.Draw(img) w = stroke(7) rows = [26, 48, 70] for i, y in enumerate(rows): color = ACCENT if i == 0 else INK d.rounded_rectangle([14 * SCALE, (y - 9) * SCALE, 34 * SCALE, (y + 9) * SCALE], radius=4 * SCALE, outline=color, width=w) if i == 0: d.line([18 * SCALE, y * SCALE, 22 * SCALE, (y + 5) * SCALE], fill=ACCENT, width=w) d.line([22 * SCALE, (y + 5) * SCALE, 30 * SCALE, (y - 6) * SCALE], fill=ACCENT, width=w) d.line([44 * SCALE, y * SCALE, 82 * SCALE, y * SCALE], fill=color, width=w) return img def draw_title_icon(): """小标题:短横线 + 下方长横线""" img = blank() d = ImageDraw.Draw(img) w = stroke(9) d.line([16 * SCALE, 36 * SCALE, 56 * SCALE, 36 * SCALE], fill=ACCENT, width=w) d.line([16 * SCALE, 58 * SCALE, 80 * SCALE, 58 * SCALE], fill=INK, width=stroke(6)) return img def draw_not_image_icon(): """无图占位:方框 + 斜杠""" img = blank() d = ImageDraw.Draw(img) w = stroke(6) d.rounded_rectangle([12 * SCALE, 20 * SCALE, 84 * SCALE, 76 * SCALE], radius=8 * SCALE, outline=MUTED, width=w) d.line([18 * SCALE, 28 * SCALE, 78 * SCALE, 68 * SCALE], fill=MUTED, width=w) return img def draw_stop_icon(): """视频遮罩上的播放按钮""" img = blank() d = ImageDraw.Draw(img) d.ellipse([8 * SCALE, 8 * SCALE, 88 * SCALE, 88 * SCALE], fill=(0, 0, 0, 110)) d.ellipse([8 * SCALE, 8 * SCALE, 88 * SCALE, 88 * SCALE], outline=(255, 255, 255, 230), width=stroke(5)) d.polygon([(40 * SCALE, 32 * SCALE), (40 * SCALE, 64 * SCALE), (68 * SCALE, 48 * SCALE)], fill=(255, 255, 255, 240)) return img def draw_is_vote_icon(): """投票空态插画""" img = blank() d = ImageDraw.Draw(img) w = stroke(6) d.rounded_rectangle([20 * SCALE, 18 * SCALE, 76 * SCALE, 80 * SCALE], radius=8 * SCALE, outline=MUTED, width=w) for i, y in enumerate([34, 50, 66]): d.ellipse([30 * SCALE, (y - 5) * SCALE, 40 * SCALE, (y + 5) * SCALE], outline=ACCENT if i == 0 else MUTED, width=w) d.line([48 * SCALE, y * SCALE, 68 * SCALE, y * SCALE], fill=MUTED, width=w) return img BUILDERS = { "text": draw_text_icon, "image": draw_image_icon, "video": draw_video_icon, "vote": draw_vote_icon, "title": draw_title_icon, "not_image": draw_not_image_icon, "stop": draw_stop_icon, "is_vote": draw_is_vote_icon, } def main(): os.makedirs(OUT_DIR, exist_ok=True) for name, fn in BUILDERS.items(): img = fn().resize((SIZE, SIZE), Image.LANCZOS) path = os.path.join(OUT_DIR, f"{name}.png") img.save(path, "PNG", optimize=True) print(f" {name + '.png':<18} {os.path.getsize(path)/1024:5.1f} KB") print(f"共 {len(BUILDERS)} 个图标 → {OUT_DIR}") if __name__ == "__main__": main()