#!/usr/bin/env python # -*- coding: utf-8 -*- """ 字体服务 - 获取服务器上可用的字体列表 """ import logging import os import platform import subprocess from functools import lru_cache from typing import Dict, List, Optional, Set logger = logging.getLogger(__name__) class FontService: """字体服务类""" CHINESE_FONTS: List[Dict[str, str]] = [ # 宋体系列 {"label": "宋体 (SimSun)", "value": "SimSun, STSong, Songti SC, Noto Serif CJK SC, serif", "family": "SimSun"}, {"label": "华文宋体 (STSong)", "value": "STSong, SimSun, Songti SC, Noto Serif CJK SC, serif", "family": "STSong"}, {"label": "Noto 宋体 (Noto Serif CJK)", "value": "Noto Serif CJK SC, SimSun, STSong, serif", "family": "Noto Serif CJK SC"}, # 黑体系列 {"label": "黑体 (SimHei)", "value": "SimHei, STHeiti, Heiti SC, Noto Sans CJK SC, sans-serif", "family": "SimHei"}, {"label": "Noto 黑体 (Noto Sans CJK)", "value": "Noto Sans CJK SC, SimHei, STHeiti, sans-serif", "family": "Noto Sans CJK SC"}, {"label": "思源黑体 (Source Han Sans)", "value": "Source Han Sans CN, Noto Sans CJK SC, SimHei, sans-serif", "family": "Source Han Sans CN"}, {"label": "微软雅黑 (Microsoft YaHei)", "value": "Microsoft YaHei, Noto Sans CJK SC, SimHei, sans-serif", "family": "Microsoft YaHei"}, {"label": "苹方 (PingFang SC)", "value": "PingFang SC, Noto Sans CJK SC, SimHei, sans-serif", "family": "PingFang SC"}, {"label": "文泉驿微米黑", "value": "WenQuanYi Micro Hei, Noto Sans CJK SC, sans-serif", "family": "WenQuanYi Micro Hei"}, {"label": "文泉驿正黑", "value": "WenQuanYi Zen Hei, Noto Sans CJK SC, sans-serif", "family": "WenQuanYi Zen Hei"}, # 楷体系列 {"label": "楷体 (KaiTi)", "value": "KaiTi, STKaiti, Kaiti SC, serif", "family": "KaiTi"}, {"label": "华文楷体 (STKaiti)", "value": "STKaiti, KaiTi, Kaiti SC, serif", "family": "STKaiti"}, # 仿宋系列 {"label": "仿宋 (FangSong)", "value": "FangSong, STFangsong, serif", "family": "FangSong"}, {"label": "华文仿宋 (STFangsong)", "value": "STFangsong, FangSong, serif", "family": "STFangsong"}, # 其他 {"label": "华文细黑 (STXihei)", "value": "STXihei, Noto Sans CJK SC, sans-serif", "family": "STXihei"}, {"label": "冬青黑体 (Hiragino Sans GB)", "value": "Hiragino Sans GB, Noto Sans CJK SC, sans-serif", "family": "Hiragino Sans GB"}, ] ENGLISH_FONTS: List[Dict[str, str]] = [ {"label": "Arial", "value": "Arial, sans-serif", "family": "Arial"}, {"label": "Helvetica", "value": "Helvetica, Arial, sans-serif", "family": "Helvetica"}, {"label": "Times New Roman", "value": "Times New Roman, serif", "family": "Times New Roman"}, {"label": "Georgia", "value": "Georgia, serif", "family": "Georgia"}, {"label": "Verdana", "value": "Verdana, sans-serif", "family": "Verdana"}, {"label": "Courier New", "value": "Courier New, monospace", "family": "Courier New"}, ] @classmethod def _get_fc_list_families(cls) -> Optional[Set[str]]: """通过 fc-list 获取系统已安装的字体族名""" try: result = subprocess.run( ["fc-list", "--format", "%{family}\n"], capture_output=True, text=True, timeout=10, ) if result.returncode != 0: return None families: Set[str] = set() for line in result.stdout.splitlines(): for part in line.split(","): families.add(part.strip()) return families except (FileNotFoundError, subprocess.TimeoutExpired, OSError): return None @classmethod def _scan_font_files(cls) -> Set[str]: """扫描字体目录,返回字体文件名集合(备用方案)""" system = platform.system() dirs: List[str] = [] if system == "Darwin": dirs = ["/System/Library/Fonts", "/Library/Fonts", os.path.expanduser("~/Library/Fonts")] elif system == "Linux": dirs = ["/usr/share/fonts", "/usr/local/share/fonts", os.path.expanduser("~/.fonts")] elif system == "Windows": dirs = [os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts")] font_names: Set[str] = set() extensions = {".ttf", ".ttc", ".otf", ".woff", ".woff2"} for font_dir in dirs: if not os.path.exists(font_dir): continue try: for root, _, files in os.walk(font_dir): for file in files: if os.path.splitext(file)[1].lower() in extensions: font_names.add(os.path.splitext(file)[0]) except (PermissionError, OSError): continue return font_names @classmethod def _is_font_available(cls, family: str, fc_families: Optional[Set[str]], file_names: Set[str]) -> bool: """判断字体是否在系统中可用""" if fc_families is not None: family_lower = family.lower() for fc_fam in fc_families: if family_lower == fc_fam.lower(): return True return False family_normalized = family.lower().replace(" ", "") for name in file_names: name_normalized = name.lower().replace(" ", "").replace("-", "").replace("_", "") if family_normalized in name_normalized or name_normalized in family_normalized: return True return False @classmethod @lru_cache(maxsize=1) def get_available_fonts(cls) -> List[Dict[str, str]]: """获取服务器上可用的字体列表""" result: List[Dict[str, str]] = [{"label": "默认字体", "value": ""}] fc_families = cls._get_fc_list_families() file_names = cls._scan_font_files() if fc_families is None else set() if fc_families is not None: logger.info(f"[字体服务] 通过 fc-list 检测到 {len(fc_families)} 个字体族") else: logger.info(f"[字体服务] fc-list 不可用,使用文件名扫描检测到 {len(file_names)} 个字体文件") added_labels: Set[str] = set() for font in cls.CHINESE_FONTS: if cls._is_font_available(font["family"], fc_families, file_names): if font["label"] not in added_labels: result.append({"label": font["label"], "value": font["value"]}) added_labels.add(font["label"]) if len(result) <= 1: logger.warning("[字体服务] 未检测到中文字体,使用平台默认列表") system = platform.system() if system == "Darwin": defaults = ["PingFang SC", "STSong", "STKaiti", "Hiragino Sans GB"] elif system == "Linux": defaults = ["Noto Sans CJK SC", "Noto Serif CJK SC", "WenQuanYi Zen Hei", "WenQuanYi Micro Hei"] else: defaults = ["Microsoft YaHei", "SimSun", "SimHei", "KaiTi", "FangSong"] for font in cls.CHINESE_FONTS: if font["family"] in defaults and font["label"] not in added_labels: result.append({"label": font["label"], "value": font["value"]}) added_labels.add(font["label"]) for font in cls.ENGLISH_FONTS: result.append({"label": font["label"], "value": font["value"]}) logger.info(f"[字体服务] 返回 {len(result)} 个字体选项") return result