新增笔记功能 图标更新

This commit is contained in:
2026-09-23 14:20:23 +08:00
parent a6e02ccbf3
commit e4cdfbc42f
34 changed files with 2511 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+211
View File
@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
"""
生成 App 图标资源(Android / iOS / Web 通用)。
背景:
品牌原图 AI-accounting-soft-uniApp/static/images/logo.png 是「熊头+3只狗+5个大字」
的贴纸海报,带虚化底纹,不适合直接当图标(缩小后文字糊成一团、背景去不干净)。
因此改用同画风的专用图标素材(熊头单主体),由本脚本完成:
1. 去除素材右下角的 AI 生成水印(用上方像素向下延展填充,适配平滑渐变底)
2. 居中裁切、按比例缩放
3. 输出两种产物
产物:
assets/icon/app_icon.png 1024x1024,无透明通道 → iOS / Android 传统图标 / Web
assets/icon/app_icon_fg.png 1024x1024,带透明通道 → Android 自适应图标前景层
assets/images/logo.png 512x512,带透明通道 → App 内品牌标识(登录页圆形 Logo)
登录页的 Logo 用透明底:外层是白色圆形,放进不透明的方图会露出四角(方图
对角线比圆直径长),看起来像贴了张方照片。
用法:
python tools/make_app_icon.py # 生成正式产物
python tools/make_app_icon.py --preview # 只输出效果预览图
"""
from __future__ import annotations
import argparse
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SRC = ROOT / "assets" / "icon" / "_source" / "raw_bear.png"
OUT_DIR = ROOT / "assets" / "icon"
LOGO_OUT = ROOT / "assets" / "images" / "logo.png"
CANVAS = 1024
BG_COLOR = (255, 246, 236) # 暖奶油色,与品牌焦糖色 #C8956E 同色系
ICON_FILL = 0.74 # 正式图标里主体的占比
FG_FILL = 0.60 # 自适应图标前景层占比(Android 安全区为画布的 66%)
LOGO_CANVAS = 512 # 登录页最大显示 120pt,512 够 4x 屏
LOGO_FILL = 0.86 # 品牌标识的占比,比自适应前景层饱满一些
BG_THRESH = 70 # 判定「这是背景」的色差阈值(各通道绝对差之和)
# 水印区域(右下角),留够边距避免碰到主体
WM_BOX = (850, 912, 1024, 1024)
def remove_watermark(im: Image.Image, box: tuple[int, int, int, int]) -> Image.Image:
"""把 box 区域用其上方一行的像素向下延展填充,抹掉水印。"""
x0, y0, x1, y1 = box
row = im.crop((x0, y0 - 1, x1, y0)).resize((x1 - x0, y1 - y0), Image.NEAREST)
out = im.copy()
out.paste(row, (x0, y0))
return out
def background_mask(im: Image.Image, thresh: int) -> np.ndarray:
"""返回「属于背景」的布尔矩阵:与四角平均色的色差小于阈值。"""
arr = np.asarray(im).astype(np.int16)
h, w, _ = arr.shape
corners = np.stack([
arr[0, 0], arr[0, w - 1], arr[h - 1, 0], arr[h - 1, w - 1],
arr[2, 2], arr[2, w - 3], arr[h - 3, 2], arr[h - 3, w - 3],
]).astype(np.int16)
seed = corners.mean(axis=0)
diff = np.abs(arr - seed).sum(axis=2)
return diff <= thresh
def to_foreground(im: Image.Image, mask: np.ndarray) -> Image.Image:
"""按背景掩码抠出主体,返回带 alpha 的图。"""
rgba = im.convert("RGBA")
a = np.asarray(rgba).copy()
a[..., 3] = np.where(mask, 0, 255)
out = Image.fromarray(a, "RGBA")
bbox = out.getbbox()
return out.crop(bbox) if bbox else out
def fit(art: Image.Image, size: int, fill: float, bg: tuple[int, int, int, int] | None) -> Image.Image:
"""等比缩放 art 至画布 size 的 fill 比例,居中合成。bg=None 表示透明底。"""
box = int(size * fill)
scale = min(box / art.width, box / art.height)
art = art.convert("RGBA").resize(
(max(1, round(art.width * scale)), max(1, round(art.height * scale))),
Image.LANCZOS,
)
canvas = Image.new("RGBA", (size, size), bg if bg else (0, 0, 0, 0))
canvas.alpha_composite(art, ((size - art.width) // 2, (size - art.height) // 2))
return canvas
def build(src: Path):
im = Image.open(src).convert("RGB")
print(f"source: {src.name} {im.size}")
cleaned = remove_watermark(im, WM_BOX)
mask = background_mask(cleaned, BG_THRESH)
print(f"background pixels: {mask.mean():.1%}")
solid_subject = Image.fromarray(
np.where(mask[..., None], np.array(BG_COLOR, np.uint8), np.asarray(cleaned)).astype(np.uint8),
"RGB",
)
bbox = background_mask(solid_subject, BG_THRESH)
ys, xs = np.nonzero(~bbox)
subject = solid_subject.crop((xs.min(), ys.min(), xs.max() + 1, ys.max() + 1))
fg_subject = to_foreground(cleaned, mask)
print(f"subject bbox: {subject.size} fg bbox: {fg_subject.size}")
icon = fit(subject, CANVAS, ICON_FILL, (*BG_COLOR, 255)).convert("RGB")
fg = fit(fg_subject, CANVAS, FG_FILL, None)
logo = fit(fg_subject, LOGO_CANVAS, LOGO_FILL, None)
return icon, fg, logo
def checkerboard(size: int, square: int = 16) -> Image.Image:
board = Image.new("RGB", (size, size), (255, 255, 255))
d = ImageDraw.Draw(board)
for y in range(0, size, square):
for x in range(0, size, square):
if (x // square + y // square) % 2:
d.rectangle([x, y, x + square - 1, y + square - 1], fill=(205, 205, 205))
return board
def preview(icon: Image.Image, fg: Image.Image) -> None:
cell = 240
sheet = Image.new("RGB", (cell * 4 + 50, cell + 50), (235, 235, 235))
sheet.paste(icon.resize((cell, cell), Image.LANCZOS), (10, 30))
tiny = icon.resize((48, 48), Image.LANCZOS).resize((cell, cell), Image.NEAREST)
sheet.paste(tiny, (cell + 20, 30))
adaptive = Image.new("RGB", (CANVAS, CANVAS), BG_COLOR)
adaptive.paste(fg, (0, 0), fg)
circ = Image.new("L", (CANVAS, CANVAS), 0)
ImageDraw.Draw(circ).ellipse([60, 60, CANVAS - 60, CANVAS - 60], fill=255)
circle_bg = Image.new("RGB", (CANVAS, CANVAS), (255, 255, 255))
circle_bg.paste(adaptive, (0, 0), circ)
sheet.paste(circle_bg.resize((cell, cell), Image.LANCZOS), (cell * 2 + 30, 30))
board = checkerboard(cell)
board.paste(fg.resize((cell, cell), Image.LANCZOS), (0, 0), fg.resize((cell, cell), Image.LANCZOS))
sheet.paste(board, (cell * 3 + 40, 30))
d = ImageDraw.Draw(sheet)
for i, label in enumerate(["1: icon 1024", "2: at 48px", "3: adaptive(circle)", "4: fg alpha"]):
d.text((10 + (cell + 10) * i, 10), label, fill=(0, 0, 0))
out = ROOT / "tools" / "_preview.png"
sheet.save(out)
print("preview ->", out)
CHECK_TARGETS = [
"assets/icon/app_icon.png",
"assets/icon/app_icon_fg.png",
"assets/images/logo.png",
"ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png",
"android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png",
"android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png",
"web/icons/Icon-512.png",
]
def check() -> None:
"""核对已生成的图标:iOS 的 1024 图必须是 RGB(无 alpha),否则上架报 ITMS-90717。"""
for rel in CHECK_TARGETS:
p = ROOT / rel
if not p.exists():
print(f"MISSING {rel}")
continue
with Image.open(p) as im:
flag = ""
if "ios" in rel and im.mode != "RGB":
flag = " <-- 必须无 alpha!"
print(f"{im.mode:<6} {str(im.size):<12} {rel}{flag}")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--src", type=Path, default=DEFAULT_SRC)
ap.add_argument("--preview", action="store_true", help="只输出效果预览图")
ap.add_argument("--check", action="store_true", help="核对已生成的图标产物")
args = ap.parse_args()
if args.check:
check()
return
icon, fg, logo = build(args.src)
if args.preview:
preview(icon, fg)
return
OUT_DIR.mkdir(parents=True, exist_ok=True)
icon.save(OUT_DIR / "app_icon.png", "PNG", optimize=True)
fg.save(OUT_DIR / "app_icon_fg.png", "PNG", optimize=True)
LOGO_OUT.parent.mkdir(parents=True, exist_ok=True)
logo.save(LOGO_OUT, "PNG", optimize=True)
print("->", OUT_DIR / "app_icon.png", icon.size)
print("->", OUT_DIR / "app_icon_fg.png", fg.size)
print("->", LOGO_OUT, logo.size)
if __name__ == "__main__":
main()