27_Java2DRenderer结合freemarker动态生成图片
27_Java2DRenderer结合freemarker动态生成图片
Java2DRender
和freemarker
属于java后端的技术,但是我们同样可以借助这两个工具在服务器端动态生成图片(如海报、名片)。生成好了之后,返回给前端使用即可。
以生成海报/名片为例,如果是在微信小程序环境中,可以通过canvas绘制,再将绘制的内容导出成图片。但是呢,有一定的局限性,比如说生成图片的模版内容很多、比较复杂…,就需要编写大量的canvas绘制代码,同时,如果处理不当,可能会出现oom
或者无法正常绘制等问题。那么这个时候同样也可以通过Java2DRender
和freemarker
在服务器端生成。
一.编写html/ftl模版
ftl模版,本质就是html,相当于在html中嵌入freemarker
语法支持的变量,同时把文件后缀由html
改成ftl
,因此,我们可以先写好html页面
,确保html页面
样式无误后,将其改为ftl模版
编写html/ftl模版时,需要注意:
由于编写好的ftl模版,需要通过
Java2DRender
来生成png图片,而Java2DRender
不支持flex布局
以及css3
的新特性,因此在编写模版的css样式时,可以使用表格布局
、浮动
、绝对定位和相对定位
去规避编写模版的时候,需要约定一个基准宽度(例如375px),作为html模版的宽度
使用
Java2DRender
将模版转换为图片时,支持高度自适应,因此模版高度是可以不用指定的
下面是一个写好的名片的html模版:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title></title>
<style type="text/css">
body {
padding: 0;
margin: auto 0;
font-family: 宋体;
background: transparent;
line-height: 1.05;
}
#root {
width: 375px;
position: relative;
overflow: hidden;
border-radius: 16px;
box-sizing: border-box;
padding: 8px;
background: #2f4f4f;
}
#content {
position: relative;
border-radius: 8px;
box-sizing: border-box;
overflow: hidden;
}
#group-item {
display: table;
width: 319px;
margin: 25px 20px 0;
overflow: hidden;
color: white;
}
#user-name {
display: table-cell;
font-size: 20px;
font-weight: bold;
}
#user-gender {
display: table-cell;
vertical-align: middle;
font-size: 20px;
padding-left: 20px;
}
#position-name, #department-name-wrapper {
display: table-cell;
font-size: 14px;
font-weight: 500;
color: white;
overflow: hidden;
vertical-align: middle;
}
#position-name {
min-width: 70px;
}
#department-name-wrapper {
display: table-cell;
text-align: right;
}
#department-name {
overflow: hidden;
display: inline-block;
text-align: left;
}
#group-item-key, #group-item-val {
display: table-cell;
font-size: 12px;
font-family: PingFang;
font-weight: 500;
}
#group-item-key {
width: 40px;
}
</style>
</head>
<body>
<div id="root">
<div id="content">
<div id="group-item" style="width: auto;">
<div id="user-name">${userName!""}</div>
<div id="user-gender">${userGender!""}</div>
</div>
<div id="group-item" style="margin-top: 15px;">
<div id="position-name">${positionName!""}</div>
<div id="department-name-wrapper">
<div id="department-name">${departmentName!""}</div>
</div>
</div>
<div id="group-item" style="margin-top: 25px;">
<div id="group-item-key">手机</div>
<div id="group-item-val">${mobile!""}</div>
</div>
<div id="group-item" style="margin-top: 15px;">
<div id="group-item-key">电话</div>
<div id="group-item-val">${telephone!""}</div>
</div>
<div id="group-item" style="margin-top: 15px;">
<div id="group-item-key">邮箱</div>
<div id="group-item-val">${email!""}</div>
</div>
<div id="group-item" style="margin-top: 15px; margin-bottom: 25px;">
<div id="group-item-key">地址</div>
<div id="group-item-val">${address!""}</div>
</div>
</div>
</div>
</body>
</html>
二.根据编写好的ftl模版生成png
代码使用javase
编写,如果是javaweb
,只需要将主要的代码实现挪到接口控制器,同时将接口给到前端即可
-
没什么好解释的,直接上车,首先
build.gradle
中导入freemarker
和Java2DRender
依赖plugins { id 'java' } group 'com.alwin' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { testCompile group: 'junit', name: 'junit', version: '4.12' implementation 'org.xhtmlrenderer:flying-saucer-core:9.1.22' //Java2DRenderer implementation 'org.freemarker:freemarker:2.3.32' //freemarker }
-
根据ftl模版,以及数据源生成png图片
public class TemplateGenerator { public static final String[] outputPathList = { System.getProperty("user.home"), "Downloads", "alwin", "freemarker_j2d", "template", "freemark" }; public static final String FTL_FILE_NAME = "business_card.ftl"; public static final String PNG_FILE_NAME = "business_card.png"; public static final int DOTS_PER_PIXEL = 3;//每个像素有多少个点 public static final int BASE_WIDTH = 1125;//模版中body标签的宽度*DOTS_PER_PIXEL,最终生成图片的宽 public static void main(String[] ars) throws Exception { String outputPath = StringUtils.join(outputPathList, File.separator); File pngFile = new File(outputPath, PNG_FILE_NAME); //mock数据源 Map<String, Object> dataModel = new HashMap(); dataModel.put("userName", "Alwin"); dataModel.put("userGender", "男"); dataModel.put("positionName", "xxx职位"); dataModel.put("departmentName", "xxx部门"); dataModel.put("mobile", "13000000000"); dataModel.put("telephone", "0000-00000000"); dataModel.put("email", "xxxxxx@sina.com"); dataModel.put("address", "xx省xx市xx区xx街道xx号"); //解析freemarker语法,根据数据源将ftl模版中的变量替换,得到html字符串 Configuration config = new Configuration(Configuration.VERSION_2_3_32); config.setDirectoryForTemplateLoading(new File(outputPath)); Template tp = config.getTemplate(FTL_FILE_NAME); tp.setEncoding("UTF-8"); StringWriter stringWriter = new StringWriter(); String htmlStr = ""; try { tp.process(dataModel, stringWriter); htmlStr = stringWriter.toString(); stringWriter.flush(); } finally { stringWriter.close(); } //Java2DRenderer将html字符串转换成png byte[] bytes = htmlStr.getBytes(); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = factory.newDocumentBuilder(); Document document = documentBuilder.parse(inputStream); int imageType = BufferedImage.TYPE_INT_ARGB; Java2DRenderer renderer = new Java2DRenderer(document, BASE_WIDTH, -1) { @Override protected BufferedImage createBufferedImage(final int width, final int height) { //支持背景透明 BufferedImage image = ImageUtil.createCompatibleBufferedImage(width, height, imageType); ImageUtil.clearImage(image, new Color(0, 0, 0, 0)); return image; } }; renderer.setBufferedImageType(imageType); SharedContext sharedContext = renderer.getSharedContext(); sharedContext.setDotsPerPixel(DOTS_PER_PIXEL); BufferedImage java2dImage = renderer.getImage(); ImageIO.write(java2dImage, "png", new FileOutputStream(pngFile)); } }
-
StringUtils.java
package com.alwin.utils; public class StringUtils { public static String join(String[] source, String separator) { if(source == null) { return ""; } String result = ""; String endsWith = ""; if(separator != null) { endsWith = separator; } for(int index = 0; index < source.length; index ++) { if(index < source.length - 1) { result += source[index] + endsWith; } else { result += source[index]; } } return result; } }
-
最后来张效果图:
三.补充: 生成的图片使用Base64输出
如果以文件形式生成,每次生成,都会产生一个png文件,这样会造成服务器空间的浪费,因此可以以Base64的方式生成,最后直接将生成的Base64字符串返回给前端即可。
-
Base64Utils.java
public class Base64Utils { public static String bufferedImage2Base64(BufferedImage bufferedImage) throws Exception { if(bufferedImage == null) { return ""; } ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, "png", pngOutputStream); String base64 = Base64.getEncoder().encodeToString(pngOutputStream.toByteArray()); base64 = "data:image/png;base64," + base64.replaceAll("\n", "").replaceAll("\r", "");//删除 \r\n pngOutputStream.flush(); pngOutputStream.close(); return base64; } }
-
根据ftl模版,以及数据源,生成图片的Base64
public class TemplateGenerator { public static final String[] outputPathList = { System.getProperty("user.home"), "Downloads", "alwin", "freemarker_j2d", "template", "freemark" }; public static final String FTL_FILE_NAME = "business_card.ftl"; public static final String PNG_FILE_NAME = "business_card.png"; public static final int DOTS_PER_PIXEL = 3;//每个像素有多少个点 public static final int BASE_WIDTH = 1125;//模版中body标签的宽度*DOTS_PER_PIXEL,最终生成图片的宽 public static void main(String[] ars) throws Exception { String outputPath = StringUtils.join(outputPathList, File.separator); File pngFile = new File(outputPath, PNG_FILE_NAME); //mock数据源 Map<String, Object> dataModel = new HashMap(); dataModel.put("userName", "Alwin"); dataModel.put("userGender", "男"); dataModel.put("positionName", "xxx职位"); dataModel.put("departmentName", "xxx部门"); dataModel.put("mobile", "13000000000"); dataModel.put("telephone", "0000-00000000"); dataModel.put("email", "xxxxxx@sina.com"); dataModel.put("address", "xx省xx市xx区xx街道xx号"); //解析freemarker语法,根据数据源将ftl模版中的变量替换,得到html字符串 Configuration config = new Configuration(Configuration.VERSION_2_3_32); config.setDirectoryForTemplateLoading(new File(outputPath)); Template tp = config.getTemplate(FTL_FILE_NAME); tp.setEncoding("UTF-8"); StringWriter stringWriter = new StringWriter(); String htmlStr = ""; try { tp.process(dataModel, stringWriter); htmlStr = stringWriter.toString(); stringWriter.flush(); } finally { stringWriter.close(); } //Java2DRenderer将html字符串转换成png byte[] bytes = htmlStr.getBytes(); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = factory.newDocumentBuilder(); Document document = documentBuilder.parse(inputStream); int imageType = BufferedImage.TYPE_INT_ARGB; Java2DRenderer renderer = new Java2DRenderer(document, BASE_WIDTH, -1) { @Override protected BufferedImage createBufferedImage(final int width, final int height) { //支持背景透明 BufferedImage image = ImageUtil.createCompatibleBufferedImage(width, height, imageType); ImageUtil.clearImage(image, new Color(0, 0, 0, 0)); return image; } }; renderer.setBufferedImageType(imageType); SharedContext sharedContext = renderer.getSharedContext(); sharedContext.setDotsPerPixel(DOTS_PER_PIXEL); BufferedImage java2dImage = renderer.getImage(); String base64 = Base64Utils.bufferedImage2Base64(java2dImage); System.out.println("base64: " + base64); } }
最后将打印出来的Base64字符串,直接使用浏览器访问,也是能正常加载的。