Files
blog-code/scripts/compress-fonts.js
2025-12-08 01:03:07 +08:00

1002 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import Fontmin from "fontmin";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 读取配置文件获取语言设置和字体配置
async function getConfig() {
const configPath = path.join(__dirname, "../src/config.ts");
const configContent = fs.readFileSync(configPath, "utf-8");
// 提取语言设置
const langMatch = configContent.match(/const SITE_LANG = ["'](.+?)["']/);
const lang = langMatch ? langMatch[1] : "zh_CN";
// 提取字体配置
const fontConfigMatch = configContent.match(/font:\s*\{([\s\S]*?)\n\t\},/);
if (!fontConfigMatch) {
console.log("⚠ Font config not found, using default settings");
return { lang, fonts: [] };
}
const fontConfigStr = fontConfigMatch[1];
const fonts = [];
// 解析每个字体类别asciiFont, cjkFont
const fontTypes = ["asciiFont", "cjkFont"];
for (const fontType of fontTypes) {
const regex = new RegExp(`${fontType}:\\s*\\{([\\s\\S]*?)\\}`, "m");
const match = fontConfigStr.match(regex);
if (match) {
const fontConfig = match[1];
// 提取 enableCompress
const compressMatch = fontConfig.match(/enableCompress:\s*(true|false)/);
const enableCompress = compressMatch
? compressMatch[1] === "true"
: false;
// 提取 localFonts 数组
const localFontsMatch = fontConfig.match(/localFonts:\s*\[(.*?)\]/s);
let localFonts = [];
if (localFontsMatch?.[1].trim()) {
// 提取数组中的字符串
const fontsStr = localFontsMatch[1];
localFonts =
fontsStr
.match(/["']([^"']+)["']/g)
?.map((s) => s.replace(/["']/g, "")) || [];
}
if (enableCompress && localFonts.length > 0) {
fonts.push({
type: fontType,
files: localFonts,
enableCompress,
});
}
}
}
return { lang, fonts };
}
// 递归读取目录下所有文件
function readFilesRecursively(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
readFilesRecursively(filePath, fileList);
} else {
fileList.push(filePath);
}
});
return fileList;
}
// 提取文本内容
function extractText(content, ext) {
let text = content;
let frontmatterText = "";
// 提取并处理 frontmatter 中的文本
if (ext === ".md" || ext === ".mdx") {
const frontmatterMatch = content.match(/^---[\s\S]*?---/m);
if (frontmatterMatch) {
const frontmatter = frontmatterMatch[0];
// 提取 frontmatter 中的字符串值(包括有引号和无引号的)
// 匹配 key: value 格式(无引号)
const unquotedMatches = frontmatter.match(/^\s*\w+:\s*([^'"\n]+)$/gm);
if (unquotedMatches) {
unquotedMatches.forEach((match) => {
const value = match.replace(/^\s*\w+:\s*/, "").trim();
// 排除布尔值、日期、数字等非文本内容
if (!value.match(/^(true|false|\d{4}-\d{2}-\d{2}|\d+)$/)) {
frontmatterText += `${value} `;
}
});
}
// 提取带引号的字符串值
const quotedMatches = frontmatter.match(/:\s*['"]([^'"]+)['"]/g);
if (quotedMatches) {
quotedMatches.forEach((match) => {
const value = match.replace(/:\s*['"]([^'"]+)['"]/, "$1");
frontmatterText += `${value} `;
});
}
// 提取列表项中的文本(如 tags 列表)
const listMatches = frontmatter.match(/^\s*-\s*([^\n]+)$/gm);
if (listMatches) {
listMatches.forEach((match) => {
const value = match.replace(/^\s*-\s*/, "").trim();
frontmatterText += `${value} `;
});
}
}
// 移除 frontmatter 后继续处理正文
text = text.replace(/^---[\s\S]*?---\s*/m, "");
// 移除代码块中的内容(通常不需要特殊字体)
text = text.replace(/```[\s\S]*?```/g, "");
text = text.replace(/`[^`]+`/g, "");
}
// 移除 HTML 标签
text = text.replace(/<[^>]*>/g, " ");
// 移除 Markdown 语法
text = text.replace(/[#*_~`[\]()]/g, " ");
// 移除 URL
text = text.replace(/https?:\/\/[^\s]+/g, "");
// 移除多余的空白字符
text = text.replace(/\s+/g, " ").trim();
// 合并 frontmatter 文本和正文
const finalText = `${frontmatterText} ${text}`.trim();
return finalText;
}
// 获取 ASCII 字符集(用于 asciiFont
function getAsciiCharset() {
const chars = new Set();
// 基本 ASCII 字符:空格到波浪号 (32-126)
for (let i = 32; i <= 126; i++) {
chars.add(String.fromCharCode(i));
}
// 常用符号和标点
const common = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
for (const char of common) {
chars.add(char);
}
// 数字
for (let i = 0; i <= 9; i++) {
chars.add(String(i));
}
// 英文字母
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for (const char of alphabet) {
chars.add(char);
}
const text = Array.from(chars).sort().join("");
return text;
}
// 获取 Meting API 歌单数据中的文字
async function fetchMetingPlaylistText() {
try {
// 读取配置文件获取音乐播放器配置
const configPath = path.join(__dirname, "../src/config.ts");
const configContent = fs.readFileSync(configPath, "utf-8");
// 检查音乐播放器是否启用
const enableMatch = configContent.match(
/musicPlayerConfig:\s*MusicPlayerConfig\s*=\s*\{[\s\S]*?enable:\s*(true|false)/,
);
if (!enableMatch || enableMatch[1] === "false") {
console.log(
" Music player disabled, skipping Meting API text collection",
);
return new Set();
}
// 提取音乐播放器配置(使用默认值,因为配置可能不完整)
// 在实际的音乐播放器组件中,如果配置中没有指定模式,默认使用 "meting"
const musicConfigMatch = configContent.match(
/musicPlayerConfig:\s*MusicPlayerConfig\s*=\s*\{([\s\S]*?)\}/,
);
let mode = "meting"; // 默认模式
let meting_api =
"https://www.bilibili.uno/api?server=:server&type=:type&id=:id&auth=:auth&r=:r";
let meting_id = "14164869977";
let meting_server = "netease";
let meting_type = "playlist";
if (musicConfigMatch) {
const configStr = musicConfigMatch[1];
const modeMatch = configStr.match(/mode:\s*["']([^"']+)["']/);
if (modeMatch) {
mode = modeMatch[1];
}
const apiMatch = configStr.match(/meting_api:\s*["']([^"']+)["']/);
if (apiMatch) {
meting_api = apiMatch[1];
}
const idMatch = configStr.match(/id:\s*["']([^"']+)["']/);
if (idMatch) {
meting_id = idMatch[1];
}
const serverMatch = configStr.match(/server:\s*["']([^"']+)["']/);
if (serverMatch) {
meting_server = serverMatch[1];
}
const typeMatch = configStr.match(/type:\s*["']([^"']+)["']/);
if (typeMatch) {
meting_type = typeMatch[1];
}
}
if (mode !== "meting") {
console.log(
' Music player mode is not "meting", skipping API text collection',
);
return new Set();
}
// 构建 API URL
const apiUrl = meting_api
.replace(":server", meting_server)
.replace(":type", meting_type)
.replace(":id", meting_id)
.replace(":auth", "")
.replace(":r", Date.now().toString());
console.log(" Fetching music playlist from Meting API...");
console.log(` URL: ${apiUrl}`);
// 设置请求超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
const textSet = new Set();
try {
const response = await fetch(apiUrl, {
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const playlist = await response.json();
if (!Array.isArray(playlist)) {
throw new Error("API response is not an array");
}
console.log(
`✓ Successfully fetched ${playlist.length} songs from Meting API`,
);
// 提取歌曲信息中的文字
let songCount = 0;
playlist.forEach((song) => {
const title = song.name ?? song.title ?? "";
const artist = song.artist ?? song.author ?? "";
// 只处理有效的歌曲信息
if (title.trim() || artist.trim()) {
songCount++;
// 添加歌名中的字符
for (const char of title) {
textSet.add(char);
}
// 添加歌手名中的字符
for (const char of artist) {
textSet.add(char);
}
}
});
if (songCount === 0) {
console.log("⚠ No valid song data found in API response");
}
} catch (fetchError) {
clearTimeout(timeoutId);
if (fetchError.name === "AbortError") {
console.log(
"⚠ Meting API request timeout (10s), skipping music text collection",
);
} else {
console.log(
`⚠ Failed to fetch Meting API data: ${fetchError.message}, skipping music text collection`,
);
}
}
return textSet;
} catch (error) {
console.log(
`⚠ Error processing Meting API config: ${error.message}, skipping music text collection`,
);
return new Set();
}
}
// 获取 Bangumi API 番剧数据中的文字
async function fetchBangumiAnimeText() {
try {
// 读取配置文件获取番剧配置
const configPath = path.join(__dirname, "../src/config.ts");
const configContent = fs.readFileSync(configPath, "utf-8");
// 检查番剧页面是否启用
const featurePagesMatch = configContent.match(
/featurePages:\s*\{([\s\S]*?)\}/,
);
if (featurePagesMatch) {
const featureConfig = featurePagesMatch[1];
const animeMatch = featureConfig.match(/anime:\s*(true|false)/);
if (!animeMatch || animeMatch[1] === "false") {
console.log(
" Anime page disabled, skipping Bangumi API text collection",
);
return new Set();
}
}
// 提取番剧配置
const bangumiUserIdMatch = configContent.match(
/bangumi:\s*\{[\s\S]*?userId:\s*["']([^"']+)["']/,
);
const animeModeMatch = configContent.match(
/anime:\s*\{[\s\S]*?mode:\s*["']([^"']+)["']/,
);
const userId = bangumiUserIdMatch ? bangumiUserIdMatch[1] : null;
const mode = animeModeMatch ? animeModeMatch[1] : "bangumi";
if (mode !== "bangumi" || !userId) {
console.log(
` Anime mode is not "bangumi" or no userId configured, skipping Bangumi API text collection`,
);
return new Set();
}
console.log(" Fetching anime data from Bangumi API...");
console.log(` User ID: ${userId}`);
const textSet = new Set();
const BANGUMI_API_BASE = "https://api.bgm.tv";
// Bangumi 收藏类型1=想看2=看过3=在看4=搁置5=抛弃
const collectionTypes = [1, 2, 3, 4, 5];
// 获取单个收藏列表
async function fetchCollection(userId, subjectType, type) {
try {
let allData = [];
let offset = 0;
const limit = 50;
let hasMore = true;
while (hasMore) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(
`${BANGUMI_API_BASE}/v0/users/${userId}/collections?subject_type=${subjectType}&type=${type}&limit=${limit}&offset=${offset}`,
{
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
},
);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.data && data.data.length > 0) {
allData = [...allData, ...data.data];
}
if (!data.data || data.data.length < limit) {
hasMore = false;
} else {
offset += limit;
}
// 防止请求过于频繁
await new Promise((resolve) => setTimeout(resolve, 200));
}
return allData;
} catch (error) {
console.log(
`⚠ Failed to fetch collection type ${type}: ${error.message}`,
);
return [];
}
}
// 获取相关人员信息(制作公司等)
async function fetchSubjectPersons(subjectId) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(
`${BANGUMI_API_BASE}/v0/subjects/${subjectId}/persons`,
{
signal: controller.signal,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
},
);
clearTimeout(timeoutId);
if (!response.ok) {
return [];
}
const data = await response.json();
return Array.isArray(data) ? data : [];
} catch (_error) {
return [];
}
}
let totalItems = 0;
// 遍历所有收藏类型
for (const type of collectionTypes) {
const collections = await fetchCollection(userId, 2, type); // 2=动画
if (collections.length === 0) {
continue;
}
console.log(
`✓ Fetched ${collections.length} items from collection type ${type}`,
);
totalItems += collections.length;
// 处理每个动画条目
for (const item of collections) {
const subject = item.subject || {};
// 提取标题
const titleCn = subject.name_cn || "";
const title = subject.name || "";
for (const char of titleCn) {
textSet.add(char);
}
for (const char of title) {
textSet.add(char);
}
// 提取简介
const summary = subject.short_summary || "";
for (const char of summary) {
textSet.add(char);
}
// 提取标签
if (subject.tags && Array.isArray(subject.tags)) {
subject.tags.forEach((tag) => {
if (tag.name) {
for (const char of tag.name) {
textSet.add(char);
}
}
});
}
// 获取制作公司信息(限制并发请求)
if (item.subject_id && Math.random() < 0.3) {
// 只获取30%的详细信息,避免请求过多
const persons = await fetchSubjectPersons(item.subject_id);
persons.forEach((person) => {
if (person.name) {
for (const char of person.name) {
textSet.add(char);
}
}
if (person.relation) {
for (const char of person.relation) {
textSet.add(char);
}
}
});
// 请求间隔
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
}
if (totalItems > 0) {
console.log(
`✓ Successfully processed ${totalItems} anime items from Bangumi API`,
);
} else {
console.log("⚠ No anime data found from Bangumi API");
}
return textSet;
} catch (error) {
console.log(
`⚠ Error processing Bangumi API config: ${error.message}, skipping anime text collection`,
);
return new Set();
}
}
// 收集所有使用的文字(用于 CJK 字体)
async function collectText() {
const { lang } = await getConfig();
const textSet = new Set();
// 1. 读取 src/data 目录
const dataDir = path.join(__dirname, "../src/data");
const dataFiles = readFilesRecursively(dataDir);
dataFiles.forEach((file) => {
if (file.endsWith(".ts") || file.endsWith(".js")) {
const content = fs.readFileSync(file, "utf-8");
// 改进的字符串匹配
const patterns = [
// 双引号字符串
/"([^"\\]|\\.|\\n|\\t)*"/g,
// 单引号字符串
/'([^'\\]|\\.|\\n|\\t)*'/g,
// 模板字符串
/`([^`\\]|\\.|\\n|\\t)*`/g,
];
patterns.forEach((pattern) => {
const matches = content.match(pattern);
if (matches) {
matches.forEach((match) => {
let text = match;
// 清理引号
if (
(text.startsWith('"') && text.endsWith('"')) ||
(text.startsWith("'") && text.endsWith("'")) ||
(text.startsWith("`") && text.endsWith("`"))
) {
text = text.slice(1, -1);
}
// 处理转义字符
text = text
.replace(/\\n/g, "\n")
.replace(/\\t/g, "\t")
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
for (const char of text) {
textSet.add(char);
}
});
}
});
// 简单正则作为补充
const stringMatches = content.match(/["'`]([^"'`]+)["'`]/g);
if (stringMatches) {
stringMatches.forEach((match) => {
const text = match.slice(1, -1);
for (const char of text) {
textSet.add(char);
}
});
}
}
});
// 2. 读取 src/config.ts 文件
const configFile = path.join(__dirname, "../src/config.ts");
if (fs.existsSync(configFile)) {
const content = fs.readFileSync(configFile, "utf-8");
// 改进的字符串匹配
const patterns = [
// 双引号字符串
/"([^"\\]|\\.|\\n|\\t)*"/g,
// 单引号字符串
/'([^'\\]|\\.|\\n|\\t)*'/g,
// 模板字符串
/`([^`\\]|\\.|\\n|\\t)*`/g,
];
patterns.forEach((pattern) => {
const matches = content.match(pattern);
if (matches) {
matches.forEach((match) => {
// 清理引号和注释标记
let text = match;
// 移除字符串的引号
if (
(text.startsWith('"') && text.endsWith('"')) ||
(text.startsWith("'") && text.endsWith("'")) ||
(text.startsWith("`") && text.endsWith("`"))
) {
text = text.slice(1, -1);
}
// 处理转义字符
text = text
.replace(/\\n/g, "\n")
.replace(/\\t/g, "\t")
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
// 提取所有字符(包括中文)
for (const char of text) {
textSet.add(char);
}
});
}
});
// 作为补充,还用原来的简单正则再扫一遍,确保不遗漏
const simpleMatches = content.match(/["'`]([^"'`]+)["'`]/g);
if (simpleMatches) {
simpleMatches.forEach((match) => {
const text = match.slice(1, -1);
for (const char of text) {
textSet.add(char);
}
});
}
}
// 3. 读取对应语言的 i18n 文件
const i18nFile = path.join(__dirname, `../src/i18n/languages/${lang}.ts`);
if (fs.existsSync(i18nFile)) {
const content = fs.readFileSync(i18nFile, "utf-8");
// 改进的字符串匹配
const patterns = [
/"([^"\\]|\\.|\\n|\\t)*"/g,
/'([^'\\]|\\.|\\n|\\t)*'/g,
/`([^`\\]|\\.|\\n|\\t)*`/g,
];
patterns.forEach((pattern) => {
const matches = content.match(pattern);
if (matches) {
matches.forEach((match) => {
let text = match;
if (
(text.startsWith('"') && text.endsWith('"')) ||
(text.startsWith("'") && text.endsWith("'")) ||
(text.startsWith("`") && text.endsWith("`"))
) {
text = text.slice(1, -1);
}
// 处理转义字符
text = text
.replace(/\\n/g, "\n")
.replace(/\\t/g, "\t")
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
for (const char of text) {
textSet.add(char);
}
});
}
});
// 简单正则作为补充
const stringMatches = content.match(/["'`]([^"'`]+)["'`]/g);
if (stringMatches) {
stringMatches.forEach((match) => {
const text = match.slice(1, -1);
for (const char of text) {
textSet.add(char);
}
});
}
}
// 4. 读取 content 目录(根据环境变量决定路径)
let contentDir;
if (process.env.ENABLE_CONTENT_SYNC === "true" && process.env.CONTENT_DIR) {
// 使用环境变量指定的目录(以项目根目录为基准)
contentDir = path.join(__dirname, "..", process.env.CONTENT_DIR);
console.log(
` Using external content directory: ${process.env.CONTENT_DIR}`,
);
} else {
// 使用默认的 src/content 目录
contentDir = path.join(__dirname, "../src/content");
}
// 检查目录是否存在
if (!fs.existsSync(contentDir)) {
console.log(`⚠ Content directory does not exist: ${contentDir}`);
console.log(" Skipping content text collection");
} else {
const contentFiles = readFilesRecursively(contentDir);
contentFiles.forEach((file) => {
const ext = path.extname(file);
if ([".md", ".mdx", ".ts", ".js"].includes(ext)) {
const content = fs.readFileSync(file, "utf-8");
const text = extractText(content, ext);
for (const char of text) {
// 只保留中文、日文、韩文等 CJK 字符和常用标点
if (
char.match(
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af\u3000-\u303f\uff00-\uffef]/,
)
) {
textSet.add(char);
}
}
}
});
}
// 添加常用标点符号和数字
const commonChars = "0123456789\"\"''()【】《》、·—…「」『』";
for (const char of commonChars) {
textSet.add(char);
}
// 添加英文字母(如果字体支持)
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for (const char of alphabet) {
textSet.add(char);
}
// 5. 从 Meting API 获取歌单数据中的文字
const metingTextSet = await fetchMetingPlaylistText();
// 将 Meting API 的文字添加到主文字集合中
for (const char of metingTextSet) {
textSet.add(char);
}
if (metingTextSet.size > 0) {
console.log(
`✓ Added ${metingTextSet.size} unique characters from music playlist`,
);
}
// 6. 从 Bangumi API 获取番剧数据中的文字
const bangumiTextSet = await fetchBangumiAnimeText();
// 将 Bangumi API 的文字添加到主文字集合中
for (const char of bangumiTextSet) {
textSet.add(char);
}
if (bangumiTextSet.size > 0) {
console.log(
`✓ Added ${bangumiTextSet.size} unique characters from anime data`,
);
}
// 漏网之鱼如散落在各处未纳入统计的UI文本等
const otherWords = ["示例", "歌曲", "艺术家"];
for (const term of otherWords) {
for (const char of term) {
textSet.add(char);
}
}
const allText = Array.from(textSet).sort().join("");
return allText;
}
// 压缩字体并输出到 dist 目录
async function compressFonts() {
try {
// 读取配置
const { fonts } = await getConfig();
if (fonts.length === 0) {
console.log(
"⚠ No fonts to compress (enableCompress=false or localFonts is empty)",
);
return;
}
console.log(`Found ${fonts.length} font configs to compress`);
// 检查 dist 目录是否存在
const distDir = path.join(__dirname, "../dist");
if (!fs.existsSync(distDir)) {
console.log(
"⚠ dist directory does not exist, please run astro build first",
);
return;
}
// 创建 dist/assets/font 目录
const distFontDir = path.join(distDir, "assets/font");
if (!fs.existsSync(distFontDir)) {
fs.mkdirSync(distFontDir, { recursive: true });
}
// 根据字体类型收集不同的字符集
const cjkText = await collectText(); // CJK 字体使用完整字符集
const asciiText = getAsciiCharset(); // ASCII 字体只使用 ASCII 字符集
console.log("Starting font compression...");
let totalOriginalSize = 0;
let totalCompressedSize = 0;
let processedCount = 0;
// 用于收集所有错误
const errors = [];
// 遍历所有需要压缩的字体
for (const fontConfig of fonts) {
// 根据字体类型选择字符集
const text = fontConfig.type === "asciiFont" ? asciiText : cjkText;
for (const fontFile of fontConfig.files) {
const fontSrc = path.join(__dirname, "../public/assets/font", fontFile);
const ext = path.extname(fontFile).toLowerCase();
const baseName = path.basename(fontFile, ext);
if (!fs.existsSync(fontSrc)) {
const errorMsg = `❌ Config error [${fontConfig.type}]: Font file does not exist In config: "${fontFile}"\n Expected path: public/assets/font/${fontFile}\n \n Please check:\n 1. Is the filename correct (case sensitive)?\n 2. Is the file in public/assets/font/?\n 3. Is ${fontConfig.type}.localFonts in src/config.ts correct?`;
errors.push(errorMsg);
console.log(`\n${errorMsg}\n`);
continue;
}
const originalSize = fs.statSync(fontSrc).size;
totalOriginalSize += originalSize;
// 根据文件类型决定处理方式
if (ext === ".woff2" || ext === ".woff") {
// woff/woff2 已经是 Web 优化格式,不支持进一步子集化压缩
console.log(`⚠ Skipping ${fontFile} (already web-optimized format)`);
// 直接复制到 dist
const destFile = path.join(distFontDir, fontFile);
fs.copyFileSync(fontSrc, destFile);
totalCompressedSize += originalSize;
// 不计入处理数量
} else if (ext === ".ttf" || ext === ".otf") {
// TTF/OTF 需要压缩为 woff2
console.log(`Compressing ${fontFile}...`);
const fontmin = new Fontmin()
.src(fontSrc)
.use(
Fontmin.glyph({
text: text,
hinting: false,
}),
)
.use(
Fontmin.ttf2woff2({
deflate: true,
}),
)
.dest(distFontDir);
await new Promise((resolve, reject) => {
fontmin.run((err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
// 检查压缩结果
const compressedFile = path.join(distFontDir, `${baseName}.woff2`);
if (fs.existsSync(compressedFile)) {
const compressedSize = fs.statSync(compressedFile).size;
totalCompressedSize += compressedSize;
const reduction = (
(1 - compressedSize / originalSize) *
100
).toFixed(2);
console.log(
`${fontFile}${baseName}.woff2 (${(compressedSize / 1024).toFixed(2)} KB, reduced ${reduction}%)`,
);
processedCount++;
}
} else {
console.log(`⚠ Unsupported font format, skipping: ${fontFile}`);
}
}
}
// 输出总结
if (errors.length > 0) {
console.log("\n❌ Font compression encountered errors!");
console.log(`${errors.length} errors, please fix and retry.\n`);
// 列出实际存在的字体文件
const fontDir = path.join(__dirname, "../public/assets/font");
if (fs.existsSync(fontDir)) {
const actualFiles = fs
.readdirSync(fontDir)
.filter((f) =>
[".ttf", ".otf", ".woff", ".woff2"].includes(
path.extname(f).toLowerCase(),
),
);
if (actualFiles.length > 0) {
console.log("Available font files:");
actualFiles.forEach((f) => console.log(` - ${f}`));
} else {
console.log(" (font directory is empty)");
}
}
process.exit(1);
}
if (processedCount > 0) {
const totalReduction = (
(1 - totalCompressedSize / totalOriginalSize) *
100
).toFixed(2);
console.log("\n✓ Font optimization complete!");
console.log(
` Files processed: ${processedCount}, Overall reduction: ${totalReduction}%`,
);
} else {
console.log("\n⚠ No font files processed");
}
} catch (error) {
console.error("❌ Font compression failed:", error);
process.exit(1);
}
}
// 运行压缩
compressFonts();