"""标博士 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''
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"^.*?\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'{SEAL_BODY["paper"]}'
if on_dark else "")
seal = f'{body}'
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'')
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'')