pdf.js 前端pdf预览 渲染文本图层支持复制 保证手机端清晰度 双指缩放 alloyfinger(手势)

摘要:
.nament)//未定义控制台。log//UnaughtTypeError:无法读取未定义的属性“昵称”这将导致某些不受支持的浏览器显示空白页面。我使用最新版本的pdfjs-dist。vue打包的演示页面在QQ和UC微信内置浏览器中是空白的。Pdf.js不必以npm的形式使用,而是可以被页面上的脚本标签直接引用,并且只能被Pdf.jss引用,Pdf.js的源代码用于判断Pdf.worker.js的地址;如果没有,判断全局中是否有pdfjsWorker对象;没有更多,pdf的地址。工人js是通过引用pdf的地址拼接的。js。

预览地址: https://feiyefeihua.gitee.io/

一、渲染pdf核心代码

首先安装 pdf.js 的 npm 版本:

npm i pdfjs-dist@2.6.347

使用:

import * as PDFJS from "pdfjs-dist"
 
其中,pdf.js 加载pdf文件依赖了 pdf.worker.js ,通过指定 pdf.worker.js 路径让 pdf.js 加载依赖:
  
PDFJS.GlobalWorkerOptions.workerSrc ="https://cdn.jsdelivr.net/npm/pdfjs-dist@2.6.347/build/pdf.worker.min.js";
这里使用了一个 CDN 地址,自己选择。也可以引用安装的 npm 包里面的 pdf.worker.js :
window.pdfjsWorker = require("pdfjs-dist/build/pdf.worker.js")
 
 
有两点需要注意:
1. npm 库的使用没有使用最新版。因为最新版的使用了一些JS的新语法,比如  ?. 可选链:
let testObj = {
  name: '可选链',
  tips: '防止访问不存在的属性而报错'
}

console.log(testObj.names?.nickname) // undefined
console.log(testObj.names.nickname) // Uncaught TypeError: Cannot read property 'nickname' of undefined

这会导致某些不支持的浏览器显示空白页面。我使用最新版的 pdfjs-dist , vue 打包的demo页面,在  QQ、UC 微信内置浏览器打开都是空白的。

所以使用了 2.6.347 版本,再新的都有用新语法。

2. pdf.worker.js 依赖问题。pdf.js 不是非要用 npm 形式使用, 直接在页面使用 script 标签引用也可以,并且可以只引用 pdf.js 即可(保证 pdf.worker.js 的名字,并且和 pdf.s 同路径)。

pdf.js 源码里有做判断,有 pdf.worker.js 的地址就是使用;没有,就判断全局是否有 pdfjsWorker 对象;再没有,就通过 引用 pdf.js 的地址拼接出来  pdf.worker.js 的地址。

pdf.js 前端pdf预览 渲染文本图层支持复制 保证手机端清晰度 双指缩放 alloyfinger(手势)第1张

使用:

const canvasBoxEl = document.getElementById('canvas-box')
this.boxWidth = canvasBoxEl.width
this.boxHeight = canvasBoxEl.height
console.log(canvasBoxEl)
var url = './static/react-native.pdf'
PDFJS.getDocument(url)
  .promise.then(pdf => {
    // pdf.numPages 总页数
    return pdf.getPage(1)
  })
  .then(page => {
    // 设置展示比例
    var scale = 1.5
    // 获取pdf尺寸
    var viewport = page.getViewport({ scale })
    console.log(viewport)
    // 获取需要渲染的元素
    var canvas = document.createElement('canvas')
    var context = canvas.getContext('2d')
    canvas.height = viewport.height
    canvas.width = viewport.width
    canvasBoxEl.appendChild(canvas)
    var renderContext = {
      canvasContext: context,
      viewport: viewport
    }

    page.render(renderContext)
  })

 如果有多页,看需求渲染。在 PC 这样差不多就可以了,但是在手上打开,可能就很模糊。

PS:比较乱,基本功能都实现了,还需要优化。

二、保证pdf清晰度

pdf.js 是 mozilla 的开源库(地址:https://mozilla.github.io/pdf.js/),并且有一个完整的web展示页面,在手机上看也清晰。就从它的源码里面去看。

其中渲染pdf有三个地方,一个是渲染的缩略图,一个是打印的样式,剩下的那个才是正文:

pdf.js 前端pdf预览 渲染文本图层支持复制 保证手机端清晰度 双指缩放 alloyfinger(手势)第2张

主要是在渲染之前做了一些计算,canvas的 标签属性 width、height 和 css属性 width、height。还可以用svg渲染的样子?

计算使用的是一些提前确认的变量和写好的工具函数,提取出来:

const CSS_UNITS = 96.0 / 72.0;
// const PRINT_UNITS = 150 / 72.0;

let userAgent = (typeof navigator !== "undefined" && navigator.userAgent) || "";
let platform = (typeof navigator !== "undefined" && navigator.platform) || "";
let maxTouchPoints =
  (typeof navigator !== "undefined" && navigator.maxTouchPoints) || 1;

let maxCanvasPixels = 16777216;

let isAndroid = /Android/.test(userAgent);
let isIOS =
  /(iPad|iPhone|iPod)(?=;)/.test(userAgent) ||
  (platform === "MacIntel" && maxTouchPoints > 1);
(function checkCanvasSizeLimitation() {
  if (isIOS || isAndroid) {
    maxCanvasPixels = 5242880;
  }
})();

function approximateFraction(x) {
  if (Math.floor(x) === x) {
    return [x, 1];
  }

  var xinv = 1 / x;
  var limit = 8;

  if (xinv > limit) {
    return [1, limit];
  } else if (Math.floor(xinv) === xinv) {
    return [1, xinv];
  }

  var x_ = x > 1 ? xinv : x;
  var a = 0,
    b = 1,
    c = 1,
    d = 1;

  while (q < limit) {
    var p = a + c,
      q = b + d;

    if (q > limit) {
      break;
    }

    if (x_ <= p / q) {
      c = p;
      d = q;
    } else {
      a = p;
      b = q;
    }
  }

  var result;

  if (x_ - a / b < c / d - x_) {
    result = x_ === x ? [a, b] : [b, a];
  } else {
    result = x_ === x ? [c, d] : [d, c];
  }

  return result;
},
function roundToDivide(x, div) {
  var r = x % div;
  return r === 0 ? x : Math.round(x - r + div);
}

其中还有个根据 canvas 的 ctx 计算 pixelRatio 的:

let devicePixelRatio = window.devicePixelRatio || 1
      let backingStoreRatio =
        ctx.webkitBackingStorePixelRatio ||
        ctx.mozBackingStorePixelRatio ||
        ctx.msBackingStorePixelRatio ||
        ctx.oBackingStorePixelRatio ||
        ctx.backingStorePixelRatio ||
        1
 const pixelRatio = devicePixelRatio / backingStoreRatio

修改过后的完整代码:

<template>
  <div class="pdf-touch-box">
    <div   :style="{  btnWidth + 'px' }">
      <button   @click="scaleCanvas(1.5)">1.5</button>
      <button   @click="scaleCanvas(0.5)">0.5</button>
      <button   @click="scaleCanvas(1)">还原(1)</button>
    </div>

    <div v-show="!loading"   :style="{  viewWidth + 'px', height: viewHeight + 'px' }"></div>
    <p   v-show="loading">正在加载...</p>
  </div>
</template>

<script>
import * as PDFJS from 'pdfjs-dist'

// 本地
// window.pdfjsWorker = require("pdfjs-dist/build/pdf.worker.js");

// cdn 2.8.335  2.6.347 2.5.207
PDFJS.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.6.347/build/pdf.worker.min.js'

// https://github.com/mozilla/pdf.js/blob/master/examples/node/getinfo.js
// Requires single file built version of PDF.js -- please run
// `gulp singlefile` before running the example.
// const pdfjsLib = require("pdfjs-dist/legacy/build/pdf.js");

const CSS_UNITS = 96.0 / 72.0
// const PRINT_UNITS = 150 / 72.0;

let userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''
let platform = (typeof navigator !== 'undefined' && navigator.platform) || ''
let maxTouchPoints = (typeof navigator !== 'undefined' && navigator.maxTouchPoints) || 1

let maxCanvasPixels = 16777216
// PDF之外占据的宽度 -18 padding -18减去滚动条宽度(不确定)
let autoWidth = 36

let isAndroid = /Android/.test(userAgent)
let isIOS = /(iPad|iPhone|iPod)(?=;)/.test(userAgent) || (platform === 'MacIntel' && maxTouchPoints > 1)
;(function checkCanvasSizeLimitation() {
  if (isIOS || isAndroid) {
    maxCanvasPixels = 5242880
    autoWidth -= 18
  }
})()

export default {
  data() {
    return {
      src: './static/react-native.pdf',
      loading: true,
      pdfDoc: null,
      boxEl: null,
      wrapEl: null,
      areaWidth: 0,
      btnWidth: 0,
      viewWidth: 0,
      viewHeight: 0,
      pixelRatio: 2,
      isFirstTimeRender: true,
      viewport: null,
      canvasEles: [],
      canvasCtxs: [],
      totallPage: 1,
      pageScale: 1, // pdf 适应窗口产生的 scale
      curCanvasCSSWh: null,
      transform: null,
      pageRendered: false
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    async init() {
      this.boxEl = document.querySelector('.pdf-touch-box')
      this.wrapEl = document.getElementsByClassName('pdf-canvas-wrap')[0]
      this.btnWidth = this.areaWidth = this.boxEl.clientWidth

      const loadingState = await this.getPDF()
      if (loadingState === 'success') this.initRenderOneByOne()
      else this.boxEl.innerText = loadingState
    },
    scaleCanvas(scale) {
      if (!this.pageRendered) return

      // 改变 viewport 大小
      this.viewport = this.viewport.clone({
        scale: this.pageScale * scale * CSS_UNITS
      })
      const { styleWidth, styleHeight } = this.getCanvasCSSWH()
      // 先改变CSS canvas 会变模糊
      this.canvasEles.forEach((canvas, index) => {
        // 不修改 width height 不然会重置 canvas
        canvas.style.width = styleWidth + 'px'
        canvas.style.height = styleHeight + 'px'
        console.log(index)
      })
      // 重新渲染 变清晰
      this.scaleRenderAll()
    },
    // 好像改变也不是很明显
    // scaleCanvas(scale) {
    //   // 改变 viewport 大小
    //   this.viewport = this.viewport.clone({
    //     scale: this.pageScale * scale * CSS_UNITS,
    //   });
    //   // 逐个重新渲染
    //   this.renderSinglePage(this.canvasEles[0], 1);
    // },

    getPDF() {
      let that = this
      return new Promise(reslove => {
        PDFJS.getDocument(that.src).promise.then(
          function (pdfDoc_) {
            that.pdfDoc = pdfDoc_
            that.totallPage = pdfDoc_.numPages
            that.loading = false
            reslove('success')
          },
          function (reason) {
            console.log(reason.message)
            that.loading = false
            reslove(reason.name)
          }
        )
      })
    },

    initRenderOneByOne() {
      for (let pageNum = 1; pageNum <= this.totallPage; pageNum++) {
        let canvas = document.createElement('canvas')
        canvas.setAttribute('id', `pdf-canvas${pageNum}`)
        canvas.setAttribute('class', `pdfcanvas`)
        // alpha 设定 canvas 背景总是不透明,可以加快浏览器绘制透明的内容和图片 初始化出来 canvas 为黑色背景
        // 实际上 导致 重新渲染的时候 闪黑屏
        // let ctx = canvas.getContext("2d", {
        //   alpha: false,
        // });
        let ctx = canvas.getContext('2d')
        this.canvasCtxs.push(ctx)
        this.canvasEles.push(canvas)
        this.wrapEl.appendChild(canvas)
      }
      this.renderSinglePage(this.canvasEles[0], 1)
    },

    renderSinglePage(canvas, pageNum) {
      let ctx = this.canvasCtxs[pageNum - 1]
      let that = this

      this.pdfDoc.getPage(pageNum).then(function (page) {
        if (that.isFirstTimeRender) that.initView(page, ctx)

        if (pageNum === 1) that.getCanvasCSSWH()

        canvas.width = that.curCanvasCSSWh.width
        canvas.height = that.curCanvasCSSWh.height
        canvas.style.width = that.curCanvasCSSWh.styleWidth + 'px'
        canvas.style.height = that.curCanvasCSSWh.styleHeight + 'px'
        canvas.style['border'] = '#d6d6d6 solid 1px'
        canvas.style.margin = '9px 0 0 0'

        let renderContext = {
          canvasContext: ctx,
          transform: that.transform,
          viewport: that.viewport,
          enableWebGL: false,
          renderInteractiveForms: false
        }
        let renderTask = page.render(renderContext)

        renderTask.promise.then(function () {
          if (that.totallPage >= ++pageNum) {
            that.renderSinglePage(that.canvasEles[pageNum - 1], pageNum)
          } else {
            that.pageRendered = true
          }
        })
      })
    },

    scaleRenderAll() {
      const len = this.canvasEles.length
      for (let pageNum = 0; pageNum < len; pageNum++) {
        let canvas = this.canvasEles[pageNum]
        let ctx = this.canvasCtxs[pageNum]

        let that = this
        this.pdfDoc.getPage(pageNum + 1).then(function (page) {
          canvas.width = that.curCanvasCSSWh.width
          canvas.height = that.curCanvasCSSWh.height

          let renderContext = {
            canvasContext: ctx,
            transform: that.transform,
            viewport: that.viewport,
            enableWebGL: false,
            renderInteractiveForms: false
          }
          let renderTask = page.render(renderContext)

          renderTask.promise.then(function (context) {
            console.log(context)
          })
        })
      }
    },

    getCanvasCSSWH() {
      let outputScale = {
        sx: this.pixelRatio,
        sy: this.pixelRatio,
        scaled: this.pixelRatio !== 1
      }

      let pixelsInViewport = this.viewport.width * this.viewport.height
      let maxScale = Math.sqrt(maxCanvasPixels / pixelsInViewport)

      if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
        outputScale.sx = maxScale
        outputScale.sy = maxScale
        outputScale.scaled = true
      }

      let sfx = (0, this.approximateFraction)(outputScale.sx)
      let sfy = (0, this.approximateFraction)(outputScale.sy)
      const width = (0, this.roundToDivide)(this.viewport.width * outputScale.sx, sfx[0])
      const height = (0, this.roundToDivide)(this.viewport.height * outputScale.sy, sfy[0])
      const styleWidth = (0, this.roundToDivide)(this.viewport.width, sfx[1])
      const styleHeight = (0, this.roundToDivide)(this.viewport.height, sfy[1])

      if (this.pixelRatio !== 1) this.transform = [this.pixelRatio, 0, 0, this.pixelRatio, 0, 0]

      this.viewWidth = styleWidth + 2
      // 12 加上 canvas border margin 误差?2 + 9 + 1
      this.viewHeight = this.totallPage * (this.viewport.height + 12) + 9

      this.curCanvasCSSWh = { width, height, styleWidth, styleHeight }
      return this.curCanvasCSSWh
    },
    approximateFraction(x) {
      if (Math.floor(x) === x) {
        return [x, 1]
      }

      var xinv = 1 / x
      var limit = 8

      if (xinv > limit) {
        return [1, limit]
      } else if (Math.floor(xinv) === xinv) {
        return [1, xinv]
      }

      var x_ = x > 1 ? xinv : x
      var a = 0,
        b = 1,
        c = 1,
        d = 1

      while (q < limit) {
        var p = a + c,
          q = b + d

        if (q > limit) {
          break
        }

        if (x_ <= p / q) {
          c = p
          d = q
        } else {
          a = p
          b = q
        }
      }

      var result

      if (x_ - a / b < c / d - x_) {
        result = x_ === x ? [a, b] : [b, a]
      } else {
        result = x_ === x ? [c, d] : [d, c]
      }

      return result
    },
    roundToDivide(x, div) {
      var r = x % div
      return r === 0 ? x : Math.round(x - r + div)
    },

    initView(page, ctx) {
      let devicePixelRatio = window.devicePixelRatio || 1
      let backingStoreRatio =
        ctx.webkitBackingStorePixelRatio ||
        ctx.mozBackingStorePixelRatio ||
        ctx.msBackingStorePixelRatio ||
        ctx.oBackingStorePixelRatio ||
        ctx.backingStorePixelRatio ||
        1
      this.pixelRatio = devicePixelRatio / backingStoreRatio

      this.viewport = page.getViewport({
        scale: CSS_UNITS
      })
      console.log(this.viewport)

      this.pageScale = (this.areaWidth - autoWidth) / this.viewport.width
      let curViewport = page.getViewport({
        scale: this.pageScale * CSS_UNITS
      })
      this.viewport = curViewport

      this.isFirstTimeRender = false
    },

    drawBorder(canvas, ctx) {
      ctx.save()
      ctx.fillStyle = 'rgb(255, 255, 255)'
      ctx.strokeRect(0, 0, canvas.width, canvas.height)
      ctx.restore()
    }
  }
}
</script>

<style scoped>
.pdf-touch-box {
  padding: 9px;
   calc(100% - 18px);
  height: calc(100% - 18px);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.scale-btn-box {
  position: fixed;
  top: 0;
  left: 0;
  height: 44px;
  display: flex;
  justify-content: space-around;
}
.scale-btn {
   25%;
  height: 100%;
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  border: 1px solid #dcdfe6;
  color: #606266;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  outline: none;
  margin: 0;
  transition: 0.1s;
  font-weight: 500;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  padding: 12px 20px;
  font-size: 14px;
  border-radius: 4px;
}
.pdf-canvas-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
  margin-top: 44px;
  padding-top: 9px;
}
.pdf-canvas-tips {
  margin-top: 44px;
}
</style>

这样手机打开就也清晰了。

三、渲染文本图层,支持手势缩放

要渲染文本图层还需要额外的依赖:

import { TextLayerBuilder, EventBus } from "pdfjs-dist/web/pdf_viewer";
import "pdfjs-dist/web/pdf_viewer.css";
 
 只要在渲染的时候添加即可:
let renderContext = {
  canvasContext: ctx,
  transform: that.transform,
  viewport: that.viewport,
  enableWebGL: false,
  renderInteractiveForms: false,
};
let renderTask = page.render(renderContext);

renderTask.promise
  .then(function () {
    if (that.totallPage >= ++pageNum) {
      that.renderSinglePage(that.canvasEles[pageNum - 1], pageNum);
      return page.getTextContent();
    } else {
      that.pageRendered = true;
    }
  })
  .then((textContent) => {
    const textLayerDiv = document.createElement("div");
    textLayerDiv.setAttribute("class", "textLayer");
    textLayerDiv.setAttribute("style", "top: 12px");
    canvas.parentElement.appendChild(textLayerDiv);

    var textLayer = new TextLayerBuilder({
      eventBus: new EventBus(),
      textLayerDiv: textLayerDiv,
      pageIndex: pageNum,
      viewport: that.viewport,
    });

    textLayer.setTextContent(textContent);

    textLayer.render();
  });

 支持手势缩放使用  alloyfinger.js :

import AlloyFinger from "alloyfinger";
//包装一下 不然 eslint 报警告
class FingerTouch {
  constructor(element, options) {
    Object.assign(this, AlloyFinger.prototype);
    AlloyFinger.call(this, element, options);
  }
}
alloyfinger 很小,点进去没多少代码,可以学习学习。

 使用:

this.alloyFinger = new FingerTouch(this.wrapEl, {})
this.alloyFinger.on('pinch', e => {
  let zoom = e.zoom
  let curScale = this.lastStyleScale * zoom
  if (curScale <= this.pageScale / 2 || curScale >= 5) return
  this.scaleEvent(curScale)
})

this.alloyFinger.on('pressMove', e => {
  this.viewTop += e.deltaY
  this.viewLeft += e.deltaX
})

这里使用了  pressMove 事件,因为 canvas 使用了 absolute 绝对定位,支持 在容器里移动。如果不需要,就让它自适应(通过滚动条移动),就不用 pressMove 事件。

完整代码: 

<template>
  <div class="pdf-touch-box">
    <div   :style="{  btnWidth + 'px' }">
      <button   @click="scaleEvent(3.5)">3.5</button>
      <button   @click="scaleEvent(2.5)">2.5</button>
      <button   @click="scaleEvent(1.8)">1.8</button>
      <button   @click="scaleEvent(1.3)">1.3</button>
      <button   @click="scaleEvent(1)">1</button>
      <button   @click="scaleEvent(0.5)">0.5</button>
    </div>

    <div
      v-show="!loading"
      class="pdf-canvas-wrap"
      :style="{
        top: viewTop + 'px',
        left: viewLeft + 'px',
         viewWidth + 'px',
        height: viewHeight + 'px'
      }"
    ></div>
    <p   v-show="loading">正在加载...</p>
  </div>
</template>

<script>
import * as PDFJS from 'pdfjs-dist'
console.log(PDFJS)

import { TextLayerBuilder, EventBus } from 'pdfjs-dist/web/pdf_viewer'
import 'pdfjs-dist/web/pdf_viewer.css'
console.log(TextLayerBuilder)

// 本地
// window.pdfjsWorker = require("pdfjs-dist/build/pdf.worker.js");

// cdn 2.8.335  2.6.347 2.5.207
PDFJS.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.6.347/build/pdf.worker.min.js'

// https://github.com/mozilla/pdf.js/blob/master/examples/node/getinfo.js
// Requires single file built version of PDF.js -- please run
// `gulp singlefile` before running the example.
// const pdfjsLib = require("pdfjs-dist/legacy/build/pdf.js");

const CSS_UNITS = 96.0 / 72.0
// const PRINT_UNITS = 150 / 72.0;

let userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''
let platform = (typeof navigator !== 'undefined' && navigator.platform) || ''
let maxTouchPoints = (typeof navigator !== 'undefined' && navigator.maxTouchPoints) || 1

let maxCanvasPixels = 16777216
// PDF之外占据的宽度 -18 padding -18减去滚动条宽度(不确定)
let autoWidth = 36
let textLayerTop = 3
let scaleInterval = 0.05

let isAndroid = /Android/.test(userAgent)
let isIOS = /(iPad|iPhone|iPod)(?=;)/.test(userAgent) || (platform === 'MacIntel' && maxTouchPoints > 1)
;(function checkCanvasSizeLimitation() {
  if (isIOS || isAndroid) {
    maxCanvasPixels = 5242880
    autoWidth -= 18
    textLayerTop -= 1
    // 手机上面缩放对清晰度影响更小
    scaleInterval = 0.4
  }
})()
import AlloyFinger from 'alloyfinger'
//包装一下 不然 eslint 报警告
class FingerTouch {
  constructor(element, options) {
    Object.assign(this, AlloyFinger.prototype)
    AlloyFinger.call(this, element, options)
  }
}

export default {
  data() {
    return {
      src: './static/react-native.pdf',
      loading: true,
      pdfDoc: null,
      boxEl: null,
      wrapEl: null,
      areaWidth: 0,
      btnWidth: 0,
      viewWidth: 0,
      viewHeight: 0,
      pixelRatio: 2,
      isFirstTimeRender: true,
      viewport: null,
      canvasEles: [],
      canvasCtxs: [],
      totallPage: 0,
      pageScale: 1, // pdf 适应窗口产生的 scale
      curCanvasCSSWh: null,
      transform: null,
      pageRenderedNum: 0,
      scaleTimer: null,
      lastStyleScale: 1,
      lastRerenderScale: 1,
      alloyFinger: null,
      viewTop: 0,
      viewLeft: 9,
      textEls: []
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    async init() {
      //禁止下拉刷新
      document.addEventListener(
        'touchmove',
        function (ev) {
          ev.preventDefault()
        },
        { passive: false }
      )
      this.boxEl = document.querySelector('.pdf-touch-box')
      this.wrapEl = document.getElementsByClassName('pdf-canvas-wrap')[0]
      this.btnWidth = this.areaWidth = this.boxEl.clientWidth

      const loadingState = await this.getPDF()
      if (loadingState === 'success') {
        this.initRenderOneByOne()
        this.initTouch()
      } else this.boxEl.innerText = loadingState
    },
    initTouch() {
      this.alloyFinger = new FingerTouch(this.wrapEl, {})
      this.alloyFinger.on('pinch', e => {
        let zoom = e.zoom
        let curScale = this.lastStyleScale * zoom
        if (curScale <= this.pageScale / 2 || curScale >= 5) return
        this.scaleEvent(curScale)
      })

      this.alloyFinger.on('pressMove', e => {
        this.viewTop += e.deltaY
        this.viewLeft += e.deltaX
      })
    },
    scaleEvent(scale) {
      // 渲染中 不让缩放 也不让重绘
      if (this.pageRenderedNum != this.totallPage || this.totallPage === 0) return

      // 没在渲染中 随意缩放
      this.scaleCanvas(scale)

      // 说明是第一次事件 或重绘完成 开始计时
      if (this.scaleTimer === null) {
        this.scaleTimer = this.renderDelayer(666)
      }
      //时间间隔内再次触发缩放 重新计时
      clearTimeout(this.scaleTimer)
      this.scaleTimer = this.renderDelayer(666)
    },
    renderDelayer(interval) {
      return setTimeout(() => {
        this.scaleRenderAll()
        this.scaleTimer = null
      }, interval)
    },
    scaleTopLeft(width, height) {
      if (Math.abs(this.viewTop) > height / 2) this.viewTop *= 1 / 2
      if (Math.abs(this.viewLeft) > width / 2) this.viewLeft *= 1 / 2
    },
    scaleCanvas(scale) {
      this.lastStyleScale = scale
      console.log(scale)
      // 改变 viewport 大小
      this.viewport = this.viewport.clone({
        scale: (this.pageScale * CSS_UNITS * scale).toFixed(3)
      })

      const { styleWidth, styleHeight } = this.getCanvasCSSWH()

      // 计算一下 top left 不然可能会显示到 窗口外面 看不到了
      this.scaleTopLeft(styleWidth, styleHeight)

      // 改变CSS canvas 会变模糊
      this.canvasEles.forEach(canvas => {
        // 不修改 width height 不然会重置 canvas 变空白
        canvas.style.width = styleWidth + 'px'
        canvas.style.height = styleHeight + 'px'
      })
    },
    // 使用新渲染的 canvas  替换 缩放过后不清晰的 canvas
    scaleRenderAll() {
      let curInterval = Math.abs(this.lastStyleScale - this.lastRerenderScale)
      let curScaleInterval = scaleInterval
      let isNarrow = this.lastStyleScale < this.lastRerenderScale
      // 如果是变小 变化不大时 清晰度影响更小
      if (isNarrow) curScaleInterval = scaleInterval * 2

      console.log('scaleRenderAll', curScaleInterval, curInterval)
      // 变化很小的时候就不计时重新渲染了 清晰度影响不大 1.1 - 1 = 0.10000000000000009
      if (curInterval <= curScaleInterval) return

      this.lastRerenderScale = this.lastStyleScale

      this.pageRenderedNum = 0
      const len = this.canvasEles.length
      for (let pageNum = 1; pageNum <= len; pageNum++) {
        let newCanvas = document.createElement('canvas')
        let newCtx = newCanvas.getContext('2d', {
          alpha: false
        })
        newCanvas.setAttribute('id', `pdf-canvas${pageNum}`)

        this.canvasCtxs[pageNum - 1] = newCtx

        let that = this
        this.pdfDoc.getPage(pageNum).then(function (page) {
          that.setCanvasCSSWh.call(that, newCanvas)

          let renderTask = that.pageRender.call(that, page, newCtx)

          renderTask.promise
            .then(function () {
              let oldCanvas = that.canvasEles[pageNum - 1]
              oldCanvas.parentElement.replaceChild(newCanvas, oldCanvas)
              that.canvasEles[pageNum - 1] = newCanvas
              that.pageRenderedNum++

              return page.getTextContent()
            })
            .then(textContent => that.textRerender.call(that, pageNum, textContent))
            .catch(e => console.log(e))
        })
      }
    },

    getPDF() {
      let that = this
      return new Promise(reslove => {
        PDFJS.getDocument(that.src).promise.then(
          function (pdfDoc_) {
            that.pdfDoc = pdfDoc_
            that.totallPage = 1
            // that.totallPage = pdfDoc_.numPages;
            that.loading = false
            reslove('success')
          },
          function (reason) {
            console.log(reason.message)
            that.loading = false
            reslove(reason.name)
          }
        )
      })
    },

    initRenderOneByOne() {
      for (let pageNum = 1; pageNum <= this.totallPage; pageNum++) {
        let canvas = document.createElement('canvas')
        canvas.setAttribute('id', `pdf-canvas${pageNum}`)
        canvas.setAttribute('class', `pdfcanvas`)
        // alpha 设定 canvas 背景总是不透明,可以加快浏览器绘制透明的内容和图片 初始化出来 canvas 为黑色背景
        // 实际上 导致 重新渲染的时候 闪黑屏
        // let ctx = canvas.getContext("2d", {
        //   alpha: false,
        // });
        let ctx = canvas.getContext('2d')
        this.canvasCtxs.push(ctx)
        this.canvasEles.push(canvas)
        //  this.wrapEl.appendChild(canvas);

        let pageDiv = document.createElement('div')
        pageDiv.setAttribute('id', 'page-' + pageNum)
        pageDiv.setAttribute('style', 'position: relative;')
        this.wrapEl.appendChild(pageDiv)
        pageDiv.appendChild(canvas)
      }
      this.renderSinglePage(this.canvasEles[0], 1)
    },

    renderSinglePage(canvas, pageNum) {
      let ctx = this.canvasCtxs[pageNum - 1]
      let that = this

      this.pdfDoc.getPage(pageNum).then(function (page) {
        if (that.isFirstTimeRender) that.initView(page, ctx)

        if (pageNum === 1) that.getCanvasCSSWH()

        that.setCanvasCSSWh.call(that, canvas)
        let renderTask = that.pageRender.call(that, page, ctx)

        renderTask.promise
          .then(function () {
            if (that.totallPage > pageNum) {
              that.renderSinglePage(that.canvasEles[pageNum], pageNum + 1)
            }
            that.pageRenderedNum++
            return page.getTextContent()
          })
          .then(textContent => that.textRender.call(that, canvas, pageNum, textContent))
      })
    },
    textRerender(pageIndex, textContent) {
      const oldDiv = this.textEls[pageIndex - 1]

      const newDiv = document.createElement('div')
      newDiv.setAttribute('class', 'textLayer')
      newDiv.setAttribute('style', `top: ${textLayerTop}px`)

      oldDiv.parentElement.replaceChild(newDiv, oldDiv)
      this.textEls[pageIndex - 1] = newDiv
      this.renderTextLayer(newDiv, pageIndex, textContent)
    },
    textRender(canvas, pageIndex, textContent) {
      const textLayerDiv = document.createElement('div')
      textLayerDiv.setAttribute('class', 'textLayer')
      textLayerDiv.setAttribute('style', `top: ${textLayerTop}px`)
      canvas.parentElement.appendChild(textLayerDiv)
      this.textEls[pageIndex - 1] = textLayerDiv
      this.renderTextLayer(textLayerDiv, pageIndex, textContent)
    },
    renderTextLayer(el, index, content) {
      var textLayer = new TextLayerBuilder({
        eventBus: new EventBus(),
        textLayerDiv: el,
        pageIndex: index,
        viewport: this.viewport
      })

      textLayer.setTextContent(content)
      textLayer.render()
    },
    setCanvasCSSWh(canvas) {
      canvas.width = this.curCanvasCSSWh.width
      canvas.height = this.curCanvasCSSWh.height
      canvas.style.width = this.curCanvasCSSWh.styleWidth + 'px'
      canvas.style.height = this.curCanvasCSSWh.styleHeight + 'px'
      canvas.style['border'] = '#d6d6d6 solid 1px'
      canvas.style.margin = '0 0 9px 0'
    },
    pageRender(page, ctx) {
      return page.render({
        canvasContext: ctx,
        transform: this.transform,
        viewport: this.viewport,
        enableWebGL: false,
        renderInteractiveForms: false
      })
    },

    drawBorder(canvas, ctx) {
      ctx.save()
      ctx.fillStyle = 'rgb(255, 255, 255)'
      ctx.strokeRect(0, 0, canvas.width, canvas.height)
      ctx.restore()
    },
    getCanvasCSSWH() {
      let outputScale = {
        sx: this.pixelRatio,
        sy: this.pixelRatio,
        scaled: this.pixelRatio !== 1
      }

      let pixelsInViewport = this.viewport.width * this.viewport.height
      let maxScale = Math.sqrt(maxCanvasPixels / pixelsInViewport)

      if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
        // 这里触发会出错
        // outputScale.sx = maxScale;
        // outputScale.sy = maxScale;
        // outputScale.scaled = true;
      }

      let sfx = (0, this.approximateFraction)(outputScale.sx)
      let sfy = (0, this.approximateFraction)(outputScale.sy)
      const width = (0, this.roundToDivide)(this.viewport.width * outputScale.sx, sfx[0])
      const height = (0, this.roundToDivide)(this.viewport.height * outputScale.sy, sfy[0])
      const styleWidth = (0, this.roundToDivide)(this.viewport.width, sfx[1])
      const styleHeight = (0, this.roundToDivide)(this.viewport.height, sfy[1])

      if (this.pixelRatio !== 1) this.transform = [this.pixelRatio, 0, 0, this.pixelRatio, 0, 0]

      this.viewWidth = styleWidth + 2
      // 12 加上 canvas border margin 误差?2 + 9 + 1
      this.viewHeight = this.totallPage * (this.viewport.height + 12) + 9

      this.curCanvasCSSWh = { width, height, styleWidth, styleHeight }
      return this.curCanvasCSSWh
    },
    approximateFraction(x) {
      if (Math.floor(x) === x) {
        return [x, 1]
      }

      var xinv = 1 / x
      var limit = 8

      if (xinv > limit) {
        return [1, limit]
      } else if (Math.floor(xinv) === xinv) {
        return [1, xinv]
      }

      var x_ = x > 1 ? xinv : x
      var a = 0,
        b = 1,
        c = 1,
        d = 1
      // eslint-disable-next-line
      while (true) {
        var p = a + c,
          q = b + d

        if (q > limit) {
          break
        }

        if (x_ <= p / q) {
          c = p
          d = q
        } else {
          a = p
          b = q
        }
      }

      var result

      if (x_ - a / b < c / d - x_) {
        result = x_ === x ? [a, b] : [b, a]
      } else {
        result = x_ === x ? [c, d] : [d, c]
      }

      return result
    },
    roundToDivide(x, div) {
      var r = x % div
      return r === 0 ? x : Math.round(x - r + div)
    },

    initView(page, ctx) {
      let devicePixelRatio = window.devicePixelRatio || 1
      let backingStoreRatio =
        ctx.webkitBackingStorePixelRatio ||
        ctx.mozBackingStorePixelRatio ||
        ctx.msBackingStorePixelRatio ||
        ctx.oBackingStorePixelRatio ||
        ctx.backingStorePixelRatio ||
        1
      this.pixelRatio = devicePixelRatio / backingStoreRatio

      this.viewport = page.getViewport({
        scale: CSS_UNITS
      })

      this.pageScale = (this.areaWidth - autoWidth) / this.viewport.width
      let curViewport = page.getViewport({
        scale: this.pageScale * CSS_UNITS
      })
      this.viewport = curViewport

      this.isFirstTimeRender = false
    }
  }
}
</script>

<style scoped>
.pdf-touch-box {
  padding: 9px;
   calc(100% - 18px);
  height: calc(100% - 18px);
  position: relative;
}
.scale-btn-box {
  position: fixed;
  top: 0;
  left: 0;
  height: 44px;
  display: flex;
  justify-content: space-around;
  z-index: 99;
}
.scale-btn-box::after {
  content: '';
   100%;
  height: 100%;
  position: absolute;
  background: #fff;
  top: 0;
  left: 0;
  filter: blur(18px);
  opacity: 0.8;
}
.scale-btn {
  position: relative;
  z-index: 2;
   25%;
  height: 100%;
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  border: 1px solid #dcdfe6;
  color: #606266;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  outline: none;
  margin: 0;
  transition: 0.1s;
  font-weight: 500;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  padding: 12px 20px;
  font-size: 14px;
  border-radius: 4px;
}
.pdf-canvas-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
  margin-top: 44px;
  padding-top: 9px;
  position: absolute;
}
.pdf-canvas-tips {
  margin-top: 44px;
}
</style>

免责声明:文章转载自《pdf.js 前端pdf预览 渲染文本图层支持复制 保证手机端清晰度 双指缩放 alloyfinger(手势)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Cocos Creator 组件ListViewAndroid开发 ExpandableListView 可折叠列表详解下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

tkinter学习(4)frame、pack、canvas学习

1.frame和pack学习 1.1 代码: importtkinter as tk window =tk.Tk() window.title('my window') window.geometry('600x400+500+0') #tk.Label(window, text='on the window').pack() #这种写法很简洁#与下...

IE低版本 canvas的支持

// Copyright 2006 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may...

Android图形系统之Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback开发实例

原文:Android图形系统之Surface、SurfaceView、SurfaceHolder及SurfaceHolder.Callback之间的联系 Surface是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。 Surface本身的作用类似一个句柄,得到了这个句柄就可...

网页在pc端与移动端的兼容问题

一、浏览器的默认font-size 火狐,谷歌,IE默认16px 二、根元素的font-size设置 1、许多方法测试所得(网上)兼容电脑浏览器缩放的html {    font-size: 62.5%;}@media only screen and (min- 481px){    html {        font-size: 94%!importa...

基于canvas实现合图引擎 设计思路

合图引擎基于json数据 解析,比较与htmltocanvas 先生成html再生成canva的解决方案, 渲染过程更可控,定制化成都高。 import Dev from '@ali/hetuCanva/dist/dev'; const data = { "name": "draw edit action", "width": 375, "h...

软件项目技术点(7)——在canvas上绘制自定义图形

AxeSlide软件项目梳理   canvas绘图系列知识点整理 图形种类 目前我们软件可以绘制出来的形状有如下这几种,作为开发者我们一直想支持用户可以拖拽的类似word里面图形库,但目前还没有找到比较合适的库。 下图中所有的图形都是我们自己写代码在canvas上绘制方法出来的,可以改变实心/空心,改变颜色,大小宽高,线条弯曲度,透明度等。 父类sha...