最近在业务中遇到使用生成图片的需求,图片只需要展示数据,没有计算密集型工作。后端生成的图片字体太单一,工作就交给了前端。从0开始作图做像素级的操作自然是不现实的,有幸的是,HTML本身就是一个很不错的做UI的语言,有CSS的支持。再借助HTML to canvas或是SVG的库,可以完成想要的需求。
实际上,后端大多数也是通过起chrome内核,绘制DOM节点生成图片的
需求
按照指定格式生成图片
保证格式正确清晰度高
生成过程用户无感知
对图片格式没有明确要求
解决方案 HTML to image有两种方案比较流行,一个是html2canvas ,一个是dom-to-image 。它们的设计初衷其实都是将已有 DOM结构转成图片类型。对比来看
流行度上,html2canvas流行度更高,资料更好找,但更新缓慢
格式支持上,dom-to-image可以将图转成SVG等更多格式,html2canvas只能输出canvas,需要用户自行处理
清晰度上,dom-to-image可以导出SVG,html2canvas则需要hack的方式(设置更大的canvas绘制再等比缩放)
实现原理上,都是通过遍历DOM树,读取格式化数据,dom-to-image通过浏览器解析CSS语法,因此支持度更高;html2canvas则自己实现了CSS解析
渲染图片的HTML模板在通常情况下,不应该展示给用户。即生成过程短暂停留的DOM需要用户不可见。不可见的方式大致有下面几种:
display: none
,这种情况,两个方案度都输出空白图片
visibility: hidden
,在输出图片时,DOM结构会短暂闪现,两种方案都输出空白图片
将DOM移出视口,html2canvas可以正确输出图片,dom-to-image不行
本场景下生成的图片需要上传,并最后展示给C端,没有对SVG的需求。测试来看,两者的输出结果清晰度类似,且html2canvas输出格式还原度更高。综合考虑,选择html2canvas。
在其他场景下,如支持SVG、需要高清截图、需要导出更多图片时,可以考虑使用dom-to-image。两者的API实际上非常类似。
容器组件 考虑到未来仍可能存在的前端图片渲染需求,将相关逻辑内聚成一个组件,同时开发接口给外部使用。
组件需要输入:
hide
,因为渲染过程是componentDidMount
阶段完成的,在每次渲染完成后要在父组件手动卸载该组件,这部分需要在hide
中实现
success
,可选的成功回调,入参是生成的canvas,hide
作为可选第二个入参,可以异步卸载组件
{children}
,无状态的函数组件,只负责图片的HTML模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import React , { Component } from 'react' ;import PropTypes from 'prop-types' ;import html2canvas from 'html2canvas' ;class GeneratedImage extends Component { static defaultProps = { hide : () => {}, success : (canvas ) => {document .body .appendChild (canvas);} }; static propTypes = { hide : PropTypes .func , success : PropTypes .func }; componentDidMount ( ) { html2canvas (document .getElementById ('html2canvas' )) .then ((canvas ) => { if (this .props .success .length > 1 ) { this .props .success (canvas, this .props .hide ); } else { this .props .success (canvas); this .props .hide (); } }) .catch ((e ) => { console .error (e); this .props .hide (); }); } render ( ) { return ( <div id ="html2canvas" style ={{ position: 'fixed ', left: '-9999px ' }} > {this.props.children} </div > ); } } export default GeneratedImage ;
使用时,像如下这样,在对应的时机展示组件即可:
1 2 3 4 5 6 7 8 9 { this .state .isGenerating ? <GeneratedImage hide ={() => {this.setState({ isGenerating: false })}} success={(canvas) => { console.log(canvas.toDataURL()); }} > <Image /> </GeneratedImage > : null }
已知缺陷
对部分CSS属性支持度有限,如box-shadow
,-webkit-line-clamp
,background-position
等
使用时需要额外的卸载操作
后端 生成图片的业务需求大多数是用内容填充的,因此使用浏览器渲染页面再截图是比较直观的生成方式(qrcode这种简单的图片需求另说)。在使用python的场景下,可以用selenium 生成,代码非常简单。
首先,pip install selenium
,如果是python3,就pip3 install selenium
。
然后,安装chromedriver 。使用headless模式打开chrome,并根据图片位置和大小截图即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from selenium import webdriverfrom PIL import Imagefrom io import BytesIOfrom os import pathdef screenshot (path ): DRIVER = 'chromedriver' chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--headless' ) chrome_options.add_argument('--disable-gpu' ) browser = webdriver.Chrome(chrome_options=chrome_options, executable_path=DRIVER) browser.get(path) ele = browser.find_element_by_id('demo' ) location = ele.location size = ele.size image = browser.get_screenshot_as_png() browser.quit() im = Image.open (BytesIO(image)) left = location['x' ] top = location['y' ] right = location['x' ] + size['width' ] * 2 bottom = location['y' ] + size['height' ] * 2 im = im.crop((left, top, right, bottom)) im.save('screenshot.png' ) if (__name__ == '__main__' ): curr_path = path.dirname(path.realpath(__file__)) screenshot('file://' + curr_path + '/demo.html' )
这样就可以获取到示例页面的截图。