有些 PDF 打开后看起来完全正常,但按 Ctrl/Cmd + F 搜索姓名或编号时,一个结果也找不到;鼠标拖动页面,也无法选中文字。
这类文件往往不是"文字 PDF",而是扫描仪生成的一组页面图片。页面里的人眼可以看到文字,PDF 阅读器却只看到了像素。
最近我在实现 WyanHub 工具箱的"扫描件变成可搜索 PDF"工作流时,把处理链路拆成了下面几步:
text
读取扫描 PDF
↓
PDF.js 逐页渲染到 Canvas
↓
Tesseract.js 识别页面
↓
生成"原页面画面 + 隐藏文字层"的单页 PDF
↓
pdf-lib 合并为多页可搜索 PDF
关键点是:只得到一段 OCR 文本还不够。要让 PDF 继续保留原来的页面画面,并支持搜索和选择,还需要一层带坐标的文字。
一、先把 PDF 页面渲染成 OCR 能识别的图像
Tesseract.js 可以直接识别图片或 Canvas,但不能直接理解一个多页 PDF。因此先用 PDF.js 读取文件,再按页渲染:
ts
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
GlobalWorkerOptions.workerSrc = pdfWorkerUrl;
const loadingTask = getDocument({
data: new Uint8Array(await file.arrayBuffer()),
});
const pdf = await loadingTask.promise;
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale: 2 });
const canvas = document.createElement('canvas');
canvas.width = Math.ceil(viewport.width);
canvas.height = Math.ceil(viewport.height);
const context = canvas.getContext('2d');
if (!context) throw new Error('当前浏览器无法渲染 PDF');
await page.render({
canvas,
canvasContext: context,
viewport,
}).promise;
放大渲染通常能让小字号更容易识别,但 Canvas 面积会按比例平方增长。A4 页面、长图或高分辨率扫描件很容易占用大量内存,所以不能无限提高倍数。
我给单页设置了 1600 万像素上限:
ts
const base = page.getViewport({ scale: 2 });
const maxPixels = 16_000_000;
const safeScale = base.width * base.height > maxPixels
? Math.sqrt(maxPixels / (base.width * base.height)) * 2
: 2;
const viewport = page.getViewport({ scale: safeScale });
这不是一个适合所有文件的固定答案,但比无条件使用高倍渲染稳妥得多。
二、一个 Worker 处理多页,不要每页重新初始化
OCR 内核和语言模型的初始化成本不低。多页 PDF 如果每处理一页就重新创建 Worker,会反复加载资源。
更合适的方式是创建一次,然后在页面循环中复用:
ts
import { createWorker } from 'tesseract.js';
const worker = await createWorker(['chi_sim', 'eng'], undefined, {
logger(message) {
if (message.status === 'recognizing text') {
console.log(`当前页进度:${Math.round(message.progress * 100)}%`);
}
},
});
try {
for (const pageNumber of selectedPages) {
const canvas = await renderPdfPage(pdf, pageNumber);
const { data } = await worker.recognize(
canvas,
{ pdfTitle: 'OCR 可搜索 PDF' },
{ text: true, pdf: true },
);
console.log(data.text, data.confidence);
}
} finally {
await worker.terminate();
}
这里同时请求了两种输出:
text:用于展示、复制、校对和导出 TXT。pdf:包含页面画面以及按识别位置生成的文字层。
如果只保存 text,虽然拿到了内容,却无法还原每个词在页面上的位置,也就不能直接得到保持原版式的可搜索 PDF。
三、把每页 OCR PDF 合并起来
Tesseract.js 每次识别一张 Canvas,得到的是当前页的 PDF 数据。多页文件还需要使用 pdf-lib 重新组合:
ts
import { PDFDocument } from 'pdf-lib';
async function mergePages(pageFiles: Uint8Array[]) {
const output = await PDFDocument.create();
for (const pageBytes of pageFiles) {
const source = await PDFDocument.load(pageBytes);
const copiedPages = await output.copyPages(
source,
source.getPageIndices(),
);
copiedPages.forEach(page => output.addPage(page));
}
output.setTitle('OCR 可搜索 PDF');
return output.save({ useObjectStreams: true });
}
最终得到的 Uint8Array 可以转换成 Blob,让用户下载:
ts
const bytes = await mergePages(ocrPages);
const blob = new Blob([bytes.slice().buffer], {
type: 'application/pdf',
});
生成后的 PDF 看起来仍是原扫描页面,但阅读器已经可以搜索和选择识别出的文字。
四、可编辑文本和 PDF 文字层要分开处理
OCR 完成后,我会同时给用户两份结果:
- 一份可以人工修改的纯文本。
- 一份保持页面版式的可搜索 PDF。
这里有一个容易产生误解的边界:用户修改文本框里的错字,不会自动修改 PDF 中的隐藏文字层。
原因是可搜索 PDF 保存的不只是一段文本,还包括文字在页面上的位置、大小和排列信息。简单替换文本可能造成位置错乱,甚至覆盖错误区域。
所以界面里应当明确提示:文本编辑只影响 TXT 或后续文本处理;可搜索 PDF 保留 OCR 当时生成的定位文字层。
如果一定要让人工校对结果同步回 PDF,就需要保存每个词的坐标,并重新生成整层文字,工作量会明显增加。
五、浏览器内存和取消操作不能忽略
PDF 渲染和 OCR 都是重任务。连续处理多页时,如果 Canvas 和 PDF.js 资源不及时释放,内存占用会快速上升。
每页完成后可以主动缩小 Canvas,并清理页面缓存:
ts
canvas.width = 1;
canvas.height = 1;
page.cleanup();
整个任务结束后销毁加载任务:
ts
try {
// 逐页渲染和 OCR
} finally {
await loadingTask.destroy();
await worker.terminate();
}
实际产品中还需要提供取消按钮。我的做法是保存当前任务编号和取消状态,在每页开始前检查一次;取消后立即终止 Worker,并拒绝旧任务继续回写界面。
另外我限制了单次处理的页数。浏览器本地 OCR 更适合几页到十页的临时文件,不适合一次吞下几百页档案。
六、本地处理也要把边界说清楚
这套方案中,文件解析、页面渲染、OCR 和 PDF 合成都在当前浏览器完成,扫描件不需要上传到业务服务器。
但"本地处理"不等于"完全不联网":首次使用 Tesseract.js 时,浏览器仍可能需要从公共 CDN 下载 OCR 内核和所选语言模型,之后通常会由浏览器缓存。
识别质量也受原文件影响:
- 倾斜、模糊、阴影和低分辨率会降低准确度。
- 表格、竖排文字、手写内容和复杂双栏版式更容易出错。
- 置信度只是参考,不能替代人工核对。
- 姓名、金额、日期、证件号和合同编号应当逐项复查。
因此可搜索 PDF 更适合作为检索和辅助阅读版本,重要资料仍应保留原件。
总结
把扫描 PDF 变成可搜索 PDF,真正需要解决的不是单独一次 OCR,而是完整的数据链路:
- 用 PDF.js 把指定页面渲染到 Canvas。
- 复用 Tesseract.js Worker,输出文本和带定位文字层的单页 PDF。
- 用 pdf-lib 合并所有页面。
- 及时释放 Canvas、页面、Worker 和 PDF 加载资源。
- 把可编辑文本与 PDF 隐藏文字层的边界明确告诉用户。
做好这些以后,扫描件仍然保留原来的视觉版式,同时可以通过关键词快速定位内容。对于票据、旧资料、扫描合同和纸质档案的临时整理,这是一个适合在浏览器中完成的实用方案。