vue中使用pdfjs-dist渲染pdf以及略缩图菜单,点击略缩图翻页

效果图

开发原因

开发中常用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大纲视图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值