feat: 完善字体包发行与 Docker 内置字体

This commit is contained in:
SkyJourney
2026-08-02 11:33:27 +08:00
parent 3a8d2c1c2e
commit 14f470454c
19 changed files with 434 additions and 252 deletions
+1
View File
@@ -22,6 +22,7 @@
"dependencies": {
"@md-to-pdf/docx-engine": "0.1.0",
"@md-to-pdf/font-pack-registry": "0.1.0",
"fflate": "0.8.3",
"zod": "^4.1.13"
},
"devDependencies": {
+116
View File
@@ -0,0 +1,116 @@
import { createHash } from "node:crypto";
import { lstat, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, posix, resolve } from "node:path";
import { zipSync } from "fflate";
import { verifyFontPack } from "./builder.js";
const ARCHIVE_MTIME = new Date("1980-01-01T00:00:00.000Z");
export interface BuildFontPackArchiveOptions {
packRoot: string;
appVersion: string;
packId: string;
packVersion: string;
outputDirectory: string;
reportPath?: string;
}
export interface FontPackArchiveReport {
packId: string;
packVersion: string;
artifactPath: string;
artifactName: string;
bytes: number;
sha256: string;
entries: string[];
}
function sha256(content: Uint8Array) {
return createHash("sha256").update(content).digest("hex").toUpperCase();
}
export function fontPackArchiveName(packId: string, version: string) {
return `MorphDoc-FontPack-${packId}-${version}.zip`;
}
async function collectFiles(directory: string, prefix: string) {
const result: Array<{ name: string; content: Uint8Array }> = [];
const visit = async (currentDirectory: string, relativeDirectory: string) => {
const entries = await readdir(currentDirectory, { withFileTypes: true });
entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
for (const entry of entries) {
const entryPath = join(currentDirectory, entry.name);
const status = await lstat(entryPath);
if (status.isSymbolicLink()) {
throw new Error(`字体包归档不允许符号链接:${entry.name}`);
}
const relativePath = posix.join(
prefix,
relativeDirectory.replaceAll("\\", "/"),
entry.name
);
if (status.isDirectory()) {
await visit(entryPath, posix.join(relativeDirectory, entry.name));
} else if (status.isFile()) {
result.push({ name: relativePath, content: await readFile(entryPath) });
} else {
throw new Error(`字体包归档只允许普通文件:${entry.name}`);
}
}
};
await visit(directory, "");
return result;
}
export async function createDeterministicFontPackArchive(
packDirectory: string,
prefix: string
) {
const files = await collectFiles(resolve(packDirectory), prefix);
const entries: Record<string, Uint8Array> = {};
for (const file of files) {
entries[file.name] = file.content;
}
return {
content: zipSync(entries, { level: 9, mtime: ARCHIVE_MTIME }),
entries: files.map((file) => file.name)
};
}
export async function buildFontPackArchive(
options: BuildFontPackArchiveOptions
): Promise<FontPackArchiveReport> {
const verified = await verifyFontPack({
root: options.packRoot,
appVersion: options.appVersion,
packId: options.packId,
packVersion: options.packVersion
});
const archive = await createDeterministicFontPackArchive(
verified.outputDirectory,
`${verified.packId}/${verified.packVersion}`
);
const outputDirectory = resolve(options.outputDirectory);
await mkdir(outputDirectory, { recursive: true });
const artifactName = fontPackArchiveName(
verified.packId,
verified.packVersion
);
const artifactPath = join(outputDirectory, artifactName);
await writeFile(artifactPath, archive.content);
const report: FontPackArchiveReport = {
packId: verified.packId,
packVersion: verified.packVersion,
artifactPath,
artifactName,
bytes: archive.content.byteLength,
sha256: sha256(archive.content),
entries: archive.entries
};
if (options.reportPath) {
const reportPath = resolve(options.reportPath);
await mkdir(dirname(reportPath), { recursive: true });
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
}
return report;
}
+20 -1
View File
@@ -1,4 +1,5 @@
import { resolve } from "node:path";
import { buildFontPackArchive } from "./archive.js";
import { buildFontPack, verifyFontPack } from "./builder.js";
import { buildWindowsFontPackInstaller } from "./windows-installer.js";
@@ -86,7 +87,25 @@ async function main() {
)
});
}
throw new Error("命令必须是 build、verify 或 windows-installer");
if (command === "archive") {
return buildFontPackArchive({
packRoot: resolve(option(values, "root", "output/font-packs/root")),
appVersion: option(values, "app-version", "0.6.0"),
packId: option(values, "pack-id", "mdtp-serif-sc"),
packVersion: option(values, "pack-version", "1.0.0"),
outputDirectory: resolve(
option(values, "output", "output/font-packs/release")
),
reportPath: resolve(
option(
values,
"report",
"output/font-packs/archive-report.json"
)
)
});
}
throw new Error("命令必须是 build、verify、archive 或 windows-installer");
}
main()
+1
View File
@@ -1,3 +1,4 @@
export * from "./archive.js";
export * from "./builder.js";
export * from "./recipe.js";
export * from "./windows-installer.js";
@@ -55,7 +55,7 @@ function define(name: string, value: string) {
}
export function windowsFontPackInstallerName(packId: string, version: string) {
return `md-to-pdf-font-pack-${packId}-${version}-${WINDOWS_FONT_PACK_ARCHITECTURE}-Setup.exe`;
return `MorphDoc-FontPack-${packId}-${version}-${WINDOWS_FONT_PACK_ARCHITECTURE}-Setup.exe`;
}
export function createWindowsFontPackInstallerArguments(options: {
@@ -0,0 +1,54 @@
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { unzipSync } from "fflate";
import { afterEach, describe, expect, it } from "vitest";
import {
createDeterministicFontPackArchive,
fontPackArchiveName
} from "../src/index.js";
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, { recursive: true, force: true })
)
);
});
describe("字体包 ZIP 发行附件", () => {
it("使用 MorphDoc 品牌和稳定文件名", () => {
expect(fontPackArchiveName("mdtp-serif-sc", "1.0.0")).toBe(
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip"
);
});
it("生成可复现且可直接解压到字体包根目录的归档", async () => {
const root = await mkdtemp(join(tmpdir(), "morphdoc-font-archive-"));
temporaryDirectories.push(root);
await mkdir(join(root, "fonts"), { recursive: true });
await Promise.all([
writeFile(join(root, "font-pack.json"), "{}\n", "utf8"),
writeFile(join(root, "fonts", "Regular.ttf"), new Uint8Array([1, 2, 3]))
]);
const first = await createDeterministicFontPackArchive(
root,
"mdtp-serif-sc/1.0.0"
);
const second = await createDeterministicFontPackArchive(
root,
"mdtp-serif-sc/1.0.0"
);
expect(first.content).toEqual(second.content);
expect(first.entries).toEqual([
"mdtp-serif-sc/1.0.0/font-pack.json",
"mdtp-serif-sc/1.0.0/fonts/Regular.ttf"
]);
const entries = unzipSync(first.content);
expect(new TextDecoder().decode(entries[first.entries[0]!])).toBe("{}\n");
expect(entries[first.entries[1]!]).toEqual(new Uint8Array([1, 2, 3]));
});
});
@@ -41,7 +41,7 @@ describe("Windows 字体包安装器构建", () => {
it("生成稳定发行文件名和完整 NSIS 定义", () => {
expect(windowsFontPackInstallerName("mdtp-serif-sc", "1.0.0")).toBe(
"md-to-pdf-font-pack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe"
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe"
);
const args = createWindowsFontPackInstallerArguments({
packDirectory: "C:/pack",