feat: 接入可选字体包同源渲染链
This commit is contained in:
@@ -52,3 +52,33 @@ export function parseThemeResourceUrl(requestUrl: string) {
|
|||||||
assetPath: assetSegments.join("/")
|
assetPath: assetSegments.join("/")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseFontPackResourceUrl(requestUrl: string) {
|
||||||
|
const url = new URL(requestUrl);
|
||||||
|
if (url.protocol !== "mdpdf:" || url.host !== "font-pack") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let segments: string[];
|
||||||
|
try {
|
||||||
|
segments = url.pathname
|
||||||
|
.split("/")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((segment) => decodeURIComponent(segment));
|
||||||
|
} catch {
|
||||||
|
throw new Error("字体包资源地址无效");
|
||||||
|
}
|
||||||
|
const [packId, packVersion, faceId, kind, ...extra] = segments;
|
||||||
|
if (
|
||||||
|
!packId ||
|
||||||
|
!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(packId) ||
|
||||||
|
!packVersion ||
|
||||||
|
!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(packVersion) ||
|
||||||
|
!faceId ||
|
||||||
|
!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(faceId) ||
|
||||||
|
kind !== "web" ||
|
||||||
|
extra.length > 0
|
||||||
|
) {
|
||||||
|
throw new Error("字体包资源地址无效");
|
||||||
|
}
|
||||||
|
return { packId, packVersion, faceId, kind } as const;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
createApplicationService,
|
createApplicationService,
|
||||||
type ApplicationService
|
type ApplicationService
|
||||||
} from "@md-to-pdf/application";
|
} from "@md-to-pdf/application";
|
||||||
import { parseThemeResourceUrl } from "./application-contract.js";
|
import {
|
||||||
|
parseFontPackResourceUrl,
|
||||||
|
parseThemeResourceUrl
|
||||||
|
} from "./application-contract.js";
|
||||||
import {
|
import {
|
||||||
DesktopApplicationController
|
DesktopApplicationController
|
||||||
} from "./desktop-application-controller.js";
|
} from "./desktop-application-controller.js";
|
||||||
@@ -26,6 +29,7 @@ import {
|
|||||||
const APP_SCHEME = "mdpdf";
|
const APP_SCHEME = "mdpdf";
|
||||||
const APP_HOST = "bundle";
|
const APP_HOST = "bundle";
|
||||||
const THEME_HOST = "theme";
|
const THEME_HOST = "theme";
|
||||||
|
const FONT_PACK_HOST = "font-pack";
|
||||||
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
|
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
let applicationController: DesktopApplicationController | undefined;
|
let applicationController: DesktopApplicationController | undefined;
|
||||||
let pendingExternalMarkdownPath = findMarkdownFileArgument(
|
let pendingExternalMarkdownPath = findMarkdownFileArgument(
|
||||||
@@ -102,6 +106,21 @@ function getDesktopThemeDirectory() {
|
|||||||
: path.resolve(currentDirectory, "../../..", ".local", "themes");
|
: path.resolve(currentDirectory, "../../..", ".local", "themes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDesktopFontPackDirectory() {
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
return path.resolve(
|
||||||
|
currentDirectory,
|
||||||
|
"../../..",
|
||||||
|
".local",
|
||||||
|
"font-packs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const localApplicationData = process.env.LOCALAPPDATA?.trim();
|
||||||
|
return localApplicationData
|
||||||
|
? path.join(localApplicationData, "md-to-pdf", "font-packs")
|
||||||
|
: path.join(app.getPath("userData"), "font-packs");
|
||||||
|
}
|
||||||
|
|
||||||
function createDesktopApplicationService() {
|
function createDesktopApplicationService() {
|
||||||
const projectRoot = path.resolve(currentDirectory, "../../..");
|
const projectRoot = path.resolve(currentDirectory, "../../..");
|
||||||
return createApplicationService({
|
return createApplicationService({
|
||||||
@@ -111,6 +130,22 @@ function createDesktopApplicationService() {
|
|||||||
localRoot: getDesktopThemeDirectory(),
|
localRoot: getDesktopThemeDirectory(),
|
||||||
createAssetUrl: (themeId, assetPath) =>
|
createAssetUrl: (themeId, assetPath) =>
|
||||||
`${APP_SCHEME}://${THEME_HOST}/${encodeURIComponent(themeId)}/${encodeThemeAssetPath(assetPath)}`,
|
`${APP_SCHEME}://${THEME_HOST}/${encodeURIComponent(themeId)}/${encodeThemeAssetPath(assetPath)}`,
|
||||||
|
fontPacks: {
|
||||||
|
roots: [getDesktopFontPackDirectory()],
|
||||||
|
appVersion: app.getVersion(),
|
||||||
|
createAssetUrl: (
|
||||||
|
packId,
|
||||||
|
version,
|
||||||
|
faceId,
|
||||||
|
kind,
|
||||||
|
sha256
|
||||||
|
) =>
|
||||||
|
`${APP_SCHEME}://${FONT_PACK_HOST}/${encodeURIComponent(
|
||||||
|
packId
|
||||||
|
)}/${encodeURIComponent(version)}/${encodeURIComponent(
|
||||||
|
faceId
|
||||||
|
)}/${kind}?v=${sha256}`
|
||||||
|
},
|
||||||
onWarning: (message) => console.warn(message)
|
onWarning: (message) => console.warn(message)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -155,6 +190,34 @@ async function registerApplicationProtocol(
|
|||||||
return new Response("Not found", { status: 404 });
|
return new Response("Not found", { status: 404 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (url.host === FONT_PACK_HOST) {
|
||||||
|
try {
|
||||||
|
const resource = parseFontPackResourceUrl(request.url);
|
||||||
|
if (!resource) {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
const asset = await applicationService.getFontPackAsset(
|
||||||
|
resource.packId,
|
||||||
|
resource.packVersion,
|
||||||
|
resource.faceId,
|
||||||
|
resource.kind
|
||||||
|
);
|
||||||
|
if (!asset) {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
return new Response(asset.content, {
|
||||||
|
headers: {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"cache-control": "public, max-age=31536000, immutable",
|
||||||
|
"content-type": asset.contentType,
|
||||||
|
etag: `\"${asset.sha256}\"`,
|
||||||
|
"x-content-type-options": "nosniff"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
if (url.host !== APP_HOST) {
|
if (url.host !== APP_HOST) {
|
||||||
return new Response("Not found", { status: 404 });
|
return new Response("Not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,10 @@ export function isAllowedPdfRuntimeUrl(
|
|||||||
if (["about:", "blob:", "data:"].includes(requested.protocol)) {
|
if (["about:", "blob:", "data:"].includes(requested.protocol)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (requested.protocol === "mdpdf:" && requested.host === "theme") {
|
if (
|
||||||
|
requested.protocol === "mdpdf:" &&
|
||||||
|
["theme", "font-pack"].includes(requested.host)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return requested.origin === new URL(renderUrl).origin;
|
return requested.origin === new URL(renderUrl).origin;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "../src/pdf-contract.js";
|
} from "../src/pdf-contract.js";
|
||||||
import {
|
import {
|
||||||
parseMarkdownRenderRequest,
|
parseMarkdownRenderRequest,
|
||||||
|
parseFontPackResourceUrl,
|
||||||
parseThemeId,
|
parseThemeId,
|
||||||
parseThemeResourceUrl
|
parseThemeResourceUrl
|
||||||
} from "../src/application-contract.js";
|
} from "../src/application-contract.js";
|
||||||
@@ -105,6 +106,12 @@ describe("桌面 PDF 网络边界", () => {
|
|||||||
renderUrl
|
renderUrl
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isAllowedPdfRuntimeUrl(
|
||||||
|
"mdpdf://font-pack/official-cjk/1.0.0/serif/web?v=abc",
|
||||||
|
renderUrl
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("拒绝外部网络资源", () => {
|
it("拒绝外部网络资源", () => {
|
||||||
@@ -143,4 +150,22 @@ describe("桌面应用服务 IPC 载荷", () => {
|
|||||||
parseThemeResourceUrl("mdpdf://bundle/index.html")
|
parseThemeResourceUrl("mdpdf://bundle/index.html")
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("只接受字体包 Web 资源地址", () => {
|
||||||
|
expect(
|
||||||
|
parseFontPackResourceUrl(
|
||||||
|
"mdpdf://font-pack/official-cjk/1.0.0/serif-regular/web?v=abc"
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
packId: "official-cjk",
|
||||||
|
packVersion: "1.0.0",
|
||||||
|
faceId: "serif-regular",
|
||||||
|
kind: "web"
|
||||||
|
});
|
||||||
|
expect(() =>
|
||||||
|
parseFontPackResourceUrl(
|
||||||
|
"mdpdf://font-pack/official-cjk/1.0.0/serif-regular/docx"
|
||||||
|
)
|
||||||
|
).toThrow("字体包资源地址无效");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+43
-2
@@ -17,7 +17,8 @@ import {
|
|||||||
DocxThemeTokenService,
|
DocxThemeTokenService,
|
||||||
createApplicationService,
|
createApplicationService,
|
||||||
readDocxExportRuntimeLimits,
|
readDocxExportRuntimeLimits,
|
||||||
type ApplicationService
|
type ApplicationService,
|
||||||
|
type ApplicationServiceOptions
|
||||||
} from "@md-to-pdf/application";
|
} from "@md-to-pdf/application";
|
||||||
import {
|
import {
|
||||||
PandocDocxConverter,
|
PandocDocxConverter,
|
||||||
@@ -88,6 +89,7 @@ export interface BuildAppOptions {
|
|||||||
"getCapability" | "generate" | "close"
|
"getCapability" | "generate" | "close"
|
||||||
>;
|
>;
|
||||||
docxMediaAdapter?: ServerDocxMediaCaptureAdapter;
|
docxMediaAdapter?: ServerDocxMediaCaptureAdapter;
|
||||||
|
fontPacks?: NonNullable<ApplicationServiceOptions["fontPacks"]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function milliseconds(value: number) {
|
function milliseconds(value: number) {
|
||||||
@@ -161,7 +163,8 @@ export function buildApp(options: BuildAppOptions = {}) {
|
|||||||
bundledRoot: resolve(projectRoot, "themes"),
|
bundledRoot: resolve(projectRoot, "themes"),
|
||||||
localRoot:
|
localRoot:
|
||||||
options.localThemeRoot ?? resolve(projectRoot, ".local", "themes"),
|
options.localThemeRoot ?? resolve(projectRoot, ".local", "themes"),
|
||||||
onWarning: (message) => app.log.warn(message)
|
onWarning: (message) => app.log.warn(message),
|
||||||
|
...(options.fontPacks ? { fontPacks: options.fontPacks } : {})
|
||||||
});
|
});
|
||||||
const pdfGenerator =
|
const pdfGenerator =
|
||||||
options.pdfGenerator ?? createPdfGenerator();
|
options.pdfGenerator ?? createPdfGenerator();
|
||||||
@@ -301,6 +304,44 @@ export function buildApp(options: BuildAppOptions = {}) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get<{
|
||||||
|
Params: {
|
||||||
|
packId: string;
|
||||||
|
packVersion: string;
|
||||||
|
faceId: string;
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
"/api/font-packs/:packId/:packVersion/:faceId/web",
|
||||||
|
async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const asset = await applicationService.getFontPackAsset(
|
||||||
|
request.params.packId,
|
||||||
|
request.params.packVersion,
|
||||||
|
request.params.faceId,
|
||||||
|
"web"
|
||||||
|
);
|
||||||
|
if (!asset) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
error: "FONT_PACK_ASSET_NOT_FOUND",
|
||||||
|
message: "未找到指定字体包资源"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return reply
|
||||||
|
.header("content-type", asset.contentType)
|
||||||
|
.header("cache-control", "public, max-age=31536000, immutable")
|
||||||
|
.header("content-security-policy", "default-src 'none'; sandbox")
|
||||||
|
.header("x-content-type-options", "nosniff")
|
||||||
|
.header("etag", `\"${asset.sha256}\"`)
|
||||||
|
.send(asset.content);
|
||||||
|
} catch {
|
||||||
|
return reply.code(404).send({
|
||||||
|
error: "FONT_PACK_ASSET_NOT_FOUND",
|
||||||
|
message: "未找到指定字体包资源"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
app.post<{ Body: RenderRequestBody }>(
|
app.post<{ Body: RenderRequestBody }>(
|
||||||
"/api/render",
|
"/api/render",
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
|||||||
@@ -2,9 +2,18 @@ import { buildApp } from "./app.js";
|
|||||||
|
|
||||||
const port = Number.parseInt(process.env.PORT ?? "3001", 10);
|
const port = Number.parseInt(process.env.PORT ?? "3001", 10);
|
||||||
const host = process.env.HOST ?? "0.0.0.0";
|
const host = process.env.HOST ?? "0.0.0.0";
|
||||||
|
const fontPackRoot = process.env.FONT_PACK_ROOT?.trim();
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
prewarmPdfBrowser: true,
|
prewarmPdfBrowser: true,
|
||||||
prewarmDocxRuntime: true
|
prewarmDocxRuntime: true,
|
||||||
|
...(fontPackRoot
|
||||||
|
? {
|
||||||
|
fontPacks: {
|
||||||
|
roots: [fontPackRoot],
|
||||||
|
appVersion: process.env.APP_VERSION ?? "0.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {})
|
||||||
});
|
});
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { FastifyInstance } from "fastify";
|
|||||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import { buildApp } from "../src/app.js";
|
import { buildApp } from "../src/app.js";
|
||||||
import {
|
import {
|
||||||
createPdfContentDisposition,
|
createPdfContentDisposition,
|
||||||
@@ -39,6 +40,61 @@ function localThemeManifest(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sha256(content: Uint8Array | string) {
|
||||||
|
return createHash("sha256").update(content).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createServerFontPack(root: string) {
|
||||||
|
const directory = join(root, "official-cjk", "1.0.0");
|
||||||
|
const web = Buffer.from([31, 32, 33, 34]);
|
||||||
|
const docx = Buffer.from([41, 42, 43, 44, 45]);
|
||||||
|
const license = "SIL Open Font License 1.1";
|
||||||
|
await mkdir(join(directory, "fonts"), { recursive: true });
|
||||||
|
await writeFile(join(directory, "fonts", "serif.woff2"), web);
|
||||||
|
await writeFile(join(directory, "fonts", "serif.ttf"), docx);
|
||||||
|
await writeFile(join(directory, "OFL.txt"), license, "utf8");
|
||||||
|
await writeFile(
|
||||||
|
join(directory, "font-pack.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
manifestVersion: 1,
|
||||||
|
id: "official-cjk",
|
||||||
|
version: "1.0.0",
|
||||||
|
name: "政务中文字体包",
|
||||||
|
description: "Server 字体包资源测试",
|
||||||
|
license: "OFL-1.1",
|
||||||
|
licenseFile: "OFL.txt",
|
||||||
|
licenseSha256: sha256(license),
|
||||||
|
licenseBytes: Buffer.byteLength(license),
|
||||||
|
compatibility: {
|
||||||
|
minimumAppVersion: "0.6.0",
|
||||||
|
maximumAppVersionExclusive: "0.7.0"
|
||||||
|
},
|
||||||
|
faces: [
|
||||||
|
{
|
||||||
|
id: "serif-regular",
|
||||||
|
targets: ["FandolSong", "Mdpdf Fandol Song"],
|
||||||
|
weight: 400,
|
||||||
|
style: "normal",
|
||||||
|
web: {
|
||||||
|
path: "fonts/serif.woff2",
|
||||||
|
format: "woff2",
|
||||||
|
bytes: web.byteLength,
|
||||||
|
sha256: sha256(web)
|
||||||
|
},
|
||||||
|
docx: {
|
||||||
|
path: "fonts/serif.ttf",
|
||||||
|
format: "truetype",
|
||||||
|
bytes: docx.byteLength,
|
||||||
|
sha256: sha256(docx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
return { web };
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await app?.close();
|
await app?.close();
|
||||||
if (temporaryDirectory) {
|
if (temporaryDirectory) {
|
||||||
@@ -136,6 +192,43 @@ describe("预览 API", () => {
|
|||||||
expect(response.body).toContain("#write");
|
expect(response.body).toContain("#write");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("为主题 CSS 提供经过注册器验证的字体包 Web 资源", async () => {
|
||||||
|
temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-font-pack-"));
|
||||||
|
const fontPack = await createServerFontPack(temporaryDirectory);
|
||||||
|
app = buildApp({
|
||||||
|
logger: false,
|
||||||
|
fontPacks: {
|
||||||
|
roots: [temporaryDirectory],
|
||||||
|
appVersion: "0.6.0"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cssResponse = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: "/api/themes/gov-red-standard/css"
|
||||||
|
});
|
||||||
|
expect(cssResponse.statusCode).toBe(200);
|
||||||
|
expect(cssResponse.body).toContain("md-to-pdf-font-packs:official-cjk@1.0.0");
|
||||||
|
expect(cssResponse.body).toContain(
|
||||||
|
"/api/font-packs/official-cjk/1.0.0/serif-regular/web?v="
|
||||||
|
);
|
||||||
|
|
||||||
|
const assetResponse = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/font-packs/official-cjk/1.0.0/serif-regular/web?v=${sha256(fontPack.web)}`
|
||||||
|
});
|
||||||
|
expect(assetResponse.statusCode).toBe(200);
|
||||||
|
expect(assetResponse.headers["content-type"]).toContain("font/woff2");
|
||||||
|
expect(assetResponse.headers["cache-control"]).toContain("immutable");
|
||||||
|
expect(assetResponse.rawPayload).toEqual(fontPack.web);
|
||||||
|
|
||||||
|
const missingResponse = await app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: "/api/font-packs/official-cjk/2.0.0/serif-regular/web"
|
||||||
|
});
|
||||||
|
expect(missingResponse.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
it("发现本地主题并安全提供相对字体资源", async () => {
|
it("发现本地主题并安全提供相对字体资源", async () => {
|
||||||
temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-theme-"));
|
temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-theme-"));
|
||||||
const themeDirectory = join(temporaryDirectory, "local-test");
|
const themeDirectory = join(temporaryDirectory, "local-test");
|
||||||
|
|||||||
+26
-5
@@ -287,6 +287,24 @@ DOCX 静态 TrueType 资源。注册器按根目录顺序和最高兼容版本
|
|||||||
结果,并把字体包指纹纳入缓存和导出诊断。全仓 454 项测试、类型检查和
|
结果,并把字体包指纹纳入缓存和导出诊断。全仓 454 项测试、类型检查和
|
||||||
生产构建通过。
|
生产构建通过。
|
||||||
|
|
||||||
|
阶段 12D-FP2 已将可选字体包接入统一主题与 DOCX 生产链。应用层按主题
|
||||||
|
声明、字体家族或别名、字重和样式解析同一逻辑字体面:Web、Preview 与
|
||||||
|
PDF 在主题 CSS 末尾注入 WOFF2 `@font-face`,DOCX 同时选用对应的静态
|
||||||
|
TrueType 字体,并保留字体包许可、版本和来源诊断;未命中的字体面继续
|
||||||
|
安全降级到主题原有字体。字体包版本、资源 SHA-256 和整体指纹已进入
|
||||||
|
主题令牌、样式快照、动态模板及导出缓存边界,替换资源不会错误复用旧
|
||||||
|
结果。Server 只暴露带 ETag 和不可变缓存头的 WOFF2 资源端点,通过
|
||||||
|
`FONT_PACK_ROOT` 配置字体包根目录;Desktop 使用受限的
|
||||||
|
`mdpdf://font-pack/.../web` 协议,并从应用数据目录发现可选字体包,均不
|
||||||
|
对渲染器暴露 DOCX TTF 原始资源。静态字体单面上限调整为 16 MiB、单次
|
||||||
|
DOCX 字体源总量调整为 48 MiB,与已验证的原生中文 TrueType 资产一致。
|
||||||
|
|
||||||
|
全仓 458 项测试、类型检查、生产构建和 14 套主题真实 Pandoc 矩阵通过;
|
||||||
|
临时字体包验证确认政企红头标准文件可从同一字体包解析两个 WOFF2/TTF
|
||||||
|
字形面,其余字体继续按主题资产降级,最终五个字体源均通过 SFNT 与嵌入
|
||||||
|
权限校验。字体二进制、独立安装包和 Docker 分发尚未进入仓库;下一步
|
||||||
|
FP3 只处理独立 NSIS 字体包、Docker 可选挂载及其安装/升级/卸载门禁。
|
||||||
|
|
||||||
## 2. 已完成
|
## 2. 已完成
|
||||||
|
|
||||||
### 2.1 项目骨架
|
### 2.1 项目骨架
|
||||||
@@ -1172,8 +1190,9 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
|||||||
栅格差异、可配置视觉门限以及 JSON/HTML 验收报告;该包仅用于开发和
|
栅格差异、可配置视觉门限以及 JSON/HTML 验收报告;该包仅用于开发和
|
||||||
发布验收,不进入应用运行时。
|
发布验收,不进入应用运行时。
|
||||||
- `packages/font-pack-registry` 负责可选字体包协议、版本兼容、安全发现、
|
- `packages/font-pack-registry` 负责可选字体包协议、版本兼容、安全发现、
|
||||||
资源哈希与容量门禁、确定性版本选择和逻辑字体面匹配;FP1 尚未将它
|
资源哈希与容量门禁、确定性版本选择和逻辑字体面匹配;应用层已将同一
|
||||||
接入应用服务或发行包。
|
解析结果用于主题 CSS 的 WOFF2 和 DOCX 的静态 TrueType,发行资产与
|
||||||
|
安装逻辑留在 FP3。
|
||||||
- `packages/application/src/docx-export-service.ts` 负责 DOCX capability、
|
- `packages/application/src/docx-export-service.ts` 负责 DOCX capability、
|
||||||
并发排队、总超时、取消、跨端转换编排、错误码和耗时诊断。
|
并发排队、总超时、取消、跨端转换编排、错误码和耗时诊断。
|
||||||
- `apps/desktop/src/desktop-application-controller.ts` 负责 Electron
|
- `apps/desktop/src/desktop-application-controller.ts` 负责 Electron
|
||||||
@@ -1235,9 +1254,11 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
|||||||
字重规范化及通用 SFNT 到 Word 字体元数据映射,并通过 Word/WPS
|
字重规范化及通用 SFNT 到 Word 字体元数据映射,并通过 Word/WPS
|
||||||
原生视觉与可编辑性验收;
|
原生视觉与可编辑性验收;
|
||||||
- 阶段 12D-FP1:已完成可选字体包协议、安全注册器、版本/根目录优先级、
|
- 阶段 12D-FP1:已完成可选字体包协议、安全注册器、版本/根目录优先级、
|
||||||
资源双重校验和字体候选匹配;下一步 FP2 接入 Preview/PDF 与 DOCX
|
资源双重校验和字体候选匹配;
|
||||||
同源字体解析,FP3 再实现独立 NSIS 字体包与 Docker 可选挂载,最终由
|
- 阶段 12D-FP2:已完成 Preview/PDF WOFF2 与 DOCX 静态 TrueType 的
|
||||||
R4 完成 14 套主题深度视觉回归;
|
同源字体解析、跨端受限资源入口、缓存指纹、降级诊断和真实字体包验证;
|
||||||
|
下一步 FP3 实现独立 NSIS 字体包与 Docker 可选挂载,最终由 R4 完成
|
||||||
|
14 套主题深度视觉回归;
|
||||||
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
||||||
|
|
||||||
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
||||||
|
|||||||
Generated
+1
@@ -9969,6 +9969,7 @@
|
|||||||
"@md-to-pdf/core": "0.1.0",
|
"@md-to-pdf/core": "0.1.0",
|
||||||
"@md-to-pdf/docx-engine": "0.1.0",
|
"@md-to-pdf/docx-engine": "0.1.0",
|
||||||
"@md-to-pdf/docx-theme-engine": "0.1.0",
|
"@md-to-pdf/docx-theme-engine": "0.1.0",
|
||||||
|
"@md-to-pdf/font-pack-registry": "0.1.0",
|
||||||
"@md-to-pdf/renderer": "0.1.0"
|
"@md-to-pdf/renderer": "0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+6
-5
@@ -10,12 +10,13 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop",
|
"build": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop",
|
||||||
"build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/web",
|
"build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/web",
|
||||||
"dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n markdown-echarts,core,semantic-document,docx-theme-engine,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:semantic-document\" \"npm:dev:docx-theme-engine\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"",
|
"dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n markdown-echarts,core,semantic-document,docx-theme-engine,font-pack-registry,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:semantic-document\" \"npm:dev:docx-theme-engine\" \"npm:dev:font-pack-registry\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"",
|
||||||
"dev:markdown-echarts": "npm run dev -w @md-to-pdf/markdown-echarts",
|
"dev:markdown-echarts": "npm run dev -w @md-to-pdf/markdown-echarts",
|
||||||
"dev:core": "npm run dev -w @md-to-pdf/core",
|
"dev:core": "npm run dev -w @md-to-pdf/core",
|
||||||
"dev:semantic-document": "npm run dev -w @md-to-pdf/semantic-document",
|
"dev:semantic-document": "npm run dev -w @md-to-pdf/semantic-document",
|
||||||
"dev:docx-theme-engine": "npm run dev -w @md-to-pdf/docx-theme-engine",
|
"dev:docx-theme-engine": "npm run dev -w @md-to-pdf/docx-theme-engine",
|
||||||
|
"dev:font-pack-registry": "tsc -p packages/font-pack-registry/tsconfig.json --watch --preserveWatchOutput",
|
||||||
"dev:docx-engine": "npm run dev -w @md-to-pdf/docx-engine",
|
"dev:docx-engine": "npm run dev -w @md-to-pdf/docx-engine",
|
||||||
"dev:renderer": "npm run dev -w @md-to-pdf/renderer",
|
"dev:renderer": "npm run dev -w @md-to-pdf/renderer",
|
||||||
"dev:application": "npm run dev -w @md-to-pdf/application",
|
"dev:application": "npm run dev -w @md-to-pdf/application",
|
||||||
@@ -23,17 +24,17 @@
|
|||||||
"dev:server": "npm run dev -w @md-to-pdf/server",
|
"dev:server": "npm run dev -w @md-to-pdf/server",
|
||||||
"dev:web": "npm run dev -w @md-to-pdf/web",
|
"dev:web": "npm run dev -w @md-to-pdf/web",
|
||||||
"dev:desktop": "npm run dev -w @md-to-pdf/desktop",
|
"dev:desktop": "npm run dev -w @md-to-pdf/desktop",
|
||||||
"desktop:dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n web,desktop \"npm:dev:web\" \"npm:dev:desktop\"",
|
"desktop:dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n web,desktop \"npm:dev:web\" \"npm:dev:desktop\"",
|
||||||
"desktop:package": "npm run package -w @md-to-pdf/desktop",
|
"desktop:package": "npm run package -w @md-to-pdf/desktop",
|
||||||
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
||||||
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-matrix": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine",
|
"verify:docx-matrix": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-themes": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/server && npm run verify:themes -w @md-to-pdf/docx-engine",
|
"verify:docx-themes": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/server && npm run verify:themes -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-theme-styles": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/desktop",
|
"verify:docx-theme-styles": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/server && npm run verify:docx-theme-styles -w @md-to-pdf/desktop",
|
||||||
"verify:docx-acceptance": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop && npm run verify:pandoc -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine && npm run verify:docx-http -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop -- desktop-docx-save.test.ts",
|
"verify:docx-acceptance": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop && npm run verify:pandoc -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine && npm run verify:docx-http -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop -- desktop-docx-save.test.ts",
|
||||||
"verify:docx-http": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-http -w @md-to-pdf/server",
|
"verify:docx-http": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-http -w @md-to-pdf/server",
|
||||||
"test": "npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
"test": "npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
||||||
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/document-visual-diff && npm run typecheck -w @md-to-pdf/font-pack-registry && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/document-visual-diff && npm run typecheck -w @md-to-pdf/font-pack-registry && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@md-to-pdf/core": "0.1.0",
|
"@md-to-pdf/core": "0.1.0",
|
||||||
"@md-to-pdf/docx-engine": "0.1.0",
|
"@md-to-pdf/docx-engine": "0.1.0",
|
||||||
"@md-to-pdf/docx-theme-engine": "0.1.0",
|
"@md-to-pdf/docx-theme-engine": "0.1.0",
|
||||||
|
"@md-to-pdf/font-pack-registry": "0.1.0",
|
||||||
"@md-to-pdf/renderer": "0.1.0"
|
"@md-to-pdf/renderer": "0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import type { DocxFontSource } from "@md-to-pdf/docx-engine";
|
import type { DocxFontSource } from "@md-to-pdf/docx-engine";
|
||||||
import {
|
import {
|
||||||
createThemeRegistry,
|
createThemeRegistry,
|
||||||
|
type ThemeFontPackStatus,
|
||||||
type ThemeRegistryOptions
|
type ThemeRegistryOptions
|
||||||
} from "./theme-registry.js";
|
} from "./theme-registry.js";
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +51,7 @@ export interface PreparedDocxExport {
|
|||||||
source: "bundled" | "local";
|
source: "bundled" | "local";
|
||||||
css: string;
|
css: string;
|
||||||
fonts: DocxFontSource[];
|
fonts: DocxFontSource[];
|
||||||
|
fontPack?: ThemeFontPackStatus;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +190,11 @@ export function createApplicationService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [document, themeCss, themeFonts] = await Promise.all([
|
const [document, themeCss, themeFonts, fontPack] = await Promise.all([
|
||||||
render(parsed.data, context),
|
render(parsed.data, context),
|
||||||
themes.getCss(theme.manifest.id),
|
themes.getCss(theme.manifest.id),
|
||||||
themes.getDocxFonts(theme.manifest.id)
|
themes.getDocxFonts(theme.manifest.id),
|
||||||
|
themes.getFontPackStatus(theme.manifest.id)
|
||||||
]);
|
]);
|
||||||
if (themeCss === undefined || themeFonts === undefined) {
|
if (themeCss === undefined || themeFonts === undefined) {
|
||||||
throw new ApplicationRequestError(
|
throw new ApplicationRequestError(
|
||||||
@@ -208,7 +211,8 @@ export function createApplicationService(
|
|||||||
manifest: theme.manifest,
|
manifest: theme.manifest,
|
||||||
source: theme.source,
|
source: theme.source,
|
||||||
css: themeCss,
|
css: themeCss,
|
||||||
fonts: themeFonts
|
fonts: themeFonts,
|
||||||
|
...(fontPack ? { fontPack } : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -216,6 +220,7 @@ export function createApplicationService(
|
|||||||
return {
|
return {
|
||||||
getThemeAsset: themes.getAsset,
|
getThemeAsset: themes.getAsset,
|
||||||
getThemeCss: themes.getCss,
|
getThemeCss: themes.getCss,
|
||||||
|
getFontPackAsset: themes.getFontPackAsset,
|
||||||
invalidateThemes: themes.invalidate,
|
invalidateThemes: themes.invalidate,
|
||||||
listThemes,
|
listThemes,
|
||||||
prepareDocxExport,
|
prepareDocxExport,
|
||||||
|
|||||||
@@ -407,7 +407,10 @@ export class DocxExportService {
|
|||||||
diagnostics: {
|
diagnostics: {
|
||||||
warnings: [
|
warnings: [
|
||||||
...media.warnings,
|
...media.warnings,
|
||||||
...themeTokens.warnings
|
...themeTokens.warnings,
|
||||||
|
...(prepared.theme.fontPack?.diagnostics ?? [])
|
||||||
|
.filter((diagnostic) => diagnostic.severity !== "info")
|
||||||
|
.map((diagnostic) => diagnostic.message)
|
||||||
],
|
],
|
||||||
echartsErrors: media.echartsErrors,
|
echartsErrors: media.echartsErrors,
|
||||||
mermaidErrors: media.mermaidErrors
|
mermaidErrors: media.mermaidErrors
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export {
|
|||||||
} from "./docx-export-service.js";
|
} from "./docx-export-service.js";
|
||||||
export {
|
export {
|
||||||
createThemeRegistry,
|
createThemeRegistry,
|
||||||
|
type ThemeFontPackStatus,
|
||||||
type ThemeRecord,
|
type ThemeRecord,
|
||||||
type ThemeRegistryOptions
|
type ThemeRegistryOptions
|
||||||
} from "./theme-registry.js";
|
} from "./theme-registry.js";
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ import {
|
|||||||
type DocxFontFace,
|
type DocxFontFace,
|
||||||
type ThemeManifest
|
type ThemeManifest
|
||||||
} from "@md-to-pdf/core";
|
} from "@md-to-pdf/core";
|
||||||
|
import {
|
||||||
|
discoverFontPacks,
|
||||||
|
readInstalledFontPackAsset,
|
||||||
|
resolveFontPackFaces,
|
||||||
|
type FontPackDiagnostic,
|
||||||
|
type FontPackFaceRequest,
|
||||||
|
type FontPackRegistryResult,
|
||||||
|
type ResolvedFontPackFace
|
||||||
|
} from "@md-to-pdf/font-pack-registry";
|
||||||
|
|
||||||
export interface ThemeRecord {
|
export interface ThemeRecord {
|
||||||
manifest: ThemeManifest;
|
manifest: ThemeManifest;
|
||||||
@@ -35,6 +44,34 @@ export interface ThemeRegistryOptions {
|
|||||||
onWarning?: (message: string) => void;
|
onWarning?: (message: string) => void;
|
||||||
cacheTtlMs?: number;
|
cacheTtlMs?: number;
|
||||||
createAssetUrl?: (themeId: string, assetPath: string) => string;
|
createAssetUrl?: (themeId: string, assetPath: string) => string;
|
||||||
|
fontPacks?: {
|
||||||
|
roots: readonly string[];
|
||||||
|
appVersion: string;
|
||||||
|
cacheTtlMs?: number;
|
||||||
|
preferredPackIds?: readonly string[];
|
||||||
|
createAssetUrl?: (
|
||||||
|
packId: string,
|
||||||
|
packVersion: string,
|
||||||
|
faceId: string,
|
||||||
|
kind: "web" | "docx",
|
||||||
|
sha256: string
|
||||||
|
) => string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeFontPackStatus {
|
||||||
|
registryFingerprint: string;
|
||||||
|
appliedFaces: Array<{
|
||||||
|
family: string;
|
||||||
|
weight: number;
|
||||||
|
style: "normal" | "italic";
|
||||||
|
packId: string;
|
||||||
|
packVersion: string;
|
||||||
|
faceId: string;
|
||||||
|
webSha256: string;
|
||||||
|
docxSha256: string;
|
||||||
|
}>;
|
||||||
|
diagnostics: FontPackDiagnostic[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetContentTypes: Record<string, string> = {
|
const assetContentTypes: Record<string, string> = {
|
||||||
@@ -322,6 +359,148 @@ export function createThemeRegistry(options: ThemeRegistryOptions) {
|
|||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
let sharedFontCssPromise: Promise<string> | undefined;
|
let sharedFontCssPromise: Promise<string> | undefined;
|
||||||
|
let cachedFontPacks:
|
||||||
|
| {
|
||||||
|
expiresAt: number;
|
||||||
|
promise: Promise<FontPackRegistryResult>;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
const reportedFontPackDiagnostics = new Set<string>();
|
||||||
|
const fontPackCacheTtlMs =
|
||||||
|
options.fontPacks?.cacheTtlMs ?? 5 * 60_000;
|
||||||
|
if (!Number.isFinite(fontPackCacheTtlMs) || fontPackCacheTtlMs < 0) {
|
||||||
|
throw new Error("字体包缓存有效期必须是非负有限数值");
|
||||||
|
}
|
||||||
|
const createFontPackAssetUrl =
|
||||||
|
options.fontPacks?.createAssetUrl ??
|
||||||
|
((
|
||||||
|
packId: string,
|
||||||
|
packVersion: string,
|
||||||
|
faceId: string,
|
||||||
|
kind: "web" | "docx",
|
||||||
|
fingerprint: string
|
||||||
|
) =>
|
||||||
|
`/api/font-packs/${encodeURIComponent(
|
||||||
|
packId
|
||||||
|
)}/${encodeURIComponent(packVersion)}/${encodeURIComponent(
|
||||||
|
faceId
|
||||||
|
)}/${kind}?v=${fingerprint}`);
|
||||||
|
|
||||||
|
function listFontPacks() {
|
||||||
|
if (!options.fontPacks) {
|
||||||
|
return Promise.resolve<FontPackRegistryResult>({
|
||||||
|
packs: [],
|
||||||
|
diagnostics: [],
|
||||||
|
fingerprint: "0".repeat(64)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
if (cachedFontPacks && now < cachedFontPacks.expiresAt) {
|
||||||
|
return cachedFontPacks.promise;
|
||||||
|
}
|
||||||
|
const promise = discoverFontPacks({
|
||||||
|
roots: options.fontPacks.roots,
|
||||||
|
appVersion: options.fontPacks.appVersion
|
||||||
|
}).then((result) => {
|
||||||
|
for (const item of result.diagnostics) {
|
||||||
|
if (item.severity === "info") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = [
|
||||||
|
item.code,
|
||||||
|
item.root ?? "",
|
||||||
|
item.packId ?? "",
|
||||||
|
item.packVersion ?? "",
|
||||||
|
item.message
|
||||||
|
].join(":");
|
||||||
|
if (!reportedFontPackDiagnostics.has(key)) {
|
||||||
|
reportedFontPackDiagnostics.add(key);
|
||||||
|
options.onWarning?.(`字体包 ${item.code}:${item.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
cachedFontPacks = {
|
||||||
|
expiresAt: now + fontPackCacheTtlMs,
|
||||||
|
promise
|
||||||
|
};
|
||||||
|
void promise.catch(() => {
|
||||||
|
if (cachedFontPacks?.promise === promise) {
|
||||||
|
cachedFontPacks = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveThemeFontPacks(theme: ThemeRecord) {
|
||||||
|
const registry = await listFontPacks();
|
||||||
|
const faces = theme.manifest.docxFonts?.faces ?? [];
|
||||||
|
const requests: FontPackFaceRequest[] = faces.map((face) => ({
|
||||||
|
family: face.family,
|
||||||
|
aliases: face.aliases,
|
||||||
|
weight: face.weight,
|
||||||
|
style: face.style
|
||||||
|
}));
|
||||||
|
const resolution = resolveFontPackFaces(requests, registry.packs, {
|
||||||
|
preferredPackIds: options.fontPacks?.preferredPackIds ?? [],
|
||||||
|
reportMissing: registry.packs.length > 0
|
||||||
|
});
|
||||||
|
const matches = new Map<DocxFontFace, ResolvedFontPackFace>();
|
||||||
|
for (const match of resolution.resolved) {
|
||||||
|
const index = requests.indexOf(match.request);
|
||||||
|
const face = index < 0 ? undefined : faces[index];
|
||||||
|
if (face) {
|
||||||
|
matches.set(face, match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { registry, resolution, matches };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFontPackCss(
|
||||||
|
matches: ReadonlyMap<DocxFontFace, ResolvedFontPackFace>
|
||||||
|
) {
|
||||||
|
if (matches.size === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const fingerprints = new Set<string>();
|
||||||
|
const rules: string[] = [];
|
||||||
|
const emitted = new Set<string>();
|
||||||
|
for (const [request, match] of matches) {
|
||||||
|
fingerprints.add(
|
||||||
|
`${match.pack.id}@${match.pack.version}:${match.pack.fingerprint}`
|
||||||
|
);
|
||||||
|
for (const target of match.face.targets) {
|
||||||
|
const key = `${target.toLocaleLowerCase("en-US")}:${
|
||||||
|
request.weight
|
||||||
|
}:${request.style}`;
|
||||||
|
if (emitted.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
emitted.add(key);
|
||||||
|
rules.push(
|
||||||
|
[
|
||||||
|
"@font-face {",
|
||||||
|
` font-family: "${target}";`,
|
||||||
|
` src: url("${createFontPackAssetUrl(
|
||||||
|
match.pack.id,
|
||||||
|
match.pack.version,
|
||||||
|
match.face.id,
|
||||||
|
"web",
|
||||||
|
match.face.web.sha256
|
||||||
|
)}") format("woff2");`,
|
||||||
|
` font-weight: ${request.weight};`,
|
||||||
|
` font-style: ${request.style};`,
|
||||||
|
" font-display: block;",
|
||||||
|
"}"
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`/* md-to-pdf-font-packs:${[...fingerprints].sort().join(",")} */`,
|
||||||
|
...rules
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function loadSharedFontCss() {
|
function loadSharedFontCss() {
|
||||||
if (!sharedFontCssPromise) {
|
if (!sharedFontCssPromise) {
|
||||||
@@ -408,6 +587,8 @@ export function createThemeRegistry(options: ThemeRegistryOptions) {
|
|||||||
function invalidate() {
|
function invalidate() {
|
||||||
cachedThemes = undefined;
|
cachedThemes = undefined;
|
||||||
sharedFontCssPromise = undefined;
|
sharedFontCssPromise = undefined;
|
||||||
|
cachedFontPacks = undefined;
|
||||||
|
reportedFontPackDiagnostics.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(themeId: string) {
|
async function get(themeId: string) {
|
||||||
@@ -427,15 +608,67 @@ export function createThemeRegistry(options: ThemeRegistryOptions) {
|
|||||||
? [theme.manifest.print]
|
? [theme.manifest.print]
|
||||||
: [])
|
: [])
|
||||||
];
|
];
|
||||||
const css = await Promise.all(
|
const [css, fontPacks] = await Promise.all([
|
||||||
|
Promise.all(
|
||||||
cssFiles.map((path) =>
|
cssFiles.map((path) =>
|
||||||
loadThemeCssFile(theme, path, createAssetUrl)
|
loadThemeCssFile(theme, path, createAssetUrl)
|
||||||
)
|
)
|
||||||
);
|
),
|
||||||
|
resolveThemeFontPacks(theme)
|
||||||
|
]);
|
||||||
if (theme.source === "bundled") {
|
if (theme.source === "bundled") {
|
||||||
css.unshift(await loadSharedFontCss());
|
css.unshift(await loadSharedFontCss());
|
||||||
}
|
}
|
||||||
return css.filter(Boolean).join("\n");
|
return [...css, createFontPackCss(fontPacks.matches)]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFontPackAsset(
|
||||||
|
packId: string,
|
||||||
|
packVersion: string,
|
||||||
|
faceId: string,
|
||||||
|
kind: "web" | "docx"
|
||||||
|
) {
|
||||||
|
const registry = await listFontPacks();
|
||||||
|
const pack = registry.packs.find(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.id === packId && candidate.version === packVersion
|
||||||
|
);
|
||||||
|
const face = pack?.faces.find((candidate) => candidate.id === faceId);
|
||||||
|
if (!face) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const asset = face[kind];
|
||||||
|
return {
|
||||||
|
contentType: kind === "web" ? "font/woff2" : "font/ttf",
|
||||||
|
content: await readInstalledFontPackAsset(asset),
|
||||||
|
sha256: asset.sha256
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFontPackStatus(
|
||||||
|
themeId: string
|
||||||
|
): Promise<ThemeFontPackStatus | undefined> {
|
||||||
|
const theme = await get(themeId);
|
||||||
|
if (!theme) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { registry, resolution } = await resolveThemeFontPacks(theme);
|
||||||
|
return {
|
||||||
|
registryFingerprint: registry.fingerprint,
|
||||||
|
appliedFaces: resolution.resolved.map((match) => ({
|
||||||
|
family: match.request.family,
|
||||||
|
weight: match.request.weight,
|
||||||
|
style: match.request.style,
|
||||||
|
packId: match.pack.id,
|
||||||
|
packVersion: match.pack.version,
|
||||||
|
faceId: match.face.id,
|
||||||
|
webSha256: match.face.web.sha256,
|
||||||
|
docxSha256: match.face.docx.sha256
|
||||||
|
})),
|
||||||
|
diagnostics: [...registry.diagnostics, ...resolution.diagnostics]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAsset(themeId: string, assetPath: string) {
|
async function getAsset(themeId: string, assetPath: string) {
|
||||||
@@ -479,8 +712,21 @@ export function createThemeRegistry(options: ThemeRegistryOptions) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const faces = theme.manifest.docxFonts?.faces ?? [];
|
const faces = theme.manifest.docxFonts?.faces ?? [];
|
||||||
|
const fontPacks = await resolveThemeFontPacks(theme);
|
||||||
const fonts = await Promise.all(
|
const fonts = await Promise.all(
|
||||||
faces.map(async (face) => {
|
faces.map(async (face) => {
|
||||||
|
const matched = fontPacks.matches.get(face);
|
||||||
|
if (matched) {
|
||||||
|
return {
|
||||||
|
...face,
|
||||||
|
aliases: [
|
||||||
|
...new Set([...face.aliases, ...matched.face.targets])
|
||||||
|
],
|
||||||
|
source: `font-pack:${matched.pack.id}/${matched.pack.version}/${matched.face.id}/docx`,
|
||||||
|
license: matched.pack.license,
|
||||||
|
content: await readInstalledFontPackAsset(matched.face.docx)
|
||||||
|
};
|
||||||
|
}
|
||||||
const shared = face.source.startsWith(
|
const shared = face.source.startsWith(
|
||||||
sharedThemeAssetScheme
|
sharedThemeAssetScheme
|
||||||
);
|
);
|
||||||
@@ -519,6 +765,8 @@ export function createThemeRegistry(options: ThemeRegistryOptions) {
|
|||||||
getAsset,
|
getAsset,
|
||||||
getCss,
|
getCss,
|
||||||
getDocxFonts,
|
getDocxFonts,
|
||||||
|
getFontPackAsset,
|
||||||
|
getFontPackStatus,
|
||||||
invalidate,
|
invalidate,
|
||||||
list
|
list
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
ApplicationRequestError,
|
ApplicationRequestError,
|
||||||
createApplicationService
|
createApplicationService
|
||||||
@@ -126,6 +127,64 @@ async function createThemeFixture() {
|
|||||||
return { bundledRoot, localRoot };
|
return { bundledRoot, localRoot };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sha256(content: Uint8Array | string) {
|
||||||
|
return createHash("sha256").update(content).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFontPackFixture(
|
||||||
|
options: { corruptWebHash?: boolean; minimumAppVersion?: string } = {}
|
||||||
|
) {
|
||||||
|
const root = join(temporaryDirectory!, "font-packs");
|
||||||
|
const directory = join(root, "test-font-pack", "1.0.0");
|
||||||
|
const web = Buffer.from([10, 11, 12, 13, 14]);
|
||||||
|
const docx = Buffer.from([20, 21, 22, 23, 24, 25]);
|
||||||
|
const license = "SIL Open Font License 1.1";
|
||||||
|
await mkdir(join(directory, "fonts"), { recursive: true });
|
||||||
|
await writeFile(join(directory, "fonts", "test.woff2"), web);
|
||||||
|
await writeFile(join(directory, "fonts", "test.ttf"), docx);
|
||||||
|
await writeFile(join(directory, "OFL.txt"), license, "utf8");
|
||||||
|
await writeFile(
|
||||||
|
join(directory, "font-pack.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
manifestVersion: 1,
|
||||||
|
id: "test-font-pack",
|
||||||
|
version: "1.0.0",
|
||||||
|
name: "测试字体包",
|
||||||
|
description: "应用服务字体包测试",
|
||||||
|
license: "OFL-1.1",
|
||||||
|
licenseFile: "OFL.txt",
|
||||||
|
licenseSha256: sha256(license),
|
||||||
|
licenseBytes: Buffer.byteLength(license),
|
||||||
|
compatibility: {
|
||||||
|
minimumAppVersion: options.minimumAppVersion ?? "0.6.0",
|
||||||
|
maximumAppVersionExclusive: "0.7.0"
|
||||||
|
},
|
||||||
|
faces: [
|
||||||
|
{
|
||||||
|
id: "test-regular",
|
||||||
|
targets: ["Test"],
|
||||||
|
weight: 400,
|
||||||
|
style: "normal",
|
||||||
|
web: {
|
||||||
|
path: "fonts/test.woff2",
|
||||||
|
format: "woff2",
|
||||||
|
bytes: web.byteLength,
|
||||||
|
sha256: options.corruptWebHash ? "0".repeat(64) : sha256(web)
|
||||||
|
},
|
||||||
|
docx: {
|
||||||
|
path: "fonts/test.ttf",
|
||||||
|
format: "truetype",
|
||||||
|
bytes: docx.byteLength,
|
||||||
|
sha256: sha256(docx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
return { root, web, docx };
|
||||||
|
}
|
||||||
|
|
||||||
describe("共享应用服务", () => {
|
describe("共享应用服务", () => {
|
||||||
it("渲染安全 Markdown 文档", async () => {
|
it("渲染安全 Markdown 文档", async () => {
|
||||||
const roots = await createThemeFixture();
|
const roots = await createThemeFixture();
|
||||||
@@ -350,6 +409,98 @@ describe("共享应用服务", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("为主题 CSS 与 DOCX 选择同一字体包的双资源", async () => {
|
||||||
|
const roots = await createThemeFixture();
|
||||||
|
const fontPack = await createFontPackFixture();
|
||||||
|
const service = createApplicationService({
|
||||||
|
...roots,
|
||||||
|
fontPacks: {
|
||||||
|
roots: [fontPack.root],
|
||||||
|
appVersion: "0.6.0",
|
||||||
|
createAssetUrl: (packId, version, faceId, kind, hash) =>
|
||||||
|
`mdpdf://font-pack/${packId}/${version}/${faceId}/${kind}?v=${hash}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const css = await service.getThemeCss("test-theme");
|
||||||
|
expect(css).toContain("md-to-pdf-font-packs:test-font-pack@1.0.0");
|
||||||
|
expect(css).toContain(
|
||||||
|
"mdpdf://font-pack/test-font-pack/1.0.0/test-regular/web?v="
|
||||||
|
);
|
||||||
|
const prepared = await service.prepareDocxExport({
|
||||||
|
markdown: "# 字体包",
|
||||||
|
fileName: "字体包.md",
|
||||||
|
exportConfig: {
|
||||||
|
...defaultExportConfig,
|
||||||
|
themeId: "test-theme"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(prepared.theme.fonts[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
family: "Test",
|
||||||
|
source: "font-pack:test-font-pack/1.0.0/test-regular/docx",
|
||||||
|
license: "OFL-1.1",
|
||||||
|
content: new Uint8Array(fontPack.docx)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(prepared.theme.fontPack?.appliedFaces).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
family: "Test",
|
||||||
|
packId: "test-font-pack",
|
||||||
|
faceId: "test-regular"
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
await expect(
|
||||||
|
service.getFontPackAsset(
|
||||||
|
"test-font-pack",
|
||||||
|
"1.0.0",
|
||||||
|
"test-regular",
|
||||||
|
"web"
|
||||||
|
)
|
||||||
|
).resolves.toEqual({
|
||||||
|
contentType: "font/woff2",
|
||||||
|
content: new Uint8Array(fontPack.web),
|
||||||
|
sha256: sha256(fontPack.web)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("字体包损坏时记录诊断并回退到主题字体", async () => {
|
||||||
|
const roots = await createThemeFixture();
|
||||||
|
const fontPack = await createFontPackFixture({ corruptWebHash: true });
|
||||||
|
const onWarning = vi.fn();
|
||||||
|
const service = createApplicationService({
|
||||||
|
...roots,
|
||||||
|
onWarning,
|
||||||
|
fontPacks: {
|
||||||
|
roots: [fontPack.root],
|
||||||
|
appVersion: "0.6.0"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const prepared = await service.prepareDocxExport({
|
||||||
|
markdown: "# 回退",
|
||||||
|
fileName: "回退.md",
|
||||||
|
exportConfig: {
|
||||||
|
...defaultExportConfig,
|
||||||
|
themeId: "test-theme"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(prepared.theme.css).not.toContain("md-to-pdf-font-packs:");
|
||||||
|
expect(prepared.theme.fonts[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
family: "Test",
|
||||||
|
source: "fonts/test.woff2",
|
||||||
|
content: Buffer.from([0, 1, 2, 3])
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(prepared.theme.fontPack?.diagnostics[0]?.code).toBe(
|
||||||
|
"MANIFEST_INVALID"
|
||||||
|
);
|
||||||
|
expect(onWarning).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("字体包 MANIFEST_INVALID")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("以稳定错误码拒绝非法 DOCX 配置和缺失主题", async () => {
|
it("以稳定错误码拒绝非法 DOCX 配置和缺失主题", async () => {
|
||||||
const roots = await createThemeFixture();
|
const roots = await createThemeFixture();
|
||||||
const service = createApplicationService(roots);
|
const service = createApplicationService(roots);
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
|
|
||||||
export const THEME_MANIFEST_VERSION = 1;
|
export const THEME_MANIFEST_VERSION = 1;
|
||||||
export const MAXIMUM_DOCX_FONT_FACE_COUNT = 16;
|
export const MAXIMUM_DOCX_FONT_FACE_COUNT = 16;
|
||||||
export const MAXIMUM_DOCX_FONT_SOURCE_BYTES = 8 * 1024 * 1024;
|
export const MAXIMUM_DOCX_FONT_SOURCE_BYTES = 16 * 1024 * 1024;
|
||||||
export const MAXIMUM_DOCX_FONT_TOTAL_SOURCE_BYTES =
|
export const MAXIMUM_DOCX_FONT_TOTAL_SOURCE_BYTES =
|
||||||
32 * 1024 * 1024;
|
48 * 1024 * 1024;
|
||||||
|
|
||||||
const docxFontNameSchema = z
|
const docxFontNameSchema = z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
Reference in New Issue
Block a user