158 lines
7.5 KiB
Python
158 lines
7.5 KiB
Python
#!/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
|