531 lines
22 KiB
Python
531 lines
22 KiB
Python
"""
|
||
字体管理模块
|
||
负责查找和管理字体文件,支持预置字体、系统字体、ziti目录字体和Google Fonts
|
||
"""
|
||
import os
|
||
import sys
|
||
import platform
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Tuple
|
||
import json
|
||
|
||
# 尝试导入loguru,如果不可用则使用print作为替代
|
||
try:
|
||
from loguru import logger
|
||
except ImportError:
|
||
# Fallback logger that uses print
|
||
class logger:
|
||
@staticmethod
|
||
def info(msg, *args, **kwargs):
|
||
print(f"[INFO] {msg}", file=sys.stderr)
|
||
@staticmethod
|
||
def warning(msg, *args, **kwargs):
|
||
print(f"[WARN] {msg}", file=sys.stderr)
|
||
@staticmethod
|
||
def error(msg, *args, **kwargs):
|
||
print(f"[ERROR] {msg}", file=sys.stderr)
|
||
@staticmethod
|
||
def debug(msg, *args, **kwargs):
|
||
print(f"[DEBUG] {msg}", file=sys.stderr)
|
||
|
||
|
||
class FontManager:
|
||
"""字体管理器"""
|
||
|
||
def __init__(self):
|
||
# 计算项目根目录(从 python/modules 向上两级)
|
||
# __file__ 应该是 .../python/modules/font_manager.py
|
||
current_file = Path(__file__).resolve()
|
||
python_dir = current_file.parent.parent # python 目录
|
||
project_root = python_dir.parent # 项目根目录
|
||
|
||
self.bundled_fonts_dir = Path("fonts/bundled")
|
||
self.fonts_metadata_file = self.bundled_fonts_dir / "fonts_metadata.json"
|
||
|
||
# ziti 目录:环境变量优先(aiclient 可指向 Electron 同款 fonts/ziti)
|
||
ziti_env = os.environ.get("AICLIENT_ZITI_FONTS_DIR", "").strip()
|
||
if ziti_env and Path(ziti_env).is_dir():
|
||
self.ziti_fonts_dir = Path(ziti_env)
|
||
print(f"✅ [font_manager] ziti from AICLIENT_ZITI_FONTS_DIR: {self.ziti_fonts_dir}")
|
||
elif app_root := os.environ.get('APP_ROOT', None):
|
||
# 🔧 修复ziti字体路径:支持ASAR打包环境
|
||
# 检查是否在打包环境中运行
|
||
app_root = app_root
|
||
ziti_path_bundle = Path(app_root) / "resources-bundles" / "ziti"
|
||
if ziti_path_bundle.exists():
|
||
self.ziti_fonts_dir = ziti_path_bundle
|
||
print(f"✅ [font_manager] 找到ziti字体目录(bundle): {ziti_path_bundle}")
|
||
else:
|
||
ziti_path = Path(app_root) / "extra" / "common" / "fonts" / "ziti"
|
||
if ziti_path.exists():
|
||
self.ziti_fonts_dir = ziti_path
|
||
print(f"✅ [font_manager] 找到ziti字体目录: {ziti_path}")
|
||
else:
|
||
ziti_path_fallback1 = Path(app_root) / "app.asar.unpacked" / "ziti"
|
||
if ziti_path_fallback1.exists():
|
||
self.ziti_fonts_dir = ziti_path_fallback1
|
||
print(f"✅ [font_manager] 找到ziti字体目录(回退1): {ziti_path_fallback1}")
|
||
else:
|
||
ziti_path_fallback2 = Path(app_root) / "ziti"
|
||
self.ziti_fonts_dir = ziti_path_fallback2
|
||
print(f"⚠️ [font_manager] 使用回退路径: {ziti_path_fallback2} (存在: {ziti_path_fallback2.exists()})")
|
||
else:
|
||
self.ziti_fonts_dir = project_root / "ziti"
|
||
print(f"🔧 [font_manager] 开发环境ziti路径: {self.ziti_fonts_dir}")
|
||
self._bundled_fonts_cache: Optional[Dict] = None
|
||
self._system_fonts_cache: Optional[List[Dict]] = None
|
||
self._ziti_fonts_cache: Optional[List[Dict]] = None # 新增:ziti字体缓存
|
||
|
||
# 诊断日志
|
||
logger.debug(f"FontManager 初始化: current_file={current_file}, python_dir={python_dir}, project_root={project_root}")
|
||
logger.debug(f"ziti_fonts_dir={self.ziti_fonts_dir.absolute()}, exists={self.ziti_fonts_dir.exists()}")
|
||
|
||
def get_bundled_fonts(self) -> List[Dict]:
|
||
"""获取预置字体列表"""
|
||
if self._bundled_fonts_cache is not None:
|
||
return self._bundled_fonts_cache.get("fonts", [])
|
||
|
||
if not self.fonts_metadata_file.exists():
|
||
logger.warning(f"字体元数据文件不存在: {self.fonts_metadata_file}")
|
||
return []
|
||
|
||
try:
|
||
with open(self.fonts_metadata_file, "r", encoding="utf-8") as f:
|
||
metadata = json.load(f)
|
||
self._bundled_fonts_cache = metadata
|
||
return metadata.get("fonts", [])
|
||
except Exception as e:
|
||
logger.error(f"读取字体元数据失败: {e}")
|
||
return []
|
||
|
||
def scan_ziti_fonts(self) -> List[Dict]:
|
||
"""扫描ziti目录中的字体文件"""
|
||
if self._ziti_fonts_cache is not None:
|
||
return self._ziti_fonts_cache
|
||
|
||
ziti_fonts = []
|
||
|
||
logger.info(f"正在扫描 ziti 目录: {self.ziti_fonts_dir.absolute()}")
|
||
logger.info(f" 当前工作目录: {os.getcwd()}")
|
||
logger.info(f" APP_ROOT环境变量: {os.environ.get('APP_ROOT', 'None')}")
|
||
|
||
if not self.ziti_fonts_dir.exists():
|
||
logger.warning(f"❌ ziti目录不存在: {self.ziti_fonts_dir.absolute()}")
|
||
logger.warning(f" 请检查路径配置和资源文件是否正确打包")
|
||
return []
|
||
else:
|
||
logger.info(f"✅ ziti目录存在,开始扫描字体文件...")
|
||
|
||
try:
|
||
# 支持的字体格式
|
||
font_extensions = ['.ttf', '.otf', '.ttc', '.woff', '.woff2']
|
||
|
||
# 扫描ziti目录中的所有字体文件
|
||
for font_file in self.ziti_fonts_dir.iterdir():
|
||
if font_file.is_file() and font_file.suffix.lower() in font_extensions:
|
||
# 从文件名提取字体信息
|
||
font_name = font_file.stem
|
||
|
||
# 尝试解析字体名称和变体
|
||
display_name = font_name
|
||
family = font_name
|
||
weight = 400
|
||
|
||
# 检测常见的字体变体
|
||
name_lower = font_name.lower()
|
||
if 'bold' in name_lower:
|
||
weight = 700
|
||
elif 'semibold' in name_lower:
|
||
weight = 600
|
||
elif 'medium' in name_lower:
|
||
weight = 500
|
||
elif 'light' in name_lower:
|
||
weight = 300
|
||
elif 'thin' in name_lower:
|
||
weight = 100
|
||
elif 'black' in name_lower or 'extrabold' in name_lower:
|
||
weight = 900
|
||
|
||
# 清理family名称(移除变体后缀)
|
||
for variant in ['-Bold', '-SemiBold', '-Medium', '-Light', '-Thin', '-Black', '-ExtraBold', '-Regular']:
|
||
if family.endswith(variant):
|
||
family = family[:-len(variant)]
|
||
break
|
||
|
||
ziti_fonts.append({
|
||
'family': family,
|
||
'display_name': display_name,
|
||
'path': str(font_file.absolute()),
|
||
'source': 'ziti',
|
||
'weight': weight,
|
||
'category': 'sans-serif', # 默认分类
|
||
'languages': ['zh-CN', 'ja', 'en'] # 假设支持中日英
|
||
})
|
||
|
||
logger.info(f"✅ 从ziti目录扫描到 {len(ziti_fonts)} 个字体")
|
||
if len(ziti_fonts) > 0:
|
||
logger.debug(f" 扫描到的字体: {', '.join([f['family'] for f in ziti_fonts[:5]])}" +
|
||
(f" 等({len(ziti_fonts)}个)" if len(ziti_fonts) > 5 else ""))
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 扫描ziti目录字体失败: {e}")
|
||
import traceback
|
||
logger.error(f" 追踪: {traceback.format_exc()}")
|
||
|
||
# 去重(按family和weight)
|
||
seen = set()
|
||
unique_fonts = []
|
||
for font in ziti_fonts:
|
||
key = (font['family'], font.get('weight', 400))
|
||
if key not in seen:
|
||
seen.add(key)
|
||
unique_fonts.append(font)
|
||
|
||
self._ziti_fonts_cache = unique_fonts
|
||
return unique_fonts
|
||
|
||
def scan_system_fonts(self) -> List[Dict]:
|
||
"""扫描系统已安装的字体"""
|
||
if self._system_fonts_cache is not None:
|
||
return self._system_fonts_cache
|
||
|
||
system_fonts = []
|
||
system = platform.system()
|
||
|
||
try:
|
||
if system == 'Windows':
|
||
font_dir = Path("C:/Windows/Fonts")
|
||
if font_dir.exists():
|
||
# 常见Windows字体
|
||
common_fonts = {
|
||
'simhei.ttf': {'family': 'SimHei', 'display_name': '黑体'},
|
||
'simsun.ttc': {'family': 'SimSun', 'display_name': '宋体'},
|
||
'msyh.ttc': {'family': 'Microsoft YaHei', 'display_name': '微软雅黑'},
|
||
'msyhbd.ttc': {'family': 'Microsoft YaHei', 'display_name': '微软雅黑 Bold', 'weight': 700},
|
||
'arial.ttf': {'family': 'Arial', 'display_name': 'Arial'},
|
||
'arialbd.ttf': {'family': 'Arial', 'display_name': 'Arial Bold', 'weight': 700},
|
||
'times.ttf': {'family': 'Times New Roman', 'display_name': 'Times New Roman'},
|
||
'timesbd.ttf': {'family': 'Times New Roman', 'display_name': 'Times New Roman Bold', 'weight': 700},
|
||
}
|
||
|
||
for font_file, font_info in common_fonts.items():
|
||
font_path = font_dir / font_file
|
||
if font_path.exists():
|
||
system_fonts.append({
|
||
'family': font_info['family'],
|
||
'display_name': font_info['display_name'],
|
||
'path': str(font_path),
|
||
'source': 'system',
|
||
'weight': font_info.get('weight', 400)
|
||
})
|
||
|
||
elif system == 'Darwin': # macOS
|
||
font_dirs = [
|
||
Path("/System/Library/Fonts"),
|
||
Path("/Library/Fonts"),
|
||
Path.home() / "Library/Fonts"
|
||
]
|
||
|
||
common_fonts = {
|
||
'PingFang.ttc': {'family': 'PingFang SC', 'display_name': '苹方'},
|
||
'Arial.ttf': {'family': 'Arial', 'display_name': 'Arial'},
|
||
}
|
||
|
||
for font_dir in font_dirs:
|
||
if font_dir.exists():
|
||
for font_file, font_info in common_fonts.items():
|
||
font_path = font_dir / font_file
|
||
if font_path.exists():
|
||
system_fonts.append({
|
||
'family': font_info['family'],
|
||
'display_name': font_info['display_name'],
|
||
'path': str(font_path),
|
||
'source': 'system',
|
||
'weight': 400
|
||
})
|
||
|
||
elif system == 'Linux':
|
||
font_dirs = [
|
||
Path("/usr/share/fonts/truetype"),
|
||
Path("/usr/share/fonts/TTF"),
|
||
Path.home() / ".fonts"
|
||
]
|
||
|
||
for font_dir in font_dirs:
|
||
if font_dir.exists():
|
||
# 查找常见字体
|
||
for font_file in font_dir.rglob("*.ttf"):
|
||
font_name = font_file.stem.lower()
|
||
if 'dejavu' in font_name or 'liberation' in font_name:
|
||
system_fonts.append({
|
||
'family': font_file.stem,
|
||
'display_name': font_file.stem,
|
||
'path': str(font_file),
|
||
'source': 'system',
|
||
'weight': 400
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"扫描系统字体失败: {e}")
|
||
|
||
# 去重(按family和weight)
|
||
seen = set()
|
||
unique_fonts = []
|
||
for font in system_fonts:
|
||
key = (font['family'], font.get('weight', 400))
|
||
if key not in seen:
|
||
seen.add(key)
|
||
unique_fonts.append(font)
|
||
|
||
self._system_fonts_cache = unique_fonts
|
||
return unique_fonts
|
||
|
||
def find_font(self, font_family: str, font_weight: int = 400) -> Optional[str]:
|
||
"""查找字体文件(按优先级:预置 > ziti > 系统 > 默认)
|
||
|
||
Args:
|
||
font_family: 字体名称
|
||
font_weight: 字体粗细 (100-1000)
|
||
|
||
Returns:
|
||
字体文件路径,如果找不到返回None
|
||
"""
|
||
# 1. 优先查找预置字体
|
||
bundled_font = self._find_bundled_font(font_family, font_weight)
|
||
if bundled_font:
|
||
return bundled_font
|
||
|
||
# 2. 查找ziti目录字体
|
||
ziti_font = self._find_ziti_font(font_family, font_weight)
|
||
if ziti_font:
|
||
return ziti_font
|
||
|
||
# 3. 查找系统字体
|
||
system_font = self._find_system_font(font_family, font_weight)
|
||
if system_font:
|
||
return system_font
|
||
|
||
# 4. 返回None(使用默认字体)
|
||
return None
|
||
|
||
def _find_bundled_font(self, font_family: str, font_weight: int) -> Optional[str]:
|
||
"""查找预置字体"""
|
||
if not self.bundled_fonts_dir.exists():
|
||
return None
|
||
|
||
bundled_fonts = self.get_bundled_fonts()
|
||
|
||
# 查找匹配的字体
|
||
for font_info in bundled_fonts:
|
||
if font_info.get('family') == font_family:
|
||
font_dir = self.bundled_fonts_dir / font_info.get('path', '')
|
||
|
||
# 根据font_weight选择变体
|
||
variant = self._get_variant_for_weight(font_weight)
|
||
|
||
# 查找字体文件(支持TTF和OTF格式)
|
||
for ext in [".ttf", ".otf"]:
|
||
variant_file = font_dir / f"{variant}{ext}"
|
||
if variant_file.exists():
|
||
return str(variant_file)
|
||
|
||
# 如果找不到指定变体,尝试查找Regular
|
||
for ext in [".ttf", ".otf"]:
|
||
regular_file = font_dir / f"Regular{ext}"
|
||
if regular_file.exists():
|
||
return str(regular_file)
|
||
|
||
return None
|
||
|
||
def _find_ziti_font(self, font_family: str, font_weight: int) -> Optional[str]:
|
||
"""查找ziti目录字体(支持模糊匹配和名称映射)"""
|
||
if not font_family:
|
||
return None
|
||
|
||
ziti_fonts = self.scan_ziti_fonts()
|
||
|
||
if not ziti_fonts:
|
||
logger.debug(f"ziti目录中没有字体文件,无法查找: {font_family}")
|
||
return None
|
||
|
||
# 字体名称映射表(与前端保持一致)
|
||
# 映射:显示名称/别名 -> 文件名(不包含扩展名)
|
||
font_name_map = {
|
||
'Dymon手写体': 'Dymon-ShouXieTi',
|
||
'Dymon-ShouXieTi': 'Dymon-ShouXieTi',
|
||
'猫啃杂糅体': 'MaokenAssortedSans',
|
||
'MaokenAssortedSans': 'MaokenAssortedSans',
|
||
'猫啃杂糅体 Lite': 'MaokenAssortedSans-Lite',
|
||
'MaokenAssortedSans-Lite': 'MaokenAssortedSans-Lite',
|
||
'Murecho 黑体': 'Murecho-Black',
|
||
'Murecho-Black': 'Murecho-Black',
|
||
'Murecho 粗体': 'Murecho-Bold',
|
||
'Murecho-Bold': 'Murecho-Bold',
|
||
'墨趣古风体': '墨趣古风体',
|
||
'平方张亚玲黑方体': '平方张亚玲黑方体',
|
||
'胡晓波骚包体': '胡晓波骚包体2.0',
|
||
'胡晓波骚包体2.0': '胡晓波骚包体2.0',
|
||
'USMCCyuanjiantecu': 'USMCCyuanjiantecu',
|
||
'庞门正道标题体免费版': '庞门正道标题体免费版',
|
||
'方正粗黑宋简体': '方正粗黑宋简体',
|
||
'千图厚黑体': '千图厚黑体',
|
||
'杨任东竹石体': '杨任东竹石体',
|
||
'泼墨体': '泼墨体',
|
||
'1.程荣光刻楷': '1.程荣光刻楷',
|
||
'斗鱼追光体2.0': '斗鱼追光体2.0',
|
||
}
|
||
|
||
# 标准化字体名称(移除空格、统一大小写)
|
||
def normalize_name(name: str) -> str:
|
||
return name.replace(' ', '').replace('-', '').replace('_', '').lower()
|
||
|
||
# 尝试通过映射表转换字体名称
|
||
mapped_font_family = font_name_map.get(font_family, font_family)
|
||
normalized_target = normalize_name(mapped_font_family)
|
||
|
||
logger.debug(f"查找ziti字体: '{font_family}' -> 映射: '{mapped_font_family}' -> 标准化: '{normalized_target}', 权重: {font_weight}")
|
||
|
||
# 查找匹配的字体
|
||
best_match = None
|
||
min_weight_diff = float('inf')
|
||
|
||
for font_info in ziti_fonts:
|
||
family = font_info.get('family', '')
|
||
display_name = font_info.get('display_name', '')
|
||
path = font_info.get('path', '')
|
||
|
||
# 获取文件名(不含扩展名)用于匹配
|
||
file_stem = None
|
||
if path:
|
||
from pathlib import Path
|
||
file_stem = Path(path).stem
|
||
|
||
# 多种匹配方式:精确匹配、模糊匹配、文件名匹配
|
||
is_match = False
|
||
match_type = None
|
||
|
||
# 1. 精确匹配
|
||
if family == font_family or family == mapped_font_family:
|
||
is_match = True
|
||
match_type = f"精确匹配(family={family})"
|
||
elif display_name == font_family or display_name == mapped_font_family:
|
||
is_match = True
|
||
match_type = f"精确匹配(display_name={display_name})"
|
||
# 2. 模糊匹配(忽略大小写和空格)
|
||
elif normalize_name(family) == normalized_target:
|
||
is_match = True
|
||
match_type = f"模糊匹配(family={family})"
|
||
elif normalize_name(display_name) == normalized_target:
|
||
is_match = True
|
||
match_type = f"模糊匹配(display_name={display_name})"
|
||
# 3. 文件名匹配
|
||
elif file_stem and normalize_name(file_stem) == normalized_target:
|
||
is_match = True
|
||
match_type = f"文件名匹配(file_stem={file_stem})"
|
||
|
||
if is_match:
|
||
font_weight_info = font_info.get('weight', 400)
|
||
weight_diff = abs(font_weight_info - font_weight)
|
||
|
||
logger.debug(f"找到匹配字体: {match_type}, 路径: {path}, 权重: {font_weight_info}, 权重差: {weight_diff}")
|
||
|
||
# 找到权重最接近的字体
|
||
if weight_diff < min_weight_diff:
|
||
min_weight_diff = weight_diff
|
||
best_match = path
|
||
|
||
if best_match:
|
||
logger.info(f"找到ziti字体 '{font_family}': {best_match}")
|
||
else:
|
||
logger.warning(f"未找到ziti字体 '{font_family}',已扫描{len(ziti_fonts)}个字体文件")
|
||
|
||
return best_match
|
||
|
||
def _find_system_font(self, font_family: str, font_weight: int) -> Optional[str]:
|
||
"""查找系统字体"""
|
||
system_fonts = self.scan_system_fonts()
|
||
|
||
# 查找匹配的字体
|
||
for font_info in system_fonts:
|
||
if font_info.get('family') == font_family:
|
||
font_weight_info = font_info.get('weight', 400)
|
||
|
||
# 如果权重匹配(或接近),返回字体路径
|
||
if abs(font_weight_info - font_weight) <= 100:
|
||
return font_info.get('path')
|
||
|
||
return None
|
||
|
||
def _get_variant_for_weight(self, font_weight: int) -> str:
|
||
"""根据字体权重获取变体名称"""
|
||
if font_weight >= 700:
|
||
return "Bold"
|
||
elif font_weight >= 600:
|
||
return "SemiBold"
|
||
elif font_weight >= 500:
|
||
return "Medium"
|
||
else:
|
||
return "Regular"
|
||
|
||
def get_all_available_fonts(self) -> List[Dict]:
|
||
"""获取所有可用字体(合并预置、ziti和系统字体)"""
|
||
all_fonts = []
|
||
|
||
# 添加预置字体
|
||
bundled_fonts = self.get_bundled_fonts()
|
||
for font_info in bundled_fonts:
|
||
all_fonts.append({
|
||
'value': font_info.get('family'),
|
||
'label': font_info.get('display_name', font_info.get('family')),
|
||
'source': 'bundled',
|
||
'category': font_info.get('category', 'sans-serif'),
|
||
'languages': font_info.get('languages', [])
|
||
})
|
||
|
||
# 添加ziti目录字体
|
||
ziti_fonts = self.scan_ziti_fonts()
|
||
seen_families = {f.get('family') for f in bundled_fonts}
|
||
|
||
for font_info in ziti_fonts:
|
||
family = font_info.get('family')
|
||
if family not in seen_families:
|
||
all_fonts.append({
|
||
'value': family,
|
||
'label': font_info.get('display_name', family),
|
||
'source': 'ziti',
|
||
'category': font_info.get('category', 'sans-serif'),
|
||
'languages': font_info.get('languages', []),
|
||
'path': font_info.get('path')
|
||
})
|
||
seen_families.add(family)
|
||
|
||
# 添加系统字体(去重)
|
||
system_fonts = self.scan_system_fonts()
|
||
|
||
for font_info in system_fonts:
|
||
family = font_info.get('family')
|
||
if family not in seen_families:
|
||
all_fonts.append({
|
||
'value': family,
|
||
'label': font_info.get('display_name', family),
|
||
'source': 'system',
|
||
'category': 'sans-serif',
|
||
'languages': []
|
||
})
|
||
seen_families.add(family)
|
||
|
||
return all_fonts
|
||
|
||
|
||
# 全局字体管理器实例
|
||
_font_manager: Optional[FontManager] = None
|
||
|
||
|
||
def get_font_manager() -> FontManager:
|
||
"""获取全局字体管理器实例"""
|
||
global _font_manager
|
||
if _font_manager is None:
|
||
_font_manager = FontManager()
|
||
return _font_manager
|
||
|