"""标博士 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'' f'{seal}{body}') 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'' f'{seal}{body}')