纯java项目后端进行HTML转图片
公司有个需求是在小程序将订单信息按一定样式整理后转成图片。客户点击按钮下载后可以将图片保存,并可以直接在微信群里分享。
由于时间紧迫的关系,这里前后端并行尝试方案,前端通过canvas方案手动绘制,后端则由我这边进行摸索。
原本想把各种尝试过的方案都记录下来,但是完成后现在一查,原来已经有人尝试过了,这里就直接他的上图。
图中说明过的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
即可将将百度截图为图片保存到当前目录:
对于一个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;