Files
ios-location-track/tools/make_icon.py
T
jbjf6666 85e9a53750 docs: 新增 Windows 移植交接文档
docs/WINDOWS_PORT.md 覆盖:
- 逐文件可移植性审计(核心逻辑层约 90% 可直接复用)
- 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC +
  ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道
- 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter
  auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel)
- 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico)
- macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils
  放进 excludes、AlreadyMountedError 当成功处理
- 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等)
- 验收基线(复用现有测试套件)与建议的工作顺序
2026-10-16 01:05:09 +08:00

199 lines
7.0 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.
#!/usr/bin/env python3
"""生成应用图标(程序化绘制,无需设计文件)。
设计:
- 深色圆角方块底(与界面深色主题一致),叠加轻微纵向渐变
- 一条虚线轨迹(用点表示途经点),从左上延伸到中心的定位针
- 定位针用「描边 + 内部圆点」造型,16px 下仍然可辨识
- 右下角一枚小圆点表示"当前位置"的脉冲
输出:resources/AppIcon.iconset/*.png + resources/AppIcon.icns
用法:./.venv/bin/python tools/make_icon.py
"""
from __future__ import annotations
import math
import shutil
import subprocess
import sys
from pathlib import Path
try:
from PIL import Image, ImageDraw
except ImportError:
print("需要 Pillow:./.venv/bin/python -m pip install pillow")
raise SystemExit(1)
ROOT = Path(__file__).resolve().parent.parent
OUT_DIR = ROOT / "resources"
ICONSET = OUT_DIR / "AppIcon.iconset"
ICNS = OUT_DIR / "AppIcon.icns"
# 主题色(与界面一致)
BG_TOP = (26, 36, 52)
BG_BOTTOM = (14, 20, 30)
ACCENT = (59, 130, 246) # 蓝
ACCENT_HI = (96, 165, 250)
ROUTE = (148, 163, 184) # 轨迹灰
PIN = (248, 113, 113) # 定位针红
PIN_HI = (254, 202, 202)
# macOS 图标规范:内容四周留出约 10% 安全边距,圆角半径约 22.37%
SAFE_RATIO = 0.085
CORNER_RATIO = 0.2237
def _bezier(points, steps=200):
"""二次贝塞尔采样(用于画出平滑的轨迹线)。"""
(x1, y1), (x2, y2), (x3, y3) = points
out = []
for i in range(steps + 1):
t = i / steps
mt = 1 - t
out.append((
mt * mt * x1 + 2 * mt * t * x2 + t * t * x3,
mt * mt * y1 + 2 * mt * t * y2 + t * t * y3,
))
return out
def _stroke(draw, points, width, color):
"""用填充多边形画出平滑的粗线(PIL 的宽 line 逐段绘制会出现"肋纹")。"""
if len(points) < 2:
return
half = width / 2.0
left, right = [], []
n = len(points)
for i, (x, y) in enumerate(points):
# 用前后点估计切线方向
px, py = points[max(0, i - 1)]
nx, ny = points[min(n - 1, i + 1)]
dx, dy = nx - px, ny - py
length = math.hypot(dx, dy)
if length < 1e-9:
dx, dy, length = 1.0, 0.0, 1.0
ux, uy = -dy / length, dx / length # 法线
left.append((x + ux * half, y + uy * half))
right.append((x - ux * half, y - uy * half))
draw.polygon(left + right[::-1], fill=color)
# 两端补圆头
r = width / 2.0
for (ex, ey) in (points[0], points[-1]):
draw.ellipse((ex - r, ey - r, ex + r, ey + r), fill=color)
def draw_icon(size: int) -> Image.Image:
"""在 size×size 画布上绘制图标(4 倍超采样后缩小,边缘平滑)。"""
ss = 4
canvas = size * ss
img = Image.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
margin = int(canvas * SAFE_RATIO)
box = (margin, margin, canvas - margin, canvas - margin)
radius = int((box[2] - box[0]) * CORNER_RATIO)
inner = box[2] - box[0]
# 1) 圆角底 + 纵向渐变(对角向,更像 macOS 应用图标)
gradient = Image.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
gdraw = ImageDraw.Draw(gradient)
for y in range(box[1], box[3]):
t = (y - box[1]) / max(1, box[3] - box[1])
color = tuple(int(BG_TOP[i] + (BG_BOTTOM[i] - BG_TOP[i]) * t) for i in range(3))
gdraw.line([(box[0], y), (box[2], y)], fill=color + (255,))
mask = Image.new("L", (canvas, canvas), 0)
ImageDraw.Draw(mask).rounded_rectangle(box, radius=radius, fill=255)
img.paste(gradient, (0, 0), mask)
draw = ImageDraw.Draw(img)
line_w = max(2, int(inner * 0.048))
# 2) 平滑轨迹线:从左下到右上的贝塞尔曲线(实线 + 途经点圆点,小尺寸下最清晰)
curve = _bezier([
(box[0] + inner * 0.14, box[1] + inner * 0.82),
(box[0] + inner * 0.44, box[1] + inner * 0.76),
(box[0] + inner * 0.60, box[1] + inner * 0.56),
])
_stroke(draw, curve, line_w, ROUTE + (215,))
# 途经点小圆(沿轨迹均匀分布,表达"添加途经点"这一核心功能)
dot_r = line_w * 0.78
for frac in (0.18, 0.45):
wx, wy = curve[int(len(curve) * frac)]
draw.ellipse((wx - dot_r, wy - dot_r, wx + dot_r, wy + dot_r), fill=ACCENT_HI + (255,))
# 3) 定位针(水滴形 + 白色内圆)
pin_cx = box[0] + inner * 0.68
pin_cy = box[1] + inner * 0.38
pin_r = inner * 0.185
tip_y = pin_cy + pin_r * 2.16
# 针身:圆 + 收窄到底部的三角,拼成水滴
draw.ellipse((pin_cx - pin_r, pin_cy - pin_r, pin_cx + pin_r, pin_cy + pin_r), fill=PIN + (255,))
draw.polygon([
(pin_cx - pin_r * 0.60, pin_cy + pin_r * 0.78),
(pin_cx + pin_r * 0.60, pin_cy + pin_r * 0.78),
(pin_cx, tip_y),
], fill=PIN + (255,))
# 白色内圆:小尺寸下也能看出是"定位标记"
hole = pin_r * 0.40
draw.ellipse((pin_cx - hole, pin_cy - hole, pin_cx + hole, pin_cy + hole), fill=(255, 255, 255, 255))
# 4) 针下方落点阴影(让针"站"在地图上)
sh_w = pin_r * 0.95
sh_h = max(1, int(pin_r * 0.22))
shadow = Image.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
ImageDraw.Draw(shadow).ellipse(
(pin_cx - sh_w, tip_y - sh_h * 0.4, pin_cx + sh_w, tip_y + sh_h * 1.6),
fill=(0, 0, 0, 90),
)
img = Image.alpha_composite(img, shadow)
draw = ImageDraw.Draw(img)
# 5) 当前位置:轨迹上的一个高亮圆点(在针的左下侧)
px, py = curve[int(len(curve) * 0.72)]
pr = line_w * 0.72
draw.ellipse((px - pr * 1.8, py - pr * 1.8, px + pr * 1.8, py + pr * 1.8),
fill=ACCENT + (60,))
draw.ellipse((px - pr, py - pr, px + pr, py + pr), fill=ACCENT_HI + (255,))
return img.resize((size, size), Image.LANCZOS)
def main() -> int:
OUT_DIR.mkdir(parents=True, exist_ok=True)
shutil.rmtree(ICONSET, ignore_errors=True)
ICONSET.mkdir(parents=True, exist_ok=True)
# iconset 需要的尺寸组合
specs = [
(16, "icon_16x16.png"), (32, "icon_16x16@2x.png"),
(32, "icon_32x32.png"), (64, "icon_32x32@2x.png"),
(128, "icon_128x128.png"), (256, "icon_128x128@2x.png"),
(256, "icon_256x256.png"), (512, "icon_256x256@2x.png"),
(512, "icon_512x512.png"), (1024, "icon_512x512@2x.png"),
]
cache: dict[int, Image.Image] = {}
for size, name in specs:
if size not in cache:
cache[size] = draw_icon(size)
cache[size].save(ICONSET / name)
print(f"已生成 {len(specs)} 个尺寸 → {ICONSET}")
# 预览图(512)
preview = OUT_DIR / "AppIcon-preview.png"
cache[512].save(preview)
print(f"预览图 → {preview}")
# 合成 icns
try:
subprocess.run(["iconutil", "-c", "icns", str(ICONSET), "-o", str(ICNS)], check=True)
print(f"已生成 {ICNS}({ICNS.stat().st_size // 1024} KB)")
except Exception as exc:
print(f"iconutil 失败:{exc}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())