StreamPDFStreamPDF
返回博客

PDF.js 从 demo 深入浅出 1: 自定义 viewer 渲染 PDF

2026年10月19日 · StreamPDF Team

PDF.js 从 demo 深入浅出 1: 自定义 viewer 渲染 PDF

前言

在之前的系列文章中, 我们探索了 PDF.js 的架构, 以及渲染层和消息通讯等技术点, 但是在写文章的过程中, 总结了一些问题:

  • 文章之间无法串联, 无法形成一个完整的知识体系
  • 无法对应 PDF.js 的代码, 因此跟随文章的过程中心之负担较重

因此, 我们决定起一个新系列, 从 demo 代码出发, 慢慢的跟随代码的调用链组件深入 PDF.js 的内部. 并且会从两个方面来诠释代码原理:

  • demo 解读: 实例化解读, 由浅入深链接起来, 深度探索
  • 代码仓库解读: 通用化解读, 可分块阅读, 独立成文

来吧, 试着从头出发.

自定义 viewer 渲染 PDF

我们来尝试自定义一个极简的 viewer 来渲染 PDF, 然后跟随每一行代码, 进入 PDF.js 的内部. 在跟随的过程中, 我们会将变量值带入代码中, 以便于理解代码的执行流程.

import * as pdfjsLib from 'https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc =
  'https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.worker.mjs';
const loadingTask = pdfjsLib.getDocument({
  url: 'https://assets.streampdf.com/PDF32000_2008.pdf',
});
const pdf = await loadingTask.promise;
// 获取第一页的渲染数据
const page = await pdf.getPage(1);
// 获取第一页的视口数据
const viewport = page.getViewport({ scale: 0.5 });
// 拿到 canvas, 我们将渲染到这个 canvas 上面
const canvas = document.getElementById('canvas-id');
const context = canvas.getContext('2d');
 
// 将 canvas 的尺寸匹配上PDF 的视口大小
canvas.width = Math.floor(viewport.width);
canvas.height = Math.floor(viewport.height);
canvas.style.width = Math.floor(viewport.width) + 'px';
canvas.style.height = Math.floor(viewport.height) + 'px';
 
// 准备渲染参数
const renderContext = {
  canvasContext: context,
  viewport,
};
 
// 调用 render 方法进行实际的渲染,await 等待渲染完成
await page.render(renderContext);

demo: https://streampdf.com/blog/demos/a-simple-pdf-render-demo.html

第 1 行-第 3 行, 配置 Worker 文件路径

引入 PDF.js 库的入口文件, 以及设置 Worker 文件的路径.

这意味着在使用 PDF.js 的时候, 我们甚至可以完全替换成自己的 Worker, 只要遵循接口规范即可.

在这里我们设置为默认的 Worker 文件路径:

GlobalWorkerOptions.workerSrc = https://cdn.jsdelivr.net/npm/pdfjs-dist@6.3.289/build/pdf.worker.mjs

第 4 行-第 7 行, 加载 PDF 文档

调用 getDocument 方法加载 PDF 文档. 传入 object 参数:

  • src: PDF 文档的 URL. 值为 https://assets.streampdf.com/PDF32000_2008.pdf

getDocument 方法返回一个 Promise, 当 PDF 文档加载完成后, 会 resolve 一个 PDF 文档对象.

const loadingTask = pdfjsLib.getDocument(...) 为同步调用: 点击查看调用

loadingTask.promise 为一个 getter, 返回 Promise.withResolvers() 方法创建的 Promise.

在这里我们 await loadingTask.promise, 等待 PDF 文档加载完成, 并将其赋值为 pdf 变量.

第 8 行-第 10 行, 获取 PDF 文档的第一页

在文档初始化完成之后, 我们可以调用 getPage 方法获取第一页的渲染数据.

getPage 方法返回一个 Promise, 当页面数据加载完成后, 会 resolve 一个页面对象.

const page = await pdf.getPage(1); 为异步调用: 点击查看调用

等待第一页数据加载完成后, 并将其赋值为 page 变量.

第 11 行-第 14 行, 获取第一页的视口数据

在 page 数据加载完成后, 我们可以调用 getViewport 方法获取第一页的视口数据.

const viewport = page.getViewport({ scale: 0.5 }); 为同步调用: 点击查看调用

获取第一页的视口数据. 传入 object 参数:

  • scale: 缩放比例. 值为 0.5

返回的 viewport 是一个对象, 包含了视口的宽度和高度. 值为:

{
  width: 297.5,
  height: 421,
  // others, 此处没有用到, 先不列出...
}

将其赋值给 viewport 变量.

第 15 行-第 18 行, 拿到 canvas, 我们将渲染到这个 canvas 上面

拿到 viewport 变量之后, 将 canvas 的 width 和 height 匹配上 PDF 的视口大小.

canvas.width = 297; // Math.floor(viewport.width);
canvas.height = 421; // Math.floor(viewport.height);

将 canvas 的 css 宽度和高度也设置上 PDF 的视口大小.

canvas.style.width = 297 + 'px'; // Math.floor(viewport.width) + 'px';
canvas.style.height = 421 + 'px'; // Math.floor(viewport.height) + 'px';

为什么需要设置 canvas 的 css 宽度和高度? canvas 的 width, height 属性控制的是 canvas 的像素尺寸, 而 css 宽度和高度是 canvas 显示在页面上的尺寸. 一般情况下, 这两者是相同的. 但是在高分辨率屏幕上, 这两者是不同的. 比如 window.devicePixelRatio 为 2 时, canvas 的像素尺寸应设为 594, 842, 而 css 宽度和高度保持 297, 421, 以防止渲染模糊.

第 19 行-第 22 行, 渲染第一页

canvas 准备好之后, 我们可以调用 render 方法渲染第一页.

将 canvas 上下文, 视口数据, 渲染参数等准备好, 以便于调用 render 方法.

await page.render(renderContext); 为异步调用: 点击查看调用

至此, 代码执行结束.

总结

本章我们跟随一个自定义的 viewer 来渲染了一个 PDF 文件的第一页.

和其他文章最大的不同是, 将变量的值带入到解析代码的链路中, 以便确定不同的执行路径. 由不同的文件和分析文章来进入不同的分支路径, 读文章的过程中就好像是在 debug 代码一样.

希望能够给你带来不一样的源码阅读体验.

happy coding!

相关文章