【PDF】Canvas绘制PDF及截图

1、简要描述

林大大来啦~最近在搞pdf文件的预览、缩放、拖动和截图,才发现其实要比普通图片的缩放拖动截图麻烦的多。

本篇重点:pdf的截图所使用的矩形框是怎么画出来的呢?想使用的童鞋参考我另一篇博客~

【JS】原生js实现矩形框的绘制/拖动/缩放

普通图片

预览、缩放、拖动:

将图片通过img标签预览,然后加入v-drag指令实现拖拽,通过transform: scale 来实现缩放

截图:

先绘制矩形框,这个我下面着重讲一下,然后获取到矩形框的x,y以及宽度高度,然后在canvas里面进行drawImage,最后导出图片就行了

想了解一下图片的预览、缩放、拖动和截图,可以参考我的另一篇博客,链接如下:

canvas图像绘制(图像放大、缩小、拖动和截图)

pdf

拖动还是用的v-drag,预览、缩放、用的pdfjs-dist, 截图用的html2canvas,pdf想要截图的话,主要是canvas和svg截图,但是我试了,svg相关的截图都是按照整个dom来截的,无法实现对应坐标下的局部截图,所以就考虑常规canvas截图

流程:pdf转canvas再drawImage截图

2、使用插件及相关核心知识

1、pdfjs-dist

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

pnpm install pdfjs-dist@2.2.228

2、html2canvas

版本 : ~1.4.1 这个依赖版本没啥要求,默认就行了

pnpm install html2canvas

 3、v-drag

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

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

4、绘制矩形框

 在父级dom下,当鼠标mousedown时开始矩形框绘制,mousemove时候进行矩形框宽高的改变,mouseup时,完成矩形框绘制

图例:

这部分重点其实当父级监听到onmousedown时,去获取鼠标clientX和clientY相当于当前父级的坐标,mouseEvent就是对dom的监听函数,参数type可以设置为true/false 方便你监听/取消监听

dom.getBoundingClientRect() : 获取dom的坐标及相关属性,这个不多说,就是获取他离window的边距的像素距离

x0 = e.clientX - originPos.x: 获取当前矩形框起始点的x坐标
y0 = e.clientY - originPos.y: 获取当前矩形框起始点的y坐标

startNew: 内有x、y、w、w四个数据,分别是起始点(左上角坐标)和宽高,当有宽高时,说明已经绘制了矩形框了,再次点击四个顶点10px的距离的话,会被判定为对矩形框的编辑拉伸。

    mouseEvent(type) {
      const mask = document.getElementById('parent-dom') // 父级盒子
      if (type) {
        let x0 = 0
        let y0 = 0
        let originPos = {}
        mask.onmousedown = e => {
          originPos = mask.getBoundingClientRect()
          x0 = e.clientX - originPos.x
          y0 = e.clientY - originPos.y
          let editFlag = false
          const difference = 10 // 模糊距离,当前点击像素点位置在这范围内都算是该点
          if (this.startNew.x > 0 && this.startNew.y > 0) {
            // 当前点击点在右上角difference px像素以内
            const neX = x0 <= this.startNew.x + this.startNew.w + difference && x0 >= this.startNew.x + this.startNew.w - difference
            const neY = y0 >= this.startNew.y - difference && y0 <= this.startNew.y + difference
            // 当前点击点在左上角difference px像素以内
            const nwX = x0 >= this.startNew.x - difference && x0 <= this.startNew.x + difference
            const nwY = y0 >= this.startNew.y - difference && y0 <= this.startNew.y + difference
            // 当前点击点在左下角difference px像素以内
            const swX = x0 >= this.startNew.x - difference && x0 <= this.startNew.x + difference
            const swY = y0 <= this.startNew.y + this.startNew.h + difference && y0 >= this.startNew.y + this.startNew.h - difference
            // 当前点击点在右下角difference px像素以内
            const seX = x0 <= this.startNew.x + this.startNew.w + difference && x0 >= this.startNew.x + this.startNew.w - difference
            const seY = y0 <= this.startNew.y + this.startNew.h + difference && y0 >= this.startNew.y + this.startNew.h - difference
            if (neX && neY) {
              x0 = this.startNew.x
              y0 = this.startNew.y + this.startNew.h
              editFlag = true
            } else if (nwX && nwY) {
              x0 = this.startNew.x + this.startNew.w
              y0 = this.startNew.y + this.startNew.h
              editFlag = true
            } else if (swX && swY) {
              x0 = this.startNew.x + this.startNew.w
              y0 = this.startNew.y
              editFlag = true
            } else if (seX && seY) {
              x0 = this.startNew.x
              y0 = this.startNew.y
              editFlag = true
            }
            if (editFlag) {
              mask.onmousemove = e => {
                this.moveCode(e, originPos, x0, y0)
              }
            }
            // 当前点击点在矩形框difference px之外 则就行保存操作
            if (x0 > (this.startNew.x + this.startNew.w + difference) || x0 < (this.startNew.x - difference) || y0 > (this.startNew.y + this.startNew.h + difference) || y0 < (this.startNew.y - difference)) {
              // 点击鼠标左键
              if (e.button === 0) {
              // 点击鼠标右键 退出截图
              } else if (e.button === 2) {
              }
            }
          } else {
            this.startNew = {
              x: x0,
              y: y0,
              w: 0,
              h: 0
            }
            if (!mask.onmousemove) {
              mask.onmousemove = e => {
                this.moveCode(e, originPos, x0, y0)
              }
            }
          }
        }
        mask.onmousemove = e => {
          this.moveCode(e, originPos, x0, y0)
        }
        mask.onmouseup = e => {
          // 点击鼠标右键 退出截图
          if (e.button === 2) {
          // 点击左键,有矩形框的话,就完成截图
          } else {
            if (this.startNew.w > 0) { // 因为作业批改项目也遇到这问题,所以当有实际绘制距离才认为是截图完成
              mask.onmousemove = null
              mask.style.cursor = ''
            }
          }
        }
      } else {
        mask.onmousedown = null
        mask.onmousemove = null
        mask.onmouseup = null
      }
    }

 mousemove过程中边界方向判定方法:

 这部分我写出来很麻烦的,花了好多时间。。主要是想实现矩形框的随意拉伸

    /**
     * mousemove逻辑处理
     * @param {*} e
     * @param {*} originPos
     * @param {*} x0
     * @param {*} y0
     */
    moveCode(e, originPos, x0, y0) {
      const x1 = e.clientX - originPos.x
      const y1 = e.clientY - originPos.y
      // 第一象限
      if (x1 >= x0 && y1 < y0) {
        // x 越界处理
        this.startNew.x = x0
        // y 越界处理
        this.startNew.y = e.clientY <= originPos.top ? 0 : y1
        this.startNew.w = e.clientX >= originPos.right ? originPos.width - x0 : x1 - x0
        this.startNew.h = e.clientY <= originPos.top ? y0 : y0 - y1
      // 第二象限
      } else if (x1 < x0 && y1 < y0) {
        // x 越界处理
        this.startNew.x = e.clientX <= originPos.left ? 0 : x1
        // y 越界处理
        this.startNew.y = e.clientY <= originPos.top ? 0 : y1
        this.startNew.w = e.clientX <= originPos.left ? x0 : x0 - x1
        this.startNew.h = e.clientY <= originPos.top ? y0 : y0 - y1
        // 第三象限
      } else if (x1 < x0 && y1 >= y0) {
        // x 越界处理
        this.startNew.x = e.clientX <= originPos.left ? 0 : x1
        // y 越界处理
        this.startNew.y = y0
        this.startNew.w = e.clientX <= originPos.left ? x0 : x0 - x1
        this.startNew.h = e.clientY >= originPos.bottom ? originPos.height - y0 : y1 - y0
      // 第四象限
      } else if (x1 >= x0 && y1 >= y0) {
        // x 越界处理
        this.startNew.x = x0
        // y 越界处理
        this.startNew.y = y0
        this.startNew.w = e.clientX >= originPos.right ? originPos.width - x0 : x1 - x0
        this.startNew.h = e.clientY >= originPos.bottom ? originPos.height - y0 : y1 - y0
      }
    }

3、源码

图例:

1、页面结构

 

 index.vue: 主界面代码

preview.vue: 预览代码(核心)

clipRect.vue: 截图矩形框代码

footer.vue: 主界面底部代码

header.vue: 主界面顶部代码

2、定义需要的数据

index.vue

      curClickArea: 1, // 当前点击的是正文栏还是答案栏 1 => 正文, 2 => 答案
      clipStatus: 0, // 是否处于截图状态 0 => 否, 1 => 是(已就绪),2 => 是(正在截图), 3 => 是(完成截图)
      mode: 0, // 0 => 双栏, 1 => 正文页, 2 => 答案页
      canPaint: true, // 当前是否能进行绘制, true => mousedown 触发绘制,false => mousedown 保存
      previewList: [
        {
          name: '正文页',
          id: 1,
          pdfUrl: 'xxx.pdf'
        },
        {
          name: '答案页',
          id: 2,
          pdfUrl: 'xxx.pdf'
        }
      ],
      startNew: {} // 绘制坐标及宽高

preview.vue

      defaultPage: 1, // pdfjs-dist 展示第一页时会倒置,所以我下面逻辑判断第一页时进行部分操作
      firstPaint: false, // 只读pdf对象 首次是否绘制完毕
      firstPaint1: false, // 可编辑pdf对象 首次是否绘制完毕
      defaultCanvas: null, // 当前正准备截图的canvas,数据取自对应页码只读pdf对象
      pageControl: { // 页码参数
        curPage: 1, // 当前页码
        pages: 0, // 总共页码
        scale: 100, // 转换后缩放比例, 默认为100, 最大缩放 2,最小缩放 0.2
        beforeScale: 1, // 转换前缩放比例
        rotation: 0 // 当前旋转参数
      },
      pdfDoc: null, // 只读pdf对象
      pdfDoc1: null, // 可编辑pdf对象
      draggable: null // 外层拖动dom

3、文件源码

index.vue

<template>
  <div class="preview-problem">
    <header-ai
      :mode="mode"
      :clip-status="clipStatus"
      :expend="expend"
      @mode="modeChange"
      @btn="btnChange"
      v-on="$listeners"
    />
    <div class="preview-body">
      <preview
        v-for="item in previewList"
        v-show="modeWatch(item.id)"
        :ref="`preview${item.id}`"
        :key="item.id"
        :item="item"
        :mode="mode"
        :offset="startNew"
        :clip-status="clipStatus"
        :can-clip="curClickArea===item.id"
        :class="{'preview-out-box': mode===0}"
        @file="fileUpload"
      />
      <div
        v-if="clipStatus"
        id="mask"
        class="mask"
        :style="{'backgroundColor': clipStatus === 1 ? 'rgba(0, 0, 0, 0.4)' : ''}"
      >
        <div class="mask-title">
          <span>截图</span>
          <span @click="clipPhoto(2)">关闭</span>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import HeaderAi from './component/header.vue'
import Preview from './preview.vue'
export default {
  name: 'PreviewProblem',
  components: {
    HeaderAi,
    Preview
  },
  props: {
    expend: {
      type: Number,
      default: 2
    }
  },
  data() {
    return {
      curClickArea: 1, // 当前点击的是正文栏还是答案栏 1 => 正文, 2 => 答案
      clipStatus: 0, // 是否处于截图状态 0 => 否, 1 => 是(已就绪),2 => 是(正在截图), 3 => 是(完成截图)
      mode: 0, // 0 => 双栏, 1 => 正文页, 2 => 答案页
      canPaint: true, // 当前是否能进行绘制, true => mousedown 触发绘制,false => mousedown 保存
      previewList: [
        {
          name: '正文页',
          id: 1,
          pdfUrl: 'xxx.pdf'
        },
        {
          name: '答案页',
          id: 2,
          pdfUrl: 'xxx.pdf'
        }
      ],
      startNew: {} // 绘制坐标及宽高
    }
  },
  mounted() {
    this.$nextTick(() => {
      window.addEventListener('keydown', this.screensEvent)
    })
  },
  beforeDestroy() {
    window.removeEventListener('keydown', this.screensEvent)
    this.mouseEvent(false)
  },
  methods: {
    fileUpload(file) {
      console.log(file);
    },
    modeWatch(id) {
      // 当从展开到缩小 并且处于双栏时, 只展示正文页
      if (this.expend === 1 && this.mode === 0) {
        if (id === 1) {
          return true
        } else {
          return false
        }
      }
      return this.mode === 0 || this.mode === id
    },
    /**
     * 当前分栏模式改变
     * @param {*} val
     */
    modeChange(val) {
      this.mode = val
    },
    btnChange(val) {
      if (val === 'clip') {
        this.clipPhoto(0)
      }
    },
    /**
     * 截图操作 0 => 开始截图 1 => 保存截图 2 => 退出截图 3 => 撤销当次截图
     * @param {*} type
     */
    clipPhoto(type) {
      switch (type) {
        case 0:
          this.startClip()
          break
        // case 1:
        //   this.saveClip()
        //   break
        case 2:
          this.closeClip()
          break
        case 3:
          this.cancelClip()
          break
      }
    },
    /**
     * 截图快捷键
     * @param {*} e
     */
    screensEvent(e) {
      if ((e.altKey || e.ctrlKey) && [81, 83, 87, 90].includes(e.keyCode)) {
        e.preventDefault();
        let typeCode = ''
        if (e.altKey && e.keyCode === 81) typeCode = 0 // 截图快捷操作:alt + q
        if (e.altKey && e.keyCode === 87) typeCode = 4 // 截图快捷操作:alt + w 粘贴
        else if (e.ctrlKey && e.keyCode === 90) typeCode = 3 // 撤销当前截图操作:ctrl + z
        if (typeCode || typeCode === 0) {
          this.clipPhoto(typeCode)
        }
      }
      return false
    },
    /**
     * 开始截图
     */
    startClip() {
      this.clipStatus = 1
      this.$nextTick(() => {
        document.oncontextmenu = e => { // 阻止右键默认事件
          e.preventDefault()
        }
        this.mouseEvent(true)
      })
    },
    /**
     * 撤销当前截图
     */
    cancelClip() {
      this.startNew.x = -9999 // 重置坐标点
      this.startNew.y = -9999
      this.startNew.w = 0
      this.startNew.h = 0
      this.clipStatus = 1 // 将状态置回就绪
    },
    /**
     * 关闭截图
     */
    closeClip() {
      this.cancelClip()
      this.clipStatus = 0
      this.mouseEvent(false) // 销毁鼠标事件
      setTimeout(() => { // 必须要加延时,不然右键退出截图状态时会立即恢复默认事件
        document.oncontextmenu = null // 恢复右键默认事件
      }, 100)
    },
    mouseEvent(type) {
      const mask = document.getElementById('mask')
      if (type) {
        let x0 = 0
        let y0 = 0
        let originPos = {}
        mask.onmousedown = e => {
          this.clipStatus = 2
          originPos = mask.getBoundingClientRect()
          x0 = e.clientX - originPos.x
          y0 = e.clientY - originPos.y
          if (this.mode !== 0) {
            this.curClickArea = this.mode
          } else {
            if (x0 < (mask.offsetWidth / 2)) { // 当点击x坐标小于蒙层一半时,我们认为点击的是左侧正文部分,否则为右侧答案部分
              this.curClickArea = 1
            } else {
              this.curClickArea = 2
            }
          }
          let editFlag = false
          const difference = 10 // 模糊距离,当前点击像素点位置在这范围内都算是该点
          if (this.startNew.x > 0 && this.startNew.y > 0) {
            // 当前点击点在右上角difference px像素以内
            const neX = x0 <= this.startNew.x + this.startNew.w + difference && x0 >= this.startNew.x + this.startNew.w - difference
            const neY = y0 >= this.startNew.y - difference && y0 <= this.startNew.y + difference
            // 当前点击点在左上角difference px像素以内
            const nwX = x0 >= this.startNew.x - difference && x0 <= this.startNew.x + difference
            const nwY = y0 >= this.startNew.y - difference && y0 <= this.startNew.y + difference
            // 当前点击点在左下角difference px像素以内
            const swX = x0 >= this.startNew.x - difference && x0 <= this.startNew.x + difference
            const swY = y0 <= this.startNew.y + this.startNew.h + difference && y0 >= this.startNew.y + this.startNew.h - difference
            // 当前点击点在右下角difference px像素以内
            const seX = x0 <= this.startNew.x + this.startNew.w + difference && x0 >= this.startNew.x + this.startNew.w - difference
            const seY = y0 <= this.startNew.y + this.startNew.h + difference && y0 >= this.startNew.y + this.startNew.h - difference
            if (neX && neY) {
              x0 = this.startNew.x
              y0 = this.startNew.y + this.startNew.h
              editFlag = true
            } else if (nwX && nwY) {
              x0 = this.startNew.x + this.startNew.w
              y0 = this.startNew.y + this.startNew.h
              editFlag = true
            } else if (swX && swY) {
              x0 = this.startNew.x + this.startNew.w
              y0 = this.startNew.y
              editFlag = true
            } else if (seX && seY) {
              x0 = this.startNew.x
              y0 = this.startNew.y
              editFlag = true
            }
            if (editFlag) {
              mask.onmousemove = e => {
                this.moveCode(e, originPos, x0, y0)
              }
            }
            // 当前点击点在矩形框difference px之外 则就行保存操作
            if (x0 > (this.startNew.x + this.startNew.w + difference) || x0 < (this.startNew.x - difference) || y0 > (this.startNew.y + this.startNew.h + difference) || y0 < (this.startNew.y - difference)) {
              // 点击鼠标左键
              if (e.button === 0) {
              // 点击鼠标右键 退出截图
              } else if (e.button === 2) {
                this.clipPhoto(2)
              }
            }
          } else {
            this.startNew = {
              x: x0,
              y: y0,
              w: 0,
              h: 0
            }
            if (!mask.onmousemove) {
              mask.onmousemove = e => {
                this.moveCode(e, originPos, x0, y0)
              }
            }
          }
        }
        mask.onmousemove = e => {
          this.moveCode(e, originPos, x0, y0)
        }
        mask.onmouseup = e => {
          // 点击鼠标右键 退出截图
          if (e.button === 2) {
            this.clipPhoto(2)
          // 点击左键,有矩形框的话,就完成截图
          } else {
            if (this.startNew.w > 0) { // 因为作业批改项目也遇到这问题,所以当有实际绘制距离才认为是截图完成
              this.clipStatus = 3
              mask.onmousemove = null
              mask.style.cursor = ''
            }
          }
        }
      } else {
        mask.onmousedown = null
        mask.onmousemove = null
        mask.onmouseup = null
      }
    },
    /**
     * mousemove逻辑处理
     * @param {*} e
     * @param {*} originPos
     * @param {*} x0
     * @param {*} y0
     */
    moveCode(e, originPos, x0, y0) {
      const x1 = e.clientX - originPos.x
      const y1 = e.clientY - originPos.y
      // 第一象限
      if (x1 >= x0 && y1 < y0) {
        // x 越界处理
        this.startNew.x = x0
        // y 越界处理
        this.startNew.y = e.clientY <= originPos.top ? 0 : y1
        this.startNew.w = e.clientX >= originPos.right ? originPos.width - x0 : x1 - x0
        this.startNew.h = e.clientY <= originPos.top ? y0 : y0 - y1
      // 第二象限
      } else if (x1 < x0 && y1 < y0) {
        // x 越界处理
        this.startNew.x = e.clientX <= originPos.left ? 0 : x1
        // y 越界处理
        this.startNew.y = e.clientY <= originPos.top ? 0 : y1
        this.startNew.w = e.clientX <= originPos.left ? x0 : x0 - x1
        this.startNew.h = e.clientY <= originPos.top ? y0 : y0 - y1
        // 第三象限
      } else if (x1 < x0 && y1 >= y0) {
        // x 越界处理
        this.startNew.x = e.clientX <= originPos.left ? 0 : x1
        // y 越界处理
        this.startNew.y = y0
        this.startNew.w = e.clientX <= originPos.left ? x0 : x0 - x1
        this.startNew.h = e.clientY >= originPos.bottom ? originPos.height - y0 : y1 - y0
      // 第四象限
      } else if (x1 >= x0 && y1 >= y0) {
        // x 越界处理
        this.startNew.x = x0
        // y 越界处理
        this.startNew.y = y0
        this.startNew.w = e.clientX >= originPos.right ? originPos.width - x0 : x1 - x0
        this.startNew.h = e.clientY >= originPos.bottom ? originPos.height - y0 : y1 - y0
      }
    }
  }
}
</script>
<style lang="scss" scoped>
.preview-problem{
  height: 100%;
  background-color: #3E3E3E;
  .preview-body{
    height: calc(100% - 40px);
    display: flex;
    flex-flow: row nowrap;
    position: relative;
    overflow: hidden;
    .preview-out-box:nth-of-type(1) {
      border-right: 2px solid #3E3E3E;
    }
    .mask{
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      border: 2px solid #FFAE0D;
      cursor: crosshair;
      .mask-title{
        display: flex;
        flex-flow: row nowrap;
        align-items: center;
        justify-content: space-between;
        height: 18px;
        line-height: 18px;
        padding: 0 10px;
        background-color: #FFAE0D;
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        z-index: 2;
        span{
          color: #fff;
          &:last-child{
            cursor: pointer;
          }
        }
      }
    }
  }
}
</style>

 preview.vue

<template>
  <div ref="preview" class="preview">
    <div
      :id="`img-box${item.id}`"
      v-drag
      class="img-box"
      @mousewheel="(e) => scaleDom(e, 'wheel')"
    >
      <canvas
        :id="`see-canvas-${item.id}`"
        class="see-canvas"
      />
      <canvas
        :id="`edit-canvas-${item.id}`"
      />
    </div>
    <clip-rect
      :id="item.id"
      :offset="offset"
      :mode="mode"
      v-bind="$attrs"
    />
    <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 html2canvas from 'html2canvas';
import FooterAi from './component/footer.vue'
import ClipRect from './component/clipRect.vue'
export default {
  name: 'Preview',
  components: {
    FooterAi,
    ClipRect
  },
  inheritAttrs: false,
  props: {
    /**
     * 当前分栏的详情
     */
    item: {
      type: Object,
      default: () => {}
    },
    /**
     * 绘制矩形框坐标对象(基于整个蒙层)
     */
    offset: {
      type: Object,
      default: () => null
    },
    /**
     * 当前模式
     */
    mode: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      defaultPage: 1, // pdfjs-dist 展示第一页时会倒置,所以我下面逻辑判断第一页时进行部分操作
      firstPaint: false, // 只读pdf对象 首次是否绘制完毕
      firstPaint1: false, // 可编辑pdf对象 首次是否绘制完毕
      defaultCanvas: null, // 当前正准备截图的canvas,数据取自对应页码只读pdf对象
      pageControl: { // 页码参数
        curPage: 1, // 当前页码
        pages: 0, // 总共页码
        scale: 100, // 转换后缩放比例, 默认为100, 最大缩放 2,最小缩放 0.2
        beforeScale: 1, // 转换前缩放比例
        rotation: 0 // 当前旋转参数
      },
      pdfDoc: null, // 只读pdf对象
      pdfDoc1: null, // 可编辑pdf对象
      draggable: null // 外层拖动dom
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.draggable = document.getElementById(`img-box${this.item.id}`)
      this.loadFile(this.item.pdfUrl)
    })
  },
  methods: {
    /**
     * canvas以canvas为底图绘制截图
     * 这一部分其实计算当前矩形框在原始比例图中坐标比较麻烦
     */
    clip() {
      const w = this.offset.w / this.pageControl.beforeScale // 100比例下的矩形框宽度
      const h = this.offset.h / this.pageControl.beforeScale // 100比例下的矩形框高度
      const dragX = this.draggable.offsetLeft // 父盒子dom拖动位移left
      const dragY = this.draggable.offsetTop // 父盒子dom拖动位移top
      let x = 0
      const y = (this.offset.y - dragY) / this.pageControl.beforeScale
      if (this.mode === 0 && this.item.id === 2) { // 双栏模式下的答案页 矩形框的x点坐标
        x = (this.offset.x - this.$el.offsetWidth - dragX) / this.pageControl.beforeScale
      } else {
        x = (this.offset.x - dragX) / this.pageControl.beforeScale
      }
      const scale = window.devicePixelRatio < 3 ? window.devicePixelRatio : 2 // 当前设备物理像素分辨率,正常默认的都为1
      html2canvas(this.defaultCanvas, {
        width: this.defaultCanvas.width,
        height: this.defaultCanvas.height,
        scale: scale, // 处理模糊问题
        useCORS: true // 开启跨域,这个是必须的
      }).then(canvas => {
        const tmpCanvas = document.createElement('canvas')
        const tmpContext = tmpCanvas.getContext('2d')
        tmpCanvas.width = w
        tmpCanvas.height = h
        tmpContext.drawImage(canvas, x, y, w, h, 0, 0, w, h)
        tmpCanvas.toBlob((blob) => console.log(URL.createObjectURL(blob)))
        tmpCanvas.toBlob((blob) => {
          this.$emit('file', this.blob2file(blob))
        }, 'image/png', 1)
      })
    },
    /**
     * 只读pdf的对应页码数据渲染
     * @param {*} num
     */
    renderPage(num) {
      // 返回单页内容实例(页面索引)
      this.pdfDoc.getPage(num).then((page) => {
        // canvas 绘制 PDF
        const canvas = document.getElementById(`see-canvas-${this.item.id}`)
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        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
        // 下面这一段if 其实算是官方的bug,因为首页pdf会倒置,然后再调用renderPage就一切正常了,所以只有代码先放大再缩小,才能让pdf正放
        if (num === this.defaultPage && !this.firstPaint) {
          const viewport = page.getViewport({ scale: 1.1 });// 这是让pdf文件的大小等于视口的大小
          canvas.width = viewport.width * ratio
          canvas.height = viewport.height * ratio// 这里会进行压缩,解决模糊问题
          const renderContext = {
            canvasContext: ctx,
            viewport: viewport,
            transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
          }
          page.render(renderContext).promise.then(() => {
            const viewport1 = page.getViewport({ scale: 1 });// 这是让pdf文件的大小等于视口的大小
            canvas.width = viewport1.width * ratio
            canvas.height = viewport1.height * ratio// 这里会进行压缩,解决模糊问题
            const renderContext1 = {
              canvasContext: ctx,
              viewport: viewport1,
              transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
            }
            page.render(renderContext1).promise.then(() => {
              this.defaultCanvas = canvas // 设置默认scale为1的canvas,方便后面以此为基准来截图
              this.firstPaint = true
            })
          })
        } else {
          const viewport = page.getViewport({ scale: 1 });// 这是让pdf文件的大小等于视口的大小
          canvas.width = viewport.width * ratio
          canvas.height = viewport.height * ratio// 这里会进行压缩,解决模糊问题
          const renderContext = {
            canvasContext: ctx,
            viewport: viewport,
            transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
          }
          page.render(renderContext).promise.then(() => {
            this.defaultCanvas = canvas
          })
        }
      })
    },
    /**
     * 可编辑pdf的对应页码数据渲染
     * @param {*} num
     */
    realRenderPage(num) {
      // 返回单页内容实例(页面索引)
      this.pdfDoc1.getPage(num).then((page) => {
        // canvas 绘制 PDF
        const canvas = document.getElementById(`edit-canvas-${this.item.id}`)
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        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
        if (num === this.defaultPage && !this.firstPaint1) {
          const viewport = page.getViewport({ scale: 1.1 });// 这是让pdf文件的大小等于视口的大小
          canvas.width = viewport.width * ratio
          canvas.height = viewport.height * ratio// 这里会进行压缩,解决模糊问题
          const renderContext = {
            canvasContext: ctx,
            viewport: viewport,
            transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
          }
          page.render(renderContext).promise.then(() => {
            const viewport1 = page.getViewport({ scale: 1 });// 这是让pdf文件的大小等于视口的大小
            canvas.width = viewport1.width * ratio
            canvas.height = viewport1.height * ratio// 这里会进行压缩,解决模糊问题
            const renderContext1 = {
              canvasContext: ctx,
              viewport: viewport1,
              transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
            }
            page.render(renderContext1).promise.then(() => {
              this.firstPaint1 = true
            })
          })
        } else {
          const viewport = page.getViewport({ scale: this.pageControl.beforeScale });// 这是让pdf文件的大小等于视口的大小
          canvas.width = viewport.width * ratio
          canvas.height = viewport.height * ratio// 这里会进行压缩,解决模糊问题
          const renderContext = {
            canvasContext: ctx,
            viewport: viewport,
            transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
          }
          page.render(renderContext)
        }
      })
    },
    /**
     * 获取整个pdf文档
     * 其实为啥会渲染两次呢,因为需要一个原始比例的图,作为截图的基准,因为pdf的page.render后的canvas是随着你viewport参数的scale来生成的,且后续canvas任何修改都会模糊
     * @param {*} url
     */
    loadFile(url) {
      PDFJS.getDocument({
        url,
        cMapPacked: true
      }).promise.then((pdf) => {
        this.pdfDoc = pdf
        this.pdfDoc1 = pdf
        this.pageControl.pages = this.pdfDoc.numPages
        this.pageControl.curPage = 1
        this.renderPage(this.pageControl.curPage)
        this.realRenderPage(this.pageControl.curPage)
      }, (err) => {
        if (err.name === 'MissingPDFException') {
          this.$message.warn('无效的PDF链接')
        }
      })
    },
    /**
     * 分页操作
     * @param {*} item
     */
    pageChange(item) {
      this.$nextTick(() => {
        this.pageControl.curPage = item.page
        this.renderPage(this.pageControl.curPage)
        this.realRenderPage(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.realRenderPage(this.pageControl.curPage)
      }
    },
    /**
     * 鼠标滑轮事件
     * @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)
        this.realRenderPage(this.pageControl.curPage)
      }
    },
    /**
     * 随机id
     */
    uuid() {
      let d = new Date().getTime();
      const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
      });
      return uuid
    },
    /**
     * canvas转base64
     * @param {*} blob
     * @param {*} type
     * @param {*} name
     */
    blob2file(blob, type = 'png', name = '') {
      const fileName = name || this.uuid() + '.' + type
      const file = new File([blob], fileName, { type: blob.type, lastModified: Date.now() })
      return file
    }
  }
}
</script>

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

clipRect.vue

<template>
  <div :id="`rectBox-${id}`" class="rect-page">
    <div v-if="clipStatus===3" class="out-rect">
      <span
        v-for="item in controls"
        :key="item.value"
        class="item"
        @click="controlChange(item)"
      >
        {{ item.label }}
      </span>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    /**
     * 矩形框坐标及宽高
     */
    offset: {
      type: Object,
      default: () => null
    },
    /**
     * 截图状态
     */
    clipStatus: {
      type: Number,
      default: 0
    },
    /**
     * 当前分栏id
     */
    id: {
      type: Number,
      default: 0
    },
    /**
     * 当前分栏模式
     */
    mode: {
      type: Number,
      default: 0
    },
    /**
     * 是否能进行截图绘制矩形框
     */
    canClip: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      rect: null, // 矩形dom
      controls: [
        {
          value: 0,
          label: '识别'
        },
        {
          value: 1,
          label: '裁剪'
        }
      ]
    }
  },
  watch: {
    offset: {
      handler(val) {
        this.$nextTick(() => {
          this.initCanvas()
          if (this.canClip) { // 当前区域为绘制区时,矩形框所有坐标参数按照父组件传递来依次绘制
            if (this.mode === 0 && this.id === 2) { // 当截图为 两栏,点击为 答案,则修改矩形框x点坐标
              this.rect.style.left = val.x - this.$parent.$el.offsetWidth + 'px'
            } else {
              this.rect.style.left = val.x + 'px'
            }
            this.rect.style.top = val.y + 'px'
            this.posChange(val)
          } else { // 当前区域为非绘制区时,就将矩形框所有坐标参数重置掉
            this.rect.style.left = -9999 + 'px'
            this.rect.style.top = -9999 + 'px'
            this.rect.style.width = 0
            this.rect.style.height = 0
          }
        })
      },
      deep: true
    }
  },
  methods: {
    /**
     * 识别、截图功能控制
     * @param {*} item
     */
    controlChange(item) {
      if (item.value) {
      // 裁剪
      } else {
      // 识别
      }
      // 通用都是要剪切截图
      this.$parent.clip()
    },
    /**
     * 当前位置控制
     * @param {*} offset
     */
    posChange(offset) {
      this.rect.style.width = offset.w + 'px'
      this.rect.style.height = offset.h + 'px'
    },
    /**
     * 初始化canvas
     */
    initCanvas() {
      if (!this.rect) {
        this.rect = document.getElementById(`rectBox-${this.id}`)
      }
    }
  }
}
</script>
<style lang="scss" scoped>
.rect-page{
  position: absolute;
  left: -9999px;
  top: 0;
  border: 1px solid #FFAE0D;
  box-shadow: 0 0 0 1999px rgba(0, 0, 0, .4);
  cursor: default;
  .rect-box {
    width: 100%;
    height: 100%;
  }
  .out-rect{
    background-color: rgba(0, 0, 0, .5);
    text-align: center;
    padding: 4px 8px;
    cursor: pointer;
    position: absolute;
    bottom: -29px;
    right: 0;
    width: 100px;
    display: flex;
    justify-content: space-between;
    z-index: 1;
    .item{
      width: 40px;
      height: 20px;
      line-height: 20px;
      text-align: center;
      color: #fff;
      &:hover{
        color: #000;
        background-color: #fff;
      }
    }
  }
}
</style>

footer.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>

header.vue 

<template>
  <div class="header-ai">
    <div class="left">
      <div class="control">
        <div
          v-for="item in controls"
          :key="item.value"
          :class="['item', {'active': item.value === mode, 'ban': clipStatus}]"
          @click="modeChange(item)"
        >
          {{ item.label }}
        </div>
      </div>
      <div class="cutter-zoom">
        <el-popover width="500" trigger="click">
          <div class="clip-desc">
            <p>截图说明</p>
            <p>截图:alt + q</p>
            <p>粘贴:alt + w或ctrl + v</p>
            <p>撤销:ctrl + z</p>
            <p>退出:鼠标右键单击关闭</p>
          </div>
          <i
            slot="reference"
            class="el-icon-_annotation"
          />
        </el-popover>
        <i
          class="el-icon-_shear_nor"
          @click="btnChange('clip')"
        />
      </div>
    </div>
    <i
      :class="[expend === 2 ? 'el-icon-s-fold' : 'el-icon-s-unfold']"
      @click="expendChange"
    />
  </div>
</template>
<script>
export default {
  name: 'HeaderAi',
  props: {
    mode: { // 当前分栏模式
      type: Number,
      default: 0
    },
    clipStatus: { // 截图状态
      type: Number,
      default: 0
    },
    expend: { // 当前是否展开
      type: Number,
      default: 2
    }
  },
  data() {
    return {
      controls: [
        {
          value: 0,
          label: '两栏'
        },
        {
          value: 1,
          label: '正文页'
        },
        {
          value: 2,
          label: '答案页'
        }
      ]
    }
  },
  methods: {
    /**
     * 当前分栏模式改变
     * @param {*} item
     */
    modeChange(item) {
      if (this.clipStatus === 1) return
      this.$emit('mode', item.value)
    },
    /**
     * 其他按钮触发
     * @param {*} val
     */
    btnChange(val) {
      if (!this.clipStatus) { // 如果处于截图状态,就不再次触发了
        this.$emit('btn', val)
      }
    },
    expendChange() {
      this.$emit('expend', this.expend === 2 ? 1 : 2)
    }
  }
}
</script>
<style lang="scss" scoped>
.header-ai{
  width: 100%;
  height: 40px;
  padding: 8px 20px;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: space-between;
  & > i {
    color: #fff;
    font-size: 24px;
    cursor: pointer;
    &:hover{
      color: #FFAE0D;
    }
  }
  .left{
    display: flex;
    flex-flow: row nowrap;
    align-items: center;
    .control{
      border-radius: 4px;
      display: flex;
      flex-flow: row nowrap;
      align-items: center;
      .item{
        color: #fff;
        background-color: rgba(255, 255, 255, .1);
        width: 66px;
        height: 24px;
        line-height: 24px;
        text-align: center;
        position: relative;
        cursor: pointer;
        &:first-child{
          border-radius: 4px 0 0 4px;
        }
        &:last-child{
          border-radius: 0 4px 4px 0;
        }
        &:not(:last-child)::after{
          content: '';
          position: absolute;
          right: 0;
          top: 0;
          width: 1px;
          height: 100%;
          background-color: #606266;
        }
      }
      .active{
        background-color: #FFAE0D;
      }
      .ban{
        cursor: not-allowed;
      }
    }
    .cutter-zoom {
      margin-left: 12px;
      display: flex;
      align-items: center;
      flex-flow: row nowrap;
      i {
        font-size: 24px;
        cursor: pointer;
        color: #ffae0d;
        margin: 0 12px;
      }
    }
  }
  .right{

  }
}
</style>

4、疑难问题解答

1、矩形框周围灰色半透明蒙层是如何出现的?

实际上是用的box-shadow阴影

box-shadow: 0 0 0 1999px rgba(0, 0, 0, .4)

 这样你绘制矩形框时,周围就会一直有蒙层效果,至于为啥设置1999px而不是9999px,这个你可以自己试试,有惊喜~[/doge],反正1999px是当蒙层是完全够用的对于一般屏幕

2、pdf首页为啥会倒置?

其实这一点官方也没有给出解释,但是再次调用renderPage则恢复正常,那我们想要让用户正常体验怎么办?我是这么解决的:

新增firstPaint变量,用于首次renderPage方法调用时使用,并且只有首页时使用,代码去改变scale,然后再改回1,这样来使pdf首页不倒置

    // renderPage方法内
    if (num === this.defaultPage && !this.firstPaint) {
          const viewport = page.getViewport({ scale: this.pageControl.beforeScale });// 这是让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]// 这里会进行放大,解决模糊问题
          }
          page.render(renderContext).promise.then(() => {
            const viewport1 = page.getViewport({ scale: 1 });// 这是让pdf文件的大小等于视口的大小
            canvas.width = viewport1.width * ratio
            canvas.height = viewport1.height * ratio// 这里会进行压缩,解决模糊问题
            canvas.style.width = viewport1.width + 'px'
            canvas.style.height = viewport1.height + 'px'
            const renderContext1 = {
              canvasContext: ctx,
              viewport: viewport1,
              transform: [ratio, 0, 0, ratio, 0, 0]// 这里会进行放大,解决模糊问题
            }
            page.render(renderContext1)
            this.firstPaint = true
          })
        } else {
          const viewport = page.getViewport({ scale: this.pageControl.beforeScale });// 这是让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)
        }

 3、如何读取pdf文件?

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

但是我们看了两次renderPage,其实为啥会渲染两次呢,因为需要一个原始比例的图,作为截图的基准,因为pdf的page.render后的canvas是随着你viewport参数的scale来生成的,且后续canvas任何修改都会模糊

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

4、pdf文件如何实现放大缩小?

放大缩小主要有两种:

鼠标滑轮控制

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

自定义按钮控制

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)
      }
    }

5、pdf的截图如何实现 ?

使用html2canvas插件,第一参数为操作的canvas,然后做法就是将操作的canvas当成底图再进行新的canvas绘制,注意噢,drawImage的绘制参数不一定和我这个一样哦,这个需要你对canvas还是要了解一些哟~

图例:

 

    html2canvas(canvas, {
        scale: window.devicePixelRatio < 3 ? window.devicePixelRatio : 2, // 处理模糊问题
        useCORS: true // 开启跨域,这个是必须的
      }).then(canvas => {
        const clipCanvas = document.createElement('canvas')
        let oX = this.offset.x / this.pageControl.beforeScale // 此处还需要进行修改
        let oY = this.offset.y / this.pageControl.beforeScale
        clipCanvas.width = this.offset.w / this.pageControl.beforeScale
        clipCanvas.height = this.offset.h / this.pageControl.beforeScale
        // 截取
        clipCanvas.getContext('2d').drawImage(canvas, oX, oY, clipCanvas.width, clipCanvas.height, 0, 0, clipCanvas.width, clipCanvas.height)
        clipCanvas.toBlob((blob) => console.log(URL.createObjectURL(blob)))
      })

6、为啥preview组件有两个canvas ,一个只读?一个可编辑?

原因是因为pdfjs-dist的page.render输出canvas是根据参数里的scale来生成的,虽然说可以实时更改canvas预览的大小,但是截图肯定还是要在原始比例下截图,所以只读canvas是作为截图的底图存在,并不作展示,可编辑canvas才是我们在页面上看到的。

7、截图框绘制和编辑时的卡顿问题

在mousemove里首先加上这几行就能解决卡顿,其他的user-select都可以不用加噢~

    document.onmousemove = (e) => {
          e.preventDefault();
          if (e.stopPropagation) {
            e.stopPropagation();
          } else {
            e.cancelBubble = true;
          }
    }

---不了解的可以评论哟,林大大哟原创---

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
PDF转换为Canvas可以使用pdf.js库,它是Mozilla开发的用于在Web上显示PDF文档的JavaScript库。以下是一个简单的示例: 首先,需要在HTML中引入pdf.js库和Canvas元素: ```html <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.min.js"></script> <canvas id="pdf-canvas"></canvas> ``` 然后,需要编写JavaScript代码来加载PDF文档,将其转换为Canvas并在Canvas绘制: ```javascript // 获取Canvas元素 var canvas = document.getElementById('pdf-canvas'); var context = canvas.getContext('2d'); // 加载PDF文档 PDFJS.getDocument('example.pdf').then(function(pdf) { // 获取第一页 return pdf.getPage(1); }).then(function(page) { // 设置Canvas尺寸 var viewport = page.getViewport(1.0); canvas.width = viewport.width; canvas.height = viewport.height; // 将PDF页面渲染到Canvas上 page.render({ canvasContext: context, viewport: viewport }); }); ``` 以上代码将加载名为“example.pdf”的PDF文档,并将其第一页渲染到Canvas上。可以根据需要修改代码来渲染特定页面或整个文档。 如果需要在JSP中使用JavaScript代码,请将上述代码放入<script>标记中,并确保在JSP页面中引入pdf.js库。例如: ```html <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>PDFCanvas示例</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.min.js"></script> </head> <body> <canvas id="pdf-canvas"></canvas> <script> // 在此处编写JavaScript代码 </script> </body> </html> ``` 请注意,将PDF转换为Canvas可能需要一些时间,具体取决于PDF文档的大小和复杂度。因此,在转换期间应该显示一些进度指示器或其他用户反馈。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值