当前位置: 首页 > news >正文

27_Java2DRenderer结合freemarker动态生成图片

27_Java2DRenderer结合freemarker动态生成图片

Java2DRenderfreemarker属于java后端的技术,但是我们同样可以借助这两个工具在服务器端动态生成图片(如海报、名片)。生成好了之后,返回给前端使用即可。

以生成海报/名片为例,如果是在微信小程序环境中,可以通过canvas绘制,再将绘制的内容导出成图片。但是呢,有一定的局限性,比如说生成图片的模版内容很多、比较复杂…,就需要编写大量的canvas绘制代码,同时,如果处理不当,可能会出现oom或者无法正常绘制等问题。那么这个时候同样也可以通过Java2DRenderfreemarker在服务器端生成。

一.编写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中导入freemarkerJava2DRender依赖

    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字符串,直接使用浏览器访问,也是能正常加载的。

    在这里插入图片描述

相关文章:

  • 可视化图解算法:合并k个已排序(升序)的链表
  • LeetCode——560. 和为 K 的子数组
  • 目前人工智能的发展,判断10年、20年后的人工智能发展的主要方向,或者带动的主要产业
  • 【openwebui 搭建本地知识库(RAG搭建本地知识库)】
  • 软件测试之测试用例
  • Microsoft Edge “无法更新” 解决办法
  • 学习笔记之车票搜索为什么用Redis而不是ES?
  • 32单片机——LED
  • 通过 Python 爬虫提高股票选股胜率
  • 【教学类-43-26】20240312 数独4宫格的所有可能(图片版 576套样式,空1格-空8格,每套65534张*576小图=3千万张小图)
  • 【web】网页崩溃
  • 【初级篇】如何使用DeepSeek和Dify构建高效的企业级智能客服系统
  • SVT-AV1源码分析函数 svt_av1_cost_coeffs_txb
  • 【Python入门】一篇掌握Python中的字典(创建、访问、修改、字典方法)【详细版】
  • Python使用FastAPI结合Word2vec来向量化200维的语言向量数值
  • Python爬取房天下二手小区数据(2025年3月更)
  • 布达佩斯召开 | 2025年第五届能源与环境工程国际会议(CoEEE 2025)
  • SpringBoot中使用kaptcha生成验证码
  • Spring 中 BeanPostProcessor 的作用和示例
  • Facebook投广告支付操作
  • 特朗普开启第二任期首次外访:中东行主打做生意,不去以色列
  • 高适配算力、行业大模型与智能体平台重塑工业城市
  • 哈马斯表示已释放一名美以双重国籍被扣押人员
  • 中共中央、国务院印发《生态环境保护督察工作条例》
  • 美元指数上涨超1%重返101上方,创1个月新高
  • 重庆一高校75万采购市价299元产品?工作人员:正在处理