最近做了一个简单版的电子合同,现在把这个项目中的一小部分功能,做了一个demo版本的PDF在线预览-签名-下载。
这个demo主要涉及三个环节,也就是PDF预览-签名-导出。
我遇到的,涉及pdfjs的坑有这么几个:
之前的项目有个功能点,签名状态可以保留。即合同需要两个人签署,如果一个人在移动端签名,放到PC端签名的位置会出现偏差;同理,PC端的签名放到移动端去看也会有差异。
分析问题:签名的数据,是会统一放到signPositionList里,上传数据库并进行保存。
//大概的签名数据信息interfaceISignPosition{id:string;x:number;//相对pdf渲染的偏移值xy:number;//偏移值yw:number;//签名宽h:number;//签名高signSrc:string;//签名图片isSelect: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=canvas容器宽度/pdf渲染出来的canvas真实宽度return{x:x/scale,y:y/scale}}通过分析排查,问题的关键点就在于scale,主要原因是在PC和移动端两种不同环境下,pdfjs所渲染出来的容器宽度不一样,这里贴下当时手里的测试机,在两个环境中,签名的pdf宽高对比:
定位出问题以后,就开始分析解决,最终有了两种解决方案:
通过分析pdfjs源码可以得知,在pdfjs渲染时,除了当时浏览器宽度,其中有根据两个参数来控制的:
devicePixelRatio大家应该不陌生了,随着各种显示器设备的不断升级,这个值也在慢慢不断升级。值1表示经典96DPI(在某些平台上为76DPI)显示,而对于HiDPI/Retina显示屏则期望值为2。在异常低分辨率的显示器中,或更常见的是,当屏幕的像素深度比简单地将96或76DPI的标准分辨率提高一倍时,可能还会返回其他值。
maxCanvasPixels字面意义就是可以渲染的最高像素数,这个值在pdfjs源码里可以进行设置,如下图:
constMAX_CANVAS_PIXELS=_viewer_compatibility.viewerCompatibilityParams.maxCanvasPixels||16777216;classPDFPageView{constructor(options){constcontainer=options.container;constdefaultViewport=options.defaultViewport;this.maxCanvasPixels=options.maxCanvasPixels||MAX_CANVAS_PIXELS;...}如果想让pdfjs保持两端一致的渲染比例,可以对devicePixelRatio和maxCanvasPixels这两个值进行如下赋值:
window.devicePixelRatio=3;constpdfViewer=newpdfjsViewer.PDFViewer({container:containerRef.current,eventBus:eventBusRef.current,linkService,maxCanvasPixels:5242880,l10n:pdfjsViewer.NullL10n,useOnlyCssZoom:USE_ONLY_CSS_ZOOM,textLayerMode:TEXT_LAYER_MODE});这种方法比较hack,简单粗暴,改动最少,但是不同的手机设备预览可能会产生更多的风险。不建议使用。
算出上图中,签名偏移值在PC->H5之间的转换公式:
h5.x=h5.width*(pc.x/pc.width)h5.y=h5.height*(pc.y/pc.height)第一步,保存签名数据时,增加签名相对canvas容器的绝对百分比定位值:
//修改签名的数据信息,增加百分比偏移值interfaceISignPosition{id:string;xPercent:number;//相对canvas真实宽度的百分比位移yPercent:number;//相对canvas真实高度的百分比位移...}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共同保存签名的功能。
eventBusRef.current.on("pagesloaded",function(){pdfViewerRef.current.currentScaleValue=DEFAULT_SCALE_VALUE;});但是如果你的pdf页数有点多,pdfjs出于性能问题考虑,会进行懒加载,如果这个时候,你想在pdfjs上层增加一个canvas渲染层,就不能监听到该事件就一起给加了,这个时候其实还有很多pdf并没有开始渲染。
pdf在浏览器中,应用canvas的地方有很多,如书写签名,签名交互以及签名的合成计算...
首先回顾一下,这个项目涉及canvas的有三处:
第一个canvas,是pdfjs渲染pdf生成的pdfCanvas,这里也就是pdf本身内容;
第二个canvas,是和pdfCanvas重叠生成的pdfSignCanvas,前边已经说过pdfSignCanvas是出于性能考虑的,主要是在执行签名的生成和交互时,这一层的canvas会频繁进行绘制,如果这个时候只有一层canvas,那么每次在pdfCanvas上开始拖拽签名进行交互时,将会频繁的重新绘制整个pdf的内容,这里的性能开销就非常大了。这一层canvas也是一开始的时候没有加,等到测试的时候,发现低端手机,各种花屏,卡顿,才给加上的。
canvas本身是没什么坑的,也就是api比较多,使用门槛相比其他库有点高...
关于canvas的一些使用技巧:先看一下,签名画板部分signPannelCanvas:
你可以在这个白色面板上进行签名/绘画,点击保存时,在将signPannelCanvas与signCanvas进行合并。以便将签名保存到pdf的画布中进行预览。如果按照传统做法,直接合并的话,那签名的区域会出现很多空白,毕竟你不可能把整个画布签满。
优化方案就是,我在两个canvas将要合并的时候,加了一个裁剪的过程:
整体优化流程:
这个项目的落地页是需要兼容PC和H5的,好在页面设计不是很复杂,没有太多需要特殊兼容的部分。
需要特殊处理的,就是签名画板的部分。因为手机的屏幕尺寸显然不如web宽敞,为了让用户有个较好的签名体验,需要将整个画布进行横向展示。
无论用户有没有开启手机的自动旋转,都要让用户横着手机进行签名。
为了考虑手机的自动旋转,需要加个监听事件,重新设置signPannelCanvas的宽高.
constgetCanvasSize=():Promise
所以在保存签名的时候,我们需要在把signPannelCanvas的签名翻转一下,才行。不然直接保存的话,签名也就跟着是反的。
//竖屏状态-翻转图像进行保存constctx=signPannelCanvas.getContext('2d');ctx.clearRect(0,0,width,height);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.最后一个环节:pdf的导出/下载pdflib-支持原汁原味的pdf下载,这个库很方便,说到html->pdf转换,网上有很多方案,大都是将htmlToCanvas,imgToCanvas,toPdf。总之生成的pdf内部大都是图片,已经不是原汁原味的pdf了,这个库可以让你在原汁原味的pdf上额外添加图片,svg,文字等等...
这里比较折腾的一点是,如何把signCanvas上的图片,准确无误的添加到pdf文件指定的区域中。这里就直接贴代码了。和上面pc->h5的方案思路一致。
想熟悉canvas的同学,可以多了解一下。其中涉及canvas的内容,我上面没有讲太多,比如签名的绘制,交互,拖拽,变形等等...
顺带附下github地址,如果觉得有帮助,可以帮忙点个star,感谢~~