PDF 和 Word 解决的是相反布局问题:PDF 保存固定视觉页面,Word 保存可编辑文档结构。浏览器转换器可以可靠地把文本层恢复成干净 DOCX,但仅靠坐标无法推断所有表格、分栏、字体和图片关系。
这套实现明确以“可读、可编辑文本”为目标。pdf.js 提取文字和 transform,坐标启发式重建行,XML 转义保护文档,fflate 再把最小 Open XML Word 包与分页符打包出来。
先缩小并说明转换承诺
带文本层的 PDF 包含字形字符串和位置 transform,并不包含语义段落;扫描件甚至可能完全没有文字项。应明确结果是带分页的可编辑提取文本,不是源设计的像素级复刻。
这个边界让工具适合引用、报告、笔记和文案恢复,同时不会对复杂宣传册、表单、公式和多栏排版作出错误承诺。
| 源 PDF 能力 | 这套流水线的结果 |
|---|---|
| 可选择文本 | 提取为 Word 段落 |
| 原始分页 | 通过分页符保留 |
| 纯扫描图片页 | 需要先做 OCR |
| 表格和分栏 | 可能被压平成阅读顺序文本 |
| 图片和精确字体 | 不重建 |
加载 pdf.js 并提取带坐标文字
动态 import pdf.js legacy browser build,并把 GlobalWorkerOptions.workerSrc 指向打包的 worker。选择的 File 读取成 Uint8Array,开启系统字体加载文档,再按顺序处理每一页。
每个文本项提供字符串和 transform,第五、第六个值可以作为轻量重建所需的 x、y 位置。规范空白,并忽略空字符串和非文本项。
const page = await pdf.getPage(pageNumber);
const content = await page.getTextContent();
const positioned = content.items.flatMap(item => {
if (!isTextItem(item) || !item.str.trim()) return [];
return [{
text: normalizeText(item.str),
x: Number(item.transform[4] ?? 0),
y: Number(item.transform[5] ?? 0),
}];
});
const lines = groupByNearbyY(positioned)
.map(line => line.sort((a, b) => a.x - b.x))
.map(line => line.map(item => item.text).join(' '));用坐标容差重建文本行
先按 y 从上到下、x 从左到右排序。y 值差小于容差的文本项进入同一行,每行再按 x 排序并拼接。普通单栏文本不需要布局模型也能得到较好结果。
容差只是启发式。上标、旋转文字、竖排、分栏、表格和定位标签可能产生意外顺序。更高级方案要先做 block segmentation 与分栏检测。
- 限制每页最大文本项,约束异常输入。
- 某页没有可用文字时仍然保留页序。
- 整份文档段落数为零时返回 empty_text。
- 返回页数和段落数,让 UI 描述结果。
生成最小有效 DOCX 包
DOCX 是 Open XML ZIP。最小包需要 [Content_Types].xml、根关系、核心与应用属性、word/document.xml。每一条重建文本行成为 w:p,第一页之后的每个源页面前插入 Word 分页符。
用户文本写入 XML 前要去除非法控制字符并转义 &、尖括号、引号和单引号。xml:space="preserve" 可以防止 Word 丢掉预期空格。
const entries = {
'[Content_Types].xml': strToU8(contentTypesXml),
'_rels/.rels': strToU8(packageRelationshipsXml),
'docProps/core.xml': strToU8(corePropertiesXml),
'docProps/app.xml': strToU8(appPropertiesXml),
'word/document.xml': strToU8(buildDocumentXml(pages)),
};
const zipped = zipSync(entries, { level: 6 });
return new Blob([zipped], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});扫描 PDF 应走单独 OCR 流程
pdf.js 没有返回可用文字时,页面可能是扫描图,或文字编码无法被提取。静默生成空 Word 比返回 empty_text 更糟。
扫描转 Word 需要先把每页渲染成图片,运行 OCR,把识别块映射回阅读顺序,再生成 DOCX。这是另一条带模型下载、图片限制、语言选择和更低确定性的流水线。
校验输入并释放输出资源
import pdf.js 前先检查空文件、PDF 类型和最大字节数。加密、损坏或不支持的 PDF 统一作为加载失败处理。UI 为结果创建一个 Object URL,并在选择新文件或组件卸载时 revoke 旧 URL。
用 Word、LibreOffice 和网页查看器测试输出。语法有效的 Open XML 包仍可能出现只有真实文档应用才能发现的顺序或 Unicode 问题。
总结
聚焦文本的 PDF 转 Word 可以既小又诚实:提取带位置文字、用明确启发式分行、保留分页、转义 XML,再生成最小 DOCX。扫描件和高保真版式恢复属于另一条更复杂的流水线。