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' //Java2DRendererimplementation '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字符串转换成pngbyte[] 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) {@Overrideprotected 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\npngOutputStream.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字符串转换成pngbyte[] 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) {@Overrideprotected 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字符串,直接使用浏览器访问,也是能正常加载的。