关于wkhtmltopdf的介绍这里就不啰嗦了,总结下来就是一款很好用的软件,可以很方便的将HTML文件转成PDF文件。

首先这个工具需要安装才能使用,其本质是使用命令行进行转化的,所以比较依赖环境。

看下github:https://github.com/jhonnymertz/java-wkhtmltopdf-wrapper
使用wkhtmltopdf工具借助freemarker动态生成PDF文档_html
第一点就是强调要安装在系统中,目前最新版是0.12.6, 根据自己的系统选择对应的安装包安装即可。

安装包下载地址:https://wkhtmltopdf.org/downloads.html
使用wkhtmltopdf工具借助freemarker动态生成PDF文档_word_02

并且如果需要使用命令行进行生成PDF的话,还需要将安装位置添加到环境变量当中去,我这里是在代码里面指定安装位置,就没用环境变量。

下面试下github里面的demo:

package com.lin.test.aboutWhhtmltopdf;

import java.io.IOException;

import com.github.jhonnymertz.wkhtmltopdf.wrapper.Pdf;
import com.github.jhonnymertz.wkhtmltopdf.wrapper.configurations.WrapperConfig;

/**
 * jar运行 需要依赖环境变量或者程序包exe
 * @author linmengmeng
 * @date 2021年1月13日 下午4:02:56
 */
public class GithubDemo {

	public static void main(String[] args) throws IOException, InterruptedException {
		
		//String wkhtmltopdfCommand = "D:\\wkhtmltopdf\\bin\\wkhtmltoimage.exe";//注意这里不要用错了wkhtmltoimage是生成图片的,文件后缀需要为png
		String wkhtmltopdfCommand = "D:\\wkhtmltopdf\\bin\\wkhtmltopdf.exe";
		
		WrapperConfig wrapperConfig = new WrapperConfig(wkhtmltopdfCommand);
		
		Pdf pdf = new Pdf();
		//Pdf pdf = new Pdf(wrapperConfig);

		//pdf.addPageFromString("<html><head><meta charset=\"utf-8\"></head><h1>Müller</h1></html>");
		pdf.addPageFromUrl("https://www.baidu.com");

		// Add a Table of Contents
		//pdf.addToc();

		// The `wkhtmltopdf` shell command accepts different types of options such as global, page, headers and footers, and toc. Please see `wkhtmltopdf -H` for a full explanation.
		// All options are passed as array, for example:
//		pdf.addParam(new Param("--no-footer-line"), new Param("--header-html", "file:///header.html"));
//		pdf.addParam(new Param("--enable-javascript"));

		// Add styling for Table of Contents
		//pdf.addTocParam(new Param("--xsl-style-sheet", "my_toc.xsl"));

		String path = "E:/baidu/";
		String outputFilename = System.currentTimeMillis() +"output.pdf";
		//pdf.set
		// Save the PDF
		//pdf.saveAs(path + outputFilename);
		pdf.saveAsDirect(path + outputFilename);//功能同saveAs
		System.out.println("success");
	}
}

因为之前用过老版本的,指定了exe文件的位置后,不需要添加环境变量,这里就先不添加了。
github的demo里面也没有指定wkhtmltopdf执行文件的地址,运行会报错:

Exception in thread "main" com.github.jhonnymertz.wkhtmltopdf.wrapper.exceptions.WkhtmltopdfConfigurationException: wkhtmltopdf command was not found in your classpath. Verify its installation or initialize wrapper configurations with correct path/to/wkhtmltopdf
	at com.github.jhonnymertz.wkhtmltopdf.wrapper.configurations.WrapperConfig.findExecutable(WrapperConfig.java:66)
	at com.github.jhonnymertz.wkhtmltopdf.wrapper.configurations.WrapperConfig.<init>(WrapperConfig.java:37)
	at com.github.jhonnymertz.wkhtmltopdf.wrapper.Pdf.<init>(Pdf.java:66)
	at com.lin.test.aboutWhhtmltopdf.GithubDemo.main(GithubDemo.java:25)

根据错误日志,我就把环境变量加上了,直接加在path最后了。
使用wkhtmltopdf工具借助freemarker动态生成PDF文档_word_03
再次运行仍然是同样的错误,不过这时可以在cmd里面直接使用命令生成pdf了:
使用wkhtmltopdf工具借助freemarker动态生成PDF文档_java_04
使用wkhtmltopdf工具借助freemarker动态生成PDF文档_freemarker_05

然后新建新的系统变量:
使用wkhtmltopdf工具借助freemarker动态生成PDF文档_word_06
发现仍然不好使。

那就看下源码,到底哪里的问题,首先是初始化PDF对象:

    @Deprecated
    /**
     * Default constructor
     * @deprecated Use the constructor with the WrapperConfig definition
     */
    public Pdf() {
        this(new WrapperConfig());
    }

    public Pdf(WrapperConfig wrapperConfig) {
        this.wrapperConfig = wrapperConfig;
        this.params = new Params();
        this.tocParams = new Params();
        this.pages = new ArrayList<Page>();
        logger.info("Initialized with {}", wrapperConfig);
    }

看到新的初始化方法,是指定了WrapperConfig对象

    /**
     * Initialize the configuration based on searching for wkhtmltopdf command to be used into the SO's path
     *
     * @deprecated Use the constructor specifying the location of wkhtmltopdf. Use the static method findExecutable() if necessary.
     */
    @Deprecated
    public WrapperConfig() {
        logger.debug("Initialized with default configurations.");
        setWkhtmltopdfCommand(findExecutable());
    }

    /**
     * Initialize the configuration based on a provided wkhtmltopdf command to be used
     *
     * @param wkhtmltopdfCommand the wkhtmltopdf command
     */
    public WrapperConfig(String wkhtmltopdfCommand) {
        setWkhtmltopdfCommand(wkhtmltopdfCommand);
    }

WrapperConfig(String wkhtmltopdfCommand)构造方法,为保证命令正常运行,需要指定命令的位置。

初始化之后,看下pdf.add...

    /**
     * Add a page from an URL to the pdf
     */
    public void addPageFromUrl(String source) {
        this.pages.add(new Page(source, PageType.url));
    }

    /**
     * Add a page from a HTML-based string to the pdf
     */
    public void addPageFromString(String source) {
        this.pages.add(new Page(source, PageType.htmlAsString));
    }

    /**
     * Add a page from a file to the pdf
     */
    public void addPageFromFile(String source) {
        this.pages.add(new Page(source, PageType.file));
    }

这里支持三种对象直接转为pdf文件:url,string,file
首先url即是动态的网页,如https://www.baidu.com
string表示静态的HTML语法拼接的字符串,如demo中的

pdf.addPageFromString("<html><head><meta charset=\"utf-8\"></head><h1>Müller</h1></html>");

file则是html的绝对路径位置:pdf.addPageFromFile("E:/baidu/1610523364109word.html");

addToc 没看懂有什么用处。
addParam 表示添加页眉或者页脚时使用。

页眉和页脚的功能没有测试,这里只是用到使用freemarker画一个word表格,然后动态渲染里面的内容,生成PDF。

弊端是会产生临时文件,一个HTML文件,一个PDF文件,暂时没有优化。
使用wkhtmltopdf工具借助freemarker动态生成PDF文档_java_07

1. 生成简单的静态HTML页面

我这里使用freemarker画了一个word表格,先生成HTML文件,然后将HTML文件转为PDF。
生成HTML工具类方法:
word.ftl

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>申请表</title>
	<style type="text/css">

		.preview-title-wrap {
			margin-top: 100px;
			margin-bottom: 20px;
			page-break-after: always;
		}

		.preview-title-wrap .preview-title1 {
			text-align: center;
			font-size: 32px;
			font-weight: bold;
			margin-bottom: 10px;
			font-family:宋体;
		}

		.statement>div {
			text-align: center;
			font-size: 22px;
			padding: 20px 0px;
			font-weight: bold;
		}

		.statement>p {
			text-indent: 2em;
			font-size: 18px;
			line-height: 36px;
			color: #333333;
		}

		.abstract>div {
			width: 160px;
			text-align: center;
			font-size: 22px;
			font-weight: bold;
			background: #FFFFFF;
			position: absolute;
			top: 50%;
			left: 50%;
			margin-left: -80px;
			margin-top: -12px;
		}

		/* 表格样式 */
		.table-style {
			border: 1px solid #000;
			box-sizing: border-box;
			border-collapse: collapse;
			width: 100%;
		}

		.table-style-shencha {
			border: 1px solid #000;
			border-top: 0px;
			border-bottom: 0px;
			box-sizing: border-box;
			border-collapse: collapse;
			width: 100%;
		}

		.table-style-shencha td {
			box-sizing: border-box;
			text-align: center;
			font-family: 宋体;
			font-size: 18.6px;
		}

		.table-style-kaohe {
			border: 1px solid #000;
			border-top: 0px;
			border-bottom: 0px;
			box-sizing: border-box;
			border-collapse: collapse;
			width: 100%;
		}

		.table-style-kaohe td {
			box-sizing: border-box;
			text-align: center;
			font-family: 宋体;
			font-size: 18.6px;
		}

		.table-style-kaohe tr {
			height: 20px;
		}

		.table-style-kaohe p {
			display: block;
			margin-block-start: 0.5em;
			margin-block-end: 0.5em;
			margin-inline-start: 0px;
			margin-inline-end: 0px;
		}

		.table-style-shencha p {
			display: block;
			margin-block-start: 0.5em;
			margin-block-end: 0.5em;
			margin-inline-start: 0px;
			margin-inline-end: 0px;
		}

		.table-style p {
			display: block;
			margin-block-start: 0.5em;
			margin-block-end: 0.5em;
			margin-inline-start: 0px;
			margin-inline-end: 0px;
		}

		.table-style td {
			border: 1px solid #000;
			box-sizing: border-box;
			padding: 5px 10px;
			text-align: center;
			font-family: 宋体;
			font-size: 18.6px;
		}

		.table-style tr {
			height: 42px;
		}

		.table-style td[colspan] {
			text-align: center;
		}

		.table-style table {
			width: 100%;
		}

		.table-style-kaohe table tr:first-child td {
			border-top: 0;
		}

		.table-style-kaohe table:last-child tr:last-child td {
			border-bottom: 0;
		}

		.table-style-kaohe table tr td:first-child {
			border-left: 0;
		}

		.table-style-kaohe table tr td:last-child {
			border-right: 0;
		}

		/*.preview-photo{
            width: 295px;
            height: 413px;
        }*/

		.a4-endwise{
			margin: 0 auto;
			width: 1070px;
			height: 1550px;
			border: 1px #ccc solid;
			overflow: hidden;
			padding: 0;
			word-break:break-all;
		}

		#td-pizhunren td.pizhunren {
			text-align: left;
			padding-left: 10px;
		}

		#shuoming-div{
			font-family: 宋体;
			font-size: 18.6px;
		}

		#shuoming-div p.qianming {
			font-size: 18.6px;
			padding-left: 500px;
		}

		#shuoming-div span.shuoming{
			font-size: 18.6px;
		}
		#shuoming-div span.content{
			font-size: 16px;
		}

		#shuoming-div span.content2{
			padding-left: 55px;
		}
	</style>
</head>
<body>
<div id="app">
	<div class="a4-endwise">
		<div style="width: 800px; margin: 0 auto;">
			<div class="preview-title-wrap">
				<div class="preview-title1">xxx办理申请表</div>
				<!-- 基本信息 -->
				<table class="table-style">
					<tbody>
					<tr>
						<td class="" rowspan="8" style="width: 5%;font-size: 19px;font-weight: bold;">xx人员基本情况</td>
						<td class="" style="width: 19%;">&nbsp;</td>
						<td class="" style="width: 19%;">${res.name}</td>
						<td class="" style="width: 19%;">&nbsp;</td>
						<td class="" style="width: 19%;">${res.sex}</td>
						<td class="" rowspan="7" style="width: 19%;">${res.photo}</td>
					</tr>
					<tr>
						<td class="">出生日期</td>
						<td class="" colspan="3">${res.birthday}</td>
					</tr>
					<tr>
						<td class="">所在单位</td>
						<td class="" colspan="3">${res.company}</td>
					</tr>
					<tr>
						<td class="">&nbsp;</td>
						<td class="">${res.dept}</td>
						<td class="">职务</td>
						<td class="">${res.position}</td>
					</tr>
					<tr>
						<td class="">联系电话</td>
						<td class="">${res.telphone}</td>
						<td class="">民族</td>
						<td class="">${res.nation}</td>
					</tr>
					<tr>
						<td class="">政治面貌</td>
						<td class="">${res.politicalLandscape}</td>
						<td class="">文化程度</td>
						<td class="">${res.eduLevel}</td>
					</tr>
					<tr>
						<td class="">身份证号</td>
						<td class="" colspan="3">${res.idNumber}</td>
					</tr>
					<tr>
						<td class="">户籍所在地</td>
						<td class="" colspan="4">${res.birthplace}</td>
					</tr>
					</tbody>
				</table>
				<table class="table-style-shencha">
					<tbody>
					<tr>
						<td class="" rowspan="4" style="width: 10%;border: 1px solid #000;border-top: 0px;border-bottom: 0px;" ><p>xx人<p>员所在<p>单位审<p>查意见</td>
						<td class="" rowspan="3" colspan="3" style="padding-top: 50px;">${res.scyj}</td>
					</tr>
					<tr></tr>
					<tr></tr>
					<tr>
						<td class="" style="text-align: left;padding-left: 10px;">审查人:</td>
						<td class="" style="text-align: center; border-right: 1px solid #000;">(盖章)</td>
					</tr>

					</tbody>
				</table>
				<table class="table-style">
					<tbody>
					<tr>
						<td class="" colspan="4" >以上由申请xx证人员填写</td>
					</tr>
					<tr>
						<td class="" colspan="4" >培训考核情况</td>
					</tr>
					<tr>
						<td class="" >理论考核</td>
						<td class="" >${res.llkh}</td>
						<td class="" >xxx</td>
						<td class="" >${res.sdsj}</td>
					</tr>
					<tr>
						<td class="" >勤务操作</td>
						<td class="" >${res.qwcz}</td>
						<td class="" >考核人意见</td>
						<td class="" >${res.khryj}</td>
					</tr>
					<tr>
						<td class="" colspan="4" >以上由培训考核部门填写</td>
					</tr>
					</tbody>
				</table>
				<table class="table-style-kaohe">
					<tbody>
					<tr>
						<td rowspan="5" style="width: 6%;border: 1px solid #000;border-top: 0px;border-bottom: 0px;">X<p>X<p>X<p><p><p><p></td>
						<td rowspan="4" colspan="2" style="padding-top: 50px;">${res.bgtshyj}</td>
						<td rowspan="5" style="width: 6%; border: 1px solid #000;border-top: 0px;border-bottom: 0px;"><p>X<p>X<p>X<p>X<p>X<p><p><p><p></td>
						<td rowspan="4" colspan="2" style="padding-top: 50px;">${res.rsxljspyj}</td>
					</tr>
					<tr></tr>
					<tr></tr>
					<tr></tr>
					<tr>
						<td rowspan="1" style="text-align: left;padding-left: 10px;">批准人:</td>
						<td rowspan="1">(盖章)</td>
						<td rowspan="1" style="text-align: left;padding-left: 10px;">批准人:</td>
						<td rowspan="1">(盖章)</td>
					</tr>
					</tbody>
				</table>
				<table class="table-style">
					<tbody>
					<tr>
						<td class="" colspan="4" >证件办理情况</td>
					</tr>
					<tr>
						<td class="" >xx证号</td>
						<td class="" >${res.cqzh}</td>
						<td class="" >发证时间</td>
						<td class="" >${res.fzsj}</td>
					</tr>
					<tr>
						<td class="" >办理单位</td>
						<td class="" >${res.bldw}</td>
						<td class="" >经 办 人</td>
						<td class="" >${res.jbr}</td>
					</tr>
					</tbody>
				</table>
				<div id="shuoming-div">
					<p><span class="shuoming">说明:</span><span class="content">1、本表由申办xx证人员填写,经人事XXX审批后,送XXX办理制作。</span></p>
					<p><span class="content content2">2、本表由申请单位存档备查。</span></p>
					<p class="qianming">本人签名:</p>
				</div>
			</div>
		</div>
	</div>
</div>
</body>
</html>

    /**
     * 通过freemarker生成静态HTML页面
	 * @auther linmengmeng
	 * @Date 2021-01-13 下午3:48:33
	 * @param templateName		模版名称
	 * @param targetFileName	模版生成后的html文件名
	 * @param data				freemarker待渲染的动态数据
	 * @return boolean true 成功生成html
	 * @throws Exception
	 */
    private static boolean createHtml(String templateName,String targetFileName,Map<String, Object> data) throws Exception{
        //创建fm的配置
        Configuration config = new Configuration();
        //指定默认编码格式 
        config.setDefaultEncoding("UTF-8");
        //设置模版文件的路径 
        //config.setClassForTemplateLoading(CreateHtmlUtils.class, "/com/test/shop/ftl");
        config.setDirectoryForTemplateLoading(new File("C:/Users/jadl/Desktop/"));
        //获得模版包
//        Template template = config.getTemplate(templateName);
//        Template template = config.getTemplate("word.ftl", "utf-8");
        Template template = config.getTemplate(templateName, "utf-8");
        //从参数文件中获取指定输出路径 ,路径示例:C:/Workspace/shop-test/src/main/webapp/html
//        String path = PropUtils.readKey("create_html_path");
        String path = "E:/baidu";
        //定义输出流,注意必须指定编码
        Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(path+"/"+targetFileName)),"UTF-8"));
        //生成模版
        template.process(data, writer);
        return true;
    }

    public static void main(String[] args) throws Exception {
    	Map<String, Object> map = new HashMap<String, Object>();
		
        Map<String, String> data = new HashMap<String, String>(33);
		data.put("name", "linmengmeng");
		String htmlFileName = System.currentTimeMillis() + "word.html";
        boolean createHtml = CreateHtmlUtils.createHtml("word.ftl",htmlFileName , map);
        String path = "E:/baidu/" + htmlFileName;
        System.out.println("path:" + path);
        if (createHtml) {
        	File file = new File(path);
        	String srcPath = file.toString();
        	
        	String destPath = "E:/baidu/" + System.currentTimeMillis() + ".pdf";
        	String toolSrc = "D:\\wkhtmltopdf\\bin\\wkhtmltopdf.exe";
        	boolean convert = PdfUtils2.convert(srcPath, destPath, toolSrc);
        	System.out.println(convert);
		}else {
			System.out.println("error.......");
		}
	}

如果指定程序位置时错误使用了wkhtmltoimage.exe,则出现Error: Could not save image异常。

log4j:WARN No appenders could be found for logger (com.github.jhonnymertz.wkhtmltopdf.wrapper.Pdf).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
Exception in thread "main" com.github.jhonnymertz.wkhtmltopdf.wrapper.exceptions.PDFExportException: Process (D:\wkhtmltopdf\bin\wkhtmltoimage.exe C:\Users\jadl\AppData\Local\Temp\java-wkhtmltopdf-wrapperd5e0a01e-8baf-45c5-86ed-78d92f39fd716511941839874131849.html E:\baidu\1610529636704output.pdf) exited with status code 1:
Loading page (1/2)
[>                                                           ] 0%
[======>                                                     ] 10%
[==============================>                             ] 50%
[============================================================] 100%
Rendering (2/2)                                                    
[>                                                           ] 0%
[===============>                                            ] 25%
Error: Could not save image                                       
[============================================================] 100%
Done                                                               
Exit with code 1, due to unknown error.

	at com.github.jhonnymertz.wkhtmltopdf.wrapper.Pdf.getPDF(Pdf.java:218)
	at com.github.jhonnymertz.wkhtmltopdf.wrapper.Pdf.saveAsDirect(Pdf.java:189)
	at com.lin.test.aboutWhhtmltopdf.GithubDemo.main(GithubDemo.java:43)

2. 复杂Html页面(包含echarts图表)

wkhtmltopdf 这点功能还是很强大的,html里面包含echarts 也是可以正常生成PDF的,并且支持动态生成页面的 js,只要按照对应的 freemarker 语法写模板即可。

需要注意的是,静态资源的引用不能使用相对路径了,否则在渲染HTML文件的时候,就会抛出异常。需要将静态文件(js,css,图片)等放到可以访问到的服务器上,保证可以用URL访问到。

后台返回的echarts动态数据,也是可以用占位符直接返回到模板文件里面的,比如动态返回 echarts 的横坐标,返回一个逗号隔开的字符串到模板文件,然后在模板文件的js里面,处理成数组,最后赋值给 echarts 的data。

java 后台代码

		List<String> topTenDataList = new ArrayList<>();
        topTenDataList.add("美国");
        topTenDataList.add("日本");
        topTenDataList.add("美国");
        topTenDataList.add("日本");
        topTenDataList.add("美国");
        topTenDataList.add("日本");
        topTenDataList.add("美国");
        topTenDataList.add("日本");
        topTenDataList.add("美国");
        topTenDataList.add("日本");
        String join = "'" + StringUtils.join(topTenDataList, ",") + "'";
        System.out.println(join);
        hashMap.put("topTenData", join);

模板文件接收参数并处理:

  var topTenDataStr = ${topTenData}.split(','),
  console.log(topTenDataStr);

使用wkhtmltopdf工具借助freemarker动态生成PDF文档_java_08
这里可能会提示异常,如果调试页面的样式,可以写个假数据赋给 topTenDataStr ,样式没问题之后,就可以使用占位符用后台动态生成的数据了。

如果页面内容过多,可能会存在某个图表被分页给分割开了,可以提前在上一个图表的div样式的最后加上一个分页的样式page-break-inside:avoid;

  .page {
      padding-top: 55px;
      height: 1784px;
      page-break-inside:avoid;
  }

这样 wkhtmltox 可以自动识别分页,确保图表不会从中间截断。