使用pdf.js引进pdf文件并实现签章选择与变量编辑功能

pdf.js的安装与使用

官网下载

cdn下载

  • 登录 www.bootcdn.cn/,搜索pdf.js,找到对应版本资源。
  • 如本项目中小蔡使用的版本:cdn.bootcdn.net/ajax/libs/p…
  • 建议将资源文件,下载然后放到自己公司的服务器上,或者项目中(不建议,因为太大了,不利于网页的加载)。

现在核心库pdf.worker.js

小蔡使用的是cdn下载的方式:cdn.bootcdn.net/ajax/libs/p…

在项目中进行引进并使用

  • 在项目的index.html文件使用script标签引进刚刚放到服务器上的pdf.js,并引进核心库。
  • 封装公用函数loadPDF,在页面中获取后端返回的PDF文件路径,进行PDF文件加载并生成canvas

加载PDF文件并生成canvas

loadPDF函数封装

需要的参数

  • el:页面中存放canvasdivid或者类名
  • fileSrc:后端返回的PDF文件路径
  • scalePDF缩放的倍数,项目中,我设置了默认值是1.35
  • call:加载成功后的回调函数

创建加载任务

  • let loadingTask = await window.pdfjsLib.getDocument(fileSrc);

加载任务开始执行,对PDF的页数(pdf.numPages)进行循环,拿到每一页PDF数据转换成canvas

函数代码

export async function loadPDF({
    el, fileSrc, scale = 1.35
}, call) {
    let pdfCol = document.querySelector(el);

    // let loadingTask = await window.pdfjsLib.getDocument(fileSrc);
    let loadingTask = await window.pdfjsLib.getDocument(fileSrc); // 创建加载任务

    loadingTask.promise.then(function (pdf) { // 开始加载任务
        for (let i = 1; i <= pdf.numPages; i++) { // 遍历每一页PDF
            pdf.getPage(i).then(async function (page) {
                let viewPort = page.getViewport({scale: scale}); // 获取PDF尺寸

                let div = document.createElement("div"); // 用于存放canvas

                div.id = `canvasBox_${i}`;
                div.className = "canvas-box";

                let canvas = document.createElement("canvas"); // 创建canvas

                let context = canvas.getContext("2d");
                
                // 将canvas的宽和高设置为与PDF视图一样大小
                canvas.height = viewPort.height;
                canvas.width = viewPort.width;

                setTimeout(() => {
                    // 画页码
                    context.font = "14px orbitron";
                    context.fillStyle = "#333";
                    context.save();
                    context.beginPath();
                    context.line = 2;
                    context.textAlign = "center";
                    context.textBaseline = "middle";
                    context.fillText(`${i}`, viewPort.width / 2, viewPort.height - 30);
                    context.restore();
                    context.closePath();
                }, 500);

                let renderContext = {
                    canvasContext: context,
                    viewport: viewPort
                };
                
                // 该函数返回一个当PDF页面成功渲染到界面上时解析的`promise`,我们可以使用成功回调来渲染文本图层。
                await page.render(renderContext); // 初始化文本图层
                canvas.className = "canvas";
                canvas.id = `canvas_${i}`;

                call({
                    pdfCol: pdfCol,
                    canvas: canvas,
                    context: context,
                    scale: scale,
                    index: i,
                    allPage: pdf.numPages
                });

                div.appendChild(canvas); // 将canvas放进对应的div中
                pdfCol.appendChild(div); // 将每一个div放到外面的大盒子中

            });
        }
    });
}
复制代码

签章位置选择

效果图

image.png

在canvas上画矩形

大家可以看到,其实每一个签章、签名或者日期,我们都可以理解成一个矩形,那么在PDF上面选取签章位置,其实就是在canvas上选取位置,然后绘制固定大小的div

绘制固定大小的div

我们首先需要获取鼠标当前在canvas上的点坐标,将签章div放置进canvas的父元素div中。

获取鼠标在canvas上的点坐标

首先应该获取canvas元素的边界框,然后再通过当前点击的位置相对于canvas的坐标,来计算出当前鼠标所在位置在canvas上的横纵坐标。

// 获取canvas鼠标的点
export function getMousePos(canvas, event) {
    let rect = canvas.getBoundingClientRect(); // 获取canvas元素的边界框来计算当前点击的位置相对于canvas的坐标

    let x = Math.round(event.clientX - rect.left);

    let y = Math.round(event.clientY - rect.top);

    return {x, y};
}
复制代码

在对应坐标绘制签章div

使用document.createElement('div')创造一个div盒子,设置宽和高,并设置绝对定位,left值和top值分别为坐标点的x、y坐标减去矩形宽和高的一半。

  • 因为绝对定位中,是以div左上角为参照点的,因此需要用此时鼠标坐标点与div的宽和高的差值做绝对定位的偏移值。

签章div跟随鼠标的滑动改变位置

此时,我们已经把签章div加入到canvas上了,但是有可能我们一开始选择不符合我们的需求,此时移动鼠标,签章需要跟着移动,这应该怎么去实现呢?

  • 首先,我们应该判断此时我们是在哪个PDF的div进行鼠标点击、移动等事件的。还记得,我们一开始在渲染PDF的时候,已经给放置canvas的每一个父盒子都设置了类名canvas-box,因此,我们可以获取class="canvas-box"的div列表,进行循环,并设置对应div的mousedown、mouseup、mousemove事件。
  • 鼠标按下时,我们需要获取此时是否选中了签章,如果没有选中签章,则创建一个新的签章div,如果选中了签章,那么我们需要判断是选择了哪一个签章(记录该div的索引)。我们可以通过获取此时选中divid来判断,是否选中了签章,并添加mousemove事件:
let id = "";

let offsetX = ev.offsetX;

let offsetY = ev.offsetY;

if (navigator.userAgent.indexOf("Firefox") > -1) { // 火狐浏览器
    id = ev.originalTarget.id;
    offsetX = ev.layerX;
    offsetY = ev.layerY;
} else {
    id = ev.toElement.id;
}

if (id === `canvas_${i + 1}`) { // 没有选中签章
    self.signatoryMouseDown({
        x: offsetX,
        y: offsetY,
        parentDiv: div,
        i
    });
} else { // 选择了签章
    self.mode = `${id.split("_")[1]}Seal`;

    self.signatoryList.forEach((item, index) => {
        if (`sealItem${item.index}` === id.split("_")[0]) {
            self.signatoryListIndex = index;
        }
    });
}
复制代码
  • 移动鼠标,签章需要跟随鼠标移动。一样也是需要获取到父盒子的边界值,然后给签章div设置lefttop的值。
let id = "";

if (navigator.userAgent.indexOf("Firefox") > -1) { // 火狐浏览器
    id = ev.originalTarget.id;
} else {
    id = ev.toElement.id;
}

if (this.mode === "pagingSeal" && this.signatoryList[this.signatoryListIndex].pagingSeal.length > 0 && id.indexOf("paging") < 0) {
    return;
}

if (document.getElementById(id) && this.isMove) {
    let innerDiv = document.getElementById(id);

    let parentDiv = document.getElementById(`canvasBox_${id.split("_")[2]}`);

    let range = parentDiv.getBoundingClientRect();

    innerDiv.style.left = `${Math.round(ev.clientX - innerDiv.offsetWidth / 2 - range.left)}px`;
    innerDiv.style.top = `${Math.round(ev.clientY - innerDiv.offsetHeight / 2 - range.top)}px`;

    if (innerDiv.offsetLeft <= 0) {
        innerDiv.style.left = "0px";
    }

    if (innerDiv.offsetTop <= 0) {
        innerDiv.style.top = "0px";
    }

    if (innerDiv.offsetLeft >= parentDiv.clientWidth - innerDiv.clientWidth) {
        innerDiv.style.left = `${parentDiv.clientWidth - innerDiv.clientWidth}px`;
    }

    if (innerDiv.offsetTop >= parentDiv.clientHeight - innerDiv.clientHeight - 15) {
        innerDiv.style.top = `${parentDiv.clientHeight - innerDiv.clientHeight - 15}px`;
    }
}
复制代码
  • 松开鼠标时,判断此时鼠标所控制的div是否已经存在(即是否已存在该签章),如果有的话,则获取该div然后重新设置其topleft值。如果是一个新的签章,则在此时的点位增加一个新的签章div。最后移除mousemove事件。
let id = "";

if (navigator.userAgent.indexOf("Firefox") > -1) { // 火狐浏览器
    id = ev.originalTarget.id;
} else {
    id = ev.toElement.id;
}

self.signatoryMouseUp({
    x: self.mode === "pagingSeal" ? `${div.offsetWidth - 78}px` : document.getElementById(id).style.left,
    y: document.getElementById(id).style.top,
    elementd: id,
    res
});

self.isMove = false;
div.removeEventListener("mousemove", self.innerDivMouseDownAndMove);
复制代码

变量编辑

  • 效果图:

image.png

  • 与设置签章不同的是,变量编辑的矩形框是跟随鼠标按下,然后移动画出来的框,需要在canvas上进行矩形绘制,因此我们需要在canvas上绑定mousedown、mousemove、mouseup事件,而签章设置是在canvas的父盒子上绑定这三个事件的。那么首先我们来看怎么在canvas上绘制矩形。

canvas上绘制矩形

我们需要记录对角的两个点坐标,然后计算出矩形的宽和高,才可以确定矩形的大小。由于鼠标是不定方向进行移动的,因此我们需要记录此时起始点和终点的xy坐标之间的大小,取小的那个值,组成canvas绘制矩形的起始点,然后差的绝对值作为矩形的宽和高。

// 画矩形
export function paintReact(originPoint, point, context, strokeStyle = "red", lineWidth = 2) {
    let w = Math.abs(point.x - originPoint.x);

    let h = Math.abs(point.y - originPoint.y);

    // let left = point.x > originPoint.x ? originPoint.x : point.x;
    let left = Math.min(point.x, originPoint.x);

    // let top = point.y > originPoint.y ? originPoint.y : point.y;
    let top = Math.min(point.y, originPoint.y);

    context.save();
    context.beginPath();
    context.lineWidth = lineWidth;
    context.strokeStyle = strokeStyle;
    context.rect(left, top, w, h);
    context.stroke();
    context.restore();
}
复制代码

解决了绘制矩形的问题后,我们需要怎样在canvas上来实现矩形随着鼠标的移动而发生大小的改变呢?一样,我们需要给canvas绑定mousedown、mousemove、mouseup三个鼠标事件。

鼠标按下事件:mousedown

当鼠标按下的时候,我们需要判断是否有记录到原始的canvas以及上一次编辑后的canvas,如果没有,我们需要存一下,方便取消此次变量编辑后,canvas可以恢复到上一次的状态。我们可以通过context.getImageData(x, y, w, h)来获取到当前的canvas状态,并存储当前鼠标的坐标点(作为后面绘画矩形的起始点)。具体代码实现如下:

if (!self.prevCanvas[`canvas${res.index}`]) {
    self.prevCanvas[`canvas${res.index}`] = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
}

if (!self.originCanvas[`canvas${res.index}`]) {
    self.originCanvas[`canvas${res.index}`] = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
}

if (self.mode === "variable") { // 记录鼠标的坐标,作为矩形的起始点
    originPoint = getMousePos(res.canvas, event);
}
复制代码

记录当前canvas状态后,我们需要获取到此时鼠标在canvas上的坐标点(使用上面的getMousePos方法),然后增加mousemove事件。

鼠标移动事件:mousemove

鼠标移动,我们可以看做是一个canvas一直在更新的一个过程,还记得我们在鼠标按下时已经保存了上一次canvas的状态了吗?因此我们鼠标移动的时候,需要每移动一个新的点,canvas就需要做两步操作:首先是使用context.putImageData将上一次的状态放置到canvas中,在根据新的点,画出一个新的矩形。具体代码实现如下:

mouseDownAndMove(res.canvas, res.context, event => {
    if (self.type === "check") return;

    res.context.putImageData(self.prevCanvas[`canvas${res.index}`], 0, 0);

    let point = getMousePos(res.canvas, event);

    if (self.mode === "variable") {
        paintReact(originPoint, point, res.context);
    }
});
复制代码

鼠标松开事件:mouseup

当鼠标松开的时候,我们需要获取当前鼠标的坐标点,然后将上一次canvas的状态放置到canvas中,并通过鼠标按下时我们记录的的矩形起始点与此时鼠标松开时的坐标点绘画出一个矩形,然后弹出确认框,当确认保存后,讲此时的canvas状态记录到prevCanvas中。到此,我们就可以完成在canvas中跟随鼠标移动绘画矩形的操作了。具体代码实现如下:

context.putImageData(self.prevCanvas[`canvas${res.index}`], 0, 0);

if (self.mode === "variable") {
    let point = getMousePos(res.canvas, event);

    if (point.x !== originPoint.x && point.y !== originPoint.y) {
        paintReact(originPoint, point, context);

        self.addTextArr({ // 确认是否保存
            originPoint: self.calcPoint(originPoint, point, res.scale).originPoint,
            endPoint: self.calcPoint(originPoint, point, res.scale).endPoint,
            scale: res.scale,
            page: res.index,
            value: "",
            oldValue: ""
        });
    }
}
复制代码

确认是否保存的函数:

addTextArr(obj) {
    this.$confirm("确定选中区域?", "提示", {
        type: "warning"
    }).then(() => {
        let context = document.getElementById(`canvas_${obj.page}`).getContext("2d");

        this.prevCanvas[`canvas${obj.page}`] = context.getImageData(0, 0, context.canvas.width, context.canvas.height);

        this.textArr.push(obj);
    }).catch(() => {
        let context = document.getElementById(`canvas_${obj.page}`).getContext("2d");

        context.putImageData(this.prevCanvas[`canvas${obj.page}`], 0, 0);
    });
},
复制代码

写在最后

这是小蔡写的第一篇比较长篇幅的文章,可能在文章布局与描述上还有待提高,希望掘友们可以多多指教!文章中,有哪些错误的描述或者可以有更好解决方案的,也欢迎大家在评论区热烈讨论。希望大家可以一起进步~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享