622 lines
21 KiB
TypeScript
622 lines
21 KiB
TypeScript
import { ipcMain, BrowserWindow } from 'electron';
|
||
import path from 'path';
|
||
import fs from 'fs-extra';
|
||
import { app } from 'electron';
|
||
import { isPackaged } from '../../lib/env';
|
||
import { AppEnv } from '../env';
|
||
|
||
/**
|
||
* 广播字体列表变更事件到所有窗口
|
||
*/
|
||
function broadcastFontListChanged() {
|
||
const windows = BrowserWindow.getAllWindows();
|
||
windows.forEach(window => {
|
||
window.webContents.send('fontManager:fontsChanged');
|
||
});
|
||
console.log('[FontManager] Broadcasted font list changed event to', windows.length, 'windows');
|
||
}
|
||
|
||
/**
|
||
* 检测字体类型(中文/英文/混合)
|
||
*/
|
||
function detectFontType(fontName: string, languages: string[] = []): 'Chinese' | 'English' | 'Mixed' {
|
||
// 中文字体关键词列表
|
||
const chineseKeywords = [
|
||
'中文', '中', '汉', '黑', '宋', '楷', '隶', '篆', '仿',
|
||
'雅黑', '微软', '思源', '猫啃', '墨趣', '古风',
|
||
'骚包', '手写', '毛峰', '张亚玲', '黑方', '朴素',
|
||
'NotoSerifCJK', 'NotoSerifCJK-VF',
|
||
'simsun', 'simhei', 'yahei', '胡晓波', '平方',
|
||
'雨晨', '字体', '书法', '装饰', '标题', '毛笔'
|
||
];
|
||
|
||
// 英文字体关键词列表
|
||
const englishKeywords = [
|
||
'Arial', 'Helvetica', 'Times', 'Courier', 'Verdana', 'Georgia',
|
||
'Comic', 'Impact', 'Trebuchet', 'Lucida', 'Palatino',
|
||
'Bodoni', 'Garamond', 'Calibri', 'Consolas', 'Monaco',
|
||
'Roboto', 'Open Sans', 'Lato', 'Raleway', 'Montserrat',
|
||
'Murecho', 'Maoken', 'Dymon', 'Fonte', 'Black', 'Bold',
|
||
'Light', 'Regular', 'Medium', 'SemiBold', 'ExtraLight'
|
||
];
|
||
|
||
const fontNameLower = fontName.toLowerCase();
|
||
|
||
// 检查支持的语言
|
||
const supportsChinese = languages.includes('zh-CN') || languages.includes('zh');
|
||
const supportsEnglish = languages.includes('en') || languages.length === 0; // 如果没有指定语言,默认认为支持英文
|
||
|
||
// 根据字体名称匹配关键词
|
||
const hasChineseKeyword = chineseKeywords.some(keyword =>
|
||
fontNameLower.includes(keyword.toLowerCase())
|
||
);
|
||
|
||
const hasEnglishKeyword = englishKeywords.some(keyword =>
|
||
fontNameLower.includes(keyword.toLowerCase())
|
||
);
|
||
|
||
// 判断逻辑
|
||
if (supportsChinese && hasChineseKeyword) {
|
||
return 'Chinese';
|
||
}
|
||
|
||
if (supportsChinese && !supportsEnglish) {
|
||
return 'Chinese';
|
||
}
|
||
|
||
if (!supportsChinese && supportsEnglish) {
|
||
return 'English';
|
||
}
|
||
|
||
// 检查字体名称中是否有中文字符
|
||
const chineseCharMatch = fontName.match(/[\u4e00-\u9fff]/);
|
||
if (chineseCharMatch) {
|
||
return 'Chinese';
|
||
}
|
||
|
||
// 根据关键词判断
|
||
if (hasChineseKeyword) {
|
||
return 'Chinese';
|
||
}
|
||
|
||
if (hasEnglishKeyword) {
|
||
return 'English';
|
||
}
|
||
|
||
// 默认判断:如果语言列表中包含中文,则为中文字体
|
||
if (supportsChinese) {
|
||
return 'Chinese';
|
||
}
|
||
|
||
return 'English'; // 默认为英文字体
|
||
}
|
||
|
||
/**
|
||
* 获取应用根目录(支持ASAR打包和非打包环境)
|
||
*/
|
||
function getAppRoot(): string {
|
||
// 优先使用 APP_ROOT 环境变量(在 env-main.ts 中设置)
|
||
if (process.env.APP_ROOT) {
|
||
return process.env.APP_ROOT;
|
||
}
|
||
// 回退方案:使用 resources 路径(生产环境)
|
||
if (process.resourcesPath) {
|
||
return process.resourcesPath;
|
||
}
|
||
// 最后回退:使用当前工作目录
|
||
return process.cwd();
|
||
}
|
||
|
||
/**
|
||
* 获取 ziti 目录路径(考虑ASAR打包)
|
||
*/
|
||
function getZitiDir(): string {
|
||
if (isPackaged) {
|
||
// 生产环境:优先使用 AppEnv.resourceBundleRoot
|
||
if (AppEnv.resourceBundleRoot && fs.existsSync(AppEnv.resourceBundleRoot)) {
|
||
const bundlePath = path.join(AppEnv.resourceBundleRoot, 'ziti');
|
||
if (fs.existsSync(bundlePath)) {
|
||
console.log('[FontManager] 使用 AppEnv.resourceBundleRoot:', bundlePath);
|
||
return bundlePath;
|
||
}
|
||
}
|
||
|
||
// 回退:通过 process.resourcesPath 计算
|
||
if (process.resourcesPath) {
|
||
const installRoot = path.dirname(process.resourcesPath);
|
||
const bundlePath = path.join(installRoot, 'resources-bundles', 'ziti');
|
||
if (fs.existsSync(bundlePath)) {
|
||
console.log('[FontManager] 使用 process.resourcesPath:', bundlePath);
|
||
return bundlePath;
|
||
}
|
||
}
|
||
|
||
// 回退:app.asar.unpacked/ziti
|
||
const appRoot = getAppRoot();
|
||
const asarUnpackedPath = path.join(appRoot, 'app.asar.unpacked', 'ziti');
|
||
if (fs.existsSync(asarUnpackedPath)) {
|
||
console.log('[FontManager] 使用 app.asar.unpacked:', asarUnpackedPath);
|
||
return asarUnpackedPath;
|
||
}
|
||
|
||
// 最后回退:返回默认路径
|
||
const fallbackRoot = process.resourcesPath ? path.dirname(process.resourcesPath) : getAppRoot();
|
||
console.warn('[FontManager] 所有 ziti 路径都不存在,返回默认路径');
|
||
return path.join(fallbackRoot, 'resources-bundles', 'ziti');
|
||
} else {
|
||
// 开发环境:ziti 在 packaging/vendor/ziti
|
||
const appRoot = getAppRoot();
|
||
const devZitiDir = path.join(appRoot, 'packaging', 'vendor', 'ziti');
|
||
if (fs.existsSync(devZitiDir)) {
|
||
return devZitiDir;
|
||
}
|
||
return path.join(appRoot, 'ziti');
|
||
}
|
||
}
|
||
|
||
const FONTS_DIR = path.join(app.getPath('userData'), 'fonts', 'custom');
|
||
const METADATA_FILE = path.join(FONTS_DIR, 'fonts_metadata.json');
|
||
|
||
// 确保字体目录存在
|
||
fs.ensureDirSync(FONTS_DIR);
|
||
|
||
/**
|
||
* 扫描 ziti 目录获取字体
|
||
*/
|
||
function scanZitiFonts(): any[] {
|
||
const zitiDir = getZitiDir();
|
||
const fonts: any[] = [];
|
||
|
||
console.log('[FontManager] 扫描ziti目录:', zitiDir);
|
||
|
||
// 字体名称映射表(英文文件名 -> 中文显示名)
|
||
const fontNameMap: Record<string, string> = {
|
||
'Dymon-ShouXieTi': 'Dymon手写体',
|
||
'MaokenAssortedSans': '猫啃杂糅体',
|
||
'MaokenAssortedSans-Lite': '猫啃杂糅体 Lite',
|
||
'Murecho-Black': 'Murecho 黑体',
|
||
'Murecho-Bold': 'Murecho 粗体',
|
||
'Murecho-ExtraBold': 'Murecho 特粗体',
|
||
'Murecho-ExtraLight': 'Murecho 特细体',
|
||
'Murecho-Light': 'Murecho 细体',
|
||
'Murecho-Medium': 'Murecho 中等',
|
||
'Murecho-Regular': 'Murecho 常规',
|
||
'Murecho-SemiBold': 'Murecho 半粗体',
|
||
'Murecho-Thin': 'Murecho 纤细体',
|
||
'けいなん丸ポップ体JP': 'けいなん丸ポップ体',
|
||
'墨趣古风体': '墨趣古风体',
|
||
'平方张亚玲黑方体': '平方张亚玲黑方体',
|
||
'胡晓波骚包体2.0': '胡晓波骚包体'
|
||
};
|
||
|
||
// 字体类型映射表
|
||
const fontTypeMap: Record<string, 'Chinese' | 'English' | 'Mixed'> = {
|
||
'Dymon-ShouXieTi': 'English',
|
||
'MaokenAssortedSans': 'Mixed',
|
||
'MaokenAssortedSans-Lite': 'Mixed',
|
||
'Murecho-Black': 'English',
|
||
'Murecho-Bold': 'English',
|
||
'Murecho-ExtraBold': 'English',
|
||
'Murecho-ExtraLight': 'English',
|
||
'Murecho-Light': 'English',
|
||
'Murecho-Medium': 'English',
|
||
'Murecho-Regular': 'English',
|
||
'Murecho-SemiBold': 'English',
|
||
'Murecho-Thin': 'English',
|
||
'けいなん丸ポップ体JP': 'English',
|
||
'墨趣古风体': 'Chinese',
|
||
'平方张亚玲黑方体': 'Chinese',
|
||
'胡晓波骚包体2.0': 'Chinese'
|
||
};
|
||
|
||
try {
|
||
if (!fs.existsSync(zitiDir)) {
|
||
console.log('[FontManager] ziti directory not found:', zitiDir);
|
||
return [];
|
||
}
|
||
|
||
const files = fs.readdirSync(zitiDir);
|
||
const fontExtensions = ['.ttf', '.otf', '.ttc', '.woff', '.woff2'];
|
||
|
||
files.forEach(file => {
|
||
const ext = path.extname(file).toLowerCase();
|
||
if (fontExtensions.includes(ext)) {
|
||
const name = path.basename(file, ext);
|
||
// 使用映射表获取中文名称,如果没有映射则使用原文件名
|
||
const displayName = fontNameMap[name] || name;
|
||
|
||
// 使用映射表获取字体类型,如果没有则自动检测
|
||
const fontType = fontTypeMap[name] || detectFontType(name, ['zh-CN', 'ja', 'en']);
|
||
|
||
fonts.push({
|
||
value: name,
|
||
label: displayName,
|
||
source: 'ziti',
|
||
fontType: fontType,
|
||
category: 'sans-serif',
|
||
languages: ['zh-CN', 'ja', 'en'],
|
||
path: path.join(zitiDir, file),
|
||
// 🔧 新增:国际字体标志(ziti字体通常不是国际字体)
|
||
isInternational: false
|
||
});
|
||
}
|
||
});
|
||
|
||
console.log('[FontManager] Scanned ziti fonts:', fonts.length);
|
||
} catch (error) {
|
||
console.error('[FontManager] Error scanning ziti:', error);
|
||
}
|
||
|
||
return fonts;
|
||
}
|
||
|
||
/**
|
||
* 获取预置字体
|
||
*/
|
||
function getBundledFonts(): any[] {
|
||
const appRoot = getAppRoot();
|
||
const metadataCandidates = [
|
||
path.join(appRoot, 'packaging', 'vendor', 'fonts', 'bundled', 'fonts_metadata.json'),
|
||
path.join(appRoot, 'fonts', 'bundled', 'fonts_metadata.json'),
|
||
];
|
||
const metadataFile = metadataCandidates.find(file => fs.existsSync(file)) || metadataCandidates[0];
|
||
|
||
try {
|
||
if (!fs.existsSync(metadataFile)) {
|
||
console.log('[FontManager] Bundled fonts metadata not found');
|
||
return [];
|
||
}
|
||
|
||
const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf-8'));
|
||
const fonts = (metadata.fonts || []).map((font: any) => {
|
||
const languages = font.languages || [];
|
||
const fontType = font.fontType || detectFontType(font.family, languages);
|
||
|
||
return {
|
||
value: font.family,
|
||
label: font.display_name || font.family,
|
||
source: 'bundled',
|
||
fontType: fontType,
|
||
category: font.category || 'sans-serif',
|
||
languages: languages,
|
||
// 🔧 关键修复:传递 isInternational 标志
|
||
isInternational: font.isInternational || false,
|
||
supportsCyrillic: font.supportsCyrillic || false,
|
||
supportsHangul: font.supportsHangul || false,
|
||
supportsKana: font.supportsKana || false
|
||
};
|
||
});
|
||
|
||
console.log('[FontManager] Bundled fonts:', fonts.length);
|
||
return fonts;
|
||
} catch (error) {
|
||
console.error('[FontManager] Error reading bundled fonts:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 🔧 字体缓存,避免频繁扫描文件系统
|
||
let fontCache: { success: boolean; data?: any[]; message?: string } | null = null;
|
||
let fontCacheTime = 0;
|
||
const FONT_CACHE_TTL = 30000; // 缓存 30 秒
|
||
|
||
/**
|
||
* 获取所有可用字体(Node.js 实现,不依赖 Python)
|
||
* 🔧 添加缓存机制,避免频繁扫描导致 CPU 100%
|
||
*/
|
||
export async function getAllFonts() {
|
||
try {
|
||
// 检查缓存是否有效
|
||
const now = Date.now();
|
||
if (fontCache && (now - fontCacheTime) < FONT_CACHE_TTL) {
|
||
// console.log('[FontManager] Using cached fonts');
|
||
return fontCache;
|
||
}
|
||
|
||
console.log('[FontManager] Getting all fonts (Node.js implementation)');
|
||
|
||
// 合并所有字体源
|
||
const allFonts = [
|
||
...getBundledFonts(),
|
||
...scanZitiFonts()
|
||
];
|
||
|
||
// 去重(按 value)
|
||
const seen = new Set();
|
||
const uniqueFonts = allFonts.filter(font => {
|
||
if (seen.has(font.value)) {
|
||
return false;
|
||
}
|
||
seen.add(font.value);
|
||
return true;
|
||
});
|
||
|
||
console.log('[FontManager] Total unique fonts:', uniqueFonts.length);
|
||
|
||
// 更新缓存
|
||
fontCache = {
|
||
success: true,
|
||
data: uniqueFonts
|
||
};
|
||
fontCacheTime = now;
|
||
|
||
return fontCache;
|
||
} catch (error: any) {
|
||
console.error('[FontManager] Exception:', error);
|
||
return {
|
||
success: false,
|
||
message: error.message || '获取字体列表失败'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除字体缓存(在上传/删除字体后调用)
|
||
*/
|
||
export function clearFontCache() {
|
||
fontCache = null;
|
||
fontCacheTime = 0;
|
||
console.log('[FontManager] Font cache cleared');
|
||
}
|
||
|
||
/**
|
||
* 上传自定义字体
|
||
*/
|
||
export async function uploadFont(params: {
|
||
fileName: string;
|
||
fileData?: number[];
|
||
fileDataBase64?: string;
|
||
displayName: string;
|
||
fontFamily: string;
|
||
category: string;
|
||
languages: string[];
|
||
fontType?: 'Chinese' | 'English' | 'Mixed';
|
||
uploadToZiti?: boolean;
|
||
}) {
|
||
try {
|
||
const { fileName, fileData, fileDataBase64, displayName, fontFamily, category, languages, uploadToZiti } = params;
|
||
|
||
// 确定保存目录:如果指定上传到ziti,则保存到ziti目录,否则保存到自定义字体目录
|
||
let fontPath: string;
|
||
let saveDir: string;
|
||
|
||
if (uploadToZiti) {
|
||
saveDir = getZitiDir();
|
||
fs.ensureDirSync(saveDir);
|
||
fontPath = path.join(saveDir, fileName);
|
||
} else {
|
||
fontPath = path.join(FONTS_DIR, fileName);
|
||
}
|
||
|
||
// 支持两种格式:Base64编码或数组
|
||
let buffer: Buffer;
|
||
if (fileDataBase64) {
|
||
console.log('[FontManager] 使用Base64解码, length:', fileDataBase64.length);
|
||
buffer = Buffer.from(fileDataBase64, 'base64');
|
||
} else if (fileData) {
|
||
console.log('[FontManager] 使用数组转换, length:', fileData.length);
|
||
buffer = Buffer.from(fileData);
|
||
} else {
|
||
throw new Error('缺少文件数据');
|
||
}
|
||
|
||
console.log('[FontManager] Buffer size:', buffer.length);
|
||
await fs.writeFile(fontPath, buffer);
|
||
console.log('[FontManager] Font file saved to:', fontPath);
|
||
|
||
// 只有上传到自定义目录时才需要保存元数据
|
||
if (!uploadToZiti) {
|
||
// 更新元数据
|
||
let metadata: any = { fonts: [] };
|
||
if (await fs.pathExists(METADATA_FILE)) {
|
||
metadata = await fs.readJSON(METADATA_FILE);
|
||
}
|
||
|
||
// 自动检测字体类型
|
||
const fontType = params.fontType || detectFontType(fontFamily, languages);
|
||
|
||
// 添加新字体信息
|
||
metadata.fonts.push({
|
||
family: fontFamily,
|
||
display_name: displayName,
|
||
path: fileName,
|
||
source: 'custom',
|
||
fontType: fontType,
|
||
category: category,
|
||
languages: languages,
|
||
uploadedAt: new Date().toISOString()
|
||
});
|
||
|
||
// 保存元数据
|
||
await fs.writeJSON(METADATA_FILE, metadata, { spaces: 2 });
|
||
}
|
||
|
||
// 延迟后广播字体列表变更事件,确保文件已写入磁盘
|
||
setTimeout(() => {
|
||
console.log('[FontManager] Broadcasting font list changed event');
|
||
broadcastFontListChanged();
|
||
}, 200);
|
||
|
||
return {
|
||
success: true,
|
||
message: '字体上传成功'
|
||
};
|
||
} catch (error: any) {
|
||
return {
|
||
success: false,
|
||
message: error.message || '上传字体失败'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除自定义字体
|
||
*/
|
||
export async function deleteFont(fontFamily: string) {
|
||
try {
|
||
// 读取元数据
|
||
if (!await fs.pathExists(METADATA_FILE)) {
|
||
return {
|
||
success: false,
|
||
message: '元数据文件不存在'
|
||
};
|
||
}
|
||
|
||
const metadata = await fs.readJSON(METADATA_FILE);
|
||
const fontIndex = metadata.fonts.findIndex((f: any) => f.family === fontFamily && f.source === 'custom');
|
||
|
||
if (fontIndex === -1) {
|
||
return {
|
||
success: false,
|
||
message: '字体不存在或不是自定义字体'
|
||
};
|
||
}
|
||
|
||
const font = metadata.fonts[fontIndex];
|
||
const fontPath = path.join(FONTS_DIR, font.path);
|
||
|
||
// 删除字体文件
|
||
if (await fs.pathExists(fontPath)) {
|
||
await fs.remove(fontPath);
|
||
}
|
||
|
||
// 从元数据中移除
|
||
metadata.fonts.splice(fontIndex, 1);
|
||
await fs.writeJSON(METADATA_FILE, metadata, { spaces: 2 });
|
||
|
||
// 广播字体列表变更事件
|
||
broadcastFontListChanged();
|
||
|
||
return {
|
||
success: true,
|
||
message: '字体删除成功'
|
||
};
|
||
} catch (error: any) {
|
||
return {
|
||
success: false,
|
||
message: error.message || '删除字体失败'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找字体文件路径
|
||
*/
|
||
export async function findFontPath(fontFamily: string, fontWeight: number = 400) {
|
||
const bundledFontPath = findBundledFontFile(fontFamily);
|
||
if (bundledFontPath && fs.existsSync(bundledFontPath)) {
|
||
console.log(`[FontManager] bundled font resolved: ${fontFamily} -> ${bundledFontPath}`);
|
||
return {
|
||
success: true,
|
||
data: bundledFontPath
|
||
};
|
||
}
|
||
|
||
console.warn(`[FontManager] bundled font not found: ${fontFamily}`, {
|
||
searchedDirs: getBundledFontDirs()
|
||
});
|
||
return {
|
||
success: false,
|
||
message: `未在内置字体包中找到字体: ${fontFamily}`
|
||
};
|
||
}
|
||
export function registerFontManagerHandlers() {
|
||
ipcMain.handle('fontManager:getAllFonts', async () => {
|
||
return await getAllFonts();
|
||
});
|
||
|
||
ipcMain.handle('fontManager:uploadFont', async (_, params) => {
|
||
return await uploadFont(params);
|
||
});
|
||
|
||
ipcMain.handle('fontManager:deleteFont', async (_, fontFamily) => {
|
||
return await deleteFont(fontFamily);
|
||
});
|
||
|
||
ipcMain.handle('fontManager:findFontPath', async (_, fontFamily, fontWeight) => {
|
||
return await findFontPath(fontFamily, fontWeight);
|
||
});
|
||
}
|
||
|
||
function getBundledFontDirs(): string[] {
|
||
const dirs: string[] = [];
|
||
const pushIfExists = (dir: string) => {
|
||
if (dir && fs.existsSync(dir) && !dirs.includes(dir)) {
|
||
dirs.push(dir);
|
||
}
|
||
};
|
||
|
||
const appRoot = getAppRoot();
|
||
|
||
if (AppEnv.resourceBundleRoot && fs.existsSync(AppEnv.resourceBundleRoot)) {
|
||
pushIfExists(path.join(AppEnv.resourceBundleRoot, 'ziti'));
|
||
pushIfExists(path.join(AppEnv.resourceBundleRoot, 'fonts'));
|
||
pushIfExists(path.join(AppEnv.resourceBundleRoot, 'fonts', 'ziti'));
|
||
}
|
||
|
||
if (process.resourcesPath) {
|
||
const installRoot = path.dirname(process.resourcesPath);
|
||
const bundleRoot = path.join(installRoot, 'resources-bundles');
|
||
pushIfExists(path.join(bundleRoot, 'ziti'));
|
||
pushIfExists(path.join(bundleRoot, 'fonts'));
|
||
pushIfExists(path.join(bundleRoot, 'fonts', 'ziti'));
|
||
}
|
||
|
||
pushIfExists(path.join(appRoot, 'packaging', 'vendor', 'ziti'));
|
||
pushIfExists(path.join(appRoot, 'packaging', 'vendor', 'fonts'));
|
||
pushIfExists(path.join(appRoot, 'packaging', 'vendor', 'fonts', 'ziti'));
|
||
pushIfExists(path.join(appRoot, 'ziti'));
|
||
pushIfExists(path.join(appRoot, 'resources-bundles', 'ziti'));
|
||
pushIfExists(path.join(appRoot, 'resources-bundles', 'fonts'));
|
||
pushIfExists(path.join(appRoot, 'resources-bundles', 'fonts', 'ziti'));
|
||
pushIfExists(path.join(appRoot, 'electron', 'resources', 'extra', 'common', 'fonts'));
|
||
|
||
return dirs;
|
||
}
|
||
|
||
function normalizeFontKey(value: string): string {
|
||
return String(value || '')
|
||
.toLowerCase()
|
||
.replace(/\.(ttf|otf|ttc|woff2?|ttf\.ttc)$/i, '')
|
||
.replace(/[\s_\-\.]+/g, '')
|
||
.trim();
|
||
}
|
||
|
||
function findBundledFontFile(fontFamily: string): string | null {
|
||
const requested = normalizeFontKey(fontFamily);
|
||
if (!requested) {
|
||
return null;
|
||
}
|
||
|
||
const fontExtensions = ['.ttf', '.otf', '.ttc', '.woff', '.woff2'];
|
||
const candidates: Array<{ path: string; base: string; key: string }> = [];
|
||
|
||
for (const dir of getBundledFontDirs()) {
|
||
try {
|
||
const files = fs.readdirSync(dir);
|
||
for (const file of files) {
|
||
const ext = path.extname(file).toLowerCase();
|
||
if (!fontExtensions.includes(ext)) {
|
||
continue;
|
||
}
|
||
const base = path.basename(file, ext);
|
||
candidates.push({
|
||
path: path.join(dir, file),
|
||
base,
|
||
key: normalizeFontKey(base),
|
||
});
|
||
}
|
||
} catch (error: any) {
|
||
console.warn('[FontManager] scan bundled font dir failed:', dir, error?.message || error);
|
||
}
|
||
}
|
||
|
||
const exact = candidates.find(item => item.key === requested);
|
||
if (exact) return exact.path;
|
||
|
||
const partial = candidates.find(item => item.key.includes(requested) || requested.includes(item.key));
|
||
if (partial) return partial.path;
|
||
|
||
return null;
|
||
}
|