当前位置: 首页 > 后端技术 > Java

java-kotlin生成echarts图片最优解

时间:2023-04-01 23:08:48 Java

1.方法探索后台生成图片的方法不多。根据我在网上搜索,有以下几种方法:前端服务提供接口,结合图表提供的生成图片,request然后返回图片数据。构建服务,和第一点类似,也是发送数据。如果有协作的前端服务,可以在前端发起下载时生成图片数据,并传回后台。使用phantomjs将图表数据整理成html,再结合相应的javascript脚本生成图片。这个方法也是本文要介绍的方法。对比了这几种方法,我发现使用phantomjs的优势还是比较大的。首先,它不依赖于额外的服务。二是生成方式自主可控。您可以灵活地处理数据并控制生成图像的质量和大小。三是适用范围更广。不仅可以使用echarts,还可以使用highchart,包括但不限于图表,只要你会想办法做图即可。唯一的缺点是您需要安装它。2.灵感来源本文的灵感来源于ECharts-Javaissuse。在寻找如何在后端生成前端图表图片的过程中,发现了这个issuse,在作者之一incandescentxxc的指导下,找到了Snapshot-PhantomJS这个项目。但是我并没有直接使用Snapshot-PhantomJS。由于源码不多,所以我选择吸收核心源码,针对性的进行压缩和优化。3、所需工具Echarts-Javaorg.icepear.echartsecharts-java1.0.7本工具有两个作用:方便的将数据组织成echarts需要的options。它可以将选项转换成所需的html,并可以直接在浏览器中打开图表。如果使用slf4j,最好把org.slf4j全部去掉,否则会出现冲突。(我觉得应该不会出现这个问题,第三方jar本身应该考虑到这个问题。)phantomjs的功能有点相当于一个后台运行的浏览器,在后台运行一个html界面。javascript脚本varpage=require("webpage").create();varsystem=require("system");varfile_type=system.args[1];vardelay=system.args[2];varpixel_ratio=system.args[3];varsnapshot="function(){"+"varele=document.querySelector('div[_echarts_instance_]');"+"varmychart=echarts.getInstanceByDom(ele);"+"returnmychart.getDataURL({type:'"+file_type+"',pixelRatio:"+pixel_ratio+",excludeComponents:['toolbox']});"+"}";varfile_content=system.stdin.read();page.setContent(file_content,"");window.setTimeout(function(){varcontent=page.evaluateJavaScript(snapshot);phantom.exit();},延迟);这个脚本的作用是在内部生成一个网页,使用来加载包含你传递的图表数据的html,等待一会加载完成,得到图片的dataURL,其实就是图片的base64数据图像。4.Demo代码因为觉得java代码写demo太麻烦,所以用kotlindemo。valbar=Bar().setLegend().setTooltip("item").addXAxis(arrayOf("抹茶拿铁","奶茶","芝士可可","核桃布朗尼")).addYAxis().addSeries("2015",arrayOf(43.3,83.1,86.4,72.4)).addSeries("2016",arrayOf(85.8,73.4,65.2,53.9)).addSeries("2017",arrayOf(93.7,55.1,82.5,39.1))valengine=Engine()valhtml=engine.renderHtml(bar)valprocess=ProcessBuilder("phantomjs","generate-images.js","jpg","10000","10").start()BufferedWriter(OutputStreamWriter(process.outputStream)).use{it.write(html)}valresult=process.inputStream.use{IoUtil.read(it,Charset.defaultCharset())}valcontentArray=result.split(",".toRegex()).dropLastWhile{it.isEmpty()}if(contentArray.size!=2){throwRuntimeException("wrongimagedata")}valimageData=contentArray[1]FileUtil.writeBytes(Base64.decode(imageData),File("test.jpg"))IoUtil和FileUtil都来了来自于hutool对命令行参数的解释:phantomjs,phantomjsgenerate-images.js的执行路径就是上面提到的javascript脚本。jpg是你需要的图片格式,svg需要自己修改javascript脚本。10000是延迟时间。这个时间是留给html加载的。耗时包括下载echarts脚本和生成图片。10.图片质量,图片越大,质量越高,图片尺寸越大。可以看到,经过我的精简,整体的代码还是比较简单的。5.优化过程以上演示代码不能称为最终版本。你要面临两个问题:生成的html需要联网才能下载echarts,且不说这部分很费时间,有些环境还面临无法联网的情况。10画质的图片超过40M肯定是不能接受的。使用本地的echarts库,只需要下载文件,有针对性地替换即可。valhtml=engine.renderHtml(bar).replace(Regex("(?is)"),"""""")压缩jpg因为生成的图片非常简单,这也意味着压缩后的空间非常巨大。经过我自己的测试,40M左右的图片压缩后可以缩小到几百K,画质基本不受影响。我直接给出实现代码。funremoveAlpha(img:BufferedImage):BufferedImage{如果(!img.colorModel.hasAlpha()){returnimg}valtarget=BufferedImage(img.width,img.height,BufferedImage.TYPE_INT_RGB)valg=target.createGraphics()G。fillRect(0,0,img.width,img.height)g.drawImage(img,0,0,null)g.dispose()returntarget}funcompress(imageData:ByteArray):ByteArray{returnByteArrayOutputStream().use{compressed->//压缩后的图片,原图尺寸太大,压缩后体积会缩小,损失不大质量ImageIO.createImageOutputStream(compressed).use{valjpgWriter=ImageIO.getImageWritersByFormatName("JPEG").next()jpgWriter.output=itvaljpgWriteParam=jpgWriter.defaultWriteParamjpgWriteParam.compressionMode=ImageWriteParam.MODE_EXPLICITjpgWriteParam.compressionQuality=0.7fvalimg=ByteArrayInputStream(imageData).use{//移除原始alpha通道(removeAlpha(ImageIO.read(it)),null,null)}jpgWriter.write(null,img,jpgWriteParam)jpgWriter.dispose()compressed.toByteArray()}}}因为压缩图片的前提是图片不能包含alphaChannel,所以在网上找了个去掉channel优化耗时的方法。其实上面本来就写到这里就结束了,但是因为灵感来了,顺带解决了这个问题。如果你充分理解了上面的例子,你会发现这个例子的耗时处理有一个很大的问题:耗时是不可控的,无法知道图表什么时候渲染出来的。耗时只能是固定的,即使画面渲染早于你设置的时间,依然需要等待很长时间。图表渲染过程中有一个动画。如果你根据上面缩短时间,你可能会在动画运行的中间得到一个画面。我们在后端使用它,可以完全节省这部分时间。因此,我针对这些问题做了进一步的优化。在此之前,你需要知道phantomjs可以监听网页页面的一些事件,其中之一就是[onConsoleMessage](https://phantomjs.org/api/webpage/handler/on-console-message.html),它可以捕获到网页的打印事件,获取打印信息。同时echarts也提供渲染完成事件。这样就可以完全控制渲染带来的耗时问题。优化后的脚本如下。同时,我还为脚本设置了一个最大超时时间。如果在这段时间内渲染没有完成,会强制结束,防止卡死。我也放弃了质量和图像格式的配置。放在echarts的finished事件中。varpage=require("webpage").create();varsystem=require("system");vardelay=system.args[1];varfile_content=system.stdin.read();page.setContent(file_content,"");page.onConsoleMessage=function(msg){console.log(msg);phantom.exit();};window.setTimeout(function(){phantom.exit();},delay);这个时候,我们再使用自定义脚本来补上之前html中缺失的finish。valscript="""""".trimIndent().replace("\n","")最后设置取消动画,进一步缩短生成时间。bar.option.animation=false此时,原本需要十多秒才能完成的动作现在只需要6秒(在MacBookProm1上测试过)。kotlin的完整代码importcn.hutool.core.codec.Base64importcn.hutool.core.io.FileUtilimportcn.hutool.core.io.IoUtilimportorg.icepear.echarts.Barimportorg.icepear.echarts.render.Engineimportjava.awt.image.BufferedImageimportjava.io.BufferedWriterimportjava.io.ByteArrayInputStreamimportjava.io.ByteArrayOutputStreamimportjava.io.Fileimportjava.io.OutputStreamWriterimportjava.nio.charset.Charsetimportjavax.imageio.IIOImageimportjavax.imageio.ImageIOimportjavax.imageio...)g.fillRect(0,0,img.width,img.height)g.drawImage(img,0,0,null)g.dispose()返回目标}funcompress(imageData:ByteArray):ByteArray{returnByteArrayOutputStream().use{compressed->//压缩图像,原有图像体积过大,压缩后尺寸减小而质量没有太大损失MODE_EXPLICITjpgWriteParam.compressionQuality=0.7fvalimg=ByteArrayInputStream(imageData).use{//移除原来的alpha通道IIOImage(removeAlpha(ImageIO.read(it)),null,null)}jpgWriter.write(null,img,jpgWriteParam)jpgWriter.dispose()compressed.toByteArray()}}}funmain(){valbar=Bar().setLegend().setTooltip("item").addXAxis(arrayOf("抹茶拿铁","奶茶","芝士可可","核桃布朗尼")).addYAxis().addSeries("2015",arrayOf(43.3,83.1,86.4,72.4)).addSeries("2016",arrayOf(85.8,73.4,65.2,53.9)).addSeries("2017",arrayOf(93.7,55.1,82.5,39.1))bar.option.animation=falsevalengine=Engine()valscript="""""".trimIndent().replace("\n","")valhtml=engine.renderHtml(bar).replace(Regex("(?is)"),"""""").replace(Regex("(?is)"),script)println(html)valprocessBuilder=ProcessBuilder("phantomjs","generate-images.js","10000")valprocess=processBuilder.start()BufferedWriter(OutputStreamWriter(process.outputStream)).use{it.write(html)}valresult=process。inputStream.use{IoUtil.read(it,Charset.defaultCharset())}valcontentArray=result.split(",".toRegex()).dropLastWhile{it.isEmpty()}if(contentArray.size!=2){throwRuntimeException("wrongimagedata")}valimageData=contentArray[1]valcompressImageData=compress(Base64.decode(imageData))FileUtil.writeBytes(compressImageData,File("test.jpg"))}