250 lines
8.4 KiB
Python
250 lines
8.4 KiB
Python
|
|
"""
|
||
|
|
字体管理模块 - 支持跨平台字体检测和配置
|
||
|
|
支持 Linux、macOS、Windows 三个平台
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import logging
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional, List, Dict
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
import matplotlib.font_manager as fm
|
||
|
|
from app.core.config import settings
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class FontManager:
|
||
|
|
"""字体管理器 - 处理跨平台字体检测和配置"""
|
||
|
|
|
||
|
|
# 支持的字体路径映射(按优先级排序)
|
||
|
|
FONT_PATHS = {
|
||
|
|
'zh': { # 中文字体
|
||
|
|
'linux': [
|
||
|
|
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
|
||
|
|
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
|
||
|
|
'/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf',
|
||
|
|
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
|
||
|
|
],
|
||
|
|
'darwin': [ # macOS
|
||
|
|
'/Library/Fonts/SimHei.ttf',
|
||
|
|
'/System/Library/Fonts/STHeiti Light.ttc',
|
||
|
|
'/Applications/Microsoft Office/Library/Fonts/SimSun.ttf',
|
||
|
|
'/Library/Fonts/Arial.ttf',
|
||
|
|
],
|
||
|
|
'win32': [
|
||
|
|
'C:\\Windows\\Fonts\\simhei.ttf',
|
||
|
|
'C:\\Windows\\Fonts\\simsun.ttc',
|
||
|
|
'C:\\Windows\\Fonts\\msyh.ttc',
|
||
|
|
'C:\\Windows\\Fonts\\arial.ttf',
|
||
|
|
]
|
||
|
|
},
|
||
|
|
'en': { # 英文字体
|
||
|
|
'linux': [
|
||
|
|
'/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf',
|
||
|
|
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
|
||
|
|
'/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf',
|
||
|
|
],
|
||
|
|
'darwin': [
|
||
|
|
'/Library/Fonts/Times New Roman.ttf',
|
||
|
|
'/Library/Fonts/Arial.ttf',
|
||
|
|
'/System/Library/Fonts/Helvetica.ttc',
|
||
|
|
],
|
||
|
|
'win32': [
|
||
|
|
'C:\\Windows\\Fonts\\times.ttf',
|
||
|
|
'C:\\Windows\\Fonts\\arial.ttf',
|
||
|
|
'C:\\Windows\\Fonts\\georgia.ttf',
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# 项目内置字体
|
||
|
|
PROJECT_FONTS = {
|
||
|
|
'zh_regular': 'SubsetOTF/CN/SourceHanSansCN-Regular.otf',
|
||
|
|
'zh_bold': 'SubsetOTF/CN/SourceHanSansCN-Bold.otf',
|
||
|
|
'en_regular': None, # 英文使用系统字体
|
||
|
|
}
|
||
|
|
|
||
|
|
def __init__(self, fonts_dir: Optional[Path] = None):
|
||
|
|
"""
|
||
|
|
初始化字体管理器
|
||
|
|
|
||
|
|
Args:
|
||
|
|
fonts_dir: 项目字体目录路径
|
||
|
|
"""
|
||
|
|
self.fonts_dir = fonts_dir or settings.FONTS_DIR
|
||
|
|
self.platform = sys.platform
|
||
|
|
self.available_fonts = {}
|
||
|
|
self._init_fonts()
|
||
|
|
|
||
|
|
def _init_fonts(self):
|
||
|
|
"""初始化字体系统"""
|
||
|
|
logger.info(f"初始化字体系统 (平台: {self.platform})")
|
||
|
|
|
||
|
|
# 扫描系统和项目字体
|
||
|
|
self._scan_system_fonts()
|
||
|
|
self._register_project_fonts()
|
||
|
|
|
||
|
|
def _scan_system_fonts(self):
|
||
|
|
"""扫描系统可用字体"""
|
||
|
|
logger.info("扫描系统字体...")
|
||
|
|
|
||
|
|
for lang, fonts in self.FONT_PATHS.items():
|
||
|
|
paths = fonts.get(self.platform, [])
|
||
|
|
for font_path in paths:
|
||
|
|
if os.path.exists(font_path):
|
||
|
|
self.available_fonts[lang] = font_path
|
||
|
|
logger.info(f"找到{lang}字体: {font_path}")
|
||
|
|
break
|
||
|
|
|
||
|
|
if lang not in self.available_fonts:
|
||
|
|
logger.warning(f"未找到系统{lang}字体")
|
||
|
|
|
||
|
|
def _register_project_fonts(self):
|
||
|
|
"""注册项目内置字体"""
|
||
|
|
logger.info(f"扫描项目字体目录: {self.fonts_dir}")
|
||
|
|
|
||
|
|
# 注册中文字体
|
||
|
|
zh_font_path = self.fonts_dir / self.PROJECT_FONTS['zh_regular']
|
||
|
|
if zh_font_path.exists():
|
||
|
|
try:
|
||
|
|
self.available_fonts['zh'] = str(zh_font_path)
|
||
|
|
logger.info(f"注册项目中文字体: {zh_font_path}")
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"注册项目中文字体失败: {e}")
|
||
|
|
|
||
|
|
def get_font(self, language: str = 'zh') -> str:
|
||
|
|
"""
|
||
|
|
获取可用的字体路径
|
||
|
|
|
||
|
|
Args:
|
||
|
|
language: 语言类型 ('zh' 或 'en')
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
字体文件路径
|
||
|
|
"""
|
||
|
|
if language in self.available_fonts:
|
||
|
|
return self.available_fonts[language]
|
||
|
|
|
||
|
|
logger.warning(f"未找到{language}字体,使用默认字体")
|
||
|
|
return 'DejaVuSans' if language == 'en' else 'Arial'
|
||
|
|
|
||
|
|
def setup_matplotlib_font(self, language: str = 'zh'):
|
||
|
|
"""
|
||
|
|
配置 Matplotlib 使用的字体
|
||
|
|
|
||
|
|
Args:
|
||
|
|
language: 语言类型 ('zh' 或 'en')
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
font_path = self.get_font(language)
|
||
|
|
|
||
|
|
if os.path.isfile(font_path):
|
||
|
|
# 注册字体文件到 Matplotlib
|
||
|
|
fm.fontManager.addfont(font_path)
|
||
|
|
|
||
|
|
# 从文件路径加载字体
|
||
|
|
prop = fm.FontProperties(fname=font_path)
|
||
|
|
plt.rcParams['font.sans-serif'] = [prop.get_name()]
|
||
|
|
# 解决负号显示问题
|
||
|
|
plt.rcParams['axes.unicode_minus'] = False
|
||
|
|
logger.info(f"Matplotlib 字体配置为: {font_path}")
|
||
|
|
else:
|
||
|
|
# 使用字体名称
|
||
|
|
plt.rcParams['font.sans-serif'] = [font_path]
|
||
|
|
plt.rcParams['axes.unicode_minus'] = False
|
||
|
|
logger.info(f"Matplotlib 字体配置为: {font_path}")
|
||
|
|
|
||
|
|
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"配置 Matplotlib 字体失败: {e}")
|
||
|
|
|
||
|
|
def get_font_installation_command(self) -> str:
|
||
|
|
"""
|
||
|
|
获取当前系统推荐的字体安装命令
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
安装命令字符串
|
||
|
|
"""
|
||
|
|
if self.platform == 'linux':
|
||
|
|
return "apt-get install fonts-wqy-microhei fonts-noto-cjk-extra -y"
|
||
|
|
elif self.platform == 'darwin':
|
||
|
|
return "brew install --cask font-noto-sans-cjk"
|
||
|
|
else:
|
||
|
|
return "请从 https://www.noto-fonts.cn 下载并安装 Noto Sans CJK 字体"
|
||
|
|
|
||
|
|
def suggest_font_installation(self) -> bool:
|
||
|
|
"""
|
||
|
|
检查并建议安装字体
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
是否建议安装字体
|
||
|
|
"""
|
||
|
|
if 'zh' not in self.available_fonts:
|
||
|
|
logger.warning("=" * 60)
|
||
|
|
logger.warning("⚠️ 警告: 未找到中文字体!")
|
||
|
|
logger.warning("推荐的安装命令:")
|
||
|
|
logger.warning(self.get_font_installation_command())
|
||
|
|
logger.warning("=" * 60)
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def check_font_available(font_name: str) -> bool:
|
||
|
|
"""
|
||
|
|
检查指定字体是否可用
|
||
|
|
|
||
|
|
Args:
|
||
|
|
font_name: 字体名称
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
字体是否可用
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
fm.findfont(fm.FontProperties(family=font_name))
|
||
|
|
return True
|
||
|
|
except:
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
# 全局字体管理器实例
|
||
|
|
_font_manager: Optional[FontManager] = None
|
||
|
|
|
||
|
|
|
||
|
|
def get_font_manager(fonts_dir: Optional[Path] = None) -> FontManager:
|
||
|
|
"""获取全局字体管理器实例"""
|
||
|
|
global _font_manager
|
||
|
|
if _font_manager is None:
|
||
|
|
_font_manager = FontManager(fonts_dir)
|
||
|
|
return _font_manager
|
||
|
|
|
||
|
|
|
||
|
|
def setup_fonts_for_app(languages: List[str] = ['zh', 'en']) -> Dict[str, str]:
|
||
|
|
"""
|
||
|
|
为应用设置字体 (一次性初始化)
|
||
|
|
|
||
|
|
Args:
|
||
|
|
languages: 需要支持的语言列表
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
字体配置字典
|
||
|
|
"""
|
||
|
|
font_manager = get_font_manager()
|
||
|
|
|
||
|
|
# 提示用户安装字体(如需要)
|
||
|
|
font_manager.suggest_font_installation()
|
||
|
|
|
||
|
|
# 为每个语言配置 Matplotlib
|
||
|
|
fonts_config = {}
|
||
|
|
for lang in languages:
|
||
|
|
try:
|
||
|
|
# 配置 Matplotlib
|
||
|
|
font_manager.setup_matplotlib_font(lang)
|
||
|
|
|
||
|
|
logger.info(f"✓ {lang} 语言字体配置完成")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"配置 {lang} 语言字体失败: {e}")
|
||
|
|
|
||
|
|
return fonts_config
|