纯java项目后端进行HTML转图片

公司有个需求是在小程序将订单信息按一定样式整理后转成图片。客户点击按钮下载后可以将图片保存,并可以直接在微信群里分享。

由于时间紧迫的关系,这里前后端并行尝试方案,前端通过canvas方案手动绘制,后端则由我这边进行摸索。

 

原本想把各种尝试过的方案都记录下来,但是完成后现在一查,原来已经有人尝试过了,这里就直接他的上图。

java html 转成mht java将html页面转化为图片_html

 

图中说明过的html2img及cssbox都尝试过,但是样式或多或少都有问题,因为这些存Java实现的基本都是通过g2d模拟绘制来生成图片,其样式和直接在浏览器上看到的肯定会存在差异。

另外我还尝试过github上的开源项目openhtmltopdf,结果都差不多。

需要没有差异的话,就必须使用类似selenium这种无头浏览器,模拟用户查看网站并截图的方案。

 

基于wkhtmltox

虽然我最先考虑过的是selenium,但是由同事推荐,最终我选择了基于wkhtmltox这个方案,这里记录下。

wkhtmltopdf核心点是使用webkit浏览器进行,基本上也是模拟用户在浏览器查看图片并截图保存的方案。

  • window按照只需要https://wkhtmltopdf.org/downloads.html下载对应的window包

然后使用命令调用即可,如下载在D:/1/,那么进入到D:/1/wkhtmltox/bin/目录,打开命令行执行

// 格式
// wkhtmltoimage程序地址 来源地址 保存地址
.\wkhtmltoimage.exe http://baidu.com 1.png

即可将将百度截图为图片保存到当前目录:

 

java html 转成mht java将html页面转化为图片_临时文件_02

 

 

 对于一个html文件也是一样,若将Html文件移入到bin目录,那么

.\wkhtmltoimage.exe 1.html 1.png
  • 部署到linux需要根据服务器的系统版本下载对应的包并在服务器上安装即可


编码部分

根据需求,我的方案是

  • 创建对应样式的html模板
  • 通过freemarker填充参数,获取html字符串。
  • 保存字符串为html文件到临时文件目录
  • 调用wkhtmltoimage生成图片到本地临时目录
  • 读取图片为byte数组
  • 删除临时文件

 

其中html字符串转图片部分代码可以参考如下

import java.io.File;
import java.io.FileWriter;
import java.nio.file.Files;

import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import lombok.extern.slf4j.Slf4j;

/**
 * HTML转图片的默认实现,采用wkhtmltox实现
 * <p>
 * wkhtmltox基于webkit浏览器将HTML转为图片,需要一个程序来执行,对于window来说是一个无需安装的exe
 * 对于linux来说需要进行安装
 * 
 */
@Slf4j
@Service
public class Html2ImageBizImpl implements Html2ImageBiz {

    /**
     * wkhtmltox的使用命令,对window来讲是exe的地址,对于linux是安装的命令地址(如/usr/local/bin/wkhtmltoimg)
     */
    private String wkcmd;
    /**
     * 用于存放临时文件的文件夹,由于wkhtmltox为系统插件独立进程,只能作为文件操作后再按流进行读取
     */
    private String tmpFilePath;

    @Override
    public byte[] stringToPng(String htmlString) {
        if (EmptyUtil.isEmpty(wkcmd)) {
            throw new UnsupportedOperationException("未配置wkhtmltox的使用命令参数,功能不可用");
        }
        if (EmptyUtil.isEmpty(tmpFilePath)) {
            throw new UnsupportedOperationException("未配置HTML转图片的临时文件目录,功能不可用");
        }
        Assert.hasLength(htmlString, "参数错误,HTML文件没有内容");

        String htmlFileName = null;
        String pngFileName = null;
        try {
            // 存HTML文件
            htmlFileName = saveHtml2File(htmlString);
            // 转为PNG,获取其文件名
            pngFileName = html2Png(htmlFileName);
            // 将PNG读取为bytes
            return readFile2ByteArray(pngFileName);

        } finally {
            // 删除临时文件
            if (htmlFileName != null) {
                removeTmpFile(htmlFileName);
            }
            if (pngFileName != null) {
                removeTmpFile(pngFileName);
            }
        }
    }

    /**
     * 将HTML内容写入文件
     * 
     * @param htmlString HTML文件内容
     */
    private String saveHtml2File(String htmlString) {
        String fileName = getRandomFileName(".html");
        try (FileWriter fileWriter = new FileWriter(new File(fileName))) {
            fileWriter.write(htmlString);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        log.info("保存HTML成功: {}", fileName);
        return fileName;
    }

    /**
     * 使用wkhtmltox将HTML转为PNG(都在硬盘上操作
     * 
     * @param htmlFileName HTML文件路径
     */
    private String html2Png(String htmlFileName) {
        String pngFileName = getRandomFileName(".png");

        StringBuilder cmd = new StringBuilder();
        cmd.append(wkcmd);
        cmd.append(" ");
        cmd.append(htmlFileName);
        cmd.append(" ");
        cmd.append(pngFileName);
        try {
            Process proc = Runtime.getRuntime().exec(cmd.toString());
            proc.getInputStream();
            proc.waitFor();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        log.info("转PNG成功: {}, {}", htmlFileName, pngFileName);
        return pngFileName;
    }

    /**
     * 读取文件二进制数组
     * 
     * @param fileName
     */
    private byte[] readFile2ByteArray(String fileName) {
        File file = new File(fileName);
        try {
            return Files.readAllBytes(file.toPath());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 删除临时文件
     * 
     * @param fileName 文件名
     */
    private void removeTmpFile(String fileName) {
        Assert.hasLength(fileName, "临时文件参数为空");
        File file = new File(fileName);
        if (file.isDirectory()) {
            throw new IllegalArgumentException("不能删除文件夹");
        }
        try {
            Files.delete(file.toPath());
            log.info("删除临时文件成功:{}", fileName);
        } catch (Exception e) {
            log.warn("删除临时文件失败", e);
        }
    }

    /**
     * 构建随机的文件地址(采用IdGenerator)
     * 
     * @param suffix 如.html
     */
    private String getRandomFileName(String suffix) {
        return tmpFilePath + "/" + "h2p" + IdGenerator.nextId() + suffix;
    }
}

这里的wkcmd和tmpFilePath是两个通过可配置的参数,如通过spring的@Value注入或者直接写死。

另外由于部分代码类如IdGenerator,用到了公司项目的包,这里删去引用。直接保存的话,需要替换为近似的方法。


 

END;