docs(logo): 新增 logo v5.1 正式资产、使用规范与构建脚本

- 白文印「标」(AI 生成原图经脚本抠图、统一为 #D9291C、矢量化)
- 字标:思源宋体 SemiBold + Libre Caslon;比例 H2,英文视觉内缩 C2
- svg/、png/、favicon:横版、竖版、反白、单色、印章、各尺寸图标
- scripts/:logo_lib.py 统一排版参数,build_assets.sh 一键生成
- logo-spec.md、logo-sheet.html、字体许可证(OFL)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-02 20:28:18 +08:00
parent f4ef7af542
commit 5753b51131
51 changed files with 1160 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
"""把位图生成的印章处理成 logo 资产:抠图、统一颜色、矢量化、小尺寸版本。
用法:.work/venv/bin/python scripts/process_seal.py <源图> <输出目录> [品牌红 HEX]
"""
import sys
from pathlib import Path
import numpy as np
import potrace
from PIL import Image
from scipy import ndimage
src, out_dir = Path(sys.argv[1]), Path(sys.argv[2])
brand = sys.argv[3] if len(sys.argv) > 3 else "#D9291C"
out_dir.mkdir(parents=True, exist_ok=True)
rgb = tuple(int(brand[i:i + 2], 16) for i in (1, 3, 5))
im = np.asarray(Image.open(src).convert("RGB")).astype(float)
redness = (im[..., 0] - (im[..., 1] + im[..., 2]) / 2) / 255
# 印泥浓淡 → 透明度:保留颗粒和毛边,背景完全透明
alpha = np.clip((redness - 0.12) / (0.55 - 0.12), 0, 1)
alpha = alpha * alpha * (3 - 2 * alpha) # smoothstep,让颗粒边缘柔和
ys, xs = np.where(redness > 0.35)
y0, y1, x0, x1 = ys.min(), ys.max() + 1, xs.min(), xs.max() + 1
side = max(y1 - y0, x1 - x0)
pad = round(side * 0.02)
canvas = side + 2 * pad
cy, cx = (y0 + y1) // 2, (x0 + x1) // 2
half = canvas // 2
def crop(a):
out = np.zeros((canvas, canvas), dtype=a.dtype)
sy0, sx0 = cy - half, cx - half
ys_, xs_ = slice(max(sy0, 0), min(sy0 + canvas, a.shape[0])), slice(max(sx0, 0), min(sx0 + canvas, a.shape[1]))
out[ys_.start - sy0:ys_.stop - sy0, xs_.start - sx0:xs_.stop - sx0] = a[ys_, xs_]
return out
alpha_c = crop(alpha)
red_c = crop(redness)
# 1. 质感主图(透明 PNG)
rgba = np.zeros((canvas, canvas, 4), dtype=np.uint8)
rgba[..., :3] = rgb
rgba[..., 3] = (alpha_c * 255).round().astype(np.uint8)
Image.fromarray(rgba).save(out_dir / "seal-textured.png")
on_white = np.full((canvas, canvas, 3), 255.0)
on_white = on_white * (1 - alpha_c[..., None]) + np.array(rgb) * alpha_c[..., None]
Image.fromarray(on_white.round().astype(np.uint8)).save(out_dir / "seal-textured-on-white.png")
def trace(mask, name, turdsize, alphamax=1.0, fill=brand):
bm = potrace.Bitmap(~mask) # potracer 把 False 当作前景,这里取反
path = bm.trace(turdsize=turdsize, alphamax=alphamax, opticurve=True, opttolerance=0.2)
parts = []
for curve in path:
s = curve.start_point
d = [f"M{s.x:.1f} {s.y:.1f}"]
for seg in curve.segments:
if seg.is_corner:
d.append(f"L{seg.c.x:.1f} {seg.c.y:.1f}L{seg.end_point.x:.1f} {seg.end_point.y:.1f}")
else:
d.append(f"C{seg.c1.x:.1f} {seg.c1.y:.1f} {seg.c2.x:.1f} {seg.c2.y:.1f} {seg.end_point.x:.1f} {seg.end_point.y:.1f}")
d.append("Z")
parts.append("".join(d))
svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {canvas} {canvas}">'
f'<path fill="{fill}" fill-rule="evenodd" d="{"".join(parts)}"/></svg>')
(out_dir / name).write_text(svg)
return len(svg)
# 2. 质感矢量版:放大 2 倍再描摹,保留颗粒
big = np.asarray(Image.fromarray((alpha_c * 255).astype(np.uint8)).resize((canvas * 2, canvas * 2), Image.LANCZOS)) / 255
n_tex = trace(big > 0.5, "seal-textured-2x.svg", turdsize=6)
# 描摹坐标是 2 倍的,修正 viewBox
p = out_dir / "seal-textured-2x.svg"
p.write_text(p.read_text().replace(f'viewBox="0 0 {canvas} {canvas}"', f'viewBox="0 0 {canvas * 2} {canvas * 2}"'))
p.rename(out_dir / "seal-textured.svg")
# 3. 干净版:先模糊去掉颗粒,再二值化描摹,得到平滑的印面和字形
solid = ndimage.gaussian_filter((red_c > 0.35).astype(float), sigma=canvas * 0.006) > 0.5
solid = ndimage.binary_fill_holes(solid) & solid # 保留白字(它们与外部不连通时是「洞」)
solid = ndimage.binary_opening(solid, iterations=2)
n_clean = trace(solid, "seal-clean.svg", turdsize=canvas * 0.0005 * canvas, alphamax=1.1)
# 4. 小尺寸版:白色笔画加粗(墨色腐蚀),保证 32px 下笔画不消失
thick = ndimage.binary_erosion(solid, iterations=max(2, round(canvas * 0.012)))
n_small = trace(thick, "seal-small.svg", turdsize=canvas * 0.0008 * canvas, alphamax=1.1)
# 5. 纸底:印面外轮廓(不含白字),向内收一点,深色背景上垫在印章下面,让白字和颗粒显示为纸色
paper = ndimage.binary_erosion(ndimage.binary_fill_holes(solid), iterations=max(2, round(canvas * 0.006)))
trace(paper, "seal-paper.svg", turdsize=canvas * 0.001 * canvas, alphamax=1.2, fill="#FFFFFF")
print(f"canvas={canvas}px brand={brand} textured.svg={n_tex // 1024}KB clean.svg={n_clean // 1024}KB small.svg={n_small // 1024}KB")