简介:pdf.js 1.9.426 是一份纯 JavaScript 实现的免费开源 PDF 阅读组件,面向需要在网页中嵌入 PDF 预览功能的开发者,尤其适合不想让用户额外安装插件的前端项目。它支持缩放、打印、查找等常用操作,兼容火狐、谷歌及 IE9 以上主流浏览器,基于 H5 技术,IE8 及更低版本无法使用;由于文件体积偏大,加载速度会受到一定影响。压缩包共 372 个文件,约 5.54MB,其中 168 个 bcmap 字符映射文件用于多语言编码支持,105 个 properties 与 74 个 png、9 个 svg 承担界面资源与本地化,另有 4 个 js、3 个 map 及 css、html 等构成核心运行文件。目前已有 989 人学习下载。借助这份资源,读者可直接获得可运行的阅读器源码与完整目录结构,便于二次集成、样式定制与功能裁剪,也能通过字符映射文件理解中文等多语言 PDF 的渲染机制,为排查显示异常提供参考。
1. 为什么还有人死磕 pdf.js 1.9.426:一份能直接跑的老版本包
接手一个老项目,前端要在浏览器里预览 PDF,后端接口返回的是二进制流,不能依赖任何外部服务。翻遍现有方案,要么体积太大,要么跟当前这套老构建链不兼容。最后翻出pdfjs1.9.426.rar这个包,解压一看,是 pdf.js 1.9.426 的完整发行版——包含build目录下的pdf.js、pdf.worker.js,以及配套的 viewer 页面和字符映射资源。它解决的核心问题很直接:在浏览器端把 PDF 渲染成 canvas,不依赖服务端转图片,也不依赖任何在线 CDN。适合谁?维护老系统、构建工具还停留在几年前、需要离线部署 PDF 预览的从业者。这个版本不算新,但胜在稳定、依赖少、能直接塞进项目里跑起来,不用折腾现代打包器的各种配置。
2. 拆包看结构:1.9.426 里到底有什么、该动哪几个文件
拿到压缩包先别急着往项目里扔,解压后把目录结构看清楚,后面改路径、配 worker 才不会翻车。pdf.js 1.9.426 的发行包结构跟现在的新版本差别不小,尤其是 viewer 的组织方式,很多人第一次用会找不到入口。
2.1 目录清单与各文件职责
解压后典型结构如下(不同打包方式可能略有出入,但核心文件一致):
| 路径 | 作用 | 是否必须 |
|---|---|---|
build/pdf.js | 主库,提供PDFJS全局对象和 API | 必须 |
build/pdf.worker.js | 渲染工作线程,解析和绘制都在这里跑 | 必须 |
web/viewer.html | 官方 viewer 页面 | 按需 |
web/viewer.js | viewer 逻辑 | 按需 |
web/cmaps/ | 中文等 CJK 字符映射 | 中文 PDF 必须 |
web/images/ | viewer 用到的图标 | 按需 |
核心就两个:pdf.js和pdf.worker.js。前者是主线程入口,后者是真正干活的 worker。两者版本必须严格一致,混用不同版本的这两个文件,最常见的现象是控制台报The API version "x" does not match the Worker version "y",然后页面白屏。
2.2 引入方式:script 标签还是模块化
1.9.426 这个年代的包,默认是 UMD 风格,直接 script 引入最省事。现代项目里如果非要用 import,得确认构建工具能处理这种老式全局导出,否则容易出问题。我一般在这种老版本上直接用 script 标签,稳。
<!-- 先引入主库,它会挂载全局 PDFJS 对象 --> <script src="./build/pdf.js"></script> <script> // 指定 worker 路径,这一步不能漏,否则 pdf.js 会去默认路径找,大概率 404 PDFJS.workerSrc = './build/pdf.worker.js'; </script>逻辑说明:pdf.js加载后会暴露全局PDFJS,workerSrc告诉主库去哪里加载 worker 脚本。参数说明:workerSrc必须是可访问的 URL,相对路径以当前页面为基准。如果项目部署在子路径下,这里要写对,否则 worker 加载失败,渲染直接卡住。
2.3 最小渲染示例:把第一页画到 canvas
光引入还不够,得验证它真能渲染。下面这段是最小可用示例,把 PDF 第一页画到 canvas 上。
// 假设页面上有个 <canvas id="pdfCanvas"></canvas> var canvas = document.getElementById('pdfCanvas'); var ctx = canvas.getContext('2d'); // 用 URL 或 ArrayBuffer 都行,这里以 URL 为例 PDFJS.getDocument('./sample.pdf').then(function (pdf) { // 取第一页 return pdf.getPage(1); }).then(function (page) { // 按 1.5 倍缩放,避免默认尺寸太小看不清 var viewport = page.getViewport(1.5); canvas.width = viewport.width; canvas.height = viewport.height; var renderContext = { canvasContext: ctx, viewport: viewport }; // render 返回一个 promise,渲染完成后再做后续操作 return page.render(renderContext).promise; }).then(function () { console.log('第一页渲染完成'); }).catch(function (err) { // 这里一定要打日志,否则出错只能看到白屏 console.error('渲染失败:', err); });逻辑说明:getDocument加载文档返回 promise,getPage取页,getViewport算尺寸,render真正绘制。参数说明:getViewport(scale)里的 scale 控制缩放,1.0 是原始尺寸;renderContext里canvasContext是 2D 上下文,viewport决定画布大小。注意render返回的是带.promise的对象,直接 await 那个对象在部分环境里行为不一致,取.promise更稳。
3. 参数调优与常见配置:缩放、分页、中文乱码怎么处理
能渲染第一页只是起点,实际项目里要处理多页、缩放、中文显示这些问题。这一章把几个高频配置讲透,都是能直接抄的。
3.1 缩放与清晰度:devicePixelRatio 不能忽略
默认渲染出来的 PDF 在高分屏上会糊,原因是 canvas 的 CSS 尺寸和实际像素尺寸没对齐。正确做法是把设备像素比算进去。
var outputScale = window.devicePixelRatio || 1; var viewport = page.getViewport(1.5); // canvas 实际像素尺寸放大 canvas.width = Math.floor(viewport.width * outputScale); canvas.height = Math.floor(viewport.height * outputScale); // CSS 显示尺寸保持逻辑尺寸 canvas.style.width = Math.floor(viewport.width) + 'px'; canvas.style.height = Math.floor(viewport.height) + 'px'; var transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null; var renderContext = { canvasContext: ctx, viewport: viewport, transform: transform }; page.render(renderContext);逻辑说明:outputScale取设备像素比,canvas 的 width/height 按比例放大,再用 CSS 把显示尺寸压回逻辑尺寸,最后通过transform让绘制内容跟着放大。参数说明:transform是一个 6 元数组,对应 canvas 的变换矩阵,缩放时前两个值就是 scaleX 和 scaleY。不加这一步,Retina 屏上文字边缘会发虚。
3.2 多页渲染与按需加载
一次性把所有页都渲染出来,页数一多内存直接爆。常见做法是只渲染当前页,翻页时再渲染目标页。
var pdfDoc = null; var currentPage = 1; function renderPage(num) { pdfDoc.getPage(num).then(function (page) { var viewport = page.getViewport(1.5); canvas.width = viewport.width; canvas.height = viewport.height; page.render({ canvasContext: ctx, viewport: viewport }); currentPage = num; }); } PDFJS.getDocument('./sample.pdf').then(function (pdf) { pdfDoc = pdf; renderPage(1); }); // 翻页按钮 document.getElementById('next').addEventListener('click', function () { if (currentPage < pdfDoc.numPages) { renderPage(currentPage + 1); } });逻辑说明:把pdfDoc缓存起来,避免每次翻页重新加载整个文档。参数说明:pdfDoc.numPages是总页数,用来做边界判断。这里没做预渲染,翻页时会有短暂空白,对体验要求高的可以提前渲染下一页到离屏 canvas。
3.3 中文乱码:cmaps 目录必须带上
渲染中文 PDF 时最常见的翻车是文字变成方块或乱码。原因是 pdf.js 需要 cmaps 里的字符映射表来解析 CJK 编码。
PDFJS.getDocument({ url: './sample.pdf', cMapUrl: './web/cmaps/', // 指向 cmaps 目录 cMapPacked: true // 1.9.426 里打包的 cmaps 是压缩过的,设为 true }).then(function (pdf) { // ... });逻辑说明:cMapUrl告诉 pdf.js 去哪里找字符映射文件,cMapPacked表示这些文件是压缩格式。参数说明:cMapUrl末尾的斜杠不能少,否则路径拼接会出错;cMapPacked在 1.9.426 里通常设为 true,因为发行包里的 cmaps 是.bcmap压缩格式。如果中文还是乱码,先检查这个目录有没有被完整拷贝到部署环境。
4. 避坑与排查:这几个问题我踩过不止一次
老版本 pdf.js 的坑集中在路径、版本、跨域和字体上。下面几条都是实际项目里反复出现的,按「现象 → 原因 → 解决」整理。
4.1 worker 加载失败,页面白屏
现象:控制台报Failed to load worker或Setting up fake worker,PDF 不渲染。 原因:workerSrc路径写错,或者部署时pdf.worker.js没被一起打包进去。 解决:打开浏览器 Network 面板,确认pdf.worker.js的请求返回 200。如果返回 404,检查workerSrc的相对路径;如果项目用了打包工具,确认 worker 文件被复制到了输出目录。
4.2 版本不匹配报错
现象:控制台报The API version "1.9.426" does not match the Worker version "x.x.x"。 原因:pdf.js和pdf.worker.js来自不同版本,或者项目里同时存在两份 pdf.js。 解决:确保这两个文件来自同一个压缩包,全局搜索项目里有没有其他地方也引入了 pdf.js,把多余的删掉。
4.3 跨域加载 PDF 失败
现象:getDocument报Unexpected server response或 CORS 相关错误。 原因:PDF 文件所在域和页面域不同,且服务端没返回允许跨域的响应头。 解决:要么让服务端加上Access-Control-Allow-Origin,要么把 PDF 通过接口以 ArrayBuffer 形式取回来再传给getDocument。后者更可控,推荐。
// 通过 fetch 取二进制,再交给 pdf.js fetch('./sample.pdf') .then(function (res) { return res.arrayBuffer(); }) .then(function (buffer) { return PDFJS.getDocument(buffer); }) .then(function (pdf) { // 后续渲染 });4.4 中文 PDF 字体缺失导致渲染异常
现象:中文能显示但字体不对,或者部分文字缺失。 原因:PDF 内嵌字体没被正确解析,或者 cmaps 没配全。 解决:确认cMapUrl指向的目录包含完整的 cmaps 文件;如果 PDF 用的是非标准字体,考虑在服务端做字体嵌入预处理。这个坑比较玄学,同样的配置换个 PDF 可能就好了,所以测试时多准备几份不同来源的中文 PDF。
4.5 内存泄漏:反复渲染不释放
现象:翻页几十次后页面卡顿,内存占用持续上涨。 原因:每次render都创建新的 canvas 上下文或没清理旧的渲染任务。 解决:翻页时复用同一个 canvas,渲染前调用page.cleanup()释放页面资源;如果用了离屏 canvas 做预渲染,记得在切换后把旧的置空。
5. 进阶技巧:把 1.9.426 塞进现代构建链并验证渲染结果
老版本包不代表只能配老项目。实际维护中,经常需要把它接进 webpack 或 vite 这类现代构建工具,同时还要有一套验证渲染是否正确的办法。这一章讲两个具体技巧。
5.1 在现代构建工具里引入老版本 pdf.js
直接 import 老版本 UMD 包,构建工具可能报错。稳妥做法是把pdf.js和pdf.worker.js当作静态资源处理,通过copy插件复制到输出目录,再用全局变量访问。
// vite.config.js 片段 import { defineConfig } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; export default defineConfig({ plugins: [ viteStaticCopy({ targets: [ // 把 pdf.js 相关文件原样复制到输出目录 { src: 'static/pdfjs/build/pdf.js', dest: 'pdfjs/build' }, { src: 'static/pdfjs/build/pdf.worker.js', dest: 'pdfjs/build' }, { src: 'static/pdfjs/web/cmaps', dest: 'pdfjs/web' } ] }) ] });逻辑说明:用静态复制插件把老包原封不动搬到输出目录,避免构建工具去解析它的模块格式。参数说明:src是源路径,dest是输出目录下的相对路径。然后在页面里用 script 标签或动态创建 script 的方式加载,workerSrc指向复制后的路径。
// 动态加载,避免打包工具介入 function loadPdfJs() { return new Promise(function (resolve) { var script = document.createElement('script'); script.src = '/pdfjs/build/pdf.js'; script.onload = function () { PDFJS.workerSrc = '/pdfjs/build/pdf.worker.js'; resolve(); }; document.head.appendChild(script); }); }逻辑说明:动态创建 script 标签加载,加载完成后设置workerSrc。参数说明:路径要和复制插件的dest对应。这种方式的好处是构建工具完全不碰 pdf.js 的源码,兼容性最好。
5.2 验证渲染结果:用 canvas 像素比对做回归
改了配置或升级了依赖后,怎么确认渲染没出问题?靠肉眼看不可靠。我一般用 canvas 像素比对做简单回归:渲染同一页到两个 canvas,逐像素比较差异比例。
function compareCanvas(canvasA, canvasB) { var ctxA = canvasA.getContext('2d'); var ctxB = canvasB.getContext('2d'); var dataA = ctxA.getImageData(0, 0, canvasA.width, canvasA.height).data; var dataB = ctxB.getImageData(0, 0, canvasB.width, canvasB.height).data; var diff = 0; var total = dataA.length / 4; for (var i = 0; i < dataA.length; i += 4) { // 只要 RGB 任一通道差异超过阈值就算不同 if (Math.abs(dataA[i] - dataB[i]) > 10 || Math.abs(dataA[i + 1] - dataB[i + 1]) > 10 || Math.abs(dataA[i + 2] - dataB[i + 2]) > 10) { diff++; } } return diff / total; // 返回差异像素占比 }逻辑说明:取两个 canvas 的像素数据,逐像素比较 RGB 通道,超过阈值就计为差异。参数说明:阈值 10 是经验值,太小会把抗锯齿差异也算进去,太大又漏掉真实问题。差异占比低于 1% 一般可以认为渲染一致。这个办法适合在 CI 里跑,把基准 canvas 存成图片,每次构建后比对。
5.3 一个容易忽略的细节:渲染完成时机
page.render返回的 promise resolve 之后,canvas 内容不一定立即可读。某些浏览器里需要等一帧。做像素比对时,如果发现结果不稳定,在 render 完成后加一个requestAnimationFrame再取像素。
page.render(renderContext).promise.then(function () { return new Promise(function (resolve) { requestAnimationFrame(resolve); }); }).then(function () { // 这时候再取 canvas 像素才稳 var data = ctx.getImageData(0, 0, canvas.width, canvas.height); });逻辑说明:requestAnimationFrame等浏览器完成当前帧的绘制,确保 canvas 内容已经落盘。参数说明:这个等待只影响读取时机,不影响渲染本身。做自动化验证时加上这一步,能避免很多偶发的比对失败。
从那以后我每次接老版本 pdf.js,都强制先跑一遍最小渲染示例,确认 worker 加载、中文显示、缩放清晰度这三项没问题,再往业务代码里集成。希望帮到你。
本文还有配套的精品资源,点击获取