fix: 增强DOCX字体实际使用门禁
This commit is contained in:
@@ -101,6 +101,14 @@ export function readBundledThemeMatrixDefinitions(themesDirectory) {
|
||||
compatibleProfiles: Array.isArray(manifest.compatibleProfiles)
|
||||
? [...manifest.compatibleProfiles]
|
||||
: [],
|
||||
docxFontFaces: Array.isArray(manifest.docxFonts?.faces)
|
||||
? manifest.docxFonts.faces.map((face) => ({
|
||||
family: face.family,
|
||||
aliases: Array.isArray(face.aliases) ? [...face.aliases] : [],
|
||||
weight: face.weight ?? 400,
|
||||
style: face.style ?? "normal"
|
||||
}))
|
||||
: [],
|
||||
themeDefaultMargins: manifest.pageDefaults?.margins
|
||||
? structuredClone(manifest.pageDefaults.margins)
|
||||
: undefined
|
||||
@@ -109,6 +117,68 @@ export function readBundledThemeMatrixDefinitions(themesDirectory) {
|
||||
.sort((left, right) => left.id.localeCompare(right.id, "en"));
|
||||
}
|
||||
|
||||
function normalizeFontName(value) {
|
||||
return value
|
||||
.replace(/^[A-Z]{6}\+/u, "")
|
||||
.replace(/(?:[-, ](?:regular|bold|italic|bolditalic|roman|medium|light|black))+$/iu, "")
|
||||
.replace(/[\s_-]+/gu, "")
|
||||
.toLocaleLowerCase("en-US");
|
||||
}
|
||||
|
||||
function isOfficeEmbeddedAlias(value, engine) {
|
||||
return engine === "word"
|
||||
? /^___WRD_EMBED_SUB_/iu.test(value)
|
||||
: engine === "wps"
|
||||
? /^WPSEMBED\d+$/iu.test(value)
|
||||
: false;
|
||||
}
|
||||
|
||||
export function getDocxFontGateFailures({
|
||||
theme,
|
||||
inspection,
|
||||
renderedFonts,
|
||||
engine
|
||||
}) {
|
||||
assert(theme && typeof theme.id === "string", "字体门禁缺少主题定义");
|
||||
assert(inspection && typeof inspection === "object", "字体门禁缺少 DOCX 检查结果");
|
||||
assert(Array.isArray(renderedFonts), "字体门禁缺少实际渲染字体");
|
||||
assert(engine === "word" || engine === "wps", "字体门禁引擎必须为 word 或 wps");
|
||||
const failures = [];
|
||||
const declaredFaces = Array.isArray(theme.docxFontFaces)
|
||||
? theme.docxFontFaces
|
||||
: [];
|
||||
const embeddedNames = Array.isArray(inspection.embeddedFontNames)
|
||||
? inspection.embeddedFontNames
|
||||
: [];
|
||||
const embeddedPartCount = Number(inspection.embeddedFontPartCount ?? 0);
|
||||
if (declaredFaces.length === 0) {
|
||||
failures.push(`主题 ${theme.id} 未声明任何 DOCX 字体`);
|
||||
}
|
||||
if (embeddedPartCount === 0) {
|
||||
failures.push(`主题 ${theme.id} 的 DOCX 没有嵌入字体部件`);
|
||||
}
|
||||
if (declaredFaces.length > 0 && embeddedPartCount !== declaredFaces.length) {
|
||||
failures.push(
|
||||
`主题 ${theme.id} 声明 ${declaredFaces.length} 个字体面,但 DOCX 实际嵌入 ${embeddedPartCount} 个部件`
|
||||
);
|
||||
}
|
||||
if (embeddedPartCount > 0) {
|
||||
const expected = new Set(embeddedNames.map(normalizeFontName));
|
||||
const actuallyUsed = renderedFonts.some((font) => {
|
||||
const name = typeof font === "string" ? font : font?.name;
|
||||
return typeof name === "string" && (
|
||||
isOfficeEmbeddedAlias(name, engine) || expected.has(normalizeFontName(name))
|
||||
);
|
||||
});
|
||||
if (!actuallyUsed) {
|
||||
failures.push(
|
||||
`${engine === "word" ? "Microsoft Word" : "WPS Writer"} 输出未实际使用 DOCX 嵌入字体`
|
||||
);
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
function createPaperConfig(orientation, marginScenario) {
|
||||
return {
|
||||
format: "A4",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createDocxLayoutVisualMatrixCases,
|
||||
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS,
|
||||
DOCX_LAYOUT_MATRIX_ORIENTATIONS,
|
||||
getDocxFontGateFailures,
|
||||
readBundledThemeMatrixDefinitions,
|
||||
summarizeDocxLayoutVisualMatrix
|
||||
} from "./docx-layout-visual-matrix-cases.mjs";
|
||||
@@ -34,6 +35,38 @@ test("生成 14×2×5 的完整 DOCX 布局视觉矩阵", () => {
|
||||
assert.deepEqual(summary.orientations, ["portrait", "landscape"]);
|
||||
});
|
||||
|
||||
test("字体门禁区分缺少声明、缺少嵌入和 Office 未实际采用", () => {
|
||||
const missing = getDocxFontGateFailures({
|
||||
theme: { id: "missing", docxFontFaces: [] },
|
||||
inspection: { embeddedFontPartCount: 0, embeddedFontNames: [] },
|
||||
renderedFonts: [],
|
||||
engine: "word"
|
||||
});
|
||||
assert.equal(missing.length, 2);
|
||||
|
||||
const fallback = getDocxFontGateFailures({
|
||||
theme: {
|
||||
id: "declared",
|
||||
docxFontFaces: [{ family: "FandolSong", aliases: [], weight: 400, style: "normal" }]
|
||||
},
|
||||
inspection: { embeddedFontPartCount: 1, embeddedFontNames: ["MdTP Serif SC"] },
|
||||
renderedFonts: [{ name: "SimSun" }],
|
||||
engine: "word"
|
||||
});
|
||||
assert.deepEqual(fallback, ["Microsoft Word 输出未实际使用 DOCX 嵌入字体"]);
|
||||
|
||||
const embedded = getDocxFontGateFailures({
|
||||
theme: {
|
||||
id: "declared",
|
||||
docxFontFaces: [{ family: "FandolSong", aliases: [], weight: 400, style: "normal" }]
|
||||
},
|
||||
inspection: { embeddedFontPartCount: 1, embeddedFontNames: ["MdTP Serif SC"] },
|
||||
renderedFonts: [{ name: "___WRD_EMBED_SUB_65" }],
|
||||
engine: "word"
|
||||
});
|
||||
assert.deepEqual(embedded, []);
|
||||
});
|
||||
|
||||
test("每套主题都覆盖两个方向和五组边距", () => {
|
||||
const { themes, cases } = matrix();
|
||||
for (const theme of themes) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "../packages/preview-engine/dist/index.js";
|
||||
import {
|
||||
createDocxLayoutVisualMatrixCases,
|
||||
getDocxFontGateFailures,
|
||||
readBundledThemeMatrixDefinitions,
|
||||
summarizeDocxLayoutVisualMatrix
|
||||
} from "./docx-layout-visual-matrix-cases.mjs";
|
||||
@@ -568,14 +569,26 @@ for (const caseDefinition of selectedCases) {
|
||||
writeReportArtifacts(rasterDirectory, "wps", wpsReport);
|
||||
writeReportArtifacts(rasterDirectory, "office", officeReport);
|
||||
|
||||
const currentFailures = [
|
||||
const currentFailures = [...new Set([
|
||||
...pageGeometryFailures(chromium, caseDefinition, "Chromium"),
|
||||
...pageGeometryFailures(word, caseDefinition, "Microsoft Word"),
|
||||
...pageGeometryFailures(wps, caseDefinition, "WPS Writer"),
|
||||
...getDocxFontGateFailures({
|
||||
theme: themeDefinitions.find((theme) => theme.id === caseDefinition.themeId),
|
||||
inspection: result.docx.inspection,
|
||||
renderedFonts: word.fonts,
|
||||
engine: "word"
|
||||
}),
|
||||
...getDocxFontGateFailures({
|
||||
theme: themeDefinitions.find((theme) => theme.id === caseDefinition.themeId),
|
||||
inspection: result.docx.inspection,
|
||||
renderedFonts: wps.fonts,
|
||||
engine: "wps"
|
||||
}),
|
||||
...reportGateFailures(wordReport, "Chromium/Word"),
|
||||
...reportGateFailures(wpsReport, "Chromium/WPS"),
|
||||
...reportGateFailures(officeReport, "Word/WPS")
|
||||
];
|
||||
])];
|
||||
gateFailures.push(
|
||||
...currentFailures.map((failure) => `${caseDefinition.id}: ${failure}`)
|
||||
);
|
||||
@@ -595,6 +608,14 @@ for (const caseDefinition of selectedCases) {
|
||||
word: word.pageCount,
|
||||
wps: wps.pageCount
|
||||
},
|
||||
fonts: {
|
||||
declared: themeDefinitions.find((theme) => theme.id === caseDefinition.themeId)?.docxFontFaces ?? [],
|
||||
embeddedNames: result.docx.inspection.embeddedFontNames,
|
||||
embeddedPartCount: result.docx.inspection.embeddedFontPartCount,
|
||||
chromium: chromium.fonts,
|
||||
word: word.fonts,
|
||||
wps: wps.fonts
|
||||
},
|
||||
editableParagraphCount: editable.paragraphs.length,
|
||||
reports: {
|
||||
word: reportSummary(wordReport),
|
||||
|
||||
Reference in New Issue
Block a user