当前位置: 首页 > Web前端 > HTML

基于React后台的PDF在线预览-签名-导出

时间:2023-03-28 19:50:00 HTML

现在电子签名越来越流行。简单来说,有些合同不再需要亲自签订,比如新员工、人事合同、采购相关业务等。直接生成在线合同,当事人通过身份认证后即可打开链接签署。最近做了一个简单版的电子合同,现在把这个项目的一小部分功能做了一个PDF在线预览-签名-下载的演示版。下面给大家分享一下项目过程中的一些经验和坑,文末附上源码。Demo在线访问地址:https://buynao.github.io/reac...功能在线预览在线签名放大缩小兼容PC&H5支持下载导出,下载后的pdf格式依然保留,不是全部转换成图片技术解决方法和一些坑这个demo主要涉及三个环节,即PDF预览-签名-导出。下面我就这三个环节的技术要点给大家简单分享一下。1、第一个链接-PDF预览这里主要是使用pdfjs解决pdf预览问题。这个库也是目前pdf预览应用最多的库。介绍pdfjs的文章很多,这里就不过多介绍了,虽然它的文档实在是晦涩难懂,但好在已经够用了,网上的例子也够多了。摸索了下,pdf预览就出来了,文章里就不贴代码了;pdfjs官网:https://pdfjs.express/遇到了几个涉及pdfjs的坑:第一个坑:PC&H5渲染比例问题之前的项目有个功能点,签名状态可以保留。也就是说,合同需要两个人签字。如果一个人在移动端签名,PC端签名的位置会出现偏差;同样,PC端的签名在移动端查看也会有所不同。问题分析:签名后的数据会放在signPositionList中,上传到数据库并保存。//近似签名数据信息接口ISignPosition{id:string;x:数字;//偏移值x相对于pdf渲染y:number;//偏移值yw:number;//签名宽度h:number;//签名高度signSrc:string;//签名图像是Select?:boolean;innerPdfIndex?:number}typeSignList=ISignPosition[];上传时会存储签名的偏移值,预览时会获取签名的偏移值进行定位预览。让我们看看签名偏移值x和y是如何存储的://返回签名相对于pdfcanvas视口的偏移值exportconstgetTouchPosition=(e:any,scale:number)=>{constevent=getEvent(e);consttarget=event.target;constrect=target.getBoundingClientRect();constx=event.pageX-rect.left;consty=event.pageY-rect.top;//scale=canvascontainerwidth/pdf渲染后的canvas实际宽度return{x:x/scale,y:y/scale}}通过分析排查,问题的关键在于scale。主要原因是在PC端和移动端两种不同的环境下,pdfjs渲染生成的容器宽度不同。这是当时手头的测试机。两种环境下签名pdf的宽高对比:定位问题后分析解决。最后,有两种解决方案:第一种解决方案强制pdfjs在两端保持相同的渲染宽高。通过分析pdfjs源码我们可以知道,pdfjs在渲染的时候,除了当时的浏览器宽度之外,还有两个参数可以控制:devicePixelRatio大家应该很熟悉。随着各种显示设备的不断更新换代,这个数值也在慢慢的不断提升。值1表示经典的96DPI(某些平台上为76DPI)显示,而值2表示HiDPI/Retina显示器。在异常低分辨率的显示器中,或者更常见的是,当屏幕的像素深度只是标准分辨率96或76DPI的两倍时,可能会返回其他值。maxCanvasPixels字面意思是可以渲染的最大像素数。这个值可以在pdfjs源码中设置,如下图所示:constMAX_CANVAS_PIXELS=_viewer_compatibility.viewerCompatibilityParams.maxCanvasPixels||16777216;类PDFPageView{构造函数(选项){constcontainer=options.container;constdefaultViewport=options.defaultViewport;this.maxCanvasPixels=options.maxCanvasPixels||MAX_CANVAS_PIXELS;...}如果想让pdfjs在两端保持一致的渲染比例,可以将devicePixelRatio和maxCanvasPixels这两个值赋值如下:devicePixelRatio=3;constpdfViewer=newpdfjsViewer.PDFViewer({container:containerRef.current,eventBus:eventBusRef.current,linkService,maxCanvasPixels:5242880,l10n:pdfjsViewer.NullL10n,useOnlyCssZoom:USE_ONLY_CSS_ZOOM,textLayerMode_:TDE_LAY};与hack相比,简单粗暴,改动最少,但不同的移动设备预览可能存在更大的风险。不推荐使用。方案二:计算PC->H5之间的转换计算上图中PC->H5之间签名偏移值的转换公式:其实只要转换成绝对定位就很好理解,偏移值毕竟总是局限于画布的宽高,一开始被比例尺误导了好久。h5.x=h5.width*(pc.x/pc.width)h5.y=h5.height*(pc.y/pc.height)第一步,保存签名数据时,增加的绝对值签名相对于画布容器的百分比定位值://修改签名数据信息,增加百分比偏移值interfaceISignPosition{id:string;x百分比:数字;//相对于画布实际宽度的百分比位移yPercent:number;//相对于画布真实高度的位移百分比...}constrequestSignList=signList.map((sign)=>{return{...sign,xPercent:sign.x/trueWidth,yPercent:sign.y/trueHeight,}})第二步,预览时,接口返回签名数据后,将百分比转换为绝对值:constsignList=apiSignList.map((sign)=>{return{...sign,x:trueWidth*xPercent,//实际偏移值/当前比例y:trueHeight*yPercent,}})这里只是贴一些伪代码,本文这个项目不涉及需要h5&pc一起保存签名的功能。pdfjs渲染时序问题渲染pdf时,有pagesloaded事件。网上的大部分文章都是说当这个事件被触发时,pdf渲染成功后的业务,比如初始化pdf的缩放大小。eventBusRef.current.on("pagesloaded",function(){pdfViewerRef.current.currentScaleValue=DEFAULT_SCALE_VALUE;});但是如果你的pdf页面很多,出于性能考虑,pdfjs会进行延迟加载。如果这个时候,如果要在pdfjs的上层添加canvas渲染层,是不能监听事件一起添加的。这个时候还有很多pdf还没有开始渲染。如果要等到pdfjs完全渲染完,就需要自己去监听下一页的滚动事件来做相关的判断。2.第二个链接:PDF签名pdf在浏览器中,应用canvas的地方有很多,比如写签名,签名交互,签名合成计算……首先要回顾一下,这个有三个方面涉及canvas的项目:首先第一个canvas是pdfjs渲染pdf生成的pdfCanvas,就是pdf本身的内容;第二个画布是通过与pdfCanvas重叠生成的pdfSignCanvas。前面说了pdfSignCanvas是出于性能的考虑,主要是在执行签名生成的时候和pdfCanvas交互的时候,会频繁的绘制这一层的画布。如果此时只有一层canvas,那么每次开始在pdfCanvas上拖放签名进行交互时,整个pdf内容都会被频繁重绘。这里的性能成本非常高。这层画布不是一开始就加的。是我在测试的时候发现低端手机各种花屏、卡顿的时候才加的。第三个canvas,signPannelCanvas,主要是绘制签名的画板。关于这个签名画板是如何实现的,网上的文章很多,本文就不详细介绍了。Canvas本身没有坑,就是API多,使用门槛比其他库高一点。。。关于此演示中的画布。使用canvas的一些小技巧:先来看看,signPannelCanvas:2.1裁剪合适的签名大小,填入pdf。您可以在这个白色面板上签名/绘画。当您点击保存时,您将合并signPannelCanvas和signCanvas。为了将签名保存到pdf的画布上以供预览。如果直接按照传统方式合并,签名区会出现大量空白,毕竟不能对整个画布进行签名。优化方案是在两个canvas即将合并的时候,我加了一个裁剪过程:在画签名的时候,保存记录下签名的minX,minY,maxX,maxY。通过minX、minY、maxX、maxY,得到签名的真实宽高constw=maxX-minX+15;//留一些空白consth=maxY-minY+15;saveClipSize({w,h,x:minX-5,y:minY-5//留一个空格});使用签名的真实宽高和x,y对签名图片进行裁剪//Cropsize,displacementtypeClipData={w:number;h:数字;x:数字;y:number;}裁剪画布,生成裁剪图像canvas:canvasclipData:裁剪尺寸,偏移量returnnewPromise((resolve,reject)=>{const{x,y,w,h}=clipData;letimage:HTMLImageElement|null=newImage();letclipCanvas:HTMLCanvasElement|null=document.createElement('画布');constclipCtx=clipCanvas.getContext('2d')asCanvasRenderingContext2D;clipCanvas.width=clipData.w;clipCanvas.height=clipData.h;constMIME_TYPE="image/png";constimgUrl=canvas.toDataURL(MIME_TYPE);image.src=imgUrl;image.onload=function(){if(image&&clipCanvas){clipCtx.drawImage(图像,x,y,w,h,0,0,w,h);resolve(clipCanvas.toDataURL(MIME_TYPE))}clipCanvas=null;图片=空;}});}裁剪后的图片填充到signCanvas整体优化流程:2.2签名画板横竖屏兼容性本项目落地页需要兼容PC和H5。好在页面设计不是很复杂,需要特别兼容的部分不多。部分。因为手机的屏幕尺寸显然没有网页那么宽敞,为了让用户有更好的签名体验,需要将整个画布横向显示。无论用户是否开启了手机自动旋转功能,都必须要求用户侧着手机进行签到。为了考虑手机自动旋转,需要添加监听事件,重新设置signPannelCanvas的宽高。constgetCanvasSize=():Promise=>{returnnewPromise((resolve)=>{setTimeout(()=>{if(window.orientation===90){resolve({width:window.innerWidth,height:window.innerHeight-TITLE_MAP,})}else{resolve({width:window.innerWidth-TITLE_MAP,height:window.innerHeight,})}},500)})}consthandleCanvasSize=async()=>{const大小=等待getCanvasSize();updateCanvasSize(尺寸);};useEffect(()=>{//加载签名模板并获取初始大小handleCanvasSize();window.addEventListener("orientationchange",handleCanvasSize);return()=>{window.removeEventListener("orientationchange",handleCanvasSize);}},[]);最后一点就是移动端强制横屏后,signPannelCanvas的宽高和手机端是相反的。所以在保存签名的时候,我们需要翻转signPannelCanvas的签名。否则,如果直接保存,签名会被反转。//纵向状态-翻转图像以保存constctx=signPannelCanvas.getContext('2d');ctx.clearRect(0,0,宽度,高度);canvas.width=clipSize.h;canvas.height=clipSize.w;constimg=createImage(imgData)img.onload=function(){//反向翻转绘制图像ctx.save();ctx.translate(clipSize.h/2,clipSize.w/2);ctx.rotate(-90*Math.PI/180);ctx.translate(-clipSize.h/2,-clipSize.w/2);ctx.drawImage(img,clipSize.h/2-img.width/2,clipSize.w/2-img.height/2);ctx.restore();//Canvas有重绘过程,不能直接保存setTimeout(async()=>{//翻转后的正确签名constsignImage=canvas.toDataURL('image/png');addSignInCanvas(signImage,canvas.width,canvas.height);})}3.最后一个链接:pdfexport/downloadpdflib-支持原版pdf下载,这个库很方便,说到html->pdf转换,网上有很多解决方案,大部分都是用htmlToCanvas,imgToCanvas,toPdf。总之,生成的pdf里面大部分都是图片,已经不是原来的pdf了。这个库可以让你在原来的pdf上添加额外的图片、svg、文本等...pdflib官网:https://pdf-lib.js.org/这里比较折腾的一点是如何准确的添加图片在signCanvas上到pdf文件指定的区域。这里直接贴代码。和上面的pc->h5方案是一致的。//将链??接转换为缓冲区。如果是本地读取的pdf文件,可以直接传到arrayBuffer中=newPdfDoc.getPages().map(async(page,pageIndex)=>{const{width,height}=page.getSize();//constsigns=newSignList.filter((sign)=>sign?.canvasIndex==pageIndex+1);constdrawIntoPageTask=signs.map(async(sign)=>{let{signSrc,x,y,w,h,pdfCanvas}=sign;//签名图像constscale=pdfCanvas.width/width;constex=x/scale;constey=y/scale;try{让img=awaitnewPdfDoc.embedPng(awaittransformPNG(signSrcasstring));return()=>page.drawImage(img,{x:ex,y:height-ey-h/scale,width:w/scale,height:h/scale,});}catch(e){console.log(e);return()=>{};}});constdrawProcesses=awaitPromise.all(drawIntoPageTask);drawProcesses.forEach((p)=>p());});awaitPromise.all(pagesProcesses);//如果直接从浏览器下载,可以使用newPdfDoc.save();constpdfBytes=awaitnewPdfDoc.save();down??load(pdfBytes,'download','application/pdf')//如果需要上传到服务器,可以使用saveAsBase64转base64constpdfBase64=awaitnewPdfDoc.saveAsBase64()awaituploadPdf(pdfBase64);最后,以上是本次demo的一些心得与分享。想熟悉canvas的同学可以了解更多。canvas的内容我就不多说了,签名绘制、交互、拖拽、变形等等……顺便附上github地址。如果觉得有帮助,可以帮我点个star,谢谢~~https://github.com/buynao/rea...