20210204 补充:
后来回显的PDF中加入了文字(pdf中的文本域),因此产生了字体问题,在原有代码的基础上增加了 cMaps 的引入;

let CMAP_URL = this.GLOBAL.dataUrl + '/staticFront/script/pdfjs-2.6.347-dist/web/cmaps/';

let loadingTask = PDFJS.getDocument({
    data: url,
    cMapUrl: CMAP_URL,
    cMapPacked: true
});

但印章花版的问题又出现了,经过了多处的排查,问题定位在文件服务器(推测文件服务器速度不稳定,也没准网络也有关系);最后由后端提供base64 编码,然后在前端进行解析

getPdfUrl() {  //获得pdf
 // BASE64 
 let pdfFileBinary = this.convertDataURIToBinary(this.pdfSrc);
 this.loadFile(pdfFileBinary);
},
//将encodeBase64解码
convertDataURIToBinary(dataURI) {
    let raw = window.atob(dataURI);
    let rawLength = raw.length;
    //转换成pdf.js能直接解析的Uint8Array类型,见pdf.js-4068
    let array = new Uint8Array(new ArrayBuffer(rawLength));
    for (let i = 0; i < rawLength; i++) {
        array[i] = raw.charCodeAt(i);
    }
    return array;
},

原 PDFJS.getDocument 中的url 改为 data模式


最近公司做了一个项目,主要功能是把印章在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;
      }
    }
  }
}

cason6810
110 声望7 粉丝