【PDF】pdf文件的预览、缩放、拖动和截图

发布于:2022-10-17 ⋅ 阅读:(1403) ⋅ 点赞:(1)

1、使用插件

pdfjs-dist

版本: 2.2.228, 这个版本目前来看bug少一些

pnpm install pdfjs-dist@2.2.228

2、使用场景

数据来源为pdf,且pdf文件里有很多页,需要在外面自己分页展示出来,并且可以自己放大缩小拖动

最终效果图

2.1、定义需要的数据

      pageControl: { // 页面参数
        curPage: 1, // 当前页码
        pages: 0, // 总共多少页
        scale: 100, // 用于展示的缩放比例, 默认为100, 
        beforeScale: 1, // 真实缩放比例,最大缩放 2,最小缩放 0.2,这两个比例为 100:1
        rotation: 0 // 当前旋转参数
      },
      pdfDoc: null, // pdf对象
      draggable: null // 拖动dom

2.2、页面结构

 preview.vue

v-drag 为自定义拖拽指令,用于一般dom在父节点范围内拖动,想 了解的在我另一篇博客查看

vue学习(6)自定义指令详解及常见自定义指令

 pdf想进行预览,有两种方式,转canvas和转svg(只修改renderPage函数)

下面是pdf转svg方式

<template>
  <div ref="preview" class="preview">
    <div
      :id="`img-box${item.id}`"
      v-drag
      class="img-box"
      @mousewheel="(e) => scaleDom(e, 'wheel')"
    />
    <footer-ai
      :mode="item.id"
      :page="pageControl"
      @scaleControl="scaleControl"
      @pageChange="pageChange"
    />
  </div>
</template>

<script>
import PDFJS from 'pdfjs-dist';
import workerSrc from 'pdfjs-dist/build/pdf.worker.entry';
PDFJS.workerSrc = workerSrc;
import FooterAi from './component/footerAi.vue' // eslint-disable-line
export default {
  name: 'Preview',
  components: {
    FooterAi
  },
  props: {
    item: {
      type: Object,
      default: () => {}
    }
  },
  data() {
    return {
      pageControl: { // 页码参数
        curPage: 1, // 当前页码
        pages: 0, // 总共页码
        scale: 100, // 转换后缩放比例, 默认为100, 最大缩放 2,最小缩放 0.2
        beforeScale: 1, // 转换前缩放比例
        rotation: 0 // 当前旋转参数
      },
      pdfDoc: null,
      draggable: null // 图片dom
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.draggable = document.getElementById(`img-box${this.item.id}`)
      this.loadFile('XXX.pdf')
    })
  },
  methods: {
    /**
     * pdf渲染
     * @param {*} num
     */
    renderPage(num) {
      // 返回单页内容实例(页面索引) pdf.getPage(index)
      this.pdfDoc.getPage(num).then((page) => {
        if (this.draggable.hasChildNodes()) {
          this.draggable.removeChild(this.draggable.firstChild)
        }
        const viewport = page.getViewport({ scale: this.pageControl.beforeScale })
        const container = document.createElement('div')
        container.id = 'yxp_svg_' + num
        container.className = 'pageContainer'
        container.style.width = viewport.width + 'px'
        container.style.height = viewport.height + 'px'
        this.draggable.appendChild(container)
        return page.getOperatorList().then(function(opList) {
          const svgGfx = new PDFJS.SVGGraphics(page.commonObjs, page.objs)
          return svgGfx.getSVG(opList, viewport).then(function(svg) {
            container.appendChild(svg)
          })
        })
      })
    },
    /**
     * 获取整个pdf文档
     * @param {*} url
     */
    loadFile(url) {
      PDFJS.getDocument({
        url,
        cMapPacked: true
      }).promise.then((pdf) => {
        this.pdfDoc = pdf
        this.pageControl.pages = this.pdfDoc.numPages
        this.$nextTick(() => {
          this.pageControl.curPage = 1
          this.renderPage(this.pageControl.curPage)
        })
      }, (err) => {
        if (err.name === 'MissingPDFException') {
          this.$message.warn('无效的PDF链接')
        }
      })
    },
    pageChange(item) {
      this.$nextTick(() => {
        this.pageControl.curPage = item.page
        this.renderPage(this.pageControl.curPage)
      })
    },
    /**
     * 当前缩放比例控制
     * @param {*} val
     */
    scaleControl(val) {
      if (val) {
        this.scaleDom(val, 'click')
      } else {
        this.pageControl.scale = 100
        this.pageControl.beforeScale = 1
        this.renderPage(this.pageControl.curPage)
        // this.draggable.style.transform = 'scale(1)'
      }
    },
    /**
     * 鼠标滑轮事件
     * @param {*} e
     */
    scaleDom(e, type = 'wheel') {
      // parseFloat((this.draggable.style.transform || `scale(1)`).replace(/[^0-9.]/gi, ''))
      let scaleReal = this.pageControl.beforeScale
      const size = type === 'wheel' ? e.wheelDelta / 1200 : parseFloat(e / 10)
      scaleReal += size;
      if (scaleReal >= 0.18 && scaleReal <= 2) { // 不能直接取 0.2, 因为浏览器不同,可能每次size都不能刚好为 0.1
        this.pageControl.beforeScale = Number(scaleReal.toFixed(2))
        this.pageControl.scale = Number((Number(scaleReal.toFixed(2)) * 100).toFixed(0))
        // this.draggable.style.transform = `scale(${scaleReal})`
        this.renderPage(this.pageControl.curPage)
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.preview{
  position: relative;
  flex: 1 0 50%;
  background-color: #606266;
  overflow: hidden;
  .img-box{
    height: 100%;
    position: absolute;
  }
}
</style>

 pdf转canvas方式(只有renderPage方法有区别)

    // canvas 绘制 PDF
    renderPage(num) {
      // 返回单页内容实例(页面索引) pdf.getPage(index)
      this.pdfDoc.getPage(num).then((page) => {
        const canvas = document.getElementById('the-canvas' + num)
        const ctx = canvas.getContext('2d');
        ctx.mozImageSmoothingEnabled = false;
        ctx.webkitImageSmoothingEnabled = false;
        ctx.msImageSmoothingEnabled = false;
        ctx.imageSmoothingEnabled = false;
        const dpr = window.devicePixelRatio || 1
        const bsr = ctx.webkitBackingStorePixelRatio ||
                  ctx.mozBackingStorePixelRatio ||
                  ctx.msBackingStorePixelRatio ||
                  ctx.oBackingStorePixelRatio ||
                  ctx.backingStorePixelRatio || 1
        const ratio = dpr / bsr
        // 返回页面内容(比例) page.getViewport({scale:2.0})语法改这么写
        const viewport = page.getViewport({ scale: this.pageControl.beforeScale, rotation: this.pageControl.rotation });// 这是让pdf文件的大小等于视口的大小
        canvas.width = viewport.width * ratio
        canvas.height = viewport.height * ratio// 这里会进行压缩,解决模糊问题
        canvas.style.width = viewport.width + 'px'
        canvas.style.height = viewport.height + 'px'
        const renderContext = {
          canvasContext: ctx,
          viewport: viewport,
          transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
        }
        const that = this
        page.render(renderContext).promise.then(function() {
          if (that.isDestory) {
            page.getTextContent()
            if (that.pageControl.pages > num) {
              that.renderPage(num + 1)
            }
          }
        });
      })
    }

footerAi.vue

<template>
  <div class="footer-ai">
    <article>{{ mode | modefilter }}</article>
    <div class="page-select">
      <div :class="['select-btn', {'select-btn-disable': banPrev}]">
        <i
          :class="['el-icon-arrow-left', {'ban-btn':banPrev}]"
          @click="handlePage('prev')"
        />
      </div>
      <span class="qti-number">
        <el-input
          v-model="currentPage"
          size="mini"
          class="ctrl-input"
          type="number"
          @change="(page) => handlePage('input', page)"
        />
        <span>/ {{ page.pages }}页</span>
      </span>
      <div class="select-btn">
        <i
          :class="['el-icon-arrow-right', {'ban-btn':banNext}]"
          @click="handlePage('next')"
        />
      </div>
    </div>
    <div class="scale-control">
      <i class="el-icon-narrow-l" @click="emitControl('scaleControl', -1)" />
      <span>{{ page.scale + "%" }}</span>
      <i class="el-icon-amplification-m" @click="emitControl('scaleControl', 1)" />
      <i class="el-icon-adapt" @click="emitControl('scaleControl', 0)" />
    </div>
  </div>
</template>

<script>
export default {
  name: 'FooterAi',
  filters: {
    modefilter(val) {
      switch (val) {
        case 0:
          return '两栏'
        case 1:
          return '正文页'
        case 2:
          return '答案页'
        default:
          return '两栏'
      }
    }
  },
  props: {
    mode: {
      type: Number,
      default: 0
    },
    page: {
      type: Object,
      default: () => null
    }
  },
  data() {
    return {
      currentPage: this.page.curPage
    }
  },
  computed: {
    banPrev() {
      return this.currentPage === 1
    },
    banNext() {
      return this.currentPage === this.page.pages
    }
  },
  methods: {
    /**
     * 事件分发
     * @param {*} type
     * @param {*} val
     */
    emitControl(type, val) {
      this.$emit(type, val)
    },
    /**
     * 页码控制
     * @param {*} type
     * @param {*} page
     */
    handlePage(type, page) {
      if (type === 'input') { // 输入框改变页码
        if (page > this.page.pages) {
          this.$message.warning('超过最大页数!');
          this.currentPage = 1;
          return
        } else if (page <= 0) {
          this.currentPage = 1;
          return
        }
        this.currentPage = page
      } else if (type === 'prev') { // 上一页
        if (this.banPrev) return
        if (this.currentPage > 1) this.currentPage--
      } else if (type === 'next') { // 下一页
        if (this.banNext) return
        this.currentPage++
      }
      this.emitControl('pageChange', {
        type,
        page: Number(this.currentPage)
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.footer-ai{
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 36px;
  padding: 2px 20px;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: space-between;
  background-color: #F5F5F5;
  .page-select{
    display: flex;
    flex-flow: row nowrap;
    align-items: center;
    height: 28px;
    line-height: 28px;
    cursor: pointer;
    .select-btn{
      margin: 0 10px;
      &:hover{
        color: #FFAE0D;
      }
    }
    .select-btn-disable{
      background-color: #edf1f2;
    }
    ::v-deep .el-input{
      width: 40px;
      margin-right: 10px;
      .el-input__inner{
        padding: 0 10px;
        color: #FFAE0D;
        text-align: center;
      }
    }
    ::v-deep input::-webkit-outer-spin-button,
    ::v-deep input::-webkit-inner-spin-button {
      -webkit-appearance: none !important;
    }
  }
  .scale-control{
    display: flex;
    align-items: center;
    flex-flow: row nowrap;
    i {
      font-size: 24px;
      cursor: pointer;
      color: #FFAE0D;
      margin: 0 5px;
      width: 24px;
      height: 24px;
      background-size: 20px;
      &:last-child{
        background-position: center;
      }
    }
  }
}
</style>

2.3、读取pdf文件

其实我们发现一进来其实默认读取第一页的数据, renderPage(1),想要切换其他页,就传入renderPage(index),注意哦,索引是从1开始的, 读取文件将文件地址传入loadFile

    loadFile(url) {
      PDFJS.getDocument({
        url,
        cMapPacked: true
      }).promise.then((pdf) => {
        this.pdfDoc = pdf
        this.pageControl.pages = this.pdfDoc.numPages
        this.$nextTick(() => {
          this.pageControl.curPage = 1
          this.renderPage(this.pageControl.curPage)
        })
      }, (err) => {
        if (err.name === 'MissingPDFException') {
          this.$message.warn('无效的PDF链接')
        }
      })
    }

2.4、pdf文件的放大缩小

上面几点把预览,分页和拖动讲了,现在开始放大缩小了

放大缩小主要有两种:

1、鼠标滑轮控制

@mousewheel="(e) => scaleDom(e, 'wheel')"

2、自定义按钮控制

this.scaleDom(val, 'click')
// 放大
this.scaleDom(1, 'click')
// 缩小
this.scaleDom(-1, 'click')

通用方法:我设置最大放大2倍,最小缩放0.2倍,当然可以改,根据你们自己想要的来,当修改缩放比例后,再调用renderPage方法重新绘制

    /**
     * 鼠标滑轮事件
     * @param {*} e
     */
    scaleDom(e, type = 'wheel') {
      let scaleReal = this.pageControl.beforeScale
      const size = type === 'wheel' ? e.wheelDelta / 1200 : parseFloat(e / 10)
      scaleReal += size;
      if (scaleReal >= 0.18 && scaleReal <= 2) { // 不能直接取 0.2, 因为浏览器不同,可能每次size都不能刚好为 0.1
        this.pageControl.beforeScale = Number(scaleReal.toFixed(2))
        this.pageControl.scale = Number((Number(scaleReal.toFixed(2)) * 100).toFixed(0))
        this.renderPage(this.pageControl.curPage)
      }
    }

2.5、pdf的截图 

关于这里,我也在弄,目前还没出来,弄出来后会更新的

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到