pdfjs 的文档奇少无比,基本使用只能靠看源码。对于想要快速上手的开发者而言是灾难性的存在。笔者趟过了这趟浑水,故分享经验,促进碳中和(bushi)
pdfjs预览有很多种方式实现,可以通过渲染canvas实现预览,再渲染一层样式相同的文本层,实现右键复制,缺点是需要大量操作dom
另一种方式是使用pdfjs封装好的viewer,这是官方更推荐的做法
基本原理
- PDF数据层 从url或者bufferArray中(fetch)获取PDF,放入 web worker 中处理,处理出来的数据结构不详
- PDF视图层 将数据层处理的内容传入给canva context , 渲染图片层和文字层
- 操作层 操作层通过eventBus与pdf交互,实际上是js主线程和webworker线程的交互
第三方封装示例
GitHub - LiuSandy/react-pdf-render:
库 源地址
GitHub - mozilla/pdf.js: PDF Reader in JavaScript
创建Viewer
四个主要viewer构造函数
-
EventBus事件总线 -
PDFLinkService将上面两项连接起来的桥梁 -
PDFFindController查找文本控件 接受一个对象- 接受一个LinkService实例
- 接受一个EventBus实例
-
PDFViewer预览控件 (接受一个对象参数)|PDFSiglePageViewer- container 元素实例(div)
- linkService 实例化的LinkService
- useOnlyCssZoom false | true
- textLayerMode 0 | 1
- renderer 'svg' | 'canvas'
- findController 实例化的findController
- eventBus 事件总线
- maxCanvasPixels 最大canvas像素
- enablePrintAutoRotate 启用打印旋转
container元素使用类名:pdfViewer 可以直接使用pdfjs提供的css样式
EventBus事件总线
事件总线令PDFViewer的各部分之间可以传递消息
EventBus 类型的对象有一个 dispatch 方法,可以用来触发事件,以及一个 on 方法,可以用来监听事件。
- PDFFindContorller / PDFViewer 的实例化都需传入EventBus实例
eventBus.on('pagesinit', () => {
console.log('All pages are initialized.')
})
eventBus.dispatch('scalechange', { scale: 2 })
原型方法
PDFViewer实例上有一些原型方法和属性
- pagesCount 总页数
- pageViewsReady 是否渲染完成,通过这个属性判断是不是所有的页面都渲染完成,因为pdf渲染是异步的,有一些事件监听是需要渲染完成后再进行
- currnetPageNumber 当前页码,翻页使用
- currentScale 缩放比率,设置页面缩放需要用currentScaleValue
- pagesRotation 页面旋转
- isPageVisible(pageNumber) 传入页码判断页面是否不可见
流程
-
配置Woker
-
创建四个重要实例
-
加载PDF
-
初始化PDF
-
绑定事件
功能实现
-
翻页和缩放
-
翻页: newViewer.currentPageNumber (newViewer是viewer实例)
-
缩放: newViewer.currentScaleValue
- 缩放的大小可也是任何数字
- 或者: auto | page-actual | page-fit | page-width
-
文字搜索
const handleSearch = (value: string) => {
eventBus?. dispatch('find', {
type: '',
query: value,
phraseSearch: searchParams.phraseSearch,
caseSensitive: searchParams.caseSensitive,
highlightAll: searchParams.highlightAll,
findPrevious: searchParams.findPrevious,
})
if (viewer?.currentPageNumber) {
setcurrentPage(viewer.currentPageNumber)
console.log(viewer?.currentPageNumber)
}
}
在旧版本的pdf中使用viewer.findController.executeCommand('findagain', searcher),但是executeCommand已被废弃
踩雷逼坑
web worker 配置
报错:No "GlobalWorkerOptions.workerSrc" specified.
解决:pdfjs的worker导入方式千奇百怪,找到当前版本和环境适配的即可
import 'pdfjs-dist/build/pdf.worker.mjs'
pdfjs.GlobalWorkerOptions.workerSrc = 'pdfjs-dist/build/pdf.worker.mjs'
container下无子元素
报错:pdf_viewer.js:251 Uncaught TypeError: Cannot read properties of null (reading 'firstElementChild')
Invalid container and/or viewer option.
解决:虽然文档里没有说,但是
const pdfViewer = new PDFViewer({
container: containerRef.current as HTMLDivElement,
eventBus: eventBus,
linkService,
textLayerMode: 1,
findController,
})
- 这里的containerRef的目标元素下一定要有一个id为viewer的元素
- 而且container必须是绝对定位
重复渲染
问题描述: 使用PDFViewer 或者 PDFSinglepageViewer的时候,无法渲染指定页面,而且渲染了两次页面
解决:这不是js的问题,而是css的问题,使用viewer的时候,所有页面都是堆叠在一起的,container和viewer都必须设置绝对定位absolute
<>
{/* contianer外面的盒子对pdf无影响 */}
<div className=' w-full h-full relative '>
{/* container 的宽度和高度是可视区域的宽高*/}
<div
ref={containerRef}
className=' absolute bg-slate-600 h-5/6 aspect-[7/10] overflow-auto'>
<div id='viewer' className='absolute m-auto '></div>
</div>
</div>
</>
无可滚动父元素
问题报错:PdfRender.tsx:141 offsetParent is not set -- cannot scroll
这个问题一般出现在通过state保存和获取currentPageNumber/currentScale等属性的时候
-
currentPageNumber/currentScale/currentScaleValue 直接使用点方法访问,而不是使用state
- 问题:无法通过currentPageNumber动态计算一些属性,比如disable
Button
disabled={//无法使用viewer?.currentPageNumber动态计算}
onClick={() => {
console.log(viewer?.currentPageNumber)
viewer && (viewer.currentPageNumber -= 1)
}}>
</Button>
实际上👆这个也是不完全的修复,不建议使用,治标不治本
- 从effect / layoutEffect中移除 currentPage ,scale,在更新参数值的时候不应当重新初始化url,故下面的这个effect /layoutEffect只在更换pdf或者更换pdf预览方式的时候执行
useEffect(() => {
initPdfViewer(pdfUrl)
}, [pdfUrl]) //pdfUrl变化时重新渲染pdf
worker线程失效
警告:Warning Setup Fake Worker
原因:Worker线程配置不正确,浏览器为了保证程序运行,设定了一个模拟worker,实际上没有在后台线程中运行。fake worker 会影响程序的性能,虽然是个warning还是建议不要出现
- pdfjs.version配置问题
- 本地worker引入问题
其实我也没找出本地引入的问题在哪,使用以下的cdn连接,以导入正确的worker文件
import * as pdfjs from 'pdfjs-dist'
pdfjs.GlobalWorkerOptions.workerSrc =`https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.mjs`
被动渲染 passive render
错误:(无报错)在使用React事件:onScroll/onWheel 到时候,希望阻止默认事件 e.preventDefault ,此时你会发现无法成功,无论如何默认事件还是会被触发
原因:passive 属性最初是为了提高触摸和滚动事件的性能而设计的。在移动设备上,滚动性能尤其重要,因为用户期望页面能够快速响应触摸滚动。在没有 passive 属性之前,浏览器必须等待事件处理函数执行完毕,以确 定是否调用了 event.preventDefault(),这可能会导致滚动延迟。
解决:
- 目前,浏览器whell/scroll事件默认开启 passive,需要手动关闭
- React事件暂时未能支持passive属性的设置,所以需要通过ref获取真实dom,然后添加监听
useEffect(() => {
//为viewer窗口绑定滚动事件
if (viewerRef.current) {
viewerRef.current.addEventListener('wheel', handleWheel)
}
}
return () => {
}
})
useEffect(() => {
//为viewer窗口绑定滚动事件
if (viewerRef.current) {
viewerRef.current.addEventListener('wheel', handleWheel, {
passive: false,
})
}
}
return () => {
}
})
STATUS_BREAKPOINT
浏览器报错: STATUS_BREAKPOINT
原因:模块导入的顺序不当,必须先导入pdfjs-dist再从pdfjs-dist/web/pdf_viewer.mjs 和 pdf_viewer.css文件,遵循从大到小的原则
解决:
import {
EventBus,
PDFViewer,
PDFSinglePageViewer,
PDFLinkService,
PDFFindController,
} from 'pdfjs-dist/web/pdf_viewer.mjs'
import 'pdfjs-dist/web/pdf_viewer.css'
import * as pdfjs from 'pdfjs-dist'=
import * as pdfjs from 'pdfjs-dist'
import {
EventBus,
PDFViewer,
PDFSinglePageViewer,
PDFLinkService,
PDFFindController,
} from 'pdfjs-dist/web/pdf_viewer.mjs'
import 'pdfjs-dist/web/pdf_viewer.css'