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
+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: {