Java SpringBoot集成Freemarker將Html轉圖片


有個關於會員營銷的功能:在用戶下單后將訂單參與的活動信息隨機生成一張宣傳海報,並附帶一個綁定用戶優惠特權信息的二維碼,最后用戶可以直接下載這張海報。

當時比較麻煩的就是需要按照設計部門設計的多個樣式生成海報,二維碼也是根據用戶信息動態的生成的,每個人的海報攜帶的信息都是不一樣的。最后使用了Freemarker語法填充Html,再將Html轉為Png的方法,在數據庫配置多個海報樣式模版,按照用戶的條件取到不同的模版后,給模版傳入用戶的Map型數據,自動填充后就能根據傳入的數據生成不同信息的海報了。

新建一個SpringBoot項目,在 pom.xml 文件里面引入:Cssbox 和 Freemarker 依賴:

<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 :

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接口。這一步主要是能夠通過條件需要從數據庫自定義查詢模版樣式。

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 {

    }
}

 因為樣式模版是多條配置在數據庫,這里先創建一個表:

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;

# 模擬數據 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>');

創建一個實體類接收數據庫的模版數據:DemoFreemarkerTemplate.java :

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 接口類:

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 :

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 {

}

后面這三個類都是用 Mybatis-Generator 自動生成的,為了方便數據庫操作的,這里只貼代碼就不詳細描述了。

准備工作做好后,最后寫html生成png的邏輯:寫一個服務接口:HtmlToPngService.java : 

package com.demo.htmltopng.service;

/**
 * @author AnYuan
 * 生成png服務接口
 */
public interface HtmlToPngService {

    void htmlToPng() throws Exception;
}

一個接口實現類:HtmlToPngServiceImpl.java :

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 : 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM