""" 字体管理模块 - 支持跨平台字体检测和配置 支持 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