Json-Python-Server/app/services/font_manager.py

250 lines
8.4 KiB
Python
Raw Normal View History

2026-01-29 18:18:32 +08:00
"""
字体管理模块 - 支持跨平台字体检测和配置
支持 LinuxmacOSWindows 三个平台
"""
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