PDF.js 如何正确处理 PDF 加载错误并等待页面渲染完成?
【免费下载链接】pdf.jsPDF Reader in JavaScript项目地址: https://gitcode.com/gh_mirrors/pd/pdf.js
在浏览器里用 PDF.js 作为库加载 PDF 时,有两个时序问题必须处理:PDF 的下载与解析是异步的,getDocument不会直接返回文档对象,加载可能失败;页面渲染同样是异步的,调用page.render()后并不等于画面已经画完,而文档明确说明同一个 canvas 不能同时用来绘制两页。本文基于仓库中的 示例文档 和 Hello World 示例、Previous/Next 示例、pdf2png Node 脚本,给出一条可执行的路径:捕获加载错误,并确定渲染何时完成。
准备条件
了解 Promise:示例文档 开头明确指出 PDF.js 重度依赖 Promise,如果 Promise 对你还不熟,建议先熟悉再继续。
必须指定 worker 路径:示例中的注释要求 "The workerSrc property shall be specified":
pdfjsLib.GlobalWorkerOptions.workerSrc = "../../node_modules/pdfjs-dist/build/pdf.worker.mjs";需要一个 HTTP 服务器:Getting Started 文档 说明 worker 不支持
file://地址,所以不能双击 HTML 直接打开,要起一个服务器;如果使用源码构建且有 Node,可以运行npx gulp server。远程 PDF 需要 CORS:Hello World 示例 中的注释写明,如果提供的是远程服务器上的绝对 URL,需要在那台服务器上配置 CORS 头。
库脚本路径:仓库示例通过
node_modules/pdfjs-dist/build/pdf.mjs引入库,例如示例文件中的<script src="../../node_modules/pdfjs-dist/build/pdf.mjs" type="module"></script>(这是相对于examples/learning/目录的路径)。你自己的项目应替换为实际安装的 pdfjs-dist 位置。Getting Started 文档 也给出了预构建下载与 CDN(jsDelivr、cdnjs、unpkg)三种获取方式。
加载文档:用 Promise 捕获错误
pdfjsLib.getDocument()返回的是一个PDFDocumentLoadingTask实例,它的promise属性会在解析完成时 resolve 出文档对象;加载失败时这个 promise 会 reject,错误处理就挂在它上面。示例文档 对 "Hello World with document load error handling" 一节给出的说明是:该示例演示了 "how promises can be used to handle errors during loading",并 "wait until a page is loaded and rendered"。
最小加载写法(取自 Hello World 示例 的注释风格,加上错误处理):
// // If absolute URL from the remote server is provided, configure the CORS // header on that server. // const url = "./helloworld.pdf"; pdfjsLib.GlobalWorkerOptions.workerSrc = "../../node_modules/pdfjs-dist/build/pdf.worker.mjs"; // // Asynchronous download PDF // const loadingTask = pdfjsLib.getDocument({ url }); try { const pdf = await loadingTask.promise; // 加载成功,pdf 是文档对象 } catch (reason) { // 加载失败:reason 是拒绝原因 console.log(reason); }仓库中一个可直接参照的try/catch实例是 Node 脚本 pdf2png.mjs,它把await loadingTask.promise之后的整个流程包在try里,catch (reason)中执行console.log(reason)。
渲染页面:用renderTask.promise等待完成
加载成功后取第一页、创建 viewport、准备 canvas,这部分照 Hello World 示例 照搬即可:
const page = await pdf.getPage(1); const scale = 1.5; const viewport = page.getViewport({ scale }); // Support HiDPI-screens. const outputScale = window.devicePixelRatio || 1; const canvas = document.getElementById("the-canvas"); const context = canvas.getContext("2d"); canvas.width = Math.floor(viewport.width * outputScale); canvas.height = Math.floor(viewport.height * outputScale); canvas.style.width = Math.floor(viewport.width) + "px"; canvas.style.height = Math.floor(viewport.height) + "px"; const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null; const renderContext = { canvasContext: context, transform, viewport, }; page.render(renderContext);这里要回答"渲染什么时候算完成":page.render()返回一个 render task,等待它的 promise 即可。Previous/Next 示例 中对应的代码和注释是:
var renderTask = page.render(renderContext); // Wait for rendering to finish renderTask.promise.then(function () { // 渲染完成,可以安全地在 canvas 上发起下一次绘制 });await renderTask.promise等价于上面的.then写法,pdf2png.mjs 用的就是这种写法,渲染完成后才调用canvas.toBuffer("image/png")导出图片。
多页切换:渲染未完就排队,不要并发
示例文档 对 Previous/Next 示例的说明是:"The same canvas cannot be used to perform to draw two pages at the same time -- the example demonstrates how to wait on previous operation to be complete." 也就是说,翻页时如果上一次渲染还没结束,新的请求必须排队。prevnext.html 的实现方式:
- 用
pageRendering标志记录是否正在渲染,用pageNumPending记录排队中的页码; queueRenderPage(num):正在渲染就把页码存入pageNumPending,否则立即渲染;- 在
renderTask.promise.then(...)回调里把pageRendering置回false,并检查pageNumPending,不为null就继续渲染那一页。
function queueRenderPage(num) { if (pageRendering) { pageNumPending = num; } else { renderPage(num); } }文档中queueRenderPage的注释说明了这个函数契约:"If another page rendering in progress, waits until the rendering is finished. Otherwise, executes rendering immediately." 前/后页按钮的点击处理里还会用pageNum <= 1和pageNum >= pdfDoc.numPages做边界保护,避免越界取页。
Node 环境下的完整链路(可选分支)
如果你不是浏览器而是 Node 脚本场景,pdf2png.mjs 展示了完整的"加载—渲染—等待—释放"链路:
import fs from "fs"; import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; // Some PDFs need external cmaps. const CMAP_URL = "../../../node_modules/pdfjs-dist/cmaps/"; const CMAP_PACKED = true; // Where the standard fonts are located. const STANDARD_FONT_DATA_URL = "../../../node_modules/pdfjs-dist/standard_fonts/"; // Loading file from file system into typed array. const pdfPath = process.argv[2] || "../../../web/compressed.tracemonkey-pldi-09.pdf"; const data = new Uint8Array(fs.readFileSync(pdfPath)); // Load the PDF file. const loadingTask = getDocument({ data, cMapUrl: CMAP_URL, cMapPacked: CMAP_PACKED, standardFontDataUrl: STANDARD_FONT_DATA_URL, }); try { const pdfDocument = await loadingTask.promise; console.log("# PDF document loaded."); // Get the first page. const page = await pdfDocument.getPage(1); // Render the page on a Node canvas with 100% scale. const canvasFactory = pdfDocument.canvasFactory; const viewport = page.getViewport({ scale: 1.0 }); const canvasAndContext = canvasFactory.create(viewport.width, viewport.height); const renderContext = { canvasContext: canvasAndContext.context, viewport, }; const renderTask = page.render(renderContext); await renderTask.promise; // Convert the canvas to an image buffer. const image = canvasAndContext.canvas.toBuffer("image/png"); fs.writeFile("output.png", image, function (error) { if (error) { console.error("Error: " + error); } else { console.log("Finished converting first page of PDF file to a PNG image."); } }); // Release page resources. page.cleanup(); } catch (reason) { console.log(reason); }注意几点适用条件:Node 路径用pdfjs-dist/legacy/build/pdf.mjs的getDocument;文件读成Uint8Array后通过data传入,而不是url;文档说明 "Some PDFs need external cmaps",所以cMapUrl和standardFontDataUrl参数需要指向 node_modules 中的实际目录(示例里是相对于examples/node/pdf2png/的路径,你复制时按自己的目录结构替换)。脚本默认处理的 PDF 是仓库自带的web/compressed.tracemonkey-pldi-09.pdf,也可以通过第一个命令行参数传入其他 PDF 路径。
结果验证
- 浏览器:没有专门的日志,判断依据就是时序本身——只有在
renderTask.promise的回调里,渲染才算完成,之后的代码(更新页码计数器、发起下一次渲染、导出 canvas)都是安全的。prevnext.html 在加载成功后用pdfDoc.numPages更新页码显示,在renderTask.promise.then里才放行下一次渲染,这就是文档给出的完成判定方式。 - Node(文档示例输出):pdf2png.mjs 的示例运行会打印
# PDF document loaded.,写文件完成后打印Finished converting first page of PDF file to a PNG image.;writeFile出错时打印Error:加错误对象。这些是文档中的示例输出,具体字符串以脚本实际内容为准。
限制与边界
file://下 worker 不可用:必须通过 HTTP 服务器访问页面(源码构建可用npx gulp server),见 Getting Started 文档 的 "Trying the Viewer" 一节。- 远程 PDF 需要服务端 CORS 配置,否则加载会在网络层失败并 reject
loadingTask.promise(示例注释见上)。 - 同一 canvas 不能并发绘制两页:这是文档明确给出的限制,因此多页场景必须按
renderTask.promise串行排队,而不是并行发起渲染。 - Core 层不在本文范围内:Getting Started 文档 说明 core 层的 API 可能变化、直接算高级用法;本文所有写法都基于 display 层 API(
getDocument/getPage/render),这一层的 API 是版本号所依据的稳定接口。
参考资料
- 示例文档:加载、取页、渲染的代码说明,以及错误处理与等待渲染的示例索引
- Hello World 示例:浏览器端最小加载与渲染
- Previous/Next 示例:等待
renderTask.promise并串行排队渲染 - pdf2png Node 脚本:Node 端加载错误处理与渲染等待的完整链路
- Getting Started 文档:分层说明、下载/CDN 方式、服务器要求
【免费下载链接】pdf.jsPDF Reader in JavaScript项目地址: https://gitcode.com/gh_mirrors/pd/pdf.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考