开发原因
开发中常用pdfjs-dist来进行pdf文件的预览。
如果是单纯的预览包括缩放、翻页、搜索内容,那么直接安装依赖调用pdf_viewer即可完成,但是当我们需要略缩图菜单、大纲视图时,viewer.mjs中却缺少了相关类。
想要使用全部功能,网上的教程大多数说将Prebuilt版本viewer.html文件放到服务器上使用,但是它的样式布局等,想要更改却有些无所下手。另外当我们对pdf进行功能外的操作,比如预设区域高亮等就无法实现。
所以结合源码与依赖,我们可以将依赖中viewer.mjs缺失的相关类搬运至本地,再引用,将报错处理后就可以使用了。
下载源码并分析
本文使用的pdfjs-dist版本为4.7.76
搬运需要的文件
下载源码到本地作为参考和搬运库:
https://github.com/mozilla/pdf.js/
根据viewer.html页面中我们可以看出PDFThumbnailViewer
是略缩图最主要的类
再根据这个文件中的引用可知,我们共搬运三个文件
- pdf_thumbnail_viewer.js
- pdf_thumbnail_view.js
- ui_utils.js
这时pdf_thumbnail_view.js中的这一句报错
import { OutputScale, RenderingCancelledException } from 'pdfjs-lib'
通过搜索源码,可以得知这两个方法来自pdf.js这个文件,但是在我们安装的pdfjs-dist依赖中,这两个方法并未被暴露,所以我们搜索后手动搬运至pdf_thumbnail_view.js
function unreachable(msg) {
throw new Error(msg)
}
const BaseException = (function BaseExceptionClosure() {
function BaseException(message, name) {
if (this.constructor === BaseException) {
unreachable('Cannot initialize BaseException.')
}
this.message = message
this.name = name
}
BaseException.prototype = new Error()
BaseException.constructor = BaseException
return BaseException
}())
class RenderingCancelledException extends BaseException {
constructor(msg, extraDelay = 0) {
super(msg, 'RenderingCancelledException')
this.extraDelay = extraDelay
}
}
class OutputScale {
constructor() {
const pixelRatio = window.devicePixelRatio || 1
this.sx = pixelRatio
this.sy = pixelRatio
}
get scaled() {
return this.sx !== 1 || this.sy !== 1
}
}
分析源码
我们通过查看app.js文件中的_initializeViewerComponents方法,可以了解初始化时对PDFThumbnailViewer的操作,搜索pdfThumbnailViewer得到其他相关事件
包括以下几点:
创建与绑定
创建了略缩图,并且将其与pdfRenderingQueue绑定,pdfRenderingQueue控制强制渲染
if (appConfig.sidebar?.thumbnailView) {
this.pdfThumbnailViewer = new PDFThumbnailViewer({
container: appConfig.sidebar.thumbnailView,
eventBus,
renderingQueue: pdfRenderingQueue,
linkService: pdfLinkService,
pageColors,
abortSignal: this._globalAbortController.signal,
enableHWA
});
pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
}
...
this.pdfThumbnailViewer?.setDocument(pdfDocument);
侧边栏打开时的事件
此处是侧边栏,当打开关闭时会执行一些方法。搜索PDFSidebar中的内容可知,当打开侧边栏并显示预览图时,先执行了onUpdateThumbnails,后执行了onToggled
if (appConfig.sidebar) {
this.pdfSidebar = new PDFSidebar({
elements: appConfig.sidebar,
eventBus,
l10n
});
// 看pdf_siderbar源码
this.pdfSidebar.onToggled = this.forceRendering.bind(this);
this.pdfSidebar.onUpdateThumbnails = () => {
for (const pageView of pdfViewer.getCachedPageViews()) {
if (pageView.renderingState === RenderingStates.FINISHED) {
this.pdfThumbnailViewer.getThumbnail(pageView.id - 1)?.setImage(pageView);
}
}
this.pdfThumbnailViewer.scrollThumbnailIntoView(pdfViewer.currentPageNumber);
};
}
// forceRendering的详情
forceRendering() {
// 这个本次功能没用所以不用管
this.pdfRenderingQueue.printing = !!this.printService;
// 当略缩图显示的时候要将isThumbnailViewEnabled置为true
this.pdfRenderingQueue.isThumbnailViewEnabled = this.pdfSidebar?.visibleView === SidebarView.THUMBS;
// 这个方法,依赖中存在可以直接调用,用于重新渲染pdf视图和略缩图
this.pdfRenderingQueue.renderHighestPriority();
},
页面渲染事件
某一页pdf渲染完毕时,渲染对应略缩图
function onPageRendered({
pageNumber,
error
}) {
if (pageNumber === this.page) {
this.toolbar?.updateLoadingIndicatorState(false);
}
if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
const pageView = this.pdfViewer.getPageView(pageNumber - 1);
const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(pageNumber - 1);
if (pageView) {
thumbnailView?.setImage(pageView);
}
}
if (error) {
this._otherError("pdfjs-rendering-error", error);
}
}
页面切换
页码改变时,控制pdfThumbnailViewer滚动到对应页
function onPageChanging({ pageNumber, pageLabel }) {
this.toolbar?.setPageNumber(pageNumber, pageLabel);
this.secondaryToolbar?.setPageNumber(pageNumber);
if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
this.pdfThumbnailViewer?.scrollThumbnailIntoView(pageNumber);
}
// Show/hide the loading indicator in the page number input element.
const currentPage = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
this.toolbar?.updateLoadingIndicatorState(
currentPage?.renderingState === RenderingStates.RUNNING
);
}
完整代码
综合上面的源码我们可以删减后引入自己的组件中,index.vue如下
<template>
<div class="pdf-container">
<div>
<i
:class="fold ? 'el-icon-s-unfold' : 'el-icon-s-fold'"
@click="openSidebar"
></i>
<i
class="el-icon-arrow-left"
@click="pdfViewer.previousPage()" />
<el-input-number
size="mini"
:precision="0"
:min="1"
:max="pageTotal"
:controls="false"
v-model="pageNum"
@change="pdfViewer.currentPageNumber = $event"
></el-input-number
>/{{ pageTotal }}
<i
class="el-icon-arrow-right"
@click="pdfViewer.nextPage()" />
</div>
<div class="content">
<div
class="sidebarContainer"
v-show="fold">
<div class="sidebarContent">
<div
class="thumbnailView"
ref="thumbnailViewRef"></div>
</div>
</div>
<div class="main-content">
<div
ref="viewerContainerRef"
class="viewerContainer">
<!-- class="pdfViewer"必须要写,匹配pdf_viewer.css中的类名,否则textLayer位置错误且页面不居中,也可以从pdf_viewer.css中复制粘贴到当前页面 -->
<div
class="pdfViewer"
id="viewer"></div>
</div>
</div>
</div>
</div>
</template>
<script>
import 'pdfjs-dist/web/pdf_viewer.css'
import * as pdfjsLib from 'pdfjs-dist'
import * as pdfjsViewer from 'pdfjs-dist/legacy/web/pdf_viewer'
import { PDFThumbnailViewer } from './pdf_thumbnail_viewer'
import { throttle } from 'lodash-es';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href;
export default {
props: {
url: {
type: String,
default: 'http://127.0.0.1:5501/web/compressed.tracemonkey-pldi-09.pdf',
},
},
data() {
return {
eventBus: null,
pdfLinkService: null,
pdfViewer: null,
pdfDocument: null,
fold: false,
pdfThumbnailViewer: null,
pageNum: 1,
pageTotal: 1,
resizeObserver: null,
}
},
mounted() {
this.initPdf()
this.initResizeObserver()
},
beforeUnmount() {
if (this.resizeObserver) {
this.resizeObserver.disconnect()
this.resizeObserver = null
}
},
methods: {
// 源码使用监听页面resize,更新缩放;此处作为一个组件,可使用ResizeObserver监听
initResizeObserver() {
if ('ResizeObserver' in window) {
this.resizeObserver = new ResizeObserver((entries) => {
this.throttledLogMessage()
})
this.resizeObserver.observe(this.$refs.viewerContainerRef)
} else {
console.warn('ResizeObserver is not supported in this browser.')
}
},
handleResize() {
const currentScaleValue = this.pdfViewer.currentScaleValue
if (
currentScaleValue === 'auto' ||
currentScaleValue === 'page-fit' ||
currentScaleValue === 'page-width'
) {
this.pdfViewer.currentScaleValue = currentScaleValue
}
this.pdfViewer.update()
},
throttledLogMessage: throttle(function() {
this.handleResize()
}, 200),
async initPdf() {
if (!pdfjsLib.getDocument || !pdfjsViewer.PDFViewer) {
return
}
const container = this.$refs.viewerContainerRef
// 用于事件监听
this.eventBus = new pdfjsViewer.EventBus()
// 用于跳转
this.pdfLinkService = new pdfjsViewer.PDFLinkService({
eventBus: this.eventBus,
})
this.pdfViewer = new pdfjsViewer.PDFViewer({
container,
eventBus: this.eventBus,
linkService: this.pdfLinkService,
})
this.pdfThumbnailViewer = new PDFThumbnailViewer({
container: this.$refs.thumbnailViewRef,
eventBus: this.eventBus,
renderingQueue: this.pdfViewer.renderingQueue,
linkService: this.pdfLinkService,
})
this.pdfLinkService.setViewer(this.pdfViewer)
this.pdfDocument = await pdfjsLib.getDocument({
url: this.url,
cMapUrl: '/pdfjs/cmaps/',
cMapPacked: true,
}).promise
this.pdfLinkService.setDocument(this.pdfDocument, null)
this.pdfViewer.renderingQueue.isThumbnailViewEnabled = true
this.pdfThumbnailViewer?.setDocument(this.pdfDocument)
this.pdfViewer.renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer)
this.pdfViewer.setDocument(this.pdfDocument)
this.pageTotal = this.pdfDocument._pdfInfo.numPages
this.bindEvent()
},
bindEvent() {
this.eventBus.on('pagerendered', (page) => {
if (this.fold) {
const pageView = this.pdfViewer.getPageView(page.pageNumber - 1)
const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
page.pageNumber - 1,
)
if (pageView) {
thumbnailView?.setImage(pageView)
}
}
})
this.eventBus.on('pagesinit', (args) => {
this.pdfViewer.currentScaleValue = 'auto'
})
this.eventBus.on('pagechanging', (args) => {
this.pageNum = args.pageNumber
this.pdfThumbnailViewer.scrollThumbnailIntoView(args.pageNumber)
})
},
changeScale(val) {
this.pdfViewer.currentScaleValue = val.value
},
async openSidebar() {
this.fold = !this.fold
if (this.fold) {
// 这个必写,需要在dom显示时再更新
await this.$nextTick()
this.updateThumbnail()
this.pdfThumbnailViewer.forceRendering()
}
},
async updateThumbnail() {
for (const pageView of this.pdfViewer.getCachedPageViews()) {
if (pageView.renderingState === 3) {
this.pdfThumbnailViewer
.getThumbnail(pageView.id - 1)
?.setImage(pageView)
}
}
this.pdfThumbnailViewer.scrollThumbnailIntoView(
this.pdfViewer.currentPageNumber,
)
},
},
}
</script>
<style lang="scss" scoped>
.pdf-container {
position: relative;
width: 100%;
height: 100%;
}
.content {
position: relative;
display: flex;
width: 100%;
height: calc(100% - 40px);
border: 1px solid #ccc;
.sidebarContainer {
width: 150px;
height: 100%;
position: absolute;
z-index: 10;
background-color: #fff;
.sidebarContent {
position: relative;
width: 100%;
height: 100%;
}
.thumbnailView {
position: absolute;
width: 100%;
height: 100%;
overflow: auto;
}
}
.main-content {
position: relative;
width: 100%;
height: 100%;
.viewerContainer {
background: #d4d4d7;
position: absolute;
display: flex;
justify-content: center;
overflow: auto;
width: 100%;
height: 100%;
}
}
}
</style>
<style lang="scss">
/* .thumbnail这个样式里的高度是一定要加的
否则略缩图翻页时,因为高度变化,导致当前页不在可视范围
*/
.thumbnail {
--thumbnail-width: 0;
--thumbnail-height: 0;
box-sizing: content-box;
float: var(--inline-start);
width: var(--thumbnail-width);
height: var(--thumbnail-height);
margin: 0 10px 5px;
padding: 1px;
border: 7px solid transparent;
border-radius: 2px;
}
/* 还有略缩图加载样式和选中样式,需要的可以从web\viewer.css中移入
* 注意使用变量的部分,如果不生效则需要一同移入
*/
.thumbnail:not([data-loaded]) > .thumbnailImage {
width: calc(var(--thumbnail-width) - 2px);
height: calc(var(--thumbnail-height) - 2px);
border: 1px dashed rgb(132 132 132);
}
a:focus > .thumbnail,
.thumbnail:hover {
border-color: rgb(0 0 0 / 0.2);
}
.thumbnail.selected {
border-color: rgb(0 0 0 / 0.2) !important;
}
.thumbnailImage {
width: var(--thumbnail-width);
height: var(--thumbnail-height);
opacity: 0.9;
}
a:focus > .thumbnail > .thumbnailImage,
.thumbnail:hover > .thumbnailImage {
opacity: 0.95;
}
.thumbnail.selected > .thumbnailImage {
opacity: 1 !important;
}
</style>
下面是需要注意的点:
- viewerContainer的样式中
scroll-behavior
不能使用smooth
,否则页码会不断变化,导致略缩图选中闪回上一页,再选中当前页 - pdf_thumbnail_view.js文件中的THUMBNAIL_WIDTH用来控制略缩图的宽度,可以自行修改
- .pdfViewer和.thumbnailView需要position: absolute;,其上层节点position: relative;不然滚动事件不会生效
- .pdfViewer类与pdfjs-dist/web/pdf_viewer.css中的名字对应,使得textLayer层相对页码定位;页宽有不同的时候使页面居中。也可以从pdf_viewer.css复制到本地使用
相关文章: 创建pdf大纲视图