在Web应用中集成 PDF.js: 通过jsdelivr实现动态加载与批注的思考
liuian 2025-05-28 18:48 5 浏览
PDF 文档在现代 Web 应用中越来越常见,无论是作为文档预览、报告展示还是在线编辑的载体。Mozilla 的 PDF.js 是一个功能强大的 JavaScript 库,它使得在浏览器端渲染和显示 PDF 文件成为可能,无需依赖原生插件。
本文将深入探讨如何在你的项目中使用 pdfjs-dist 库的 5.2 版本,特别关注其通过 jsdelivr 引入的 ESM (ECMAScript Module) 版本 (.mjs 文件),并在此基础上实现 PDF 文件的动态加载,同时对实现批注功能给出思路和指导。
为什么选择 pdfjs-dist 5.2 ESM 和 jsdelivr?
- pdfjs-dist: 这是 PDF.js 的发布版本,包含了核心渲染代码和相关的构建产物,方便在项目中使用。
- 5.2 版本: 选择特定版本有助于保证代码的稳定性,避免未来版本更新可能带来的兼容性问题。
- ESM (.mjs): ECMAScript Modules 是现代 JavaScript 的标准模块系统。使用 ESM 可以更好地组织代码、提高性能(如 tree shaking)并避免全局变量污染。它需要现代浏览器的支持,并通过 <script type="module"> 标签引入。
- jsdelivr: 这是一个免费的、快速的 CDN (Content Delivery Network),可以直接从 npm 包获取文件。使用 CDN 可以加速库的加载,减轻自己服务器的压力。
第一步:核心集成 - 引入pdfjs-distESM 版本
使用 ESM 格式引入库需要在你的 HTML 文件中做一些调整。我们需要引入 pdf.min.mjs(核心库)和 pdf.worker.min.mjs(用于在 Web Worker 中执行耗时任务)。
在你的 HTML 文件中,使用 <script type="module"> 标签来编写或引用你的 JavaScript 代码:
<!DOCTYPE html>
<html>
<head>
<title>PDF.js ESM Integration</title>
<meta charset="UTF-8">
<style>
/* 可以添加一些基本的样式 */
#pdf-viewer-container {
width: 800px; /* 根据需要设置容器宽度 */
margin: 20px auto;
border: 1px solid #ccc;
overflow: auto; /* 如果PDF很大需要滚动 */
}
/* 其他样式将在后续步骤中添加 */
</style>
</head>
<body>
<h1>PDF.js ESM Example</h1>
<!-- 你的 PDF 查看器 UI 元素将在这里 -->
<!-- 使用 type="module" 引入你的主要 JavaScript 文件 -->
<!-- 假设你的主要逻辑在 main.js 中 -->
<script type="module" src="./main.js"></script>
</body>
</html>
在你的 main.js 文件中,使用 import 语句从 jsdelivr 引入 pdfjs-dist:
// 从 jsdelivr 引入 pdfjs-dist 的核心模块
// 使用具体的版本号 5.2.133 (这是一个示例版本号,请根据实际需要调整)
import { getDocument, GlobalWorkerOptions } from 'https://cdn.jsdelivr.net/npm/pdfjs-dist@5.2.133/build/pdf.min.mjs';
// 设置 workerSrc,这是 pdfjs-dist 必须的配置
// workerSrc 应该指向 pdf.worker.min.mjs 文件,且版本应与主库一致
GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@5.2.133/build/pdf.worker.min.mjs';
// 现在你可以使用导入的 getDocument 函数来加载 PDF
// 例如加载一个远程 PDF 文件 (这将在下一节详细展开)
/*
async function loadSamplePdf() {
const samplePdfUrl = 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf';
try {
const loadingTask = getDocument(samplePdfUrl);
const pdfDocument = await loadingTask.promise;
console.log('Sample PDF loaded:', pdfDocument);
// TODO: 渲染 PDF 页面
} catch (error) {
console.error('Error loading sample PDF:', error);
}
}
loadSamplePdf(); // 页面加载后尝试加载一个示例 PDF
*/
解释:
- <script type="module" src="./main.js">:告诉浏览器加载 ./main.js 文件作为一个 ES 模块。
- import { getDocument, GlobalWorkerOptions } from '...':使用 ESM 导入语法,从 jsdelivr 上的 pdf.min.mjs 导入 getDocument 函数和 GlobalWorkerOptions 对象。
- GlobalWorkerOptions.workerSrc = '...':非常重要,这配置了 PDF.js worker 脚本的 URL。worker 负责在后台处理 PDF 的解析,避免阻塞主线程,提高用户体验。
第二步:实现 PDF 文件的动态加载与渲染
在实际应用中,你通常需要根据用户的操作(例如选择文件或输入 URL)来加载不同的 PDF 文件。我们需要一个 HTML 结构来接收用户输入,并编写 JavaScript 代码来处理加载和渲染过程。
扩展你的 HTML (在 <body> 内):
<!-- ... (head and previous body content) ... -->
<body>
<h1>My Dynamic PDF Viewer</h1>
<input type="file" id="pdfFilePicker" accept="application/pdf">
<button id="loadPdfButton">Load Selected PDF</button>
<button id="loadUrlPdfButton">Load Sample PDF from URL</button>
<div id="pdf-viewer-container">
<!-- PDF 页面将渲染到这里 -->
</div>
<script type="module" src="./main.js"></script>
</body>
</html>
添加必要的 CSS (在 <style> 标签内):
/* ... (previous styles) ... */
.pdfPage {
margin-bottom: 10px; /* 页与页之间的间距 */
border-bottom: 1px solid #eee; /* 页之间分隔线 */
position: relative; /* 为后续添加批注层做准备 */
box-shadow: 0 0 8px rgba(0,0,0,0.1); /* 添加一些阴影效果 */
}
.pdfPage canvas {
display: block; /* 防止 canvas 下方出现空白 */
margin: 0 auto; /* canvas 居中 */
}
/* 批注层样式,如果需要 */
.annotationLayer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* 默认不捕获鼠标事件,除非需要交互 */
overflow: hidden; /* 避免批注超出页面边界 */
}
修改 main.js 来实现动态加载和渲染逻辑:
// ... (import and workerSrc setup) ...
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
const pdfFilePicker = document.getElementById('pdfFilePicker');
const loadPdfButton = document.getElementById('loadPdfButton');
const loadUrlPdfButton = document.getElementById('loadUrlPdfButton');
let pdfDocument = null; // 存储当前加载的 PDF 文档对象
// 函数:渲染单个页面
async function renderPage(pageNum, pdfDocument) {
if (!pdfDocument) return; // 确保文档已加载
try {
const page = await pdfDocument.getPage(pageNum);
const scale = 1.5; // 渲染比例,可以根据需要调整
const viewport = page.getViewport({ scale: scale });
// 创建一个 div 容器用于包裹 canvas 和其他层 (如批注层)
const pageDiv = document.createElement('div');
pageDiv.className = 'pdfPage';
// 设置 pageDiv 的尺寸以匹配渲染后的页面尺寸
pageDiv.style.width = `${viewport.width}px`;
pageDiv.style.height = `${viewport.height}px`;
pageDiv.dataset.pageNumber = pageNum; // 存储页码方便后续查找
// 创建 canvas 元素用于渲染 PDF 内容
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
pageDiv.appendChild(canvas); // 将 canvas 添加到 pageDiv 中
// 创建一个用于自定义批注的层 (将在第三步讨论)
const annotationLayer = document.createElement('div');
annotationLayer.className = 'annotationLayer';
// annotationLayer.style.width = `${viewport.width}px`; // 批注层尺寸通常与 viewport 一致
// annotationLayer.style.height = `${viewport.height}px`;
// 批注层需要定位在 pageDiv 内部,且覆盖 canvas
// 通过 CSS .annotationLayer 设置 absolute position 和 top/left 0 即可
pageDiv.appendChild(annotationLayer); // 将批注层添加到 pageDiv 中
pdfViewerContainer.appendChild(pageDiv); // 将 pageDiv 添加到主容器
// 渲染 PDF 页面内容到 canvas
const renderContext = {
canvasContext: context,
viewport: viewport,
// 如果需要渲染内置的文本层或批注层,可以在这里指定容器
// 引入并使用 TextLayerBuilder 和 AnnotationLayerBuilder 会增加代码复杂度
// textLayer: textLayerDiv,
// annotationLayer: annotationLayerDiv,
// annotationMode: pdfjsLib.AnnotationMode.ENABLE_FORMS,
};
// 执行渲染,这是一个异步操作
await page.render(renderContext).promise;
console.log(`Page ${pageNum} rendered.`);
// TODO: 如果需要渲染内置文本层和批注层,在这里调用其 render 方法
// 例如 textLayer.render(); annotationLayer.render();
} catch (error) {
console.error(`Error rendering page ${pageNum}:`, error);
// 可以在页面位置显示一个错误消息
}
}
// 函数:加载并渲染整个 PDF
async function loadAndRenderPdf(pdfData) {
// 清空之前的渲染内容
pdfViewerContainer.innerHTML = '';
pdfDocument = null; // 清除之前加载的文档对象
try {
// 加载 PDF 文档,pdfData 可以是 URL 字符串、ArrayBuffer、Blob 等
const loadingTask = getDocument(pdfData);
pdfDocument = await loadingTask.promise;
console.log('PDF loaded:', pdfDocument);
const numPages = pdfDocument.numPages;
console.log('Number of pages:', numPages);
// 循环渲染每一页
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
// 使用 Promise.resolve() 包裹 renderPage 可以让循环继续,而无需等待每页渲染完成
// 这样可以更快地显示第一页
Promise.resolve().then(() => renderPage(pageNum, pdfDocument));
}
// TODO: 如果需要处理 PDF 元数据、大纲、缩略图等,可以在这里访问 pdfDocument 对象
} catch (reason) {
console.error('Error during PDF loading:', reason);
alert(`Failed to load PDF: ${reason.message || reason}`); // 给用户提示
}
}
// 事件监听器:加载本地文件
loadPdfButton.addEventListener('click', () => {
const file = pdfFilePicker.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const arrayBuffer = e.target.result;
loadAndRenderPdf(arrayBuffer); // 加载 ArrayBuffer
};
reader.onerror = function(e) {
console.error("FileReader error:", e);
alert("Error reading file.");
}
reader.readAsArrayBuffer(file);
} else {
alert('Please select a PDF file.');
}
});
// 事件监听器:加载示例 URL
loadUrlPdfButton.addEventListener('click', () => {
const samplePdfUrl = 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'; // 替换为你自己的 PDF URL
loadAndRenderPdf(samplePdfUrl); // 加载 URL
});
// 注意:你的 HTML 文件需要通过 Web 服务器打开 (http:// 或 https://),
// 直接用浏览器打开本地文件 (file://) 可能因为跨域问题导致 worker 加载失败或 PDF 文件无法加载。
解释上述代码:
- #pdf-viewer-container:一个容器,用于容纳所有渲染后的 PDF 页面。
- input[type="file"] 和 button:用于触发本地文件选择和加载示例 URL。
- pdfDocument: 存储通过 getDocument 加载成功后的 PDF 文档对象。
- loadAndRenderPdf(pdfData):核心函数,接收 PDF 数据(可以是 URL 或 ArrayBuffer),清空容器,调用 getDocument 加载,然后遍历所有页码,为每一页调用 renderPage。
- renderPage(pageNum, pdfDocument):为指定的页码创建一个 div.pdfPage,内部包含一个 canvas 用于绘制 PDF 内容,以及一个 div.annotationLayer 用于后续的自定义批注。计算 viewport 并设置 canvas 尺寸,然后调用 page.render() 将页面内容绘制到 canvas。
- 事件监听器:分别为文件选择按钮和加载 URL 按钮添加点击事件,读取文件或指定 URL,然后调用 loadAndRenderPdf。
至此,你已经构建了一个基本的 PDF 查看器,可以动态加载本地或远程的 PDF 文件并将其渲染到页面上。
第三步:实现自定义批注功能
重要提示: pdfjs-dist 库的主要功能是渲染 PDF 内容,包括显示 PDF 文件中已有的批注。它不提供添加、编辑或保存新的批注的功能。实现批注(如高亮、下划线、矩形框、文本框等)是一个需要在 PDF 渲染层之上自定义构建的功能。
实现自定义批注功能的整体思路是在每个 PDF 页面渲染出的 canvas 上方,叠加一个透明的 HTML 元素(我们在第二步中创建了 div.annotationLayer),然后在这个叠加层上通过 DOM 操作、SVG 绘制或额外的 Canvas 绘制来表示批注。
以下是实现批注功能的关键步骤和考虑因素:
- 批注层管理: 确保每个 PDF 页面都有一个精确覆盖其渲染区域的批注层 (div.annotationLayer)。通过 CSS position: absolute; top: 0; left: 0; width: 100%; height: 100%; 来定位。
- 用户交互:工具选择: 提供 UI 元素(按钮、工具栏)让用户选择要添加的批注类型(例如,高亮、矩形、文本框、直线等)。事件监听: 在每个页面的 annotationLayer 或一个委托的父容器上监听鼠标事件 (mousedown, mousemove, mouseup) 或触摸事件 (touchstart, touchmove, touchend)。绘制反馈: 在 mousemove/touchmove 过程中,根据用户选择的工具和鼠标/触摸位置,在批注层上实时绘制一个临时图形(例如,绘制矩形时显示一个虚线框),给用户即时反馈。
- 数据模型: 设计一个数据结构来存储每个批注的信息。这些信息至少应该包括:批注所在的页码。批注的类型(如 ‘highlight’, ‘rectangle’, ‘text’, ‘line’)。批注在页面上的位置和尺寸信息。这通常需要将屏幕坐标转换为 PDF 页面内部的坐标系统。批注的样式信息(颜色、线宽、透明度等)。如果是文本批注,则需要存储文本内容。
- 坐标转换: 这是实现批注的关键难点之一。鼠标/触摸事件提供的坐标是相对于浏览器视口或页面的像素坐标。你需要将这些像素坐标转换为 PDF 页面内部的坐标(PDF 坐标系统通常以点为单位,原点在左下角)。pdfjs-dist 提供的 page.getViewport(scale).convertToPdfPoint(x, y) 方法可以将视口像素坐标转换为 PDF 坐标,而 page.getViewport(scale).convertToViewportPoint(x, y) 可以将 PDF 坐标转换为视口像素坐标。在处理不同缩放比例时,正确进行坐标转换至关重要。
- 批注渲染: 当页面加载、缩放或批注数据更新时,遍历当前页面的批注数据。根据批注类型,在对应的 annotationLayer 中创建并添加相应的 HTML 元素、SVG 元素或在批注层的 Canvas 上绘制图形来显示批注。例如:高亮:创建 <span> 或 <div> 元素,设置背景颜色和位置。矩形/直线:创建 SVG 元素 (<rect>, <line>) 并设置属性,或者在批注层的 Canvas 上使用 2D Context 绘制。文本框:创建 <div> 或 <textarea>,设置位置和内容。
- 批注管理界面: 实现选中批注、显示编辑框、拖动、改变大小、删除等功能。这涉及到监听批注元素的事件,更新批注数据,并重新渲染批注层。
- 数据持久化: 实现将批注数据保存到后端服务器或浏览器的本地存储中,以便下次打开同一个 PDF 时可以加载并恢复批注。批注数据通常需要与 PDF 文件本身关联(例如通过 PDF 的哈希值或文件名)。
关于改造官方 viewer.html:
pdfjs-dist 源码中的 web/viewer.html 和 web/viewer.js 提供了一个完整的 PDF 查看器实现。虽然你可以借鉴其结构和逻辑(尤其是文本层和内置批注层的渲染方式),但直接修改和嵌入到你的项目会非常复杂。viewer.js 是为一个独立应用设计的,其内部耦合度高,依赖于许多辅助类和资源。从头开始,使用核心库构建你自己的查看器,并逐步添加所需功能(包括批注),通常是更灵活和易于维护的方式。
总结
通过 jsdelivr 引入 pdfjs-dist 的 5.2 版本 ESM 文件,你可以轻松地在现代 Web 应用中集成 PDF 查看功能。使用 <script type="module"> 和 import 是 ESM 的标准方式。实现 PDF 的动态加载需要处理文件读取或 URL 请求,并通过 getDocument 和 renderPage 函数来完成。
然而,实现自定义的 PDF 批注功能是一个相对独立的任务,它建立在 PDF 渲染之上,需要你自行设计批注的数据模型、用户交互、坐标转换以及批注的渲染和管理逻辑。这部分功能需要投入额外的开发工作,并且可能需要处理复杂的细节,尤其是在保证批注位置准确性和在不同缩放级别下同步更新方面。如果你需要开箱即用的复杂批注功能,可能需要考虑集成商业的 PDF SDK。
希望这篇博文能帮助你理解如何在现代 Web 项目中集成和使用 pdfjs-dist,并为实现动态加载和批注功能提供清晰的思路。
相关推荐
- Firefox火狐浏览器126版更新修复PDF.js漏洞
-
IT之家5月28日消息,Mozilla基金会在5月14日推出了Firefox火狐浏览器126版本,官方在更新信息中提到该版本主要修复了浏览器内置的PDF组件(PDF.js...
- 在Web应用中集成 PDF.js: 通过jsdelivr实现动态加载与批注的思考
-
PDF文档在现代Web应用中越来越常见,无论是作为文档预览、报告展示还是在线编辑的载体。Mozilla的PDF.js是一个功能强大的JavaScript库,它使得在浏览器端渲染和显示...
- PDF文件长出“AI大脑”?网友惊呼:这操作太“黑科技”了
-
你以为PDF只是用来阅读文档的?这次它彻底颠覆了你的想象!极客AidenBai最新整活——直接把大语言模型(LLM)塞进PDF里,打开文件就能让AI讲故事、陪你聊天!更夸张的是,连Linux系统都能...
- 5种开源PDF解析方案(JS/Node.js)及实战教程
-
hi,大家好,我是徐小夕.徐小夕【知乎专栏作家】掘金签约作者,定期分享AI创业,可视化,企业实战项目知识,深度复盘企业中经常遇到的500+技术问题解决方案。【关注趣谈前端,技术路上不迷茫】最近一直...
- 好用的JavaScript客户端PDF插件——jsPDF
-
介绍和往常一样,jsPDF是一个开源的客户端的PDF解决方案,在之前的文章中已经介绍过几个Web端和PDF相关的库,jsPDF同样是一个不错的客户端PDF引SDK,你可以通过jsPDF在客户端完成相...
- 为wps增加node.js npm创建wpsjs加载项
-
选择环境:windows764位版版本:wps官方2019个人版:一。wps安装后,可以选择关闭广告:打开WPSOffice,点击左上角“首页”图标,依次点击右上角“设置”--->“配置...
- TypeScript 1.5发布,支持大量ES6新特性
-
TypeScript1.5正式发布,此版本是VisualStudio2015更新的一部分,可以单独下载VisualStudio2013和npm,或直接从GitHub获得最新版本。值得关注的改...
- 1.5k+ 开源的高品质音乐命令行下载工具
-
大家好,我是开源探索者,持续分享开源项目,关注技术的最新动态,分享自己的经验和见解。今天为大家带来一款下载音乐的命令行工具:musicn,基于Node.js开发,可播放和下载高品质的音乐,支持咪...
- 1天搭建免费微信小程序商店卖茶(3)连载中
-
前期准备前两篇文章,分别架设好了小程序商站的后台服务端(提供小程序的数据接口,存储商品和交易信息等等),编译并且在手机上成功打开了测试版小程序,成功拉取到了服务器上的测试数据。本篇开始,为“真实”运营...
- 3200+ Cursor 用户被恶意“劫持”!贪图“便宜 API”却惨遭收割, AI 开发者们要小心了
-
整理|华卫近日,有网络安全研究人员标记出三个恶意的npm(Node.js包管理器)软件包,这些软件包的攻击目标是一款颇受欢迎的由AI驱动的源代码编辑器Cursor,且针对的是苹果mac...
- npm install常见问题
-
npm编译npminstall叮当问题来了PSD:\wp\project\newPorject\tyzhhw-mysql\code\tyzhhw_sheshi>npminstalln...
- 微软TypeScript Native预览版发布,带来10倍以上编译性能提升
-
IT之家5月23日消息,微软首席产品经理丹尼尔罗森瓦瑟(DanielRosenwasser)昨晚发文,宣布TypeScriptNative预览版(最终将演变为TypeScript7...
- 如何在 Windows 11 或 10 上安装 ASK CLI
-
ASKCLI是亚马逊为开发人员提供的一个工具,用于创建Alexa技能并随后部署和管理它们。因此,初学者和经验丰富的开发人员都可以通过使用ASKCLI简化开发Alexa技能的任务。所以...
- 如何将package.json中的每个依赖项更新到最新版本
-
技术背景在前端开发中,项目的package.json文件管理着项目的依赖信息。随着时间推移,依赖项可能会发布新的版本,包含性能优化、功能增强和安全修复等。因此,将依赖项更新到最新版本对于项目的稳定...
- 全网最全的 Windows 系统下 Node.js 安装与配置
-
各位代码江湖的“萌新大侠”们!今天详细介绍windows下node.js的安装与配置,看这篇文章就够了。一、下载安装官网下载:下载|Node.js中文网选择需要下载的版本,这是之前的...
- 一周热门
-
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
python使用fitz模块提取pdf中的图片
-
《人人译客》如何规划你的移动电商网站(2)
-
Jupyterhub安装教程 jupyter怎么安装包
-
- 最近发表
-
- Firefox火狐浏览器126版更新修复PDF.js漏洞
- 在Web应用中集成 PDF.js: 通过jsdelivr实现动态加载与批注的思考
- PDF文件长出“AI大脑”?网友惊呼:这操作太“黑科技”了
- 5种开源PDF解析方案(JS/Node.js)及实战教程
- 好用的JavaScript客户端PDF插件——jsPDF
- 为wps增加node.js npm创建wpsjs加载项
- TypeScript 1.5发布,支持大量ES6新特性
- 1.5k+ 开源的高品质音乐命令行下载工具
- 1天搭建免费微信小程序商店卖茶(3)连载中
- 3200+ Cursor 用户被恶意“劫持”!贪图“便宜 API”却惨遭收割, AI 开发者们要小心了
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- table.render (33)
- uniapp textarea (33)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)