|
揭秘海报生成技术
大转转FE
大转转FE
大转转FE 定期分享一些团队对前端的想法与沉淀 430篇内容
2024年09月20日 09:01
北京
1 引言随着裂变营销策略的兴起,定制化海报分享的需求不断增加。作为开发者,一张背景图+一个二维码的海报合成的需求便会出现在我们的工作中,如下图。本文给大家介绍海报生成相关知识以及使用中常见的问题。希望能够抛砖引玉,为遇到类似需求或问题的伙伴们提供参考。2 实现方式2.1 生成步骤 在用户视角,海报生成像是“截图”,点击生成海报按钮之后,定制化海报便会呈现在屏幕上,再点击保存按钮,海报便会保存在手机相册里。而在程序内部,还需要开发者做一些其他工作。这里的客户端包含原生和前端,两者在实现原理上类似——首先用画布绘制海报,然后将海报转成图片。在服务端,常用的方案是开启一个无头浏览器,先在浏览器上渲染出海报,然后截图生成图片,将生成的图片下发给前端。2.2 各端类库 如下图,服务端不同语言、客户端不同系统都有可供直接上手的类库。简单对三端进行下对比端生成效率海报效果兼容性其他服务端与服务器性能成正比中等好开发成本高客户端高好较差维护成本高前端中等较好好复杂排版受限服务端:在服务端生成海报大部分会选择使用puppeteer等插件,模拟一个浏览器然后渲染海报并截图。服务端的生成效率以及质量强依赖于服务器性能,不过与之对应的客户端的压力也会变小。另外,在查阅资料时,发现一个有趣的实现方式--图片水印。如果是由一个背景和一个二维码拼成的这种简单场景,可以把背景当作图片,二维码当作水印,直接调用第三方的图片水印能力,就能便捷实现海报生成能力。客户端:客户端可以直接利用设备的CPU和GPU,所以在生成海报的效率上有着天然优势。但是端的维护成本较高,需要各端分别去实现。前端:前端海报生成的类库多种多样,普通的海报生成需求都能满足,但也有一些跨域、复杂排版受限等问题。笔者作为前端开发,深入学习了前端实现的方案,下面讲一下前端实现及遇到的问题。2.3 前端实现 前端的实现方案除使用Canvas API纯手写外,还有三类可参考的js库。Canvas API较为底层,上手成本高,海报生成需求真正用此实现的极少,下文一笔带过,重点展开讲一下现有的js库。2.3.1 Canvas APICanvas API(画布)用于在网页实时生成图像,并且可以操作图像内容。这种方案是开发者直接在画布上进行海报绘制,然后使用canvas.toDataURL将画布转为图片,如果绘制一些简单的图像还是可以使用的。//html中创建Canvas元素//获取Canvas元素constcanvasEl=document.getElementById('canvas');//获取上下文constctx=canvasEl.getContext('2d');//设置填充颜色ctx.fillStyle='red';ctx.fillRect(100,100,20,20);2.3.2 JS库常用可以实现海报生成的JS库有三种类型。第一种是以Fabric.js为代表的,通过直接封装底层API实现的。第二种是重写渲染引擎的,代表类库是html2canvas。第三种使用了SVG的foreignObject,常用的库是dom-to-image。类型实现思路代表类库优点缺点直接封装Canvas API封装底层APIFabric.js海报可定制使用门槛高DOM->Canvas重写一套新的渲染引擎html2canvas使用门槛低、内置跨域方案部分css不兼容DOM->SVG->Canvas使用了SVG的foreignObjectdom-to-image使用门槛低、还原度高不支持跨域下面依次详细讲一下提及到的代表类库。Fabric.jsFabric.js是活跃在github上的明星项目,这个库对canvas进行封装,提供更丰富的图形支持以及事件处理。下面是Fabric.js的用法。//html中创建Canvas元素//创建一个fabric实例letcanvas=newfabric.Canvas("canvas");//创建一个矩形对象letrect=newfabric.Rect({left:100,//距离左边的距离top:100,//距离上边的距离fill:"red",//填充的颜色width:20,//矩形宽度height:20,//矩形高度});//将矩形添加到canvas画布上canvas.add(rect);//在画布上绘制一张图片fabric.Image.fromURL('imagePath.jpg',function(img){img.set({left:400,top:200,});canvas.add(img);});通过对比不难看出,Fabric.js的简单用法是和原生语法类似的。但当需要海报样式可DIY时,就体现出了Fabric.js的强大之处。如下图,用户可以对图形使用拖动、缩放、旋转、改变大小和形状等操作,可以实现高度定制化的海报。html2canvashtml2canvas官方是这样介绍的——该脚本允许您直接在用户浏览器上对网页或部分网页进行“截图”。截图基于 DOM,因此可能与实际表示不完全一致,因为它不会制作实际的截图,而是基于页面上可用的信息构建截图。此外,截止到2024年8月,html2canvas库在github已经有30.3k star。html2canvas使用html2canvas不同于前两种方式,无需调用绘制API,只需将DOM传入js库提供的方法,便可得到对应的图片。//html中创建需要绘制的元素
海报文本importhtml2canvasfrom'html2canvas'//获取绘制元素constel=document.getElementById('绘制div');//调用html2canvas方法进行绘制html2canvas(el).then(function(canvas){//使用toDataURL处理Canvas即可})html2canvas原理虽然使用简单,但是这个库的底层实现是极其复杂的,可以大致理解为参考浏览器渲染原理又实现了一套新的渲染引擎,使用Canvas API将HTML+CSS画出来。大体实现流程如下重点步骤一:获取节点树获取节点树用到的方法是parseTree。parseTree的入参就是一个普通的DOM元素,返回值是一个ElementContainer对象,该对象主要包含DOM元素的位置信息(bounds: width|height|left|top)、样式数据、文本节点数据等(只是节点树的相关信息,不包含层叠数据,层叠数据在parseStackingContexts方法中取得)。解析的方法就是递归整个DOM树,并取得每一层节点的数据。ElementContainer对象大致如下:{bounds:{height:260,left:6,top:-100,width:1440},elements:[{bounds:{left:6,top:-100,width:1440,height:240},elements:[{bounds:{left:6,top:-100,width:1440,height:240},elements:[{styles:CSSParsedDeclaration,textNodes:Array(1),elements:Array(0),bounds:Bounds,flags:0},{styles:CSSParsedDeclaration,textNodes:Array(1),elements:Array(0),bounds:Bounds,flags:0},...],flags:0,styles:{backgroundClip:Array(1),backgroundColor:0,backgroundImage:Array(0),backgroundOrigin:Array(1),backgroundPosition:Array(1),…},textNodes:[]}],flags:0,styles:CSSParsedDeclaration{backgroundClip:Array(1),backgroundColor:0,backgroundImage:Array(0),backgroundOrigin:Array(1),backgroundPosition:Array(1),…},textNodes:[]}],flags:4,styles:CSSParsedDeclaration{backgroundClip:Array(1),backgroundColor:0,backgroundImage:Array(0),backgroundOrigin:Array(1),backgroundPosition:Array(1),…},textNodes:[]}重点步骤二:渲染离屏Canvas将节点树遍历得到层叠数据后,将层叠数据渲染到离屏Canvas的过程,是html2canvas最核心的事情,这件事由renderStackContent方法来实现,为了避免渲染过程中流式布局被浮动或定位元素打破布局,renderStackContent使用了CSS层叠布局规则,如下图。默认情况下,CSS是流式布局,按顺序渲染即可,但如果遇到浮动或定位时,原有的简单布局就会被打破,脱离正常文档流的元素会形成层叠上下文,可以理解为PS中的图层,将这些图层叠在一起,最终绘制出看到的海报。下面的源码可以理解为html2canvas是对CSS层叠布局规则的一个实现。asyncrenderStackContent(stack:StackingContext){//1.最底层是background/borderawaitthis.renderNodeBackgroundAndBorders(stack.element);//2.第二层是负z-indexfor(constchildofstack.negativeZIndex){awaitthis.renderStack(child);}//3.第三层是block块状盒子awaitthis.renderNodeContent(stack.element);for(constchildofstack.nonInlineLevel){awaitthis.renderNode(child);}//4.第四层是float浮动盒子for(constchildofstack.nonPositionedFloats){awaitthis.renderStack(child);}//5.第五层是inline/inline-block水平盒子for(constchildofstack.nonPositionedInlineLevel){awaitthis.renderStack(child);}for(constchildofstack.inlineLevel){awaitthis.renderNode(child);}// 6. 第六层是以下三种://(1)‘z-index: auto’或‘z-index:0’。//(2)‘transform:none’//(3)opacity小于1for(constchildofstack.zeroOrAutoZIndexOrTransformedOrOpacity){awaitthis.renderStack(child);}//7.第七层是正z-indexfor(constchildofstack.positiveZIndex){awaitthis.renderStack(child);}}正因为html2canvas重写了渲染引擎,所以对CSS的支持并不是很友好,如果有较为复杂的样式,需要进行充分的调试。即便如此,html2canvas仍保持每周150w+的下载量,是DOM直接绘制图片领域的霸主。dom-to-imagedom-to-image是另外一种类型的“截图”工具,同样适用于海报绘制。使用方式和html2canvas大同小异,传入DOM即可生成对应的图片。dom-to-image使用//引入dom-to-image库importdomtoimagefrom'dom-to-image';//需要转换成图像的DOM节点constnode=document.getElementById('绘制div');//使用domtoimage.toPng将DOM节点转换成PNG图像domtoimage.toPng(node).then(function(dataUrl){//创建一个图片元素并设置src属性为转换后的图像数据URLvarimg=newImage();img.src=dataUrl;//将图片添加到文档中document.body.appendChild(img);})dom-to-image原理dom-to-image实现原理要比html2canvas简单的多,直接使用SVG的foreignObject,只需要把DOM放在这个方法里,便可以在SVG绘制出对应的图片,因为SVG是浏览器的标准,所以不用担心此类方法对CSS的支持不友好问题。需要注意的是,dom-to-image在将DOM绘制成SVG后,也使用了Canvas进行重新绘制。SVG已经是图片,为什么还要再使用Canvas呢,因为SVG方案生成的图片体积很大,包含很多冗余信息,使用Canvas进行重新绘制,可以大大降低图片体积,还能导出想要的图片格式。其他dom-to-image-more 基于dom-to-image,解决了跨域问题html-to-image 基于dom-to-image,增加了typescript支持modern-screenshot 基于dom-to-image,整合了以上的优化,是个理想的选择Painter.js 适用于小程序端3 常见问题3.1 跨域 问题原因不论用Canvas还是SVG生成海报时,海报中的图片会重新加载再进行绘制,虽然img标签本身不会跨域,但用于绘制时会触发浏览器的限制。解决方案请求图片时增加属性img.crossOrigin = 'anonymous',但是这样使用会有一个风险,如果需要canvas绘制的图片在页面中已经加载过一次,图片会被浏览器缓存,当绘制时,设置过的crossOrigin便会失效。使用html2canvas、dom-to-image-more等库中封装好的内置跨域能力,大多实现原理也比较简单,为了避免浏览器缓存,会在资源请求时附带时间戳。3.2 图片白屏(html2canvas) 问题原因当使用html2canvas时,如果先将生成页滑动到底部再生成海报,就会出现图片白屏问题,排除跨域问题、资源加载问题,发现是触发了html2canvas天然bug。正常情况下,html2canvas会从顶部开始绘制传入的DOM,但当同时满足以下三点时便会出现保存在本地的海报有白屏情况。海报生成页超出一屏,也就是y轴有滚动条滚动条发生滚动预期绘制的海报是通过弹窗形式展现在屏幕中间的产生的原因在源码中找到了答案,在renderCanvas方法中进行了下面操作:this.ctx.translate(-options.x+options.scrollX,-options.y+options.scrollY);在绘制时,画布的宽度高度默认为DOM的宽度高度,问题就出现在y轴的坐标上。y轴起始坐标=-options.y + options.scrollY,其中的y默认值为0,scrollY默认值是window.pageYOffset,也就是默认绘制的y轴坐标为已经滚出视窗的y轴的高度。所以实际截图时便会出现下面这种情况。解决方案直接使用window.scrollTo(0, 0),但底部页面会发生滚动,如果在没有过高体验要求的前提下可以解决白屏问题。html2canvas内置的兼容此问题的方案,如下代码constscrollTop=document.documentElement.scrollTop||document.body.scrollTop;//得到滚动条高度constdomObj=document.querySelector("#canvas")html2canvas(domObj,{y:scrollTop,//解决有滚动条时,生成海报顶部有空白问题})3.3 海报中图片比例不正确(html2canvas) 问题原因在海报实现过程中,大部分需要对图片进行保留原始比例的剪切、缩放或者直接进行拉伸,这也就用到了object-fit属性。前文提到过html2canvas有一套自己的渲染引擎,对CSS、尤其是新属性支持不太友好,这也就导致再使用html2canvas生成海报时object-fit不生效,与浏览器渲染的海报图片不一致的情况。解决方案如果使用html2canvas,可以将标签转为背景图模式,背景图的几个属性html2canvas是支持的。如果必须使用标签,可以使用SVG生成的js库,如modern-screenshot、dom-to-image等来规避这个问题。4 总结现有的技术已经满足服务端、客户端(原生)、前端去实现海报的绘制工作。上文提及到的类库大多底层还是通过Canvas API实现的,在实际使用过程中,可以根据自己的需求及现状选择不同的技术。下面是选型指南:如服务器性能稳定且排版复杂,推荐使用服务端生成方式。如需要复杂排版的完美呈现或者有用户交互的场景,推荐使用客户端生成。如普通的排版或者是较大并发的场景,使用前端生成即可。前端推荐使用html2canvas或modern-screenshot,两者各有小缺陷,实践中可以替换使用,会规避大部分问题;如果有操作海报元素、高度DIY海报的需求推荐使用Fabric.js。5 参考文章https://www.npmjs.com/package/html2canvashttp://fabricjs.com/https://juejin.cn/post/7339671825646338057https://zhuanlan.zhihu.com/p/701919912https://zhuanlan.zhihu.com/p/338265679想了解更多转转公司的业务实践,点击关注下方的公众号吧!
|
|