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 = { '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 = { '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; }