172 lines
5.1 KiB
Python
172 lines
5.1 KiB
Python
# -*- 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)
|
||
|
||
try:
|
||
from modules.font_manager import get_font_manager
|
||
|
||
ziti_path = get_font_manager().find_font(family, weight)
|
||
if ziti_path and os.path.isfile(ziti_path):
|
||
return str(Path(ziti_path).resolve())
|
||
except Exception as e:
|
||
print(f"[chinese_font] font_manager lookup skipped: {e}", file=sys.stderr)
|
||
|
||
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
|