扫描 PDF 看得见却搜不到?用 Tesseract.js + PDF.js 生成可搜索 PDF

有些 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 完成后,我会同时给用户两份结果:

  1. 一份可以人工修改的纯文本。
  2. 一份保持页面版式的可搜索 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,而是完整的数据链路:

  1. 用 PDF.js 把指定页面渲染到 Canvas。
  2. 复用 Tesseract.js Worker,输出文本和带定位文字层的单页 PDF。
  3. 用 pdf-lib 合并所有页面。
  4. 及时释放 Canvas、页面、Worker 和 PDF 加载资源。
  5. 把可编辑文本与 PDF 隐藏文字层的边界明确告诉用户。

做好这些以后,扫描件仍然保留原来的视觉版式,同时可以通过关键词快速定位内容。对于票据、旧资料、扫描合同和纸质档案的临时整理,这是一个适合在浏览器中完成的实用方案。

相关推荐
小万7421 小时前
ECC(Everything Claude Code)深度解析:把编程 Agent 从“聊天写代码”拉进工程现场的 Harness 系统
前端
用户2145549310151 小时前
VeapAI 自然语言操作实战|第 02 篇:语义层——把系统讲给模型听的四个构件
前端·后端
定栓2 小时前
Webpack迁到Vite第七节(一):全家桶移除 — 配置文件与构建核心
前端·webpack·node.js
码艺-Alimjan2 小时前
维吾尔语 RTL 排版的隐形利器:`\u200F`
前端·javascript
alwaysrun2 小时前
Web之wasm-pack详解
前端·rust·web·wasm
广州华水科技2 小时前
如何通过单北斗GNSS实现精准的变形监测?
前端
云和数据.ChenGuang3 小时前
print(f“a+b=“,result) print(f“a+b={result}“)区别
java·前端·人工智能·python·深度学习
速易达网络3 小时前
基于 Vue3 + Flask + ECharts 构建政务 OA 系统门户
前端·echarts·政务
何何____3 小时前
TypeScript 函数类型兼容性
前端·javascript·typescript