SpringBoot 生成报告分享图设计方案与实现

产品需求

在日常开发中有一些需要生成分享图(分享报告)的需求,比如需要对某个Html页面截屏,或者生成一些指定的图片比如动态生成用户头像等功能或者在用户下单后将订单参与的活动信息随机生成一张宣传海报,并附带一个绑定用户优惠特权信息的二维码,最后用户可以直接下载这张海报。。

解决方案

前面有使用过Robot技术生成页面的快照,但是这个是一种应急的方法,只能抓取屏幕显示的内容,对于当前未显示的页面内容则是抓取不到了。

以此今天介绍一种可以依据URL获取到整个页面的进行一个长截图的纯Java的方法。

CSSBox,不是CSS Box层叠样式【盒子模型】,而是一个采用纯Java开发的(X)HTML/CSS渲染引擎,是一个网络客户端组件。它能够提供关于已渲染页面内容和布局的完整详细信息。CSSBox还能够展示渲染过的文档。可以实现将HTML生成图片。以Html作为模板 然后通过CssBox对Html进行图片的转化更加的高效。

前情提示

如果您在使用CssBox中产生了一些乱码,图片失真等问题,请阅读到文章底部的结束语里面具体描述了如何解决这些小问题,当然您也可以微信搜索 傲浮刷题 来查找相应的解决方案。

具体落地实现

  1. 新建一个SpringBoot项目,在 pom.xml 文件里面引入:Cssbox 和 Freemarker 依赖:
....
	<!--将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. 第一步创建一个配置类来配置 Freemarker 的自定义模版加载器:HtmlToPngTemplateConfig.java :
	/**
 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;
    }
}
  1. 写好配置类的后,再创建一个模版加载器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 {

    }
}
  1. 因为样式模版是多条配置在数据库,这里先创建一个表:
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>

  1. 创建一个实体类接收数据库的模版数据:DemoFreemarkerTemplate.java :

/**
 * @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;
}
  1. 还有一个 HtmlToPngService .java 接口类:
/**
 * @author 傲浮
 * @create 2022/5/13 14:50
 */
public interface FreemarkerTemplateService extends IService<FreemarkerTemplate> {
}

  1. 接口接口实现类:FreemarkerTemplateServiceImpl.java :
/**
 1. @author 傲浮
 2. @create 2022/5/13 14:52
 */
@Service
public class FreemarkerTemplateServiceImpl extends ServiceImpl<FreemarkerTemplateMapper, FreemarkerTemplate> implements FreemarkerTemplateService {
}
  1. 后面这三个类都是用 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. 一个接口实现类:HtmlToPngServiceImpl.java :

/**
 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();
        }
    }
}
  1. 以上就已经能够根据Html填充数据后生成一张Png的图了,最后我们进行测试 我这里采用的MQ来进行测试,你也可以使用测试类 等其他方法进行测试
 
/**
 * 批次报告队列
 *
 * @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”;
}

欢迎大家扫描关注我的小程序(傲浮刷题)

在这里插入图片描述

CssBox官方文档:https://cssbox.sourceforge.net/
Freemarker语法参考:http://freemarker.foofun.cn/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

傲浮刷题

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值