pdfjs-dist实现pdf阅读器,可实现大批量渲染canvas,兼容Safari,Chrome,Android

1,232 阅读1分钟

Safari对canvas最大内存限制,完美解决方案

解决方案:采用虚拟dom,只渲染可视化片段中的canvas元素

<template>
  <div ref="viewRef" class="pdf-viewer-container">
    <div v-if="viewport" ref="viewInRef" class="pdf-viewer">
      <div v-for="pageNum in numPages" class="pdf-viewer-page" :style="{ height: viewport.height * scale+'px'  }">
        <canvas v-if="isIncludePage(pageNum)" :id="'pdf-canvas-'+(pageNum-1)" />
        <div class="page-viewer-num"> {{ pageNum }} / {{ numPages }}</div>
      </div>
    </div>
  </div>
</template>
<script>
// 参考链接: canvas最大内存限制问题 https://mp.weixin.qq.com/s?__biz=MzAxODE4MTEzMA==&mid=2650101325&idx=1&sn=8f5b859360bcefbf62563e600d81d719&chksm=83dbcb28b4ac423e5cd64f73bffc18e5567bac752d75e1c19a9fa773812f787398787ee16902&scene=27
import * as PDFJS from "pdfjs-dist/build/pdf.min.js";
export default {
  name: 'PDFViewer',
  props: {
    src: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      numPages: 0,
      bufferSize: 3,
      pageRendering: false,
      scale: 1,
      frages: [],
      viewport: null, // 全局viewport
      pdfDoc: null,
      dpr: window.devicePixelRatio || 1,
      oldStartIndex: undefined,
      oldFrages: [],
      marginBottom: 10
    }
  },
  computed: {
    viewEl() {
      return this.$refs.viewRef;
    },
    viewElRect() {
      return this.viewEl.getBoundingClientRect();
    },
    isIncludePage() {
      return (pageNum) => {
        return this.frages.map(frage => frage.pageNum).includes(pageNum);
      }
    }
  },
  async mounted() {
    await this.initConfig();
    this.initVirtualList();
    this.viewEl.addEventListener('scroll', this.debounce(() => this.initVirtualList()));
  },
  methods: {
    loadPDF(url) {
      PDFJS.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'; //require("pdf-dist/build/pdf.worker.min.js");
      return PDFJS.getDocument({
        url,
        disableAutoFetch: true,
        disableFontFace: true,
        ignoreErrors: true,
        // https://unpkg.com/pdfjs-dist/cmaps/
        cMapUrl: 'https://unpkg.com/pdfjs-dist/cmaps/',
        cMapPacked: true,
      }).promise.then(pdfDoc => {
        this.numPages = pdfDoc.numPages;
        return pdfDoc;
      })
    },
    getViewport() {
      return this.pdfDoc.getPage(1).then(page => page.getViewport({ scale: 1}) );
    },
    async initConfig() {
      this.pdfDoc = await this.loadPDF(this.src);
      if(!this.numPages) {
        return;
      }
      this.viewport = await this.getViewport();
      this.scale = this.viewElRect.width / this.viewport.width;
      this.itemHeight = this.viewport.height * this.scale;
    },
    async initVirtualList() {
      const scrollTop = this.viewEl.scrollTop;
      const itemHeight = this.itemHeight + this.marginBottom;
      const startIndex = Math.floor(scrollTop / itemHeight);
      const endIndex = Math.min(
        this.numPages-1,
        Math.floor((scrollTop + this.viewElRect.height) / itemHeight)
      );
      const startBufferIndex = Math.max(0, startIndex - this.bufferSize);
      const endBufferIndex = Math.min(this.numPages-1, endIndex + this.bufferSize);
      this.pageRendering = true;
      this.oldStartIndex = startBufferIndex;
      // 创建切片
      this.frages = Array.from(
        { length: (endBufferIndex - startBufferIndex)+1 },
        (item, i) => ({ id: 'pdf-canvas-'+(i + startBufferIndex), pageNum: i+startBufferIndex+1, pageRendering: true })
      );
      await this.$nextTick();
      // 渲染切片
      return this.renderPage(startBufferIndex, endBufferIndex, () => {
        this.pageRendering = false;
        this.oldFrages = this.frages;
      });
    },
    renderPage(startIndex, endIndex, rendered) {
      // 当向上滚动时,会出现startIndex > endIndex的情况
      if(startIndex > endIndex) {
        return;
      }
      const id = 'pdf-canvas-'+startIndex;
      const frageIndex = this.frages.findIndex(frage => frage.id === id);
      const frage = this.frages[frageIndex];
      const oldFrageIndex = this.oldFrages.findIndex(frage => frage.id === id);
      const { width: clientWidth } = this.viewElRect;
      if(oldFrageIndex > -1) {
        this.renderPage(startIndex+1, endIndex, rendered);
        return;
      }
      this.pdfDoc.getPage(startIndex+1).then(async page => {
        Object.assign(frage, { id, page, pageRendering: true });
        await this.$nextTick();
        const canvas = document.getElementById(id);
        const ctx = canvas.getContext("2d");
        const viewport = this.viewport;
        canvas.width = Math.floor(viewport.width * this.dpr);
        canvas.height = Math.floor(viewport.height * this.dpr);
        canvas.style.width = Math.floor(clientWidth) + "px";
        canvas.style.height = Math.floor(viewport.height * (clientWidth / viewport.width)) + "px";
        const renderContext = {
          canvasContext: ctx,
          transform: [this.dpr, 0, 0, this.dpr, 0, 0],
          viewport
        };
        const renderTask = page.render(renderContext);
        renderTask.promise.then(() => {
          Object.assign(frage, { renderContext, pageRendering: false });
          if(startIndex < endIndex) {
            this.renderPage(startIndex+1, endIndex, rendered)
          }else{
            typeof rendered == 'function' && rendered();
          }
        });
      });
    },
    debounce(handle, wait =300){
      let timer;
      return function(...args){
        //清空上一次定时器
        timer && clearTimeout(timer)
        timer = setTimeout(()=>{
          handle.apply(this,args);
        }, wait);
      }
    }
  }
}
</script>
<style scoped>
.pdf-viewer-container {
  height: 100vh;
  width: 100vw;
  overflow-x: hidden;
}
.pdf-viewer-page{
  box-shadow: rgb(51, 51, 51) 0 1px 4px 0;
  margin-bottom: 10px;
  position: relative;
}
.page-viewer-num {
  position: absolute;
  left: 5px;
  bottom: 5px;
  background: rgba(0, 0, 0, .6);
  padding: 3px;
  border-radius: 5px;
  color: #ffffff;
}
</style>