乐趣区

关于pdf:vuepdfjs-canvas-踩坑心得

最近公司做了一个我的项目,次要性能是把印章在 PDF 上拖拽定位的。

这个性能次要两个点:

  1. 块的拖拽即时显示印章内容;
  2. PDF 回显(因波及多人操作,回显包含之前人所签的最终成果)。

应用的 pdf.js@2.2.28 读取 PDF 文件,通过 canvas 以图像的模式回显在页面上(因为不是单纯的显示,还有很多交互,最终抉择了这个计划)

开始所有都挺顺利的,拖拽计算坐标,PDF 回显封装,印章属性设置。。。

PDF 回显电子签章

在开发过程中没成想 PDF 回显深了一个大坑,开始是曾经有印章不能回显,在网上搜寻要改 pdf.work.js 源码,搜寻上面这个判断,都注掉就行了

// if (data.fieldType === 'Sig') {
//      data.fieldValue = null;
//      _this3.setFlags(_util.AnnotationFlag.HIDDEN);
//    }

尽管可能显示印章了,但在翻页时会产生只显示一半色彩的状况,钻研了好几天,更换 pdf.js 版本、缩小 canvas 配置项都没胜利。
忽然在一次测试中发现,要是上来就把所有都渲染进去如同就没问题,随即用了一个比拟 low 的办法,就是上来就把所有 pdf 都渲染 DOM,但不显示进去(display:none),只有与以后页码雷同的才显示,目前测试还没发现问题。

另,现有几个小问题:

  1. 布局时尽量用 padding、box-sizing: content-box;,用 margin 有时取坐标数据不好管制;
  2. 因为找的例子是适配挪动端的,外面有个参数 window.devicePixelRatio,这个在 mac 下通常为 2,因为挪动端用这个计算了宽度,在 mac 下应用的时候根底坐标都 * 2 了,PC 无脑改成 1;

对于 window.devicePixelRatio 能够具体参考:
张鑫旭 - 设施像素比 devicePixelRatio 简略介绍

附上 pdf 的例子

外围渲染 pdf 的局部也是网上收集的,依据业务再进行了批改;

pdfjs-fix-sig 只是把电子签章那块解决了,顺手公布在本人的私服上了,npm 上必定没有,失常引到的名称是 pdfjs-dist

<template>
    <div class="pdf-box" id="pdf-box">
        <div class="pdf-page-btn-box">
            <y-button @click="handleSub()" :disabled="disabledSub"> 上一页 </y-button>
            <div class="input-box">
                <input class="input-wrap"
                       :title="pdfCurrentPage"
                       v-model="tunePage"
                       @blur="pageChange"
                       @keyup.13="pageChange"/>
            </div>
            <div class="num-page-box"> / {{pdf_pages}}</div>
            <y-button @click="handleAdd()" :disabled="disabledAdd"> 下一页 </y-button>
        </div>
        <div class="pdf-view-wrap">
            <div ref="canvasBox"
                 class="canvas-box" id="canvas-box"
                 :style="{width:`${pdf_div_width}px`,margin:'0 auto'}">

                <slot name="drag-collection-box"></slot>

                <!--                <canvas ref="canvasPage" :id="'the-canvas'+pdfCurrentPage"-->
                <!--                        style="border: 1px #eeeeee solid;"></canvas>-->

                <template v-for="index in pdf_pages">
                    <canvas ref="canvasPage" :id="'the-canvas'+index"v-show="index === Number(pdfCurrentPage)"style="border: 1px #eeeeee solid;"></canvas>
                </template>

            </div>
        </div>

        <ul v-if="sealPlaceShow" class="seal-place" ref="sealPlace" :style="`height: ${canvasHeight}px`">
            <li v-for="index in 5"></li>
        </ul>
    </div>
</template>

<script>
    // 筹备 PDF 文件和签章数量、类型
    import Vue from "vue";

    let PDFJS = require('pdfjs-fix-sig');
    PDFJS.GlobalWorkerOptions.workerSrc = Vue.prototype.GLOBAL.dataUrl + '/staticFront/script/pdfjs-fix-sig/build/pdf.worker.min.js';

    export default {
        name: "pdf-box",
        props: {
            pdfSrc: {type: String},
            sealPlaceShow: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                pdf_scale: 1,//pdf 放大系数(1.33 回显的 pdf 章是 100,但思考到回显的 pdf 是按 595*841 展现【所对应应的章是 75*75】),A4: 595 * 841
                pdf_pages: null,
                pdfDoc: null,
                pdf_div_width: '595',
                pdf_src: null,
                pdfCurrentPage: 1,
                tunePage: 1,
                disabledAdd: false,
                disabledSub: true, // 初始不能减页

                canvasWidth: 0,
                canvasHeight: 0,

                sealPlaceFlag: false,
            }
        },
        watch: {
            sealPlaceShow: {handler(val) {this.sealPlaceFlag = val;},
                immediate: true
            }
        },
        created() {if (this.pdfSrc.length > 0) {this.getPdfUrl();
            } else {this.$Message.error({content: '请应用正确的 PDF 文件'});
            }
        },
        methods: {getPdfUrl() {  // 取得 pdf
                // 例子: 加载 pdf 文件示例;
                this.loadFile(this.pdfSrc);

                // 线上申请 文件流 (2020 年 11 月 03 日 08:55:02 未测试)
                // this.$http.get(this.pdfSrc).then((res) => {
                //     this.pdf_src = res.url;
                //     this.loadFile(this.pdf_src)
                // }, (err) => {//     console.log(err);
                // });
            },
            // 初始化 pdf
            loadFile(url) {
                let _this = this;
                let loadingTask = PDFJS.getDocument(url);
                loadingTask.promise.then((pdf) => {
                    _this.pdfDoc = pdf;
                    _this.pdf_pages = _this.pdfDoc.numPages; // 总页数

                    _this.$nextTick(() => {_this.setAddSubBtn();
                        for (let i = 1; i <= _this.pdf_pages; i++) {_this.renderPage(i)
                        }
                    })
                })
            },
            // 渲染 pdf 页
            renderPage(num) {
                let _this = this;

                this.pdfDoc.getPage(num)
                    .then((page) => {let canvas = document.getElementById('the-canvas' + num);
                        let ctx = canvas.getContext('2d');
                        // let dpr = window.devicePixelRatio || 1; // mac 的 window.devicePixelRatio 为 2
                        let dpr = 1;
                        let bsr = ctx.webkitBackingStorePixelRatio ||
                            ctx.mozBackingStorePixelRatio ||
                            ctx.msBackingStorePixelRatio ||
                            ctx.oBackingStorePixelRatio ||
                            ctx.backingStorePixelRatio || 1;
                        let ratio = dpr / bsr;
                        let viewport = page.getViewport({scale: this.pdf_scale});

                        canvas.width = viewport.width * ratio;
                        canvas.height = viewport.height * ratio;

                        _this.canvasWidth = canvas.width;
                        _this.canvasHeight = canvas.height;
                        _this.$emit('setCanvasAttr', {
                            canvasWidth: _this.canvasWidth,
                            canvasHeight: _this.canvasHeight,
                            pdfCurrentPage: _this.pdfCurrentPage,
                            numPages: this.pdfDoc.numPages
                        });

                        canvas.style.width = viewport.width + 'px';

                        _this.pdf_div_width = viewport.width; // 定位在左边时须要 canvas 的宽度进行计算


                        canvas.style.height = viewport.height + 'px';

                        ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
                        let renderContext = {
                            canvasContext: ctx,
                            viewport: viewport
                        };
                        page.render(renderContext).promise.then((res) => {// console.log(this.getBounding('canvasPage'));
                            // console.log(this.getEleClass('layout-menu-left').clientWidth);
                            // console.log(this.getBounding('canvasPage').right - this.getEleClass('layout-menu-left').clientWidth + 'px');

                        });
                    });
            },
            // 减页
            handleSub() {
                this.pdfCurrentPage--;
                this.tunePage = this.pdfCurrentPage;
                this.setAddSubBtn();},
            // 加页
            handleAdd(_str) {
                this.pdfCurrentPage++;
                this.tunePage = this.pdfCurrentPage;
                this.setAddSubBtn();},
            // 判断翻页按钮是否禁用并渲染 canvas
            setAddSubBtn() {
                this.disabledSub = this.pdfCurrentPage <= 1;
                this.disabledAdd = this.pdfCurrentPage >= this.pdf_pages;
                // this.renderPage(this.pdfCurrentPage);

                this.$emit('setCanvasAttr', {
                    canvasWidth: this.canvasWidth,
                    canvasHeight: this.canvasHeight,
                    pdfCurrentPage: this.pdfCurrentPage,
                    numPages: this.pdfDoc.numPages
                });

            },
            pdfChangePage() {this.$emit('pdf-change-page', true);
            },
            getEleClass(_str) {return document.getElementsByClassName(_str)[0];
            },
            getBounding(_str) {return this.$refs[_str].getBoundingClientRect();},
            pageChange() {
                // 输出字符容错
                this.pdfCurrentPage = isNaN(Number(this.tunePage)) ? 1 : Number(this.tunePage);
                // 输出大数容错
                this.pdfCurrentPage = this.tunePage > this.pdf_pages ? this.pdf_pages : this.tunePage;
                this.setAddSubBtn();}
        }
    }
</script>
父级援用
<pdfBox ref="pdfBox"
                    :pdfSrc="doSignData.originFile"    
                    :canvasHeight="canvasHeight"
                    :sealPlaceShow="true">
                <div slot="drag-collection-box" v-if="$route.meta.viewType !=='view'">
                    <!-- 这个插槽放在 pdf 上显示的印章 -->
                </div>
            </pdfBox>
最初放上款式
@drag-block-size: 75px;
.e-sign-wrap {

  .pdf-page-btn-box {
    position: absolute;
    text-align: right;
    font-size: 12px;
    right: 0;
    z-index: 5;
    padding: 7px;
    color: #42474f;

    .input-box, .num-page-box {
      display: inline-block;
      height: 22px;
      line-height: 22px;
    }

    .input-box {
      width: 30px;
      .input-wrap {
        width: 100%;
        padding: 0 2px;
        font-size: 12px;
        border: 1px transparent solid;
        border-bottom: 1px #dddee1 solid;
        border-radius: 2px;
        color: #495060;
        text-align: right;

        &:focus {outline: none;}
      }
    }

    .num-page-box {vertical-align: middle;}
  }

  .pdf-box {
    float: left;
    font-size: 0;
    position: relative;


    .pdf-view-wrap {
      position: relative;
      display: inline-block;

      .drag-box {

        li {
          cursor: move;
          user-select: none;
          position: absolute;
          opacity: .7;
          z-index: 5;
          border: 1px dashed transparent;

          .party {
            color: #ffffff;
            height: 24px;
            line-height: 24px;
            padding-left: 5px;
            width: 100%;
            font-size: 12px;
          }

          .content {
            width: 98px;
            height: 49px;
            line-height: 18px;
            padding: 5px;
            font-size: 14px;
            text-align: center;
            display: table-cell;
            vertical-align: middle;
            word-break: break-all;

            .sign-type {font-size: 12px;}
          }

          &.drag-block {
            width: @drag-block-size;
            height: @drag-block-size;
            background-color: #cfddfb;

            .party {background-color: #9bbcff;}

            .content {color: #78a5ff;}
          }

          &.place-block {
            width: @drag-block-size;
            height: @drag-block-size;
            background-color: #ffcccc;

            .party {background-color: #f09494;}

            .content {
              color: #f09494;
              line-height: 16px;
              padding: 0;
              font-size: 12px;
            }

            //.btn-box {
            //  .btn {
            //    width: 100%;
            //  }
            //}
          }

          &.drag-block, &.place-block {
            &.checked::after {
              content: '';
              width: 100%;
              height: 100%;
              border: 2px #004CC0 solid;
              display: block;
              position: absolute;
              top: 0;
              left: 0;
              z-index: 0;
            }

            img {-webkit-user-drag: none;}
          }
          .btn-box {
            cursor: pointer;

            .btn {
              //float: left;
              width: 100%;
              font-size: 12px;
              text-align: center;
              height: 24px;
              line-height: 24px;
              background-color: #004cc0;
              color: #ffffff;
            }

          }
        }
      }

      canvas {
        width: 595px;
        height: 842px;
        //max-width: 595px;
        //max-height: 841px;
      }
    }

    .seal-place {
      display: inline-block;
      position: absolute;
      right: -25px;
      min-height: 841px;

      li {
        width: 5px;
        height: 100%;
        display: inline-block;
        border: 1px #eee solid;
        border-left: none;
      }
    }
  }
}
退出移动版