图片合成的前世今生
作为一个后端,为什么要做图片合成?为什么要实现类xml标记语言的渲染?
本片博文准备详细的记录一下,一个java后端如何去支持图片合成,在这个过程中采用了哪些猥琐的方案,又遇到了哪些鬼畜的问题
I. 背景 0. 无聊的技术研究 最开始萌发支持图片合成的想法,那时候还是在做二维码的时候,用了一些awt的画图工具,感觉还挺有意思的,这是一个和当前的电商主流完全不搭边的技术分支,开始用的时候感慨,这东西牛逼了,什么都可以干(虽然操作非常不友好),再加上用到有道云,它的会员功能支持加功能将笔记以图片方式生成,所以就有个想法,java后端能不能支持markdown输出图片呢?
1. 蛋疼的小程序 不是一个专业的小程序开发者,虽然写过一个小程序,但是很多特性依然不知道;
突然很多前端突然提了这么一个需求,要求后端支持图片合成,用于分享到朋友圈
至于原因:
有的说小程序没有提供截屏接口
小程序不支持绘图(这个我不太确定真实性)
小程序绘图的api不可控(如果他们有bug,我们就没法玩了;对此我的看法是,你整个东西都是在小程序的体系里了,要是有个严重bug,那我们的小程序干脆就不玩好了…)
前端这么多,每个人都去绘制一遍低效,有个后端通用的,各个平台都释放了,都可以直接用… (对此我也没啥好说的,如果我是前端我也挺这一点;然而我不是,所以我拒绝😢)
声明
上面括号的内容纯粹是个人吐槽,没有任何偏向性,
2. 开动 有需求了,就必须去支持了,而且从技术角度出发,这是一个非常有意思的点,新的挑战,可以一试
II. 技术尝试 为了支持这个需求,尝试了不少的手段,接下来一一说明,当然由于个人见识有限,最终选择的也不一定是啥好东西,目前也只是处于可用的状态,离友好支持,还比较遥远
0. java的html渲染库
最先想到的就是这个,有没有直接可以渲染的库,大Java号称是在github上拥有最多开源工具的语言
查了一些开源库,也主动去尝试过一些,下面给出使用姿势
a. html2image 直接在Github上搜,找一个最多star的就可以了,测试的框架
接入及测试方式
pmo 依赖引入
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > gui.ava</groupId > <artifactId > html2image</artifactId > <version > 0.9</version > </dependency > <repositories > <repository > <id > yoava</id > <name > AOL yoava</name > <url > http://yoava.artifactoryonline.com/yoava/repo</url > </repository > </repositories >
测试代码也比较简单
1 2 3 4 5 6 7 8 9 @Test public void testRenderHtml () { String url = "http://www.baidu.com" ; HtmlImageGenerator generator = new HtmlImageGenerator(); generator.loadUrl(url); BufferedImage img = generator.getBufferedImage(); System.out.println("---" ); }
接下来就是看输出的图片了,看下是否和我们预期相同
这个颜色,样式有点鬼畜,折腾了一番,实际验证这个框架挺不错的,就是有以下几个问题
很久很久很久很久很久以前的产物了
没人维护
css样式支持不友好
换个复杂点的url,比如淘宝or蘑菇街商品详情页,返回就更鬼畜了,有兴趣的童鞋可自己尝试一下
b. xhtml渲染包 这个也可以实现html渲染,又是一个老古董级别的东西,已经忘记从哪里捞出来的,最初实现markdown渲染成图片,就是采用的这个包,对简单的css的支持还算友好
pom依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > org.xhtmlrenderer</groupId > <artifactId > core-renderer</artifactId > <version > R8</version > </dependency > <dependency > <groupId > net.sourceforge.nekohtml</groupId > <artifactId > nekohtml</artifactId > <version > 1.9.14</version > </dependency >
测试case
1 2 3 4 5 6 7 8 9 10 @Test public void testRender () { try { String url = "http://www.baidu.com" ; BufferedImage buf = ImageRenderer.renderToImage(url, "/Users/yihui/html2image.pdf" , 800 ); System.out.println("---" ); } catch (Exception e) { e.printStackTrace(); } }
使用起来还是比较简单的,但是,上面这种直接执行,会抛异常,说访问的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 55 56 57 58 59 60 61 62 63 64 65 private static DOMParser domParser;static { domParser = new DOMParser(new HTMLConfiguration()); try { domParser.setProperty("http://cyberneko.org/html/properties/names/elems" , "lower" ); } catch (Exception e) { throw new RuntimeException("Can't create HtmlParserImpl" , e); } } private Document parseDocument (String content) throws Exception { domParser.parse(new InputSource(new StringReader(content))); return domParser.getDocument(); } private String readHtmlContent (String url) throws Exception { InputStream in = HttpUtil.downFile(url); StringBuilder out = new StringBuilder(); byte [] b = new byte [4096 ]; for (int n; (n = in.read(b)) != -1 ; ) { out.append(new String(b, 0 , n)); } return out.toString(); } @Test public void testRender () { try { String url = "http://www.baidu.com" ; Document doc = parseDocument(readHtmlContent(url)); int width = 800 ; int height = 1024 ; Graphics2DRenderer renderer = new Graphics2DRenderer(); renderer.setDocument(doc, doc.getDocumentURI()); BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D graphics2D = GraphicUtil.getG2d(bufferedImage); renderer.layout(graphics2D, new Dimension(width, height)); graphics2D.dispose(); Rectangle size = renderer.getMinimumSize(); final int autoWidth = width; final int autoHeight = (int ) size.getHeight(); bufferedImage = new BufferedImage(autoWidth, autoHeight, BufferedImage.TYPE_INT_RGB); Dimension dimension = new Dimension(autoWidth, autoHeight); graphics2D = GraphicUtil.getG2d(bufferedImage); renderer.layout(graphics2D, dimension); renderer.render(graphics2D); graphics2D.dispose(); System.out.println("---------" ); } catch (Exception e) { e.printStackTrace(); } }
结果输出图片为空白的页面,为啥? 仔细去看百度的网页,发现没有dom结构,一堆的js和css代码,换个本地的html来试一下,输出效果还不错,我之前做了一个小工具,实现markdown转image,就是用的这个框架做中转,将markdown生成的html渲染为图片,当然复杂一点的css就不行了
相信看到这里,这个库的缺陷也好很明显了,不适合生产环境,自己玩玩还行
过于古老,基本没人维护
对html的格式有要求
复杂的css没法玩
指定宽度也比较恶心
c. 借助转pdf的包 java中,提供html转pdf的包还不少,借助这些工具,也是可以间接实现这个功能的,具体的就不贴了,可以用的不少,收钱的,免费的都有
推荐几个搞标记的
d. 小结 基本上,没有找到合乎心意的转换包,其实有些包也不错,如果深入进去改一波,应该也能使用,然实际就是深入进去,基本上挖不动
1. imagemagic的合成 大名鼎鼎的图片处理工具,c++的,可以提供图片的各种姿势的操作,当然也包括了图片合成,要玩这个,首先得搭建这个环境(这个成本比上面会大一点)
a. 环境准备 简单搭建方式:
1 2 3 4 5 6 7 8 yum install libjpeg-devel yum install libpng-devel # 本地环境搭建 sudo brew install jpeg sudo brew install libpng sudo brew install GraphicsMagick
搭建完毕后,测试先是否可用
1 2 3 4 ## 搭建完毕,开始测试 gm convert input.jpg -thumbnail '100x100' output_1.jpg gm convert -crop 640x960+0+0 test.jpg output.jpg
如果上面的搞不定,也可以用下面的下载包的方式安装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 安装jpeg 包 `wget ftp://223.202.54.10/pub/web/php/libjpeg-6b.tar.gz` 安装webp 包 `wget http://www.imagemagick.org/download/delegates/libwebp-0.5.1.tar.gz` 安装png 包 `wget http://www.imagemagick.org/download/delegates/libpng-1.6.24.tar.gz` 安装 graphicsmagick `wget http://nchc.dl.sourceforge.net/project/graphicsmagick/graphicsmagick/1.3.22/GraphicsMagick-1.3.22.tar.gz` ## ---------- make distclean ## 清楚上次make的东西 imagemagick : `wget http://www.imagemagick.org/download/ImageMagick.tar.gz` 安装命令 `sudo ./configure; sudo make; sudo make install` 裁图命令 `convert test.jpg -crop 640x960+0+0 output.jpg`
linux 安装imagemagick 发现一直找不到 png的依赖,
linux 安装之后,可能有两个问题
b. java调用 当然,我们是java的后端,现在就需要用java来调用imagemagic的执行了
依赖包
1 2 3 4 5 <dependency > <groupId > org.im4java</groupId > <artifactId > im4java</artifactId > <version > 1.4.0</version > </dependency >
下面给一个图片裁剪的测试
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 public static boolean cut (String imagePath, String outPath, int x, int y, int width, int height) { boolean flag; try { IMOperation op = new IMOperation(); op.addImage(imagePath); op.crop(width, height, x, y); op.addImage(outPath); ConvertCmd convert = new ConvertCmd(); convert.run(op); flag = true ; } catch (IOException e) { flag = false ; } catch (InterruptedException e) { flag = false ; } catch (IM4JavaException e) { flag = false ; } return flag; }
具体使用姿势就不说了,这个框架本身是支持简单的图片合成的,几张图和一下,加上文字水印啥的,主要说一下有什么问题
图片合成参数不是一般的复杂,想实现一个模板的合成,这个命令可以说很难完美的写出来
性能一般般
总得来说,这个用来做图片的基本操作还很好,真心不太合适复杂点的图片合成,分分钟虐哭
c. 其他一些不得不说的故事 说到imagemagic,就不得不说graphicmagic,两者基本差不多,有说法是 graphicmagic的性能要高与imagemagic,那么我们为什么选择 imagemagic
graphicmagic 处理jpg图片,会有精度丢失的问题(不知道是不是我的使用姿势不对,同样的case,imagemagic不会)
公司的基线是支持imagemagic的
很久以前写了一篇博文,就是如何利用 imagegraphic 搭建一个图片处理服务器的
2. awt的绘制 利用java的awt包,也是可以实现绘图的,而且功能也比较强大,完全可以实现各种姿势的绘图场景, 一个case如 :
上面这个图的合成,就是基于awt做到的,这一张图,我们需要做些什么?
图片的绘制
圆角图片
文字输出
文字对其方式
直线
矩形
纯色背景
一般来将,上面几种场景的支持,可以满足绝大多数的合图要求,接下来看一下是如何支持上面的几种case的
o. 接口定义 定义一个基本的绘图单元接口
1 2 public interface IDrawBO {}
a. 图片 : ImgBO 图片的定义比较简单,一般只需要知道坐标,和宽高就ok了,所以我们的定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Data @NoArgsConstructor @AllArgsConstructor public class ImgBO implements IDrawBO { private BufferedImage image; private int x; private int y; private int w; private int h; }
b. 文字:FontBO 文字相比较图片就有些额外的区别,有字体,样式、颜色,坐标,删除线
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @NoArgsConstructor @AllArgsConstructor public class FontBO implements IDrawBO { private String msgs; private Font font; private Color color; private int x; private int y; private boolean deleted = false ; }
c. 直线: LineBO 直线,除了我们常规的起点坐标,末尾坐标之外,颜色的设置,虚线样式也是常见的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Data public class LineBO implements IDrawBO { public static final Stroke DEFAULT_STROKE = new BasicStroke(2 , BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 3.5f , new float []{12 , 6 , 6 , 6 }, 0f ); private Color color; private int x1; private int y1; private int x2; private int y2; private boolean dashed; }
d. 矩形: RoundRectBO 和直线的属性差不多, 但是会多一些有意思的东西,如是否为圆角矩形
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 public class RoundRectBO implements IDrawBO { public static final Stroke DEFAULT_DASH = new BasicStroke(1 , BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 3.5f , new float []{4 , 2 ,}, 0f ); private int x; private int y; private int w; private int h; private Color color; private boolean dashed; private int radius; }
e. 纯色: ColorBgBO 纯色背景,相比较其他的会多一个透明度的属性,主要是因为很多场景下,会做一层纯色的浮层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class ColorBgBO implements IDrawBO { private Color color; private int w; private int h; private int x; private int y; private int radius; private boolean transparence; }
上面定义了这些BO对象,仅仅是定义又什么用?接下来就需要实现对BO对象的绘制,也是核心的逻辑层了
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 public interface IShareModule { void draw (Graphics2D g2d) ; default void drawFont (Graphics2D g2d, FontBO fontBo) { if (fontBo != null ) { g2d.setFont(fontBo.getFont()); g2d.setColor(fontBo.getColor()); g2d.drawString(fontBo.getMsgs(), fontBo.getX(), fontBo.getY()); if (fontBo.isDeleted()) { FontMetrics fontMetrics = FontUtil.getFontMetric(fontBo.getFont()); int y = fontBo.getY() - (fontBo.getFont().getSize() >> 1 ) + fontMetrics.getDescent(); int w = fontMetrics.stringWidth(fontBo.getMsgs()); g2d.drawLine(fontBo.getX(), y, fontBo.getX() + w, y); } } } default void drawImage (Graphics2D g2d, ImgBO imgBo) { if (imgBo != null ) { g2d.drawImage(imgBo.getImage(), imgBo.getX(), imgBo.getY(), imgBo.getW(), imgBo.getH(), null ); } } default void drawLine (Graphics2D g2d, LineBO lineBO) { if (lineBo == null ) return ; g2d.setColor(lineBO.getColor()); if (lineBO.isDashed()) { Stroke stroke = g2d.getStroke(); g2d.setStroke(LineBO.DEFAULT_STROKE); g2d.drawLine(lineBO.getX(), lineBO.getY(), lineBO.getX() + lineBO.getW(), lineBO.getY()); g2d.setStroke(stroke); } else { g2d.drawLine(lineBO.getX(), lineBO.getY(), lineBO.getX() + lineBO.getW(), lineBO.getY()); } } default void drawRoundRect (Graphics2D g2d, RoundRectBO roundRectBO) { if (roundRectBO == null ) return ; g2d.setColor(roundRectBO.getColor()); if (!roundRectBO.isDashed()) { g2d.drawRoundRect(roundRectBO.getX(), roundRectBO.getY(), roundRectBO.getW(), roundRectBO.getH(), roundRectBO.getRadius(), roundRectBO.getRadius()); } else { Stroke stroke = g2d.getStroke(); g2d.setStroke(RoundRectBO.DEFAULT_DASH); g2d.drawRoundRect(roundRectBO.getX(), roundRectBO.getY(), roundRectBO.getW(), roundRectBO.getH(), roundRectBO.getRadius(), roundRectBO.getRadius()); g2d.setStroke(stroke); } if (roundRectBO.getSpaceW() > 0 ) { int x = roundRectBO.getX() + (roundRectBO.getW() - roundRectBO.getSpaceW() >> 1 ); int y = roundRectBO.getY() - 2 ; int w = roundRectBO.getSpaceW(); int h = 4 ; g2d.setColor(roundRectBO.getSpaceColor()); g2d.fillRect(x, y, w, h); } } default void drawColorBG (Graphics2D g2d, ColorBgBO color) { if (color == null ) return ; g2d.setColor(color.getColor()); Composite composite = null ; if (color.isTransparence()) { composite = g2d.getComposite(); g2d.setComposite(AlphaComposite.Src); } if (color.getRadius() == 0 ) { g2d.fillRect(color.getX(), color.getY(), color.getW(), color.getH()); } else { g2d.fill(new RoundRectangle2D.Float(color.getX(), color.getY(), color.getW(), color.getH(), color.getRadius(), color.getRadius())); } if (color.isTransparence()) { g2d.setComposite(composite); } } }
上面配合起来使用,就可以实现基本的模板图片的合成需求了,当然我们提供的服务比上面列出的要丰富一些,我们还支持
图片的处理:圆角,裁剪贴图
文字对齐:三种对齐方式,自动换行
小结&问题 上面虽然说可以支持合图的需求,但有个最大的问题,就是对后端的工作太多,每个模板,都需要后端来配合,进行参数指定,联调,极其繁琐和费时费力,分分钟搞死人
对这种方式,想的一个方法是,采用搭积木的方式支持,事先定义一系列的基本绘图组建,然后前端自己填入参数来组装
当然没有做,原因也很简单,接口太复杂,对前端不友好,没人愿意这么用,换成我也是不想这么干的
3. html 转 图片 接着又来的是一个猥琐的方案,html转图,到github上一搜,发现还是js靠谱,比较多,一种常见的思路是:
采用无界面浏览器加载html页面,然后截图
在无界面浏览器中,非常有名的是 phantomjs,以及后起之秀chrome,这里主要说一下phantomjs的接入方式,简单提起chrmoe的无界面使用方式
a. 环境准备 phantomjs 安装
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 # 1. 下载 ## mac 系统 wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip ## linux 系统 wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 ## windows 系统 ## 就不要玩了,没啥意思 # 2. 解压 sudo su tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2 # 如果解压报错,则安装下面的 # yum -y install bzip2 # 3. 安装 ## 简单点,移动到bin目录下 cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin # 4. 验证是否ok phantomjs --version # 输出版本号,则表示ok
pom依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependency > <groupId > org.seleniumhq.selenium</groupId > <artifactId > selenium-java</artifactId > <version > 2.53.1</version > </dependency > <dependency > <groupId > com.github.detro</groupId > <artifactId > ghostdriver</artifactId > <version > 2.1.0</version > </dependency > <repositories > <repository > <id > jitpack.io</id > <url > https://jitpack.io</url > </repository > </repositories >
b. 实测 思路比较清晰,在服务器上搭建一个phantomjs服务,然后java来调用,主要借助的是selenium和ghostdriver两个开源包,额外提一句,selenium在自动化测试和爬虫使用中非常有名,有兴趣的可以自己搜索相关资料,非常有意思的一个东西
图片渲染的主要业务逻辑:
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 public class Html2ImageByJsWrapper { private static PhantomJSDriver webDriver = getPhantomJs(); private static PhantomJSDriver getPhantomJs() { //设置必要参数 DesiredCapabilities dcaps = new DesiredCapabilities(); //ssl证书支持 dcaps.setCapability("acceptSslCerts", true); //截屏支持 dcaps.setCapability("takesScreenshot", true); //css搜索支持 dcaps.setCapability("cssSelectorsEnabled", true); //js支持 dcaps.setJavascriptEnabled(true); //驱动支持(第二参数表明的是你的phantomjs引擎所在的路径,which/whereis phantomjs可以查看) // fixme 这里写了执行, 可以考虑判断系统是否有安装,并获取对应的路径 or 开放出来指定路径 dcaps.setCapability(PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY, "/usr/local/bin/phantomjs"); //创建无界面浏览器对象 return new PhantomJSDriver(dcaps); } public static BufferedImage renderHtml2Image(String url) throws IOException { webDriver.get(url); File file = webDriver.getScreenshotAs(OutputType.FILE); return ImageIO.read(file); } }
那么测试case就很好写了
1 2 3 4 5 @Test public void testRender() throws IOException { String url = "https://www.baidu.com"; BufferedImage img = Html2ImageByJsWrapper.renderHtml2Image(url); }
输出图片
看到这个结果之后,是否会觉得已经完美了?
然而并不是,测试一些需要异步请求的接口,比较渣,性能差,返回的样式会错乱
c. 分析小结 这个方案从实现来讲,是没有什么问题的,从支持情况来说,问题其实也不太大,那为什么不用这个方案呢?
这个方案的支持,原本我的希望是前端传给我们需要渲染的html
是直出好的页面
所有的dom结构已经很清晰了,
尽量不要有什么js,
不要有异步请求,
不要又复杂的css依赖,
没有大量的图片
然而事与愿违,至于为什么不实现这样的html,我也不太懂前端的技术难点在哪,不好多评,那么也就只好转方案了
还有一点,对这个方案我不太满意的就是性能太渣,而且我也不知道可以怎么去优化,简单来讲,就是这个js渲染,完全不在我的把控之内,有什么问题、如何去优化、如何防止ssrf攻击,我都没有好的解决办法,所以我本人也是不喜欢这个方案的
d. chrome 方式 chrome浏览器,大家都知道,chrome还有一种无界面启动方式,可能知道的比较少了
只要你本机安装了chrome浏览器,打开控制台就可以愉快的玩耍了,html输出图片的指令为
1 2 3 4 5 /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --headless --print -to-pdf http://www.baidu.com /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --headless --screenshot http://www.baidu.com
输出截图
说明
chrome headless有很多指令,可设置窗口的大小解决上面的边框问题,有兴趣的可以百度
4. svg 转 图片 然后万能的前端同学又提出了svg渲染图片,在提这个之前,完全没接触过svg,也不知道svg是个什么鬼,更不知道svg能不能渲染出图片(最重要的是java有没有现成可用的库)
查了一番,不错,发现apace有个batik,就是干这个事情的
插播一句,感觉无论多偏的东西,apache或者是google都至少有那么一个可以支持的开源项目,虽然有不少都已经不怎么维护了
a. 依赖整理 依赖包有那么点多
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-svggen</artifactId > <version > 1.8</version > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-bridge</artifactId > <version > 1.8</version > <exclusions > <exclusion > <groupId > xalan</groupId > <artifactId > xalan</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-dom</artifactId > <version > 1.8</version > <exclusions > <exclusion > <groupId > xalan</groupId > <artifactId > xalan</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-parser</artifactId > <version > 1.8</version > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-svg-dom</artifactId > <version > 1.8</version > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-transcoder</artifactId > <version > 1.8</version > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-util</artifactId > <version > 1.8</version > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-xml</artifactId > <version > 1.8</version > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > xmlgraphics-commons</artifactId > <version > 2.1</version > </dependency > <dependency > <groupId > org.apache.xmlgraphics</groupId > <artifactId > batik-codec</artifactId > <version > 1.8</version > </dependency > <dependency > <groupId > xml-apis</groupId > <artifactId > xmlParserAPIs</artifactId > <version > 2.0.2</version > </dependency > <dependency > <groupId > org.axsl.org.w3c.dom.svg</groupId > <artifactId > svg-dom-java</artifactId > <version > 1.1</version > </dependency > <dependency > <groupId > xml-apis</groupId > <artifactId > xml-apis</artifactId > <version > 2.0.0</version > </dependency > <dependency > <groupId > org.w3c.css</groupId > <artifactId > sac</artifactId > <version > 1.3</version > </dependency >
b. 实测 一个简单的接口支持
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 public static void convertToPngByFile (String path, OutputStream outputStream, Map<String, String> parmMap) throws TranscoderException, IOException { try { File file = new File(path); String parser = XMLResourceDescriptor.getXMLParserClassName(); SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); Document doc = f.createDocument(file.toURI().toString()); Set<String> keySet = parmMap.keySet(); for (Map.Entry<String, String> entry : parmMap.entrySet()) { doc.getElementById(entry.getKey()).setTextContent(entry.getValue()); } PNGTranscoder t = new PNGTranscoder(); TranscoderInput input = new TranscoderInput(doc); TranscoderOutput output = new TranscoderOutput(outputStream); t.transcode(input, output); outputStream.flush(); } catch (Exception e) { e.printStackTrace(); } finally { if (outputStream != null ) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
上面主要是为了演示下使用姿势,实际的项目中肯定不会这么简陋,官方使用链接: https://xmlgraphics.apache.org/batik/using/transcoder.html
分析下主要流程
解析svg文件,加载Document对象
根据传入的参数,填充Document中的节点
渲染输出图片
测试演示就不来了,最终方案就是这个,成品也没啥好说的
c. 问题
文本的边框支持问题: 即 outline属性
测试了好久,发现不支持这个属性
图片内容替换与文本内容替换是不一样的,需要区分对待
多个标签填充同样的内容时
从接口上来看,支持一个根据Name来获取节点功能,但是实际测试,发现标签name属性,并没有什么鸟用;不知道是使用姿势问题还是别的
然后翻看源码,发现当多个标签的id相同时,在Document的底层存储单元中,elementById 这个Map结构中,value会是一个数组
然后自然而然的想法就是,直接遍历这个数组,依次填充内容就好;结果发现压根就没有暴露这个接口,而这个属性是protectd,也无法直接访问
然后采用反射获取这个属性值,来绕过限制
模板加载缓存
实际场景中,模板往往是固定的,每次都进行渲染是非常消耗性能的,因此想的是能不能缓存住这个Document,再使用的时候,直接深拷贝一个对象出来,这样就避免了重复加载的开销
直接使用 AbstractDocumen#deepClone(true)
方法
然后,出现了一个鬼畜的并发问题,这个单独领出来细说,此处不展开
III. 最后收尾 鉴于篇幅太长,有一些有意思的东西没有深入展开,特别是svg方案的支持中,遇到了一些比较有趣的问题,也涉及到三个好玩的知识点: 深拷贝+反射+并发,后面准备等这一块完结之后,好好的沉淀下,分析下这个case
1. 吐槽 后端支持已经很勉强了,请大家都友好点,比如下面几个我实在支持不了
自定义设置字体(jdk字体,没新加一个都需要pe安装到jre的字体库)
图片的左上角圆角(暂时没想到好的解决方法)
渐变色(这个有点难)
这个需求,做得比较恶心,支持得也比较蛋疼,实现得比较猥琐,调bug修问题也比较闹心,总得来说,是一个开始前很有趣,做时让人吐血又很不爽,做完之后又特么的很有收获的需求
发现特别能有收获的事情,往往不是哪种做的特别爽的需求(爽,是因为这些东西你都完全能hold住,没什么难度了),相反是那些让你很闹心,完全不想继续下去的需求(因为你不了解,但是又不得不支持,还会遇到一堆鬼畜的bug,做完简直是吐血三升)
2. 小结 图片合成的方式,我想应该不仅限于上面几种,由于限制于见识,终究是没有一个让人特别满意的方案,简单小结下上面的几种case
java的开源包
html2image, xhtmlrender, pdfTech
一般来说,不怎么好用,大多不维护状态,对CSS的支持友好度待检验
imagemagic
awt绘图
属于基本的接口了,啥都可以干,只要你可以弄出来
但是工作量太大
js实现html渲染
phantmjs,效果不错,性能略渣,异步请求不友好,且完全不可控
chrome 性能由于上面的
svg渲染
batik
并不能非常完美的支持svg的渲染,有较多的限制要求,各种属性的必填,某些style的无法支持等
基本场景的支持,ok,优化后,性能高于html渲染,且可控
III. 其他 体验网址 基于react写了个前端,可以来体验渲染
基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
声明 尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正
扫描关注