feat: 实现 DOCX 可编辑字体嵌入

This commit is contained in:
SkyJourney
2026-07-31 16:18:30 +08:00
parent f3847f3e4b
commit 10bc62cee4
32 changed files with 2195 additions and 76 deletions
+68
View File
@@ -8,6 +8,73 @@ import {
} from "./export-config.js";
export const THEME_MANIFEST_VERSION = 1;
export const MAXIMUM_DOCX_FONT_FACE_COUNT = 16;
export const MAXIMUM_DOCX_FONT_SOURCE_BYTES = 8 * 1024 * 1024;
export const MAXIMUM_DOCX_FONT_TOTAL_SOURCE_BYTES =
32 * 1024 * 1024;
const docxFontNameSchema = z
.string()
.trim()
.min(1)
.max(100)
.regex(
/^[\p{L}\p{N}\s._-]+$/u,
"DOCX 字体名称包含不支持的字符"
);
const docxFontSourceSchema = z
.string()
.trim()
.min(1)
.max(500)
.refine(
(value) =>
!value.includes("\0") &&
!value.startsWith("/") &&
!value.startsWith("\\") &&
!/^[a-z]:[\\/]/iu.test(value) &&
!/^(?:https?:)?\/\//iu.test(value) &&
!value
.replace(/^theme-shared:/u, "")
.split(/[\\/]/u)
.includes(".."),
"DOCX 字体资源路径不安全"
)
.refine(
(value) => /\.(?:otf|ttf|woff2?)$/iu.test(value),
"DOCX 字体资源格式必须为 OTF、TTF、WOFF 或 WOFF2"
);
export const docxFontFaceSchema = z.object({
family: docxFontNameSchema,
aliases: z
.array(docxFontNameSchema)
.max(16)
.default([]),
source: docxFontSourceSchema,
weight: z
.number()
.int()
.min(100)
.max(900)
.multipleOf(100)
.default(400),
style: z.enum(["normal", "italic"]).default("normal"),
license: z.string().trim().min(1).max(200)
});
export const docxFontManifestSchema = z.object({
faces: z
.array(docxFontFaceSchema)
.min(1)
.max(MAXIMUM_DOCX_FONT_FACE_COUNT)
});
export type DocxFontFace = z.infer<typeof docxFontFaceSchema>;
export type DocxFontManifest = z.infer<
typeof docxFontManifestSchema
>;
export const themeFeatureSchema = z.enum([
"code",
@@ -51,6 +118,7 @@ export const themeManifestSchema = z.object({
footer: footerSchema.optional()
})
.optional(),
docxFonts: docxFontManifestSchema.optional(),
docxStyle: docxThemeStyleSchema.optional(),
bundled: z.boolean()
});
+74
View File
@@ -57,4 +57,78 @@ describe("主题清单", () => {
showOnFirstPage: false
});
});
it("允许声明可嵌入的本地 DOCX 字体字形面", () => {
const parsed = themeManifestSchema.parse({
manifestVersion: 1,
id: "font-test",
name: "字体测试主题",
version: "0.6.0",
description: "测试可移植 DOCX 字体声明。",
author: "md-to-pdf contributors",
license: "项目自有",
entry: "theme.css",
domPreset: "generic",
defaultFontSize: "16px",
supportedFeatures: [],
docxFonts: {
faces: [
{
family: "Mdpdf Fandol Fang",
aliases: ["FangSong", "STFangsong"],
source:
"theme-shared:official-fonts/FandolFang-Regular.woff2",
weight: 400,
style: "normal",
license: "GPL-3.0-with-font-exception"
}
]
},
bundled: true
});
expect(parsed.docxFonts?.faces[0]).toMatchObject({
family: "Mdpdf Fandol Fang",
weight: 400,
style: "normal"
});
});
it("拒绝远程或越界的 DOCX 字体资源", () => {
const base = {
manifestVersion: 1,
id: "font-test",
name: "字体测试主题",
version: "0.6.0",
description: "测试字体路径门禁。",
author: "md-to-pdf contributors",
license: "项目自有",
entry: "theme.css",
domPreset: "generic",
defaultFontSize: "16px",
supportedFeatures: [],
bundled: true
};
for (const source of [
"https://example.com/font.woff2",
"../font.woff2",
"C:\\fonts\\font.ttf"
]) {
expect(() =>
themeManifestSchema.parse({
...base,
docxFonts: {
faces: [
{
family: "Unsafe Font",
source,
license: "unknown"
}
]
}
})
).toThrow();
}
});
});