新增能力:将 DOCX 发布验收拆分为四套独立 140,支持真实语料冻结、指纹复用、失败与基础设施错误独立统计,并为表格换行、全 JSON 围栏、代码连续性和长文档分页建立通用门禁。 问题修复:冻结 Paged.js 分片前的逻辑表格列轨并传递打印几何,统一 Markdown 表格换行、代码、段落与 OOXML 翻译;改进 PDF 文本流排序、语义块映射、颜色与栅格比较,消除窄字符重叠和跨行范围符号误报。 兼容与部署:版本统一为 0.6.2;正式 Docker 镜像内置固定 Chromium、Pandoc 3.9.0.2 和 Serif/Sans/Mono 字体;Desktop NSIS 与 ZIP 继续直接内置字体,无需系统字体安装。 验证结果:合成基线与长庆严格 280/280,M4N 140/140;健康数据残余误报 6/115(5.22%),均核查为重复表头自动对齐/取样误报且基础设施错误为 0。全项目测试、类型检查、生产构建和 git diff --check 通过;正式 Docker、NSIS、ZIP、离线镜像、部署包、清单及 SHA-256 均已生成并校验。
156 lines
4.6 KiB
TypeScript
156 lines
4.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
|
||
import {
|
||
aggregatePdfTextLines,
|
||
buildPageContentText,
|
||
normalizePdfEditableText,
|
||
normalizePdfText,
|
||
type PdfTextItemSnapshot,
|
||
} from "../src/index.js";
|
||
|
||
function item(
|
||
text: string,
|
||
x: number,
|
||
y: number,
|
||
width = 20,
|
||
height = 10,
|
||
): PdfTextItemSnapshot {
|
||
return {
|
||
text,
|
||
normalizedText: normalizePdfText(text),
|
||
bounds: { x, y, width, height },
|
||
baselineY: y + height,
|
||
hasEol: false,
|
||
};
|
||
}
|
||
|
||
describe("PDF 文本行聚合", () => {
|
||
it("按坐标聚合中文文本并规范化兼容字符", () => {
|
||
const lines = aggregatePdfTextLines(
|
||
[
|
||
item("ABC", 10, 100, 30),
|
||
item("中文", 41, 100, 20),
|
||
item("第二行", 10, 120, 40),
|
||
],
|
||
842,
|
||
);
|
||
expect(lines.map((line) => line.normalizedText)).toEqual([
|
||
"ABC中文",
|
||
"第二行",
|
||
]);
|
||
});
|
||
|
||
it("合并 Office 同一视觉行内小于字体高度两成的基线漂移", () => {
|
||
const left = item("源系统", 10, 100, 30, 10);
|
||
const middle = item("nut_pickup_order.meal_type", 42, 101.8, 140, 10);
|
||
const right = item("原值", 184, 100, 20, 10);
|
||
const lines = aggregatePdfTextLines([left, middle, right], 842);
|
||
|
||
expect(lines).toHaveLength(1);
|
||
expect(lines[0]?.normalizedText).toBe(
|
||
"源系统nut_pickup_order.meal_type原值",
|
||
);
|
||
});
|
||
|
||
it("将同行公式的上标片段按横坐标并回正文", () => {
|
||
const lines = aggregatePdfTextLines(
|
||
[
|
||
item("减因子", 10, 100, 42, 12),
|
||
item("e", 53, 97, 8, 15),
|
||
item("−λΔt", 62, 96, 24, 10),
|
||
item("的动态权重", 87, 100, 70, 12),
|
||
],
|
||
842,
|
||
);
|
||
|
||
expect(lines).toHaveLength(1);
|
||
expect(lines[0]?.normalizedText).toBe("减因子e−λΔt的动态权重");
|
||
});
|
||
|
||
it("将超出正文右缘但紧邻行末的公式片段并回正文", () => {
|
||
const lines = aggregatePdfTextLines(
|
||
[
|
||
item("性、记忆重要程度、时效性衰减权重(须支持基于时间衰减因子 e", 150, 469.5, 346, 12),
|
||
item("−λΔt", 495.7, 466.1, 26, 10.2),
|
||
item("下一视觉行", 150, 489.7, 60, 12),
|
||
],
|
||
842,
|
||
);
|
||
|
||
expect(lines.map((line) => line.normalizedText)).toEqual([
|
||
"性、记忆重要程度、时效性衰减权重(须支持基于时间衰减因子 e−λΔt",
|
||
"下一视觉行",
|
||
]);
|
||
});
|
||
|
||
it("将字体 ToUnicode 中的传统户部件归一为简体字符", () => {
|
||
expect(normalizePdfText("戶⼾")).toBe("户户");
|
||
expect(normalizePdfText("⻅⻆⻓⻔⻚⻛⻝⻣⻋⻬")).toBe(
|
||
"见角长门页风食骨车齐",
|
||
);
|
||
});
|
||
|
||
it("统一 Chromium 与 Office PDF 文本层的中英文弯引号", () => {
|
||
expect(normalizePdfText("“记录”与‘计算’")).toBe('"记录"与\'计算\'');
|
||
});
|
||
|
||
it("保留与全角括号重叠的窄引号文本流顺序", () => {
|
||
const lines = aggregatePdfTextLines(
|
||
[
|
||
item("主观定性判断", 532.3, 151.5, 58.5),
|
||
item("”", 590.8, 151.5, 3.4),
|
||
item("(如", 589.3, 151.5, 19.5),
|
||
item("“", 608.8, 151.5, 3.4),
|
||
item("高心率占比", 612.2, 151.5, 48.8),
|
||
],
|
||
595,
|
||
);
|
||
|
||
expect(lines).toHaveLength(1);
|
||
expect(lines[0]?.normalizedText).toBe('主观定性判断"(如"高心率占比');
|
||
});
|
||
|
||
it("忽略 Emoji 文本层可选的变体选择符", () => {
|
||
expect(normalizePdfText("⚠️ 提示")).toBe("⚠ 提示");
|
||
});
|
||
|
||
it("统一 CJK 字体文本层的 em dash 与 horizontal bar", () => {
|
||
expect(normalizePdfText("仓库―仓区")).toBe("仓库—仓区");
|
||
});
|
||
|
||
it("统一数字区间中被 Chromium 提取为双连字符的 en dash", () => {
|
||
expect(normalizePdfText("10% -- 20%")).toBe("10%–20%");
|
||
expect(normalizePdfText("10% --")).toBe("10%–");
|
||
expect(normalizePdfText("命令 --flag")).toBe("命令 --flag");
|
||
});
|
||
|
||
it("从可编辑正文契约中移除 Word 自动列表装饰符", () => {
|
||
expect(normalizePdfEditableText("• 项目一 ◦ 子项 ▪ 末项 WPS")).toBe(
|
||
"项目一子项末项WPS"
|
||
);
|
||
});
|
||
|
||
it("只在页眉页脚坐标带识别独立页码", () => {
|
||
const lines = aggregatePdfTextLines(
|
||
[
|
||
item("章节 1", 72, 400, 50),
|
||
item("— 1 —", 280, 731, 35, 14),
|
||
],
|
||
842,
|
||
);
|
||
expect(lines.map((line) => line.role)).toEqual([
|
||
"content",
|
||
"page-number",
|
||
]);
|
||
expect(buildPageContentText(lines)).toBe("章节 1");
|
||
});
|
||
|
||
it("不会把页脚中的普通说明误判为页码", () => {
|
||
const lines = aggregatePdfTextLines(
|
||
[item("内部资料 1", 72, 800, 70)],
|
||
842,
|
||
);
|
||
expect(lines[0]?.role).toBe("content");
|
||
});
|
||
});
|