- 白文印「标」(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>
150 lines
6.8 KiB
Python
150 lines
6.8 KiB
Python
"""标博士 logo 的公共排版逻辑:字标排字、印章几何、横版 / 竖版拼合。
|
||
|
||
被 build_logo.py(正式资产)和 wordmark_compare.py(方案对比)共用。
|
||
字体文件在 .work/fonts/(不进 git),许可证见 licenses/。
|
||
"""
|
||
import re
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
from fontTools.pens.boundsPen import BoundsPen
|
||
from fontTools.pens.svgPathPen import SVGPathPen
|
||
from fontTools.pens.transformPen import TransformPen
|
||
from fontTools.ttLib import TTFont
|
||
from PIL import Image
|
||
|
||
LOGO = Path(__file__).resolve().parent.parent
|
||
FONTS = LOGO / ".work/fonts"
|
||
NAVY, WHITE, SEAL_RED = "#1C2554", "#FFFFFF", "#D9291C"
|
||
|
||
CN_TEXT, PY_TEXT = "标博士", "BIAOBOSHI"
|
||
CN_H = 100.0 # 中文字形实际高度(单位)
|
||
PY_CAP = 0.20 # 拼音大写字母高度 / 中文高度
|
||
GAP = 0.14 # 中文底部到拼音顶部的距离 / 中文高度
|
||
SEAL_SCALE = 1.02 # H2:印章实心高度 = 文字块高度 × 1.02(补偿颗粒边缘显虚)
|
||
SEAL_GAP = 0.3 # 横版:印章与文字的间距 / 印章高度
|
||
|
||
# 正式方案(决策 B4):F2 中文 + E5 英文 + C2 视觉对齐补偿
|
||
FINAL = ("FINAL", "NotoSerifSC[wght].ttf", 600, 0.04, "LibreCaslonText[wght].ttf", 600)
|
||
FINAL_INSET = (5.0, 5.8)
|
||
|
||
_font_cache = {}
|
||
|
||
|
||
def glyphset(file, wght):
|
||
key = (file, wght)
|
||
if key not in _font_cache:
|
||
f = TTFont(FONTS / file)
|
||
loc = {"wght": wght} if wght and "fvar" in f else None
|
||
_font_cache[key] = (f, f.getGlyphSet(location=loc) if loc else f.getGlyphSet())
|
||
return _font_cache[key]
|
||
|
||
|
||
def outline(file, wght, text, tracking_em):
|
||
"""返回 [(glyph, x_offset)] 和整体墨迹边界(字体单位,y 向上)。"""
|
||
font, gs = glyphset(file, wght)
|
||
cmap = font.getBestCmap()
|
||
upm = font["head"].unitsPerEm
|
||
x, items, bounds = 0.0, [], None
|
||
for ch in text:
|
||
g = cmap[ord(ch)]
|
||
bp = BoundsPen(gs)
|
||
gs[g].draw(bp)
|
||
if bp.bounds:
|
||
b = (bp.bounds[0] + x, bp.bounds[1], bp.bounds[2] + x, bp.bounds[3])
|
||
bounds = b if bounds is None else (min(bounds[0], b[0]), min(bounds[1], b[1]), max(bounds[2], b[2]), max(bounds[3], b[3]))
|
||
items.append((g, x))
|
||
x += gs[g].width + tracking_em * upm
|
||
return gs, items, bounds
|
||
|
||
|
||
def path_for(gs, items, scale, dx, dy):
|
||
"""把字形变换到目标坐标(y 向下):X = (x+off)*s + dx,Y = -y*s + dy。"""
|
||
pen = SVGPathPen(gs)
|
||
for g, off in items:
|
||
tp = TransformPen(pen, (scale, 0, 0, -scale, off * scale + dx, dy))
|
||
gs[g].draw(tp)
|
||
return pen.getCommands()
|
||
|
||
|
||
def wordmark(pair, color, inset=(0.0, 0.0)):
|
||
"""排出「标博士 / BIAOBOSHI」两行字标,返回 (SVG 片段, 宽, 高)。
|
||
|
||
pair:(编号, 中文字体, 中文字重, 中文字距 em, 拼音字体, 拼音字重, ...)
|
||
inset:拼音左右各内缩多少(单位同中文字高 100),用于视觉对齐补偿。
|
||
"""
|
||
_, cnf, cnw, cntr, pyf, pyw, *_ = pair
|
||
gs, items, b = outline(cnf, cnw, CN_TEXT, cntr)
|
||
s = CN_H / (b[3] - b[1])
|
||
cn_w = (b[2] - b[0]) * s
|
||
cn_d = path_for(gs, items, s, -b[0] * s, b[3] * s)
|
||
# 拼音:先量大写字母高度,再按(中文宽度 − 内缩)两端对齐(逐字母分配字距)
|
||
pgs, pitems, pb = outline(pyf, pyw, PY_TEXT, 0)
|
||
ps = CN_H * PY_CAP / (pb[3] - pb[1])
|
||
natural = (pb[2] - pb[0]) * ps
|
||
extra = (cn_w - inset[0] - inset[1] - natural) / (len(PY_TEXT) - 1)
|
||
pitems = [(g, off + i * extra / ps) for i, (g, off) in enumerate(pitems)]
|
||
py_top = CN_H * (1 + GAP)
|
||
py_d = path_for(pgs, pitems, ps, -pb[0] * ps + inset[0], py_top + pb[3] * ps)
|
||
height = py_top + CN_H * PY_CAP
|
||
body = f'<path fill="{color}" d="{cn_d}"/><path fill="{color}" d="{py_d}"/>'
|
||
return body, cn_w, height
|
||
|
||
|
||
def _seal_geometry():
|
||
"""量出印章在自身画布中的实心边界(用干净版),返回 (画布边长, 上, 下, 左, 右)。"""
|
||
svg = (LOGO / "svg/seal.svg").read_text()
|
||
size = float(re.search(r'viewBox="0 0 ([\d.]+)', svg).group(1))
|
||
probe = LOGO / ".work/seal-probe.png"
|
||
subprocess.run(["rsvg-convert", "-w", "2000", str(LOGO / "svg/seal.svg"), "-o", str(probe)], check=True)
|
||
a = np.asarray(Image.open(probe))[..., 3] > 128
|
||
ys, xs = np.where(a)
|
||
k = size / 2000
|
||
return size, ys.min() * k, (ys.max() + 1) * k, xs.min() * k, (xs.max() + 1) * k
|
||
|
||
|
||
SEAL_SIZE, ST, SB, SL, SR = _seal_geometry()
|
||
_SEAL_FILES = (("textured", "seal-textured.svg"), ("clean", "seal.svg"), ("paper", "seal-paper.svg"))
|
||
SEAL_BODY = {k: re.sub(r"^.*?<svg[^>]*>|</svg>\s*$", "", (LOGO / f"svg/{f}").read_text(), flags=re.S) for k, f in _SEAL_FILES}
|
||
SEAL_VB = {k: float(re.search(r'viewBox="0 0 ([\d.]+)', (LOGO / f"svg/{f}").read_text()).group(1)) for k, f in _SEAL_FILES}
|
||
|
||
|
||
def seal_group(kind, x, y, solid_h, on_dark=False, seal_color=None):
|
||
"""把印章放到 (x, y),使其实心部分左上角落在该点、实心高度为 solid_h。返回 (SVG 片段, 实心宽)。"""
|
||
s = solid_h / (SB - ST)
|
||
x0, y0 = x - SL * s, y - ST * s
|
||
body = SEAL_BODY[kind]
|
||
if seal_color:
|
||
body = body.replace(SEAL_RED, seal_color)
|
||
paper = (f'<g transform="translate({x0:.2f} {y0:.2f}) scale({s * SEAL_SIZE / SEAL_VB["paper"]:.6f})">{SEAL_BODY["paper"]}</g>'
|
||
if on_dark else "")
|
||
seal = f'<g transform="translate({x0:.2f} {y0:.2f}) scale({s * SEAL_SIZE / SEAL_VB[kind]:.6f})">{body}</g>'
|
||
return paper + seal, (SR - SL) * s
|
||
|
||
|
||
def lockup(body, wm_w, wm_h, seal_h, seal_top, on_dark=False, kind="textured", seal_color=None, pad=6):
|
||
"""横版:印章在左、字标在右。seal_h 是印章实心高度,seal_top 是实心顶端的 y。"""
|
||
seal, seal_w = seal_group(kind, 0, seal_top, seal_h, on_dark, seal_color)
|
||
tx = seal_w + max(seal_h, wm_h) * SEAL_GAP
|
||
top, bottom = min(0, seal_top), max(wm_h, seal_top + seal_h)
|
||
w = tx + wm_w
|
||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{-pad:.1f} {top - pad:.1f} {w + 2 * pad:.1f} {bottom - top + 2 * pad:.1f}">'
|
||
f'{seal}<g transform="translate({tx:.2f} 0)">{body}</g></svg>')
|
||
|
||
|
||
def lockup_h2(body, wm_w, wm_h, **kw):
|
||
"""按 H2 比例拼横版:印章实心高度 = 文字块 × SEAL_SCALE,上下居中。"""
|
||
seal_h = wm_h * SEAL_SCALE
|
||
return lockup(body, wm_w, wm_h, seal_h, -(seal_h - wm_h) / 2, **kw)
|
||
|
||
|
||
def lockup_vertical(body, wm_w, wm_h, on_dark=False, kind="textured", seal_color=None, pad=6):
|
||
"""竖版:印章在上(实心边长 = 字标宽度的 50%),字标居中在下,间距 = 印章边长的 18%。"""
|
||
seal_h = wm_w * 0.5
|
||
seal, _ = seal_group(kind, (wm_w - seal_h) / 2, 0, seal_h, on_dark, seal_color)
|
||
gap = seal_h * 0.18
|
||
h = seal_h + gap + wm_h
|
||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{-pad:.1f} {-pad:.1f} {wm_w + 2 * pad:.1f} {h + 2 * pad:.1f}">'
|
||
f'{seal}<g transform="translate(0 {seal_h + gap:.2f})">{body}</g></svg>')
|