feat: 完成 DOCX R4 元素级视觉门禁
建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。 支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。 修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import type {
|
||||
PdfDocumentSnapshot,
|
||||
PdfEditableContentLine,
|
||||
PdfPointBounds,
|
||||
PdfTextItemSnapshot,
|
||||
PdfTextLineSnapshot,
|
||||
VisualPageSemanticExpectation,
|
||||
} from "./types.js";
|
||||
|
||||
const ZERO_WIDTH_AND_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u200b-\u200d\u2060\ufeff]/gu;
|
||||
const CJK_RADICAL_VARIANTS = new Map([
|
||||
["⺠", "民"],
|
||||
["⻔", "门"],
|
||||
]);
|
||||
const PAGE_NUMBER_PATTERNS = [
|
||||
/^(?:[-—–]\s*)?\d+(?:\s*[//]\s*\d+)?(?:\s*[-—–])?$/u,
|
||||
/^第\s*\d+\s*页(?:\s*(?:[//]|共)\s*\d+\s*页)?$/u,
|
||||
@@ -14,6 +21,10 @@ const PAGE_NUMBER_PATTERNS = [
|
||||
export function normalizePdfText(value: string): string {
|
||||
return value
|
||||
.normalize("NFKC")
|
||||
.replace(/戶/gu, "户")
|
||||
.replace(/[\u2e80-\u2eff]/gu, (character) =>
|
||||
CJK_RADICAL_VARIANTS.get(character) ?? character
|
||||
)
|
||||
.replace(ZERO_WIDTH_AND_CONTROL, "")
|
||||
.replace(/\u00a0/gu, " ")
|
||||
.replace(/[ \t]+/gu, " ")
|
||||
@@ -24,6 +35,69 @@ export function normalizePdfContentText(value: string): string {
|
||||
return normalizePdfText(value).replace(/\s+/gu, "");
|
||||
}
|
||||
|
||||
export function normalizePdfEditableText(value: string): string {
|
||||
return normalizePdfContentText(value).replace(/[☐☑☒•◦▪\uf0b7]/gu, "");
|
||||
}
|
||||
|
||||
function isExpectedHeaderLine(
|
||||
line: PdfTextLineSnapshot,
|
||||
pageHeightPt: number,
|
||||
expectation: VisualPageSemanticExpectation | undefined,
|
||||
): boolean {
|
||||
if (
|
||||
!expectation?.headerVisible ||
|
||||
line.bounds.y + line.bounds.height / 2 > pageHeightPt * 0.12
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const lineText = normalizePdfEditableText(line.normalizedText);
|
||||
return (expectation.headerSlots ?? []).some((slot) => {
|
||||
const slotText = normalizePdfEditableText(slot.text);
|
||||
return slotText.length > 0 && lineText.includes(slotText);
|
||||
});
|
||||
}
|
||||
|
||||
function isInternalLayoutSpacerLine(line: PdfTextLineSnapshot): boolean {
|
||||
return (
|
||||
normalizePdfEditableText(line.normalizedText) === "." &&
|
||||
line.bounds.height <= 1.5 &&
|
||||
line.items.length === 1
|
||||
);
|
||||
}
|
||||
|
||||
export function buildPdfEditableContentText(
|
||||
snapshot: PdfDocumentSnapshot,
|
||||
pageSemantics: readonly VisualPageSemanticExpectation[] = [],
|
||||
): string {
|
||||
return buildPdfEditableContentLines(snapshot, pageSemantics)
|
||||
.map((item) => item.normalizedText)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function buildPdfEditableContentLines(
|
||||
snapshot: PdfDocumentSnapshot,
|
||||
pageSemantics: readonly VisualPageSemanticExpectation[] = [],
|
||||
): PdfEditableContentLine[] {
|
||||
return snapshot.pages.flatMap((page) => {
|
||||
const expectation = pageSemantics.find(
|
||||
(item) => item.physicalPageNumber === page.pageNumber,
|
||||
);
|
||||
return page.lines.flatMap((line) => {
|
||||
if (
|
||||
line.role !== "content" ||
|
||||
isExpectedHeaderLine(line, page.heightPt, expectation) ||
|
||||
isInternalLayoutSpacerLine(line)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const normalizedText = normalizePdfEditableText(line.normalizedText);
|
||||
return normalizedText
|
||||
? [{ pageNumber: page.pageNumber, line, normalizedText }]
|
||||
: [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unionBounds(items: readonly PdfTextItemSnapshot[]): PdfPointBounds {
|
||||
const left = Math.min(...items.map((item) => item.bounds.x));
|
||||
const top = Math.min(...items.map((item) => item.bounds.y));
|
||||
|
||||
Reference in New Issue
Block a user