# -*- coding: utf-8 -*- """封面中文 PIL / ffmpeg 字体解析(避免 load_default 导致乱码)""" from __future__ import annotations import os import platform import sys from pathlib import Path from typing import List, Optional try: from PIL import ImageFont except ImportError: ImageFont = None # type: ignore _FAMILY_ALIASES = { "microsoft yahei": "Microsoft YaHei", "微软雅黑": "Microsoft YaHei", "simhei": "SimHei", "黑体": "SimHei", "simsun": "SimSun", "宋体": "SimSun", "arial": "Arial", "times new roman": "Times New Roman", } _WIN_FAMILY_FILES = { "SimHei": ["simhei.ttf"], "Microsoft YaHei": ["msyh.ttc", "msyh.ttf", "msyhbd.ttc"], "SimSun": ["simsun.ttc", "simsunb.ttf"], "Arial": ["arial.ttf", "arialbd.ttf"], "Times New Roman": ["times.ttf", "timesbd.ttf"], } def _scripts_dir() -> Optional[Path]: env = os.environ.get("AICLIENT_COVER_SCRIPTS_DIR", "").strip() if env: p = Path(env) if p.is_dir(): return p # advanced_cover_generator.py 位于 scripts/cover/ here = Path(__file__).resolve().parent.parent if (here / "advanced_cover_generator.py").is_file(): return here return None def _windows_fonts_dir() -> Path: windir = os.environ.get("WINDIR", "C:\\Windows") return Path(windir) / "Fonts" def _normalize_family(font_family: Optional[str]) -> str: raw = (font_family or "Microsoft YaHei").strip() key = raw.lower() return _FAMILY_ALIASES.get(key, raw) def _candidate_filenames(family: str, weight: int = 400) -> List[str]: names = list(_WIN_FAMILY_FILES.get(family, [])) if weight >= 700 and family == "Microsoft YaHei": names = ["msyhbd.ttc", "msyhl.ttc"] + names # 通用回退:.ttf 优先于 .ttc(PIL 对 ttf 更稳) fallback = ["simhei.ttf", "msyh.ttc", "simsun.ttc", "msyh.ttf"] out: List[str] = [] for n in names + fallback: if n not in out: out.append(n) return out def resolve_cjk_font_path( font_family: Optional[str] = None, weight: int = 400, ) -> str: """ 解析可用于 PIL / ffmpeg 的中文字体文件路径。 """ family = _normalize_family(font_family) candidates = _candidate_filenames(family, weight) scripts = _scripts_dir() if scripts: fonts_dir = scripts / "fonts" if fonts_dir.is_dir(): for name in candidates: p = fonts_dir / name if p.is_file(): return str(p.resolve()) if platform.system() == "Windows": win_fonts = _windows_fonts_dir() for name in candidates: p = win_fonts / name if p.is_file(): return str(p.resolve()) if platform.system() == "Darwin": mac_candidates = [ "/System/Library/Fonts/PingFang.ttc", "/System/Library/Fonts/STHeiti Light.ttc", "/Library/Fonts/Arial Unicode.ttf", ] for p in mac_candidates: if os.path.isfile(p): return p # Linux linux_candidates = [ "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", ] for p in linux_candidates: if os.path.isfile(p): return p # 最后尝试:Windows 黑体 if platform.system() == "Windows": simhei = _windows_fonts_dir() / "simhei.ttf" if simhei.is_file(): return str(simhei.resolve()) raise FileNotFoundError( f"未找到中文字体(family={family}),请在 cover/fonts 放置 simhei.ttf 或安装系统字体" ) def load_cjk_font( font_family: Optional[str], size: int, weight: int = 400, ) -> "ImageFont.FreeTypeFont": """加载支持中文的 PIL 字体,禁止回退到 load_default。""" if ImageFont is None: raise RuntimeError("PIL ImageFont 不可用") path = resolve_cjk_font_path(font_family, weight) ext = os.path.splitext(path)[1].lower() if ext == ".ttc": last_err: Optional[Exception] = None for index in range(8): try: font = ImageFont.truetype(path, size, index=index) print( f"[chinese_font] Loaded TTC index={index}: {path} size={size}", file=sys.stderr, ) return font except OSError as e: last_err = e continue raise OSError(f"无法加载字体集合 {path}: {last_err}") font = ImageFont.truetype(path, size) print(f"[chinese_font] Loaded: {path} size={size}", file=sys.stderr) return font