HTML 转 PDF
几种HTML转PDF工具的对比
工具 | 特点 |
---|---|
html2image | 简单html转化,对CSS的支持不好 |
itextpdf | 需要自己写模板,可以动态填充 |
wkhtmltopdf | 转化速度快,效果好 |
所以此处我们重点将wkhtmltopdf的使用做一个示例,完整的项目地址在末尾的链接处
使用
springboot是现在开发的主流框架,所以此处主要是示例在springboot项目中如何集成,其他项目请自行参考使用
准备
需要准备三个基础的文件,分别如下:
- simsun.ttc:字体文件
- wkhtmltopdf.exe:转换工具,window系统下使用,适用于64为系统,32位系统自行去官网下载对应版本
- wkhtmltox:转换工具,Linux系统下使用,同样适用于64位系统
将以上三个文件拷贝到springboot的resources根目录,具体的文件可到文章末尾的项目地址链接中获取,如下图:
pom依赖
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.4.0</version>
</dependency>
<!-- 好用的工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.0</version>
</dependency>
新建工具类
import com.sun.jna.Platform;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemHeaders;
import org.apache.commons.fileupload.util.FileItemHeadersImpl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import cn.hutool.core.io.FileUtil;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
/**
* Html转PDF的工具类
* @author zhongyj <1126834403@qq.com><br/>
* @date 2020/8/29
*/
@Slf4j
public class Html2PdfUtils {
private static final File WK_HOME_DIR = FileUtil.file(FileUtil.getUserHomePath()+"/wkHome");
private static final File WK_TMP_DIR = FileUtil.file(FileUtil.getTmpDirPath()+"/wkTemp");
private static final File SIM_SUN_FONT_DIR = Platform.isLinux() ? FileUtil.file("/usr/share/fonts/chinese/TrueType")
: FileUtil.file("C:\\Windows\\Fonts");
private static File wkTool;
private static File simSunFont;
private static boolean canUse = true;
private static boolean able = true;
static {
log.info("Tools are only available for Windows 64 and Linux 64 platforms !!!");
boolean init = forceInit();
log.info("Tools init result: {}", init);
}
/**
* 初始化
* @return 是否初始化成功
*/
public static boolean forceInit() {
long initc = 0L;
if (!WK_HOME_DIR.exists()) {
able = WK_HOME_DIR.mkdirs();
}
log.info("{},check wkHomeDir ,result:{}", ++initc, able);
if (!WK_TMP_DIR.exists()) {
able = WK_TMP_DIR.mkdirs();
}
log.info("{},check wkTmpDir ,result:{}", ++initc, able);
if (!SIM_SUN_FONT_DIR.exists()) {
able = SIM_SUN_FONT_DIR.mkdirs();
}
log.info("{},check simsunFontDir ,result:{}", ++initc, able);
InputStream wkHtmlToxAsStream = null;
InputStream simSunAsStream = null;
if (able) {
wkHtmlToxAsStream = Platform.isLinux() ? Html2PdfUtils.class.getResourceAsStream("/wkhtmltox") : Html2PdfUtils.class.getResourceAsStream("/wkhtmltopdf.exe");
simSunAsStream = Html2PdfUtils.class.getResourceAsStream("/simsun.ttc");
}
if (null == wkHtmlToxAsStream || simSunAsStream == null) {
log.error("{},load wkHtmlToxAsStream :{},load simSunAsStream:{}", ++initc, null == wkHtmlToxAsStream, simSunAsStream == null);
able = false;
}
log.info("{},load wktool and font source ,result:{}", ++initc, able);
if (able) {
File font = new File(SIM_SUN_FONT_DIR, "simsun.ttc");
File wk = new File(WK_HOME_DIR, Platform.isLinux() ? "wkhtmltox" : "wkhtmltopdf.exe");
try {
if (!font.exists()) {
assert simSunAsStream != null;
able = 1 < Files.copy(simSunAsStream, font.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
log.info("{},copy font source to {},result:{}", ++initc, font.toPath(), able);
if (!wk.exists()) {
assert wkHtmlToxAsStream != null;
able = 1 < Files.copy(wkHtmlToxAsStream, wk.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
log.info("{},copy wktools source to {},result:{}", ++initc, font.toPath(), able);
if (able) {
wkTool = wk;
simSunFont = font;
}
} catch (IOException e) {
e.printStackTrace();
able = false;
log.error("{}, error when copy source : {} ", ++initc, e.getMessage());
}
}
if (able) {
if (Platform.isLinux()) {
boolean canExe = exePermissionCheck();
log.info("{},check run permission,result: {} ", ++initc, canExe ? "has permission" : "no permission");
if (!canExe) {
simpleExecCommand("chmod +x " + wkTool.getPath());
if (!exePermissionCheck()) {
log.error("{},add permission failed", ++initc);
able = false;
}
}
}
}
if (able) {
able = cleanTempDir();
}
if (able) {
log.info("{},init success!", ++initc);
} else {
log.info("{},init failed!", ++initc);
canUse = false;
}
return able;
}
public String getSimsunPath() {
log.info("world path:" + simSunFont.getPath());
return simSunFont.getPath();
}
public static Html2PdfUtils build() {
return new Html2PdfUtils();
}
private static boolean exePermissionCheck() {
String permissionLog = simpleExecCommand("ls -l " + wkTool.getPath());
return null != permissionLog && permissionLog.length() >= 10 && 120 == permissionLog.charAt(9);
}
private static boolean cleanTempDir() {
if (WK_TMP_DIR.exists()) {
canUse = deleteFiles(WK_TMP_DIR) ? WK_TMP_DIR.mkdirs() : canUse;
log.info("cleanTempDir,result:{} ", canUse);
} else {
canUse = WK_TMP_DIR.mkdirs();
}
return canUse;
}
public synchronized FileItem convertPdfFromText(String text, String fileName) {
cleanTempDir();
if (!canUse) {
log.info("tools crash,can invoke forceInit() method see reason !!!");
return null;
}
File html = new File(WK_TMP_DIR, fileName + ".html");
File pdf = new File(WK_TMP_DIR, fileName + ".pdf");
// 将html字符串写入到临时的html文件
FileUtil.writeUtf8String(text, html);
if (html.exists() && html.isFile()) {
log.info("exec html to pdf ,wktoolPath=>{}", wkTool.getPath());
simpleExecCommand(wkTool.getPath() + " " + html.getPath() + " " + pdf.getPath());
}
if (pdf.exists() && pdf.isFile()) {
try (FileInputStream fileInputStream = new FileInputStream(pdf); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[2014];
while (fileInputStream.read(buffer) != -1) {
byteArrayOutputStream.write(buffer);
}
byteArrayOutputStream.flush();
byte[] data = byteArrayOutputStream.toByteArray();
if (data.length > 0) {
log.info("html to pdf success ");
SimplePdfFileItem file = new SimplePdfFileItem(pdf, data, Files.probeContentType(pdf.toPath()), "file");
log.info("pdf size :{}", file.getSize());
return file;
}
} catch (IOException e) {
log.error("html to pdf failed");
e.printStackTrace();
return null;
}
}
log.info("html to pdf failed,no data");
return null;
}
private static boolean deleteFiles(File file) {
if (!file.exists()) {
log.info("del the file:{},is not exists", file.getPath());
return false;
}
if (file.isFile()) {
return file.delete();
}
File[] subFiles = file.listFiles();
if (null != subFiles && subFiles.length > 0) {
Arrays.asList(subFiles).forEach(Html2PdfUtils::deleteFiles);
}
return file.delete();
}
private static String simpleExecCommand(String cmd) {
try {
String[] linux = {"/bin/sh", "-c", cmd};
String[] windows = {"cmd", "/c", cmd};
String[] cmdA = Platform.isLinux() ? linux : windows;
Process process = Runtime.getRuntime().exec(cmdA);
LineNumberReader br = new LineNumberReader(new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
log.info(line);
sb.append(line).append("\n");
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@ToString
static class SimplePdfFileItem implements FileItem {
private static final long serialVersionUID = 2237570099615271025L;
public static final String DEFAULT_CHARSET = "ISO-8859-1";
private String fieldName;
private final String fileName;
private boolean isFormField;
private final byte[] cachedContent;
private final String contentType;
private final File dFosFile;
private FileItemHeaders headers;
public SimplePdfFileItem(File dFosFile, byte[] cachedContent, String contentType, String fieldName) {
this.fieldName = fieldName;
this.fileName = dFosFile.getName();
this.isFormField = false;
this.cachedContent = null == cachedContent || cachedContent.length < 1 ? new byte[0] : cachedContent;
this.contentType = contentType;
this.dFosFile = dFosFile;
this.headers = new FileItemHeadersImpl();
}
public SimplePdfFileItem(String fieldName, String fileName, boolean isFormField, byte[] cachedContent
, String contentType, File dFosFile, FileItemHeaders headers) {
this.fieldName = fieldName;
this.fileName = fileName;
this.isFormField = isFormField;
this.cachedContent = cachedContent;
this.contentType = contentType;
this.dFosFile = dFosFile;
this.headers = headers;
}
@Override
public InputStream getInputStream() throws IOException {
if (null == this.dFosFile) {
return new ByteArrayInputStream(this.cachedContent);
}
return new FileInputStream(dFosFile);
}
@Override
public String getContentType() {
return this.contentType;
}
@Override
public String getName() {
return this.fileName;
}
@Override
public boolean isInMemory() {
return this.cachedContent.length > 0;
}
@Override
public long getSize() {
return this.cachedContent.length;
}
@Override
public byte[] get() {
return this.cachedContent;
}
@Override
public String getString(String s) throws UnsupportedEncodingException {
return getString();
}
@Override
public String getString() {
return new String(cachedContent, StandardCharsets.UTF_8);
}
@Override
public void write(File file) throws Exception {
Files.write(file.toPath(), cachedContent, StandardOpenOption.CREATE);
}
@Override
public void delete() {
boolean delete = dFosFile.delete();
}
@Override
public String getFieldName() {
return this.fieldName;
}
@Override
public void setFieldName(String s) {
this.fieldName = s;
}
@Override
public boolean isFormField() {
return this.isFormField;
}
@Override
public void setFormField(boolean b) {
this.isFormField = b;
}
@Override
public OutputStream getOutputStream() throws IOException {
if (null == this.dFosFile) {
return new ByteArrayOutputStream(1024);
}
return new FileOutputStream(this.dFosFile);
}
@Override
public FileItemHeaders getHeaders() {
return this.headers;
}
@Override
public void setHeaders(FileItemHeaders fileItemHeaders) {
this.headers = fileItemHeaders;
}
}
}
转换
@Test
public void down() throws UnsupportedEncodingException {
String html = FileUtil.readUtf8String("E:\\入院记录.html");
FileItem sx = Html2PdfUtils.build().convertPdfFromText(html, "sx");
log.info(sx.toString());
byte[] bytes = sx.get();
FileUtil.writeBytes(bytes,new File("E:\\入院记录-1.pdf"));
}