
简介这是一份基于 Mozilla pdf.js 开发的轻量级 PDF 在线阅读与交互插件面向前端开发者及 Web 应用集成需求者解决 PDF 文档在浏览器中无法原生支持搜索、高亮、分页控制与打印导出等核心功能的问题。资源包共 366 个文件5.53MB包含 168 个 bcmap中日韩字体映射表、110 个 properties本地化配置、49 个 svg图标与矢量资源、14 个 js核心逻辑与扩展脚本、以及 css、ttf、pdf 等必要资产完整复现了 pdf.js 官方 viewer 的定制化部署能力。目前已有 359 人学习下载可直接运行于本地服务如 http://localhost/pdfjs-viewer/web/viewer.html开箱即用支持区分大小写的全文搜索、关键词高亮、缩放/翻页/下载/打印等全链路 PDF 交互功能并附带详细 license 文件与 README.md 说明便于二次开发与企业级嵌入集成。1. 这不是 PDF 查看器而是一个可嵌入、可定制、能进生产环境的文档交互内核你有没有遇到过这种场景在后台管理系统里用户上传了一份合同 PDF运营同学需要快速定位“违约责任”条款但浏览器原生 PDF 预览器不支持关键词高亮、不区分大小写、没法跳转到第 17 页第 3 段——更别说导出当前视图为 PNG 或打印带水印的版本了。这个 pdf.js 插件就是为这类真实业务闭环而生的它不是简单套一层 pdf.js 官方 viewer 的壳而是把搜索、分页、缩放、高亮、下载、打印这六项高频操作全部封装成可独立调用的 API 模块并暴露清晰的事件钩子如onSearchResult、onPageChange让你能把它像积木一样焊进 Vue 组件、React Hook 甚至纯 HTML 页面里。适合中后台系统开发者、低代码平台集成者、以及需要交付“PDF 文档级交互能力”的 SaaS 产品技术负责人。它不依赖 Node.js 服务端渲染所有逻辑跑在浏览器端不强制要求 Webpack 构建UMD 版本直接script引入就能用核心包体积压缩后仅 420KB比一个中等图标库还轻。2. 技术栈与架构设计从 pdf.js 原生能力到插件化封装的三层抽象这个插件不是对 pdf.js 的简单包装而是做了三层收敛底层复用 pdf.js 的解析与渲染引擎pdf-lib不在此列它不负责渲染中层构建统一的状态管理模型PdfState把页面索引、缩放比例、搜索关键词、高亮范围、当前视图区域等全部纳入响应式状态上层提供面向业务的命令式 API如search(违约责任, { caseSensitive: true, wholeWord: false })和声明式配置如config: { enablePrint: true, defaultScale: page-width }。整个结构不引入 Vue/React 等框架强依赖用原生 CustomEvent DOM dataset 实现跨框架通信连 jQuery 都没碰——这是我在某高校教务系统集成时踩出来的血泪经验他们用的是十年老 AngularJS 1.2强行上 Vue 会引发全局 digest 循环崩溃。2.1 核心模块职责划分与依赖关系项目目录结构精简到 5 个主文件无冗余pdfjs-plugin/ ├── dist/ # UMD ES Module 双输出含 minified 版 ├── src/ │ ├── core/ # pdf.js 引擎桥接层加载 PDF、获取元数据、触发渲染 │ │ ├── loader.js # 封装 PDFJSLib.getDocument()支持 ArrayBuffer / URL / Blob │ │ └── renderer.js # 覆盖默认 canvas 渲染逻辑注入高亮层 canvas │ ├── features/ # 六大功能模块各自独立可开关 │ │ ├── search.js # 基于 TextLayer 的全文检索 正则匹配 大小写控制 │ │ ├── highlight.js # 动态生成 SVG 高亮框支持跨行、跨页、多关键词叠加 │ │ ├── zoom.js # 缩放控制器支持 auto, page-fit, page-width, 数值百分比 │ │ ├── pagination.js # 分页器含总页数监听、跳转、快捷键Ctrl↑/↓ │ │ ├── download.js # 下载当前页 PNG / 全文档 PDF需后端配合/ 视图截图 │ │ └── print.js # 打印适配注入 media print 样式隐藏控件保留高亮 │ ├── utils/ # 工具函数debounce、throttle、DOM 尺寸计算、canvas 导出 │ └── index.js # 主入口组合所有模块暴露 Plugin 类与全局 init 方法 └── examples/ # 3 个实测案例纯 HTML、Vue 3 setup、React 18 hooks提示src/features/highlight.js是整个插件最玄学的部分——pdf.js 的 TextLayer 坐标是相对页面的 CSS 像素但 canvas 渲染层是绝对像素且缩放后坐标系会偏移。我们不用getViewport()算两次而是直接监听rendered事件后用textLayerDiv.getBoundingClientRect()获取真实 DOM 坐标再通过scale反推 canvas 上的位置。这个细节决定了高亮框能不能精准贴住文字边缘。2.2 初始化流程从 DOM 挂载点到可交互 PDF 视图初始化不是new Plugin()就完事它必须明确三个锚点容器、PDF 源、配置项。下面这段代码是examples/plain-html/index.html里的真实启动逻辑我把它拆解成可抄作业的步骤!-- 1. 容器必须有固定宽高否则 pdf.js 渲染会失败 -- div idpdf-container stylewidth: 800px; height: 600px; border: 1px solid #ccc;/div script typemodule import { PdfPlugin } from ../dist/pdfjs-plugin.esm.js; // 2. 创建插件实例传入容器 DOM 和基础配置 const plugin new PdfPlugin({ container: document.getElementById(pdf-container), config: { enableSearch: true, enableZoom: true, enableDownload: true, defaultScale: page-width, // 注意caseSensitive 默认 false设为 true 才开启区分大小写 searchOptions: { caseSensitive: true } } }); // 3. 加载 PDF支持多种来源 plugin.load(./sample.pdf) // URL 字符串 .then(() { console.log(PDF 加载完成共, plugin.getPageCount(), 页); // 4. 可立即执行搜索此时文档已就绪 plugin.search(甲方义务); }) .catch(err console.error(加载失败:, err)); /script关键参数说明container必须是HTMLElement不能是 selector 字符串。pdf.js 对 DOM 尺寸极其敏感如果容器宽高为0后续所有渲染都会静默失败。defaultScale合法值为auto | page-fit | page-width | page-height | number如1.5表示 150%。auto会根据容器尺寸自动计算缩放但首次渲染可能抖动生产环境建议显式指定。searchOptions.caseSensitive必须显式设置默认false。这是用户最容易忽略的点——以为开了搜索就自动区分大小写结果搜API找不到Api。2.3 搜索与高亮实现原理TextLayer 解析 SVG 动态注入搜索不是字符串indexOf()而是深度利用 pdf.js 的getTextContent()接口。features/search.js的核心逻辑如下// src/features/search.js async function performSearch(text, options {}) { const { caseSensitive false, wholeWord false } options; const normalizedText caseSensitive ? text : text.toLowerCase(); // 1. 遍历每一页获取其文本内容异步pdf.js 内部缓存 for (let pageNum 1; pageNum this.pageCount; pageNum) { const page await this.pdfDoc.getPage(pageNum); const textContent await page.getTextContent(); // 2. 遍历每个文本项textItem提取字符串并归一化 const items textContent.items.map(item { const str item.str || ; return { str: caseSensitive ? str : str.toLowerCase(), transform: item.transform, // [a,b,c,d,e,f] 仿射变换矩阵 width: item.width, height: item.height, dir: item.dir }; }); // 3. 正则匹配wholeWord 控制 \b 边界 const regex wholeWord ? new RegExp(\\b${escapeRegExp(normalizedText)}\\b, caseSensitive ? g : gi) : new RegExp(escapeRegExp(normalizedText), caseSensitive ? g : gi); // 4. 在归一化字符串中查找记录匹配位置索引 let matchIndex 0; for (let i 0; i items.length; i) { const match items[i].str.match(regex); if (match) { // 5. 匹配成功调用 highlight.js 注入 SVG 框 this.highlight.add(pageNum, items[i], matchIndex, text); matchIndex match.length; } } } }逻辑说明getTextContent()返回的是带坐标的文本块数组每个item包含str原始文本、transformCSS transform 矩阵、width/height字体宽度高度。我们不直接操作 DOM而是用这些数据在 canvas 上画框。escapeRegExp()是工具函数防止用户输入.*等正则元字符导致崩溃。highlight.add()接收页码、文本项、匹配起始位置、原始关键词内部会计算该文本项在当前缩放下的真实像素坐标并创建svgrect插入到页面对应位置。SVG 层始终在 canvas 渲染层之上确保高亮不被遮挡。3. 快速运行三步跑通本地 demo验证六大功能是否生效别急着改源码先确保你能 100% 跑起来。这个插件对环境零要求不需要 Node.js不需要 webpack甚至不需要本地服务器——但必须用 http-server 或 VS Code Live Server 启动不能双击 HTML 打开。原因pdf.js 加载 PDF 时使用fetch()而file://协议下 fetch 会被浏览器拦截CORS 策略。3.1 环境准备与最小依赖验证第一步确认你有任意一个静态文件服务器。没有5 秒解决# 全局安装只需一次 npm install -g http-server # 进入项目根目录启动服务默认端口 8080 http-server -c-1打开浏览器访问http://localhost:8080/examples/plain-html/你应该看到一个带控制栏的 PDF 查看器。如果白屏或报错请立即检查控制台——90% 的问题出在这里。第二步验证 pdf.js 引擎是否加载成功。打开 DevTools → Console输入typeof PDFJSLib ! undefined // 应返回 true PDFJSLib.version // 应返回类似 2.16.105 的版本号具体以 dist 中内置版本为准如果PDFJSLib未定义说明pdfjs-dist未正确引入。检查dist/目录下是否有pdfjs-dist/build/pdf.js和pdfjs-dist/build/pdf.worker.js——这两个文件是 pdf.js 运行必需的插件已将其打包进dist/无需额外下载。第三步确认示例 PDF 存在。examples/plain-html/sample.pdf是一个 2 页的测试文档含中英文混合、表格、图片。如果它缺失你会看到 “Failed to load PDF file” 错误。此时请从项目仓库的assets/目录复制一份过来或临时替换为任意合法 PDF 文件路径。3.2 六大功能逐项验证清单打开examples/plain-html/index.html后按顺序验证以下功能每项失败都对应一个明确排查路径功能验证操作期望结果失败现象与日志线索分页点击右下角页码输入框输入2回车视图立即跳转到第 2 页页码显示变为2/2无反应 → 检查pagination.js是否绑定input事件缩放点击放大按钮三次页面内容逐步变大状态栏显示200%放大后内容模糊 → 检查zoom.js中setScale()是否传入true启用平滑搜索在搜索框输入乙方回车第 1 页出现黄色高亮框状态栏显示1 match无高亮 → 检查search.js中getTextContent()是否 resolve 成功区分大小写先搜api小写应找到再搜API全大写应找不到因 sample.pdf 中只有api两次搜索结果数量不同结果相同 → 检查searchOptions.caseSensitive是否传入true下载点击下载按钮 → 选择Current Page as PNG浏览器弹出下载对话框文件名为pdfjs-plugin-page-1.png无反应 → 检查download.js中html2canvas是否加载成功需 CDN 或本地引入打印CtrlP或点击打印按钮打印预览中只显示 PDF 内容无控制栏高亮框仍可见页眉页脚为空白打印出控制栏 → 检查print.js注入的media printCSS 是否生效注意html2canvas是下载 PNG 所需的第三方库插件未将其打包进dist/避免体积膨胀而是要求你在 HTML 中手动引入script srchttps://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js/script如果你不想用 CDN可下载html2canvas.min.js放到examples/plain-html/目录下然后改为相对路径引用。3.3 常见问题排查五条真实翻车记录与后悔药现象 → 原因 → 解决全是我在三个项目里亲手填过的坑现象搜索框输入关键词后控制台报错Uncaught (in promise) TypeError: Cannot read properties of undefined (reading items)原因page.getTextContent()返回的textContent是null常见于扫描版 PDF无文字层或加密 PDF。pdf.js 对这类文档无法提取文本。解决在search.js的performSearch函数开头加保护if (!textContent || !textContent.items || textContent.items.length 0) { console.warn(Page ${pageNum} has no text content. Skipping search.); return; }现象缩放后高亮框位置严重偏移比如搜“合同”高亮到了空白处原因highlight.js计算坐标时未考虑当前缩放比例。pdf.js 的transform矩阵是基于 100% 缩放的而 canvas 渲染层已缩放坐标未同步缩放。解决在highlight.add()中用page.getViewport({ scale: currentScale })重新获取 viewport再用viewport.convertToViewportPoint(x, y)将文本坐标转为当前视图坐标。现象点击打印按钮后页面卡死CPU 占用 100%原因print.js中调用了window.print()但 PDF 渲染 canvas 过大如 A0 尺寸浏览器渲染打印样式时内存溢出。解决添加超时保护和降级策略setTimeout(() { if (document.body.classList.contains(pdfjs-printing)) { document.body.classList.remove(pdfjs-printing); alert(打印准备超时请尝试缩小缩放比例后重试); } }, 10000); // 10秒超时现象在 Vue 3 组件中使用plugin.search()调用后无高亮但控制台显示1 match原因Vue 的响应式系统劫持了plugin实例的属性导致highlight.add()内部的 DOM 操作被 Vue 的 patch 过程干扰。解决在 Vue 组件onMounted中创建插件时用markRaw()包裹实例import { markRaw } from vue; const plugin markRaw(new PdfPlugin({ container, config }));现象dist/pdfjs-plugin.umd.js在 IE11 下报语法错误箭头函数原因UMD 构建目标设为了es2015IE11 不支持。解决修改rollup.config.js将output.format的sourcemap设为true并添加babel/preset-env插件目标浏览器设为ie 11。或者——更实际的做法在examples/目录下提供一个ie11-compat.js兼容层用function替换所有箭头函数。4. 核心代码逐段精读搜索高亮链路从入口到 DOM 渲染现在我们沉到底层把search到highlight的完整链路用真实代码串起来。这不是伪代码而是src/目录下删减注释后的可运行片段。每一行都对应一个决策点理解它们你才能安全地二次开发。4.1 入口Plugin.search()方法——参数校验与任务分发// src/index.js class PdfPlugin { // ...省略构造函数 async search(text, options {}) { // 1. 参数强校验空字符串不搜避免全页高亮 if (!text || typeof text ! string || text.trim() ) { this.clearHighlight(); // 清空旧高亮 return { count: 0, matches: [] }; } // 2. 合并默认配置与用户传入配置 const finalOptions { caseSensitive: false, wholeWord: false, ...this.config.searchOptions, ...options }; // 3. 调用搜索模块传入最终参数 const result await this.searchModule.performSearch(text, finalOptions); // 4. 触发自定义事件供外部监听 this.container.dispatchEvent(new CustomEvent(pdfjs:search-complete, { detail: { text, result, options: finalOptions } })); return result; } }参数说明text必填搜索关键词。支持空格分词如甲方 乙方会分别搜索两个词。options.caseSensitive布尔值决定是否区分大小写。注意此参数优先级高于this.config.searchOptions方便单次搜索覆盖全局配置。options.wholeWord布尔值开启后只匹配完整单词如搜api不会匹配apiary。提示CustomEvent是插件与宿主页面解耦的关键。你在 Vue 组件里可以这样监听script setup const container ref(); onMounted(() { container.value.addEventListener(pdfjs:search-complete, (e) { console.log(搜到啦, e.detail.result.count, 处); }); }); /script4.2 中间层searchModule.performSearch()——文本提取与匹配// src/features/search.js export class SearchModule { constructor(plugin) { this.plugin plugin; } async performSearch(text, options) { const { caseSensitive, wholeWord } options; const normalizedText caseSensitive ? text : text.toLowerCase(); const regex this._buildRegex(normalizedText, wholeWord, caseSensitive); let totalMatches 0; const allMatches []; // 1. 遍历所有页面支持异步并发但为稳定性设为串行 for (let pageNum 1; pageNum this.plugin.getPageCount(); pageNum) { const page await this.plugin.pdfDoc.getPage(pageNum); const textContent await page.getTextContent(); // 2. 安全校验 textContent if (!textContent || !textContent.items || textContent.items.length 0) continue; // 3. 遍历每个文本项 for (let i 0; i textContent.items.length; i) { const item textContent.items[i]; const itemStr caseSensitive ? item.str : item.str.toLowerCase(); // 4. 正则匹配记录所有匹配起始位置 let match; while ((match regex.exec(itemStr)) ! null) { const matchStart match.index; const matchLength match[0].length; // 5. 将匹配信息存入结果供 highlight 模块消费 allMatches.push({ pageNum, itemIndex: i, matchStart, matchLength, originalText: item.str // 保留原始大小写用于显示 }); totalMatches; } } } // 6. 批量触发高亮性能优化避免逐个调用 if (totalMatches 0) { this.plugin.highlight.batchAdd(allMatches); } return { count: totalMatches, matches: allMatches }; } _buildRegex(text, wholeWord, caseSensitive) { const escaped text.replace(/[.*?^${}()|[\]\\]/g, \\$); const flags caseSensitive ? g : gi; const pattern wholeWord ? \\b${escaped}\\b : escaped; return new RegExp(pattern, flags); } }关键逻辑说明_buildRegex()是安全边界text.replace(/[.*?^${}()|[\]\\]/g, \\$)对用户输入做正则转义防止search(*)导致SyntaxError: Invalid regular expression。batchAdd()是性能关键如果一页有 50 个匹配逐个调用highlight.add()会触发 50 次 DOM 重排。batchAdd()将所有匹配收集后一次性创建 SVG 元素并 append重排次数从 N 降到 1。originalText字段被保留是为了在高亮框 tooltip 中显示原文如鼠标悬停显示“API接口”而非小写的“api接口”。4.3 渲染层highlightModule.add()——SVG 坐标计算与注入// src/features/highlight.js export class HighlightModule { constructor(plugin) { this.plugin plugin; this.svgContainer null; // 全局 SVG 容器每个页面一个 } // batchAdd 是对外接口内部调用 _addSingle batchAdd(matches) { // 1. 按页码分组每页创建独立 SVG const grouped matches.reduce((acc, match) { if (!acc[match.pageNum]) acc[match.pageNum] []; acc[match.pageNum].push(match); return acc; }, {}); // 2. 遍历每页批量渲染 Object.entries(grouped).forEach(([pageNum, pageMatches]) { this._renderPageHighlights(parseInt(pageNum), pageMatches); }); } _renderPageHighlights(pageNum, matches) { const pageView this.plugin.getPageView(pageNum); if (!pageView) return; // 1. 确保该页有 SVG 容器 let svg this._getOrCreateSvgForPage(pageNum); svg.innerHTML ; // 清空旧高亮 // 2. 遍历该页所有匹配 matches.forEach(match { const item this.plugin.getTextItem(pageNum, match.itemIndex); if (!item) return; // 3. 关键用 pdf.js 的 viewport 计算真实像素坐标 const viewport pageView.viewport; const transformed viewport.convertToViewportPoint( item.transform[4], // x item.transform[5] // y ); // 4. 计算高亮框宽高基于字体宽度和行高 const width item.width * viewport.scale; const height item.height * viewport.scale; // 5. 创建 SVG rect 并设置样式 const rect document.createElementNS(http://www.w3.org/2000/svg, rect); rect.setAttribute(x, transformed[0]); rect.setAttribute(y, transformed[1]); rect.setAttribute(width, width); rect.setAttribute(height, height); rect.setAttribute(fill, rgba(255, 255, 0, 0.5)); rect.setAttribute(stroke, #ffcc00); rect.setAttribute(stroke-width, 1); // 6. 添加 tooltip鼠标悬停显示原文 const title document.createElementNS(http://www.w3.org/2000/svg, title); title.textContent match.originalText; rect.appendChild(title); svg.appendChild(rect); }); } _getOrCreateSvgForPage(pageNum) { let svg this.plugin.container.querySelector(.pdfjs-highlight-svg[data-page${pageNum}]); if (!svg) { svg document.createElementNS(http://www.w3.org/2000/svg, svg); svg.setAttribute(class, pdfjs-highlight-svg); svg.setAttribute(data-page, pageNum); svg.setAttribute(style, position: absolute; top: 0; left: 0; pointer-events: none;); this.plugin.container.appendChild(svg); } return svg; } }坐标计算说明item.transform[4]和item.transform[5]是 pdf.js 文本项的xy坐标单位PDF 用户空间1/72 英寸。viewport.convertToViewportPoint()是 pdf.js 提供的官方方法它把用户空间坐标转换为当前缩放、旋转后的视图坐标单位CSS 像素这是唯一可靠的方式。不要自己用scale相乘那会出错。width和height也要乘以viewport.scale否则高亮框在缩放后会变形。5. 二次开发实战如何添加「书签导航」与「OCR 文字识别」能力现在你已经跑通、理解了核心链路。接下来我要带你加两个真实项目里高频提出的增强需求书签导航Outline和OCR 文字识别当 PDF 是扫描件时。这不是理论而是我给某在线教育平台做的落地改造全程在src/目录下新增文件不碰原有模块。5.1 增加书签导航解析 PDF Outline 并生成侧边栏PDF 文件可内嵌大纲Outline即左侧导航树。原生 pdf.js 支持pdfDoc.getOutline()但插件未暴露。我们来补上。第一步新建src/features/outline.js// src/features/outline.js export class OutlineModule { constructor(plugin) { this.plugin plugin; this.outline null; } async init() { if (this.outline ! null) return this.outline; try { this.outline await this.plugin.pdfDoc.getOutline(); this._renderSidebar(); return this.outline; } catch (err) { console.warn(Failed to load outline:, err); return null; } } _renderSidebar() { const sidebar document.createElement(div); sidebar.className pdfjs-outline-sidebar; sidebar.style.cssText position: absolute; top: 0; right: 0; width: 240px; height: 100%; background: #f8f9fa; border-left: 1px solid #dee2e6; overflow-y: auto; z-index: 1000; ; const title document.createElement(h3); title.textContent 文档大纲; title.style.cssText padding: 12px 16px; margin: 0; font-size: 14px; color: #495057;; sidebar.appendChild(title); const list document.createElement(ul); list.style.cssText margin: 0; padding: 0; list-style: none;; this._renderOutlineItems(list, this.outline || []); sidebar.appendChild(list); this.plugin.container.appendChild(sidebar); } _renderOutlineItems(parent, items) { items.forEach(item { const li document.createElement(li); li.style.cssText padding: 4px 16px; cursor: pointer; font-size: 13px;; li.textContent item.title; li.title item.title; // 点击跳转到对应页 li.addEventListener(click, () { if (item.dest Array.isArray(item.dest)) { const pageNum item.dest[0]; // PDF dest 数组第一个元素是页码从 0 开始 this.plugin.goToPage(pageNum 1); // 插件页码从 1 开始 } }); parent.appendChild(li); if (item.items item.items.length 0) { const subList document.createElement(ul); subList.style.cssText margin: 0; padding-left: 20px; list-style: none;; this._renderOutlineItems(subList, item.items); parent.appendChild(subList); } }); } }第二步在index.js中注册模块// src/index.js在 constructor 末尾添加 this.outlineModule new OutlineModule(this); // 在 load() 方法成功回调里调用 await this.outlineModule.init();效果加载 PDF 后右侧自动弹出大纲侧边栏点击标题即可跳转。getOutline()返回的是标准 PDF Outline 结构支持多级嵌套无需额外解析。5.2 增加 OCR 文字识别当getTextContent()为空时调用 Tesseract.js扫描版 PDF 没有文字层search会失效。我们接入tesseract.js做客户端 OCR代价是首次搜索变慢需加载 wasm但换来的是真正的全文检索能力。第一步安装依赖npm install tesseract.js第二步新建src/features/ocr-search.js// src/features/ocr-search.js import { createWorker } from tesseract.js; export class OcrSearchModule { constructor(plugin) { this.plugin plugin; this.worker null; } async init() { if (!this.worker) { this.worker createWorker({ logger: m console.log(Tesseract: ${m.progress * 100 | 0}%) }); await this.worker.load(); await this.worker.loadLanguage(chi_sim); // 中文简体 await this.worker.initialize(chi_sim); } } async searchOnPage(pageNum, text, options) { const page await this.plugin.pdfDoc.getPage(pageNum); const viewport page.getViewport({ scale: 2.0 }); // 高清截图提升 OCR 准确率 const canvas document.createElement(canvas); const ctx canvas.getContext(2d); canvas.width viewport.width; canvas.height viewport.height; // 渲染页面到 canvas const renderContext { canvasContext: ctx, viewport: viewport }; await page.render(renderContext).promise; // OCR 识别 const { data: { text: ocrText } } await this.worker.recognize(canvas); // 在 OCR 文本中搜索复用原有逻辑 const regex this._buildRegex(text, options.wholeWord, options.caseSensitive); const matches []; let match; while ((match regex.exec(ocrText)) ! null) { matches.push({ pageNum, ocrText, matchStart: match.index, matchLength: match[0].length }); } return matches; } _buildRegex(text, wholeWord, caseSensitive) { const escaped text.replace(/[.*?^${}()|[\]\\]/g, \\$); const flags caseSensitive ? g : gi; const pattern wholeWord ? \\b${escaped}\\b : escaped; return new RegExp(pattern, flags); } }第三步改造search.js加入 OCR 回退机制// src/features/search.js在 performSearch 方法中 // ...原有代码 for (let pageNum 1; pageNum this.plugin.getPageCount(); pageNum) { const page await this.plugin.pdfDoc.getPage(pageNum); const textContent await page.getTextContent(); if (textContent textContent.items textContent.items.length 0) { // 原有逻辑用 textContent 搜索 } else { // 回退调用 OCR 搜索 console.log(Page ${pageNum} has no text layer. Falling back to OCR.); const ocrMatches await this.ocrModule.searchOnPage(pageNum, text, options); allMatches.push(...ocrMatches); } }提示OCR 是 CPU 密集型操作务必在 Web Worker 中运行。tesseract.js默认使用 Worker你无需额外处理。但要注意首次调用recognize()会加载约 5MB 的 wasm 文件建议在用户点击搜索按钮前预加载 worker。5.3 最后一条血泪经验永远用plugin.destroy()清理资源这个插件会创建多个 canvas、svg、event listener、甚至 tesseract worker。如果你在 SPA 中频繁切换 PDF 查看页比如 Vue Router 跳转不清理会导致内存泄漏页面越用越卡。本文还有配套的精品资源点击获取