产品需求
在日常开发中有一些需要生成分享图(分享报告)的需求,比如需要对某个Html页面截屏,或者生成一些指定的图片比如动态生成用户头像等功能或者在用户下单后将订单参与的活动信息随机生成一张宣传海报,并附带一个绑定用户优惠特权信息的二维码,最后用户可以直接下载这张海报。。
解决方案
前面有使用过Robot技术生成页面的快照,但是这个是一种应急的方法,只能抓取屏幕显示的内容,对于当前未显示的页面内容则是抓取不到了。
以此今天介绍一种可以依据URL获取到整个页面的进行一个长截图的纯Java的方法。
CSSBox,不是CSS Box层叠样式【盒子模型】,而是一个采用纯Java开发的(X)HTML/CSS渲染引擎,是一个网络客户端组件。它能够提供关于已渲染页面内容和布局的完整详细信息。CSSBox还能够展示渲染过的文档。可以实现将HTML生成图片。以Html作为模板 然后通过CssBox对Html进行图片的转化更加的高效。
前情提示
如果您在使用CssBox中产生了一些乱码,图片失真等问题,请阅读到文章底部的结束语里面具体描述了如何解决这些小问题,当然您也可以微信搜索 傲浮刷题 来查找相应的解决方案。
具体落地实现
....
<!--将html转换为 png图片的插件-->
<dependency>
<groupId>net.sf.cssbox</groupId>
<artifactId>cssbox</artifactId>
<version>5.0.0</version>
</dependency>
<!--使用Freemarker模板来做Html的数据填充-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- 其他项目依赖 比如使用Mq来做异步的图片生成 在这里就不具体的展示了-->
....
/**
1. 将网页转换为图片的加载器
2. @author 傲浮
3. @create 2022/5/13 14:30
*/
@Slf4j
@Component
public class HtmlToPngTemplateConfig {
/**
* 自定义的模版加载器
* @param demoTemplateLoader demoTemplateLoader 自定义从数据库取样式
* @return FreeMarkerConfigurer
*/
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer(HtmlToPngTemplateLoader demoTemplateLoader) {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setPreTemplateLoaders(demoTemplateLoader);
return configurer;
}
}
-
写好配置类的后,再创建一个模版加载器HtmlToPngTemplateLoader.java ,实现Freemarker包的TemplateLoader接口。这一步主要是能够通过条件需要从数据库自定义查询模版样式。
/**
1. @author 傲浮
2. @create 2022/5/13 14:39
*/
@Slf4j
@Component
public class HtmlToPngTemplateLoader implements TemplateLoader {
@Autowired
private FreemarkerTemplateService freemarkerTemplateService;
/**
* 从mysql根据 code 查询一个样式模版
* @param code 查询条件
* @return Object 模版html
* @throws IOException IOException
*/
@Override
public Object findTemplateSource(String code) throws IOException {
return freemarkerTemplateService.getOne(new QueryWrapper<FreemarkerTemplate>().lambda().eq(FreemarkerTemplate::getCode, code));
}
/**
* 获取模版的最后更新时间 这里直接使用当前时间
* @param o 模版
* @return long 最后时间
*/
@Override
public long getLastModified(Object o) {
return LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
}
/**
* 根据查询的模版得到Reader
* @return Reader
* @throws IOException IOException
*/
@Override
public Reader getReader(Object o, String s) throws IOException {
return new StringReader(((FreemarkerTemplate) o).getValue());
}
/**
* 关闭模版源
* @throws IOException IOException
*/
@Override
public void closeTemplateSource(Object o) throws IOException {
}
}
CREATE TABLE `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 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>
/**
* @author 傲浮
* @create 2022/5/13 14:49
*/
@Data
@TableName("freemarker_template")
public class FreemarkerTemplate implements Serializable {
private static final long serialVersionUID=1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 模版编号
*/
private String code;
/**
*模版样式Html
*/
private String value;
/**
* 0检测报告分享 1月度统计报告
*/
private Integer type;
@TableField(value = "create_time",fill = FieldFill.INSERT)
private Date createTime;
}
/**
* @author 傲浮
* @create 2022/5/13 14:50
*/
public interface FreemarkerTemplateService extends IService<FreemarkerTemplate> {
}
/**
1. @author 傲浮
2. @create 2022/5/13 14:52
*/
@Service
public class FreemarkerTemplateServiceImpl extends ServiceImpl<FreemarkerTemplateMapper, FreemarkerTemplate> implements FreemarkerTemplateService {
}
-
后面这三个类都是用 Mybatis-Generator 自动生成的,为了方便数据库操作的,这里只贴代码就不详细描述了。 准备工作做好后,最后写html生成png的逻辑:写一个服务接口:HtmlToPngService.java :
/**
* 将html转换为图片的接口
* @author 傲浮
* @create 2022/5/13 14:31
*/
public interface HtmlToPngService {
/**
* 将html转换为 png图片
* @param templateCode 查询模版条件
* @param fileName 文件名称
* @param templateParams 模板填充的值
* @throws Exception
*/
void asyncHtmlToPng(String templateCode,String fileName, Map<String, Object> templateParams) throws Exception;
}
/**
1. @author 傲浮
2. @create 2022/5/13 14:32
*/
@Service
public class HtmlToPngServiceImpl implements HtmlToPngService {
private static final Logger LOGGER = LoggerFactory.getLogger(HtmlToPngServiceImpl.class);
/**
* FreeMarkerConfigurer 由 HtmlToPngTemplateConfig 注入
*/
@Autowired
private FreeMarkerConfigurer configuration;
// 本地文件上传保存路径(最终文件生成后存储的地址)
// 可以是 E:/test 也可以是服务的文件地址 这个属性写到配置文件中即可
@Value("${file.upload-dir}")
private String fileUrl;
@Override
public void asyncHtmlToPng(String templateCode,String fileName,Map<String, Object> templateParams) throws Exception {
LOGGER.info("[asyncHtmlToPng]:开始html转png");
// 在这里有一个小坑 关于图片的宽高问题 如果设置的不对的话
// 可能会导致生成出来的图片产生失真的问题
// 建议动态的传递 或者将Html模板的大小固定 然后宽高固定
int width = 800;
// png图片高度
int height = 600;
// 从数据库查询一个模版
Template template = configuration.getConfiguration().getTemplate(templateCode);
// 将数据替换模版里面的参数
String readyParsedTemplate = FreeMarkerTemplateUtils.processTemplateIntoString(template, templateParams);
// 创建一个字节流
InputStream is = IOUtils.toInputStream(readyParsedTemplate, StandardCharsets.UTF_8);
// 创建一个文档资源
DocumentSource docSource = new StreamDocumentSource(is, null, "text/html; charset=utf-8");
File file = new File(fileUrl + "/" + fileName);
if (!file.getParentFile().exists()){
//文件夹不存在,先创建文件夹
file.getParentFile().mkdirs();
}
// 创建一个文件流
FileOutputStream out = new FileOutputStream(file);
try {
// 解析输入文档
DOMSource parser = new DefaultDOMSource(docSource);
MediaSpec media = new MediaSpec("screen");
media.setDimensions(width, height);
media.setDeviceDimensions(width, height);
// 创建CSS解析器
DOMAnalyzer da = new DOMAnalyzer(parser.parse(), docSource.getURL());
// 设置样式属性
da.setMediaSpec(media);
// 将文档正文中的HTML表示属性转换为内联样式。
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(); //load the author style sheets
da.stylesToDomInherited();
GraphicsEngine contentCanvas = new GraphicsEngine(da.getRoot(), da, docSource.getURL());
contentCanvas.setAutoMediaUpdate(false);
contentCanvas.createLayout(new Dimension(width, height));
// 生成png文件 org.fit.cssbox.demo.ImageRenderer
ImageIO.write(contentCanvas.getImage(), "png", out);
LOGGER.info("[asyncHtmlToPng]:结束html转png");
} catch (Exception e) {
e.printStackTrace();
} finally {
out.close();
is.close();
docSource.close();
}
}
}
/**
* 批次报告队列
*
* @author 傲浮
* @create 2022/5/27 15:32
*/
@Component
@RocketMQMessageListener(topic = "BatchSharePng", //topic主题
consumerGroup = "${rocketmq.producer.groupName.batch.share}", //消费组
messageModel = MessageModel.CLUSTERING,
consumeMode = ConsumeMode.ORDERLY)
public class BatchShraePngConsumer implements RocketMQListener<JSONObject> {
private static final Logger LOGGER = LoggerFactory.getLogger(BatchShraePngConsumer.class);
@Autowired
private HtmlToPngService htmlTemplateService;
@Autowired
private FileUploadService fileUploadService;
// 图片资源请求前缀
public static final String URL_PREFIX = "https://xxxxxxxxxx/";
@Override
public void onMessage(JSONObject jsonObject) {
String fileName = jsonObject.getString("fileName");
String tCode = jsonObject.getString("tCode");
// 判断资源是否存在
if (!fileUploadService.existenceFile(fileName)) {
try {
LOGGER.info("[BatchShraePngConsumer]开始生成批次报告" + batchNo);
// 生成Html要填充的数据
HashMap info = new HashMap();
info.put("模板填充数据key1", "具体的数据");
info.put("模板填充数据key2", 具体的数据);
// 调用生成图片的方法
// tCode : 模板id
// fileName:将要生成的文件名称
// info: 具体要填充的内容
htmlTemplateService.asyncHtmlToPng(tCode, fileName, info);
} catch (Exception e) {
LOGGER.info("[BatchShraePngConsumer]生成批次报告失败["+batchNo+"]" + e.getMessage());
}
}else{
LOGGER.info("[BatchShraePngConsumer]资源存在:不生成分享图" + batchNo);
}
}
}
结束语:
这个方案虽然还行 但是也有一些小问题
目前已知坑点是对css3支持很差, 但其实css2已经可以满足大部分样式要求了
部署到Linux系统上发现 中文乱码
解决方案如下
cd /usr/share/fonts
把字体包解压进去 下载地址 链接: https://pan.baidu.com/s/1nlGfWl8kSSEomnik4wzrrg?pwd=offe 提取码: offe
执行 yum install mkfontscale
执行 mkfontscale
执行 mkfontscale
执行 fc-cache
重启服务
模板文件加上以下css样式
html{
font-family: “Microsoft YaHei”, “Microsoft YaHei UI”, “FangSong”, “SimSun”, “微软雅黑”,“sans-serif”;
}