Java SpringBoot集成Freemarker将Html转图片
有个会员营销的功能:用户在下单后将绑定了订单和用户活动信息的二维码生成一张样式随机的宣传海报,用户可以直接下载这张海报进行推广
分析有不同的用户、不同的活动信息、以及不同样式的海报模板。这时候使用了Freemarker填充Html,再将Html转为Png是一个很好的办法。在数据库配置多个海报样式模版,按照用户的维度取到对应的活动信息和模板信息,给模版组装好Map类型数据,自动填充后生成不同信息的海报了。
新建一个SpringBoot项目,在 pom.xml 文件里面引入:Cssbox 和 Freemarker 依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <dependencies> ... <dependency> <groupId>net.sf.cssbox</groupId> <artifactId>cssbox</artifactId> <version> 4.12 </version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> ... </dependencies> |
第一步创建一个配置类来配置 Freemarker 的自定义模版加载器:FreemarkerConfig.java :
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 | package com.demo.htmltopng.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; /** * @author AnYuan */ @Configuration public class FreemarkerConfig{ /** * 自定义的模版加载器 * @param demoTemplateLoader demoTemplateLoader 自定义从数据库取样式 * @return FreeMarkerConfigurer */ @Bean public FreeMarkerConfigurer freeMarkerConfigurer(DemoTemplateLoader demoTemplateLoader) { FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); configurer.setPreTemplateLoaders(demoTemplateLoader); return configurer; } } |
创建一个模版加载器DemoTemplateLoader.java ,实现Freemarker包的TemplateLoader接口。这一步主要是能够通过条件需要从数据库自定义查询模版样式。
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 | package com.demo.htmltopng.config; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.demo.htmltopng.model.entity.DemoFreemarkerTemplate; import com.demo.htmltopng.service.DemoTemplateService; import freemarker.cache.TemplateLoader; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.time.LocalDateTime; import java.time.ZoneOffset; /** * @author AnYuan */ @Slf4j @Component ( "DemoTemplateLoader" ) public class DemoTemplateLoader implements TemplateLoader { @Autowired private DemoTemplateService demoTemplateService; /** * 从mysql根据 code 查询一个样式模版 * @param code 查询条件 * @return Object 模版html * @throws IOException IOException */ @Override public Object findTemplateSource(String code) throws IOException { return demoTemplateService.getOne( new QueryWrapper<DemoFreemarkerTemplate>().lambda().eq(DemoFreemarkerTemplate::getCode, code)); } /** * 获取模版的最后更新时间 这里直接使用当前时间 * @param o 模版 * @return long 最后时间 */ @Override public long getLastModified(Object o) { return LocalDateTime.now().toEpochSecond(ZoneOffset.of( "+8" )); } /** * 根据查询的模版得到Reader * @param Object 模版对象 * @param String s * @return Reader * @throws IOException IOException */ @Override public Reader getReader(Object o, String s) throws IOException { return new StringReader(((DemoFreemarkerTemplate) o).getValue()); } /** * 关闭模版源 * @param Object o * @throws IOException IOException */ @Override public void closeTemplateSource(Object o) throws IOException { } } |
因为样式模版是多条配置在数据库,先创建一个表:
1 2 3 4 5 6 7 8 9 | CREATE TABLE `demo_freemarker_template` ( `id` int (11) NOT NULL AUTO_INCREMENT, `code` varchar (4) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '模版名称' , `value` text COLLATE utf8_unicode_ci COMMENT '模版样式Html' , PRIMARY KEY (`id`) ) DEFAULT CHARSET=utf8 COLLATE =utf8_unicode_ci; <br> # 模拟数据 INSERT INTO demo_freemarker_template(`code`, `value`) VALUES ( 'T001' , '<html><head><title>Welcome!</title></head><body style=\"margin:0px\"><h1>Welcome ${user}!</h1><img src=\"${info.url}\"></body></html>' );<br><br> |
创建一个实体类接收数据库的模版数据:DemoFreemarkerTemplate.java :
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 | package com.demo.htmltopng.model.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.io.Serializable; /** * @author AnYuan */ @Data @EqualsAndHashCode (callSuper = false ) @Accessors (chain = true ) public class DemoFreemarkerTemplate implements Serializable { private static final long serialVersionUID=1L; @TableId (value = "id" , type = IdType.AUTO) private Integer id; private String code; private String value; } |
还有一个 DemoTemplateService.java 接口类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package com.demo.htmltopng.service; import com.demo.htmltopng.model.entity.DemoFreemarkerTemplate; import com.baomidou.mybatisplus.extension.service.IService; /** * <p> * 服务类 * </p> * * @author AnYuan */ public interface DemoTemplateService extends IService<DemoFreemarkerTemplate> { } |
接口接口实现类:DemoTemplateServiceImpl.java :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package com.demo.htmltopng.service.impl; import com.demo.htmltopng.model.entity.DemoFreemarkerTemplate; import com.demo.htmltopng.repository.DemoTemplateMapper; import com.demo.htmltopng.service.DemoTemplateService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; /** * <p> * 服务实现类 * </p> * * @author AnYuan */ @Service public class DemoTemplateServiceImpl extends ServiceImpl<DemoTemplateMapper, DemoFreemarkerTemplate> implements DemoTemplateService { } |
写填充html生成png的逻辑,一个服务接口:HtmlToPngService.java :
1 2 3 4 5 6 7 8 9 10 | package com.demo.htmltopng.service; /** * @author AnYuan * 生成png服务接口 */ public interface HtmlToPngService { void htmlToPng() throws Exception; } |
一个接口实现类:HtmlToPngServiceImpl.java :
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 102 103 104 105 106 107 108 109 | package com.demo.htmltopng.service.impl; import com.demo.htmltopng.service.HtmlToPngService; import freemarker.template.Template; import lombok.extern.slf4j.Slf4j; import org.fit.cssbox.css.CSSNorm; import org.fit.cssbox.css.DOMAnalyzer; import org.fit.cssbox.io.DOMSource; import org.fit.cssbox.io.DefaultDOMSource; import org.fit.cssbox.io.DocumentSource; import org.fit.cssbox.io.StreamDocumentSource; import org.fit.cssbox.layout.BrowserCanvas; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import javax.imageio.ImageIO; import java.awt.*; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * @author AnYuan * 生成png服务实现 */ @Slf4j @Service public class HtmlToPngServiceImpl implements HtmlToPngService { /** * Mock 的数据 * @return Map<String, String> */ private Map<String, String> getUser() { HashMap info = new HashMap( 2 ); info.put( "age" , "18" ); info.put( "url" , "https://pic.cnblogs.com/avatar/2319511/20210304170859.png" ); Map user = new HashMap( 2 ); user.put( "user" , "安逺" ); user.put( "info" , info); return user; } @Autowired private FreeMarkerConfigurer configuration; @Override public void htmlToPng() throws Exception { // 查询模版条件 String templateCode = "T001" ; // png图片宽度 int width = 220 ; // png图片高度 int height = 150 ; // 从数据库查询一个模版 Template template = configuration.getConfiguration().getTemplate(templateCode); // 将数据替换模版里面的参数 String readyParsedTemplate = FreeMarkerTemplateUtils.processTemplateIntoString(template, getUser()); // 创建一个字节流 InputStream is = new ByteArrayInputStream(readyParsedTemplate.getBytes(StandardCharsets.UTF_8)); // 创建一个文档资源 DocumentSource docSource = new StreamDocumentSource(is, null , "text/html; charset=utf-8" ); // 创建一个文件流 String fileName = UUID.randomUUID().toString(); FileOutputStream out = new FileOutputStream( "./" + new File(fileName + ".png" )); try { // 解析输入文档 DOMSource parser = new DefaultDOMSource(docSource); // 创建CSS解析器 DOMAnalyzer da = new DOMAnalyzer(parser.parse(), docSource.getURL()); // 设置样式属性 da.attributesToStyles(); da.addStyleSheet( null , CSSNorm.stdStyleSheet(), DOMAnalyzer.Origin.AGENT); da.addStyleSheet( null , CSSNorm.userStyleSheet(), DOMAnalyzer.Origin.AGENT); da.addStyleSheet( null , CSSNorm.formsStyleSheet(), DOMAnalyzer.Origin.AGENT); da.getStyleSheets(); BrowserCanvas contentCanvas = new BrowserCanvas(da.getRoot(), da, docSource.getURL()); contentCanvas.createLayout( new Dimension(width, height)); // 生成png文件 ImageIO.write(contentCanvas.getImage(), "png" , out); } catch (Exception e) { log.info( "HtmlToPng Exception" , e); } finally { out.close(); is.close(); docSource.close(); } } } |
以上就已经能够根据Html填充数据后生成一张Png的图了,最后写一个单元测试:HtmlToPngServiceImplTest.java :
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 | package com.demo.htmltopng.service.impl; import com.demo.htmltopng.service.HtmlToPngService; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @Slf4j @SpringBootTest class HtmlToPngServiceImplTest { @Autowired private HtmlToPngService htmlToPngService; @Test public void htmlToPngTest(){ try { htmlToPngService.htmlToPng(); } catch (Exception e) { log.info( "PngError:{}" , e.getMessage()); } } } |
运行后会在设置好的路径生成一张Png的图片了,这里生成的Png在项目根目录:
只要先把Html的样式先写好配置在数据库,再传入与Html模版里面填充的字段名相同的Map数据,就能直接生成Html页面对应的Png图片了
Freemarker语法参考:http://freemarker.foofun.cn/
本篇代码Github地址:https://github.com/Journeyerr/cnblogs/tree/master/htmltopng
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?