Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+396
View File
@@ -0,0 +1,396 @@
"""
字体管理模块
负责查找和管理字体文件,支持预置字体、系统字体、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字体路径:支持ASAR打包环境
# 检查是否在打包环境中运行
app_root = os.environ.get('APP_ROOT', None)
if 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 dir: {ziti_path_bundle}", file=sys.stderr)
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 dir: {ziti_path}", file=sys.stderr)
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 fallback dir: {ziti_path_fallback1}", file=sys.stderr)
else:
ziti_path_fallback2 = Path(app_root) / "ziti"
self.ziti_fonts_dir = ziti_path_fallback2
print(f"[font_manager] ziti fallback path: {ziti_path_fallback2} (exists: {ziti_path_fallback2.exists()})", file=sys.stderr)
else:
self.ziti_fonts_dir = project_root / "ziti"
print(f"[font_manager] dev ziti path: {self.ziti_fonts_dir}", file=sys.stderr)
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]:
"""Do not scan OS fonts. Runtime must use bundled font package only."""
self._system_fonts_cache = []
return []
def find_font(self, font_family: str, font_weight: int = 400) -> Optional[str]:
"""Find a font file from bundled metadata or ziti only."""
bundled_font = self._find_bundled_font(font_family, font_weight)
if bundled_font:
return bundled_font
ziti_font = self._find_ziti_font(font_family, font_weight)
if ziti_font:
return ziti_font
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',
}
# 标准化字体名称(移除空格、统一大小写)
def normalize_name(name: str) -> str:
name = name.lower()
for ext in ['.ttf', '.otf', '.ttc', '.woff', '.woff2']:
while name.endswith(ext):
name = name[:-len(ext)]
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)
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