fix: 增强DOCX字体实际使用门禁

This commit is contained in:
SkyJourney
2026-08-03 11:32:06 +08:00
parent f0fff7f726
commit a094dabf02
7 changed files with 226 additions and 9 deletions
+69 -1
View File
@@ -7,6 +7,7 @@ import { aggregatePdfTextLines, buildPageContentText, normalizePdfText } from ".
import type {
CreatePdfSnapshotOptions,
PdfDocumentSnapshot,
PdfFontSnapshot,
PdfPageSnapshot,
PdfSnapshotLimits,
PdfTextItemSnapshot,
@@ -25,6 +26,13 @@ interface PdfJsTextStyle {
fontFamily?: string;
}
interface PdfJsResolvedFont {
name?: string;
fallbackName?: string;
isType3Font?: boolean;
missingFile?: boolean;
}
export const DEFAULT_PDF_SNAPSHOT_LIMITS: PdfSnapshotLimits = {
maxPdfBytes: 100 * 1024 * 1024,
maxPages: 500,
@@ -60,6 +68,7 @@ function createTextItemSnapshot(
item: PdfJsTextItem,
pageHeightPt: number,
styles: Readonly<Record<string, PdfJsTextStyle>>,
fonts: ReadonlyMap<string, PdfFontSnapshot>,
): PdfTextItemSnapshot | undefined {
const normalizedText = normalizePdfText(item.str);
if (!normalizedText) {
@@ -77,7 +86,8 @@ function createTextItemSnapshot(
0,
);
const baselineY = pageHeightPt - baselineFromBottom;
const fontFamily = styles[item.fontName]?.fontFamily;
const font = fonts.get(item.fontName);
const fontFamily = font?.name ?? styles[item.fontName]?.fontFamily;
return {
text: item.str,
normalizedText,
@@ -90,10 +100,45 @@ function createTextItemSnapshot(
baselineY,
fontName: item.fontName,
...(fontFamily ? { fontFamily } : {}),
...(font?.fallbackFamily
? { fallbackFontFamily: font.fallbackFamily }
: {}),
hasEol: item.hasEOL,
};
}
function normalizePdfFontName(value: string) {
return value.replace(/^[A-Z]{6}\+/u, "");
}
function resolvePageFonts(
page: Awaited<ReturnType<Awaited<ReturnType<typeof getDocument>["promise"]>["getPage"]>>,
fontNames: readonly string[],
) {
const fonts = new Map<string, PdfFontSnapshot>();
for (const internalName of fontNames) {
try {
const resolved = page.commonObjs.get(internalName) as PdfJsResolvedFont;
const name = resolved.name?.trim();
if (!name) {
continue;
}
fonts.set(internalName, {
internalName,
name: normalizePdfFontName(name),
...(resolved.fallbackName
? { fallbackFamily: resolved.fallbackName }
: {}),
isType3Font: resolved.isType3Font === true,
missingFile: resolved.missingFile === true,
});
} catch {
// PDF.js 可能不给未实际绘制的字体建立公共对象;保留文本但不伪造字体。
}
}
return fonts;
}
function resolveLimits(
input: Partial<PdfSnapshotLimits> | undefined,
): PdfSnapshotLimits {
@@ -145,6 +190,17 @@ export async function createPdfDocumentSnapshot(
disableNormalization: false,
includeMarkedContent: false,
});
await page.getOperatorList();
const pageFonts = resolvePageFonts(
page,
[
...new Set(
textContent.items.flatMap((item) =>
isTextItem(item) ? [item.fontName] : [],
),
),
],
);
const styles = textContent.styles as Readonly<
Record<string, PdfJsTextStyle>
>;
@@ -156,6 +212,7 @@ export async function createPdfDocumentSnapshot(
item,
pointViewport.height,
styles,
pageFonts,
);
return snapshot ? [snapshot] : [];
});
@@ -168,6 +225,9 @@ export async function createPdfDocumentSnapshot(
items,
lines,
contentText: buildPageContentText(lines),
fonts: [...pageFonts.values()].sort((left, right) =>
left.name.localeCompare(right.name, "en"),
),
};
const pageNumberLine = lines.find((line) => line.role === "page-number");
if (pageNumberLine) {
@@ -218,12 +278,20 @@ export async function createPdfDocumentSnapshot(
.map((page) => page.contentText)
.filter(Boolean)
.join("\n\f\n");
const fonts = [
...new Map(
pages
.flatMap((page) => page.fonts)
.map((font) => [font.name.toLocaleLowerCase("en-US"), font]),
).values(),
].sort((left, right) => left.name.localeCompare(right.name, "en"));
return {
schemaVersion: 1,
source: options.source,
sha256: sha256(pdfBytes),
pageCount: pages.length,
contentText,
fonts,
pages,
};
} finally {
@@ -20,9 +20,18 @@ export interface PdfTextItemSnapshot {
baselineY: number;
fontName?: string;
fontFamily?: string;
fallbackFontFamily?: string;
hasEol: boolean;
}
export interface PdfFontSnapshot {
internalName: string;
name: string;
fallbackFamily?: string;
isType3Font: boolean;
missingFile: boolean;
}
export type PdfTextLineRole = "content" | "page-number";
export interface PdfTextLineSnapshot {
@@ -132,6 +141,7 @@ export interface PdfPageSnapshot {
items: PdfTextItemSnapshot[];
lines: PdfTextLineSnapshot[];
contentText: string;
fonts: PdfFontSnapshot[];
pageNumberText?: string;
raster?: PdfPageRaster;
}
@@ -142,6 +152,7 @@ export interface PdfDocumentSnapshot {
sha256: string;
pageCount: number;
contentText: string;
fonts: PdfFontSnapshot[];
pages: PdfPageSnapshot[];
}
@@ -19,6 +19,18 @@ describe("PDF.js 页面快照", () => {
expect(first.pages[0]?.widthPt).toBeCloseTo(595, 3);
expect(first.pages[0]?.heightPt).toBeCloseTo(842, 3);
expect(first.contentText).toContain("Visual diff fixture");
expect(first.fonts).toEqual([
expect.objectContaining({
name: "Helvetica",
fallbackFamily: "sans-serif",
isType3Font: false,
missingFile: true,
}),
]);
expect(first.pages[0]?.items[0]).toMatchObject({
fontFamily: "Helvetica",
fallbackFontFamily: "sans-serif",
});
expect(first.pages[0]?.raster).toMatchObject({
widthPx: 1190,
heightPx: 1684,