图片合成支持的前世今生

文章目录
  1. 图片合成的前世今生
  2. I. 背景
    1. 0. 无聊的技术研究
    2. 1. 蛋疼的小程序
    3. 2. 开动
  3. II. 技术尝试
    1. 0. java的html渲染库
      1. a. html2image
      2. b. xhtml渲染包
      3. c. 借助转pdf的包
      4. d. 小结
    2. 1. imagemagic的合成
      1. a. 环境准备
      2. b. java调用
      3. c. 其他一些不得不说的故事
    3. 2. awt的绘制
      1. o. 接口定义
      2. a. 图片 : ImgBO
      3. b. 文字:FontBO
      4. c. 直线: LineBO
      5. d. 矩形: RoundRectBO
      6. e. 纯色: ColorBgBO
      7. 小结&问题
    4. 3. html 转 图片
      1. a. 环境准备
      2. b. 实测
      3. c. 分析小结
      4. d. chrome 方式
    5. 4. svg 转 图片
      1. a. 依赖整理
      2. b. 实测
      3. c. 问题
  4. III. 最后收尾
    1. 1. 吐槽
    2. 2. 小结
  5. III. 其他
    1. 体验网址
    2. 个人博客: 一灰灰Blog
    3. 声明
    4. 扫描关注

图片合成的前世今生

作为一个后端,为什么要做图片合成?为什么要实现类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("---");
}

接下来就是看输出的图片了,看下是否和我们预期相同

html2img.png

这个颜色,样式有点鬼畜,折腾了一番,实际验证这个框架挺不错的,就是有以下几个问题

  • 很久很久很久很久很久以前的产物了
  • 没人维护
  • css样式支持不友好

换个复杂点的url,比如淘宝or蘑菇街商品详情页,返回就更鬼畜了,有兴趣的童鞋可自己尝试一下

b. xhtml渲染包

这个也可以实现html渲染,又是一个老古董级别的东西,已经忘记从哪里捞出来的,最初实现markdown渲染成图片,就是采用的这个包,对简单的css的支持还算友好

pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--html to image render-->
<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>
<!--html to image render-->

测试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);


// do layout with temp buffer
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
/**
* 裁剪图片
*
* @param imagePath 源图片路径
* @param outPath 处理后图片路径
* @param x 起始X坐标
* @param y 起始Y坐标
* @param width 裁剪宽度
* @param height 裁剪高度
* @return 返回true说明裁剪成功, 否则失败
*/
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);
/** width:裁剪的宽度 * height:裁剪的高度 * x:裁剪的横坐标 * y:裁剪纵坐标 */
op.crop(width, height, x, y);
op.addImage(outPath);
// 传true到构造函数中,则表示使用GraphicMagic, 裁图时,图片大小会变
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如 :

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
/**
* Created by yihui on 2017/9/21.
*/
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
<!--phantomjs -->
<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);
}

输出图片

IMAGE

看到这个结果之后,是否会觉得已经完美了?

然而并不是,测试一些需要异步请求的接口,比较渣,性能差,返回的样式会错乱

c. 分析小结

这个方案从实现来讲,是没有什么问题的,从支持情况来说,问题其实也不太大,那为什么不用这个方案呢?

这个方案的支持,原本我的希望是前端传给我们需要渲染的html

  • 是直出好的页面
  • 所有的dom结构已经很清晰了,
  • 尽量不要有什么js,
  • 不要有异步请求,
  • 不要又复杂的css依赖,
  • 没有大量的图片

然而事与愿违,至于为什么不实现这样的html,我也不太懂前端的技术难点在哪,不好多评,那么也就只好转方案了

还有一点,对这个方案我不太满意的就是性能太渣,而且我也不知道可以怎么去优化,简单来讲,就是这个js渲染,完全不在我的把控之内,有什么问题、如何去优化、如何防止ssrf攻击,我都没有好的解决办法,所以我本人也是不喜欢这个方案的

d. chrome 方式

chrome浏览器,大家都知道,chrome还有一种无界面启动方式,可能知道的比较少了

只要你本机安装了chrome浏览器,打开控制台就可以愉快的玩耍了,html输出图片的指令为

1
2
3
4
5
## 输出pdf
/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

输出截图

screenshot.png

说明

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
<!--batik svg to image-->
<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>
<!-- https://mvnrepository.com/artifact/org.apache.xmlgraphics/xmlgraphics-commons -->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>xmlgraphics-commons</artifactId>
<version>2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.xmlgraphics/batik-codec -->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId>
<version>1.8</version>
</dependency> <!-- 此处不能使用2.9.1版本,使用2.9.1生成png会失败 -->
<!--<dependency>-->
<!--<groupId>xerces</groupId>-->
<!--<artifactId>xercesImpl</artifactId>-->
<!--<version>2.5.0</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 {
// 1. 加载document
File file = new File(path);
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
Document doc = f.createDocument(file.toURI().toString());


// 2. 遍历参数,填充渲染的svg节点
Set<String> keySet = parmMap.keySet();
for (Map.Entry<String, String> entry : parmMap.entrySet()) {
doc.getElementById(entry.getKey()).setTextContent(entry.getValue());
}

// 3. 输出图片
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. 问题

  1. 文本的边框支持问题: 即 outline属性

    测试了好久,发现不支持这个属性

  2. 图片内容替换与文本内容替换是不一样的,需要区分对待

  3. 多个标签填充同样的内容时

    从接口上来看,支持一个根据Name来获取节点功能,但是实际测试,发现标签name属性,并没有什么鸟用;不知道是使用姿势问题还是别的

    然后翻看源码,发现当多个标签的id相同时,在Document的底层存储单元中,elementById 这个Map结构中,value会是一个数组

    然后自然而然的想法就是,直接遍历这个数组,依次填充内容就好;结果发现压根就没有暴露这个接口,而这个属性是protectd,也无法直接访问

    然后采用反射获取这个属性值,来绕过限制

  4. 模板加载缓存

    实际场景中,模板往往是固定的,每次都进行渲染是非常消耗性能的,因此想的是能不能缓存住这个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写了个前端,可以来体验渲染

个人博客: 一灰灰Blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

扫描关注

QrCode

# 手记

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×