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:
97
docs/design/logo/scripts/process_seal.py
Normal file
97
docs/design/logo/scripts/process_seal.py
Normal 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")
|
||||
Reference in New Issue
Block a user