Java使用freemarker、wkhtmltopdf根据自定义ftl模板导出html以及pdf
这段时间要搞一个导出pdf功能,刚开始想着用模板导出,结果发现不符合要求,因为我要动态的导出pdf,如果用一个pdf模板导出的话就不太灵活,因为你的模板是什么样的你导出的pdf也就是什么样的,所以我费了千辛万苦找到了一个解决办法,利用freemarker的ftl模板导出成html然后通过html导出相应的pdf,因为html里面自己想怎么写就怎么写,这样不就是非常完美了吗,想想就很激动,话不多说,下面我把相应的jar、模板、工具类以及测试类都粘出来了,有兴趣的小伙伴可以搞搞。
我这里用到的是SpringBoot项目演示的
一、先看一下效果:
(把下面的所有文件都弄到自己的项目中,然后运行第五个HtmlToPdfMain.java中的runMain方法就行了)
1. 控制台输出的:
2. 生成的文件:
3. pdf内容:
(我这里弄得是一个表格数据,如果有别的需求可以修改model.ftl模板来响应的时间具体页面)
二、准备工作
1.先在网上下个wkhtmltopdf,地址是:点击下载(如果不想在官网上下载的话、随便找个下载就行)
2.然后安装
3.记住你的安装路径。这个地址后面会用到,在HtmlToPdf.java中的toPdfTool就是你的安装路径拼上你安装的应用程序
安装工作就到这里了,下面来看具体的实现
三、项目结构如下:
四、具体实现代码如下:
1. 用到的jar(主要的就是这几个)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.9</version>
</dependency>
<!--PDF-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.4.3</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>compile</scope>
</dependency>
<!--单元测试的时候获取request-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
2. 将模板生成html (HtmlUtil.java)
package com.example.demo.pro;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.*;
import java.util.Map;
/**
* 使用.ftl模板 生成Html工具类
*/
public class HtmlUtil {
/**
* @param modelpath 模板路径 不包含模板名称
* @param template 模板名称
* @param dataMap 数据
* @param descPath 临时路径
* @param fileName 临时文件名称
* @throws IOException
* @throws TemplateException
*/
public static void createHtml(String modelpath,String template, Map dataMap,String descPath,String fileName) throws IOException, TemplateException {
Configuration configuration=new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
//如果你用的是版本低的freemarker 可以用地下的这个创建Configuration
//Configuration configuration=new Configuration();
configuration.setDefaultEncoding("utf-8");
//如果你的模板放到了静态资源下的话就用这个 传入相应的路径就行
configuration.setDirectoryForTemplateLoading(new File(modelpath));
//如果模板和HtmlUtil这了类放在同一个文件夹下的话用这个
//configuration.setClassForTemplateLoading(HtmlUtil.class,"");
Template t = configuration.getTemplate(template);
configuration.setClassicCompatible(true);
File file = new File(descPath+File.separator+fileName);
String parent = file.getParent();
File dir = new File(parent);
if (parent!=null&&!dir.exists()){
dir.mkdirs();
}
boolean exists = file.exists();
if (!exists){
file.createNewFile();
}
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file),"UTF-8"));
t.process(dataMap, out);
out.flush();
out.close();
}
}
3. HtmlToPdfInterceptor.java
package com.example.demo.pro;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class HtmlToPdfInterceptor extends Thread {
private InputStream is;
public HtmlToPdfInterceptor(InputStream is) {
// TODO Auto-generated constructor stub
this.is = is;
}
/**
* 起一个线程执行流的结果 可有可无 想看日志的话可以要
*/
public void run(){
try{
InputStreamReader isr = new InputStreamReader(is, "utf-8");
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null) {
System.out.println("-------*******-------"+line.toString()); //输出内容
}
}catch (IOException e){
e.printStackTrace();
}
}
}
4. Html转pdf (HtmlToPdf.java)
package com.example.demo.pro;
import java.io.File;
import java.io.IOException;
/**
* html转pdf
*/
public class HtmlToPdf {
// wkhtmltopdf在系统中安装的路径 第一个wkhtmltopdf是安装的路径 后面是安装后文件中的exe
private static String toPdfTool = "D:\\Exe\\wkhtmltopdf\\bin\\wkhtmltopdf.exe";
/**
* html转pdf
*
* @param srcPath
* html路径,可以是硬盘上的路径,也可以是网络路径
* @param destPath
* pdf保存路径
* @return 转换成功返回true
*/
public static boolean convert(String srcPath, String destPath) throws IOException {
File file = new File(destPath);
File parent = file.getParentFile();
// 如果pdf保存路径不存在,则创建路径
if (!parent.exists()) {
parent.mkdirs();
}
if (!file.exists()){
file.createNewFile();
}
StringBuilder cmd = new StringBuilder();
if (System.getProperty("os.name").indexOf("Windows") == -1) {
// 非windows 系统
toPdfTool = "/inco/usr/local/bin/wkhtmltopdf";
}
cmd.append(toPdfTool);
cmd.append(" ");
cmd.append(" --header-line");// 页眉下面的线
cmd.append(" --header-center Pdf标头 ");//页眉中间内容
cmd.append(" --margin-top 3cm ");// 设置页面上边距 (default 10mm)
//cmd.append(" --header-html file:///" + "https://blog.csdn.net/x6582026/article/details/53835835");// (添加一个HTML页眉,后面是网址)
cmd.append(" --header-spacing 5 ");// (设置页眉和内容的距离,默认0)
cmd.append(" --footer-center 第[page]页/共[topage]页");//设置在中心位置的页脚内容
//cmd.append(" --footer-html file:///" + "https://blog.csdn.net/x6582026/article/details/53835835");// (添加一个HTML页脚,后面是网址)
cmd.append(" --footer-line");// * 显示一条线在页脚内容上)
cmd.append(" --footer-spacing 5 ");// (设置页脚和内容的距离)
File file1 = new File(srcPath);
cmd.append(file1.getAbsolutePath());
cmd.append(" ");
String absolutePath = file.getAbsolutePath();
cmd.append(absolutePath);
boolean result = true;
try {
Process proc = Runtime.getRuntime().exec(cmd.toString());
//起一个线程执行流的结果 可有可无 想看日志的话可以要 不想的话可以删除当前行的下面四行
HtmlToPdfInterceptor error = new HtmlToPdfInterceptor(proc.getErrorStream());
HtmlToPdfInterceptor output = new HtmlToPdfInterceptor(proc.getInputStream());
error.start();
output.start();
proc.waitFor();
} catch (Exception e) {
result = false;
e.printStackTrace();
}
return result;
}
}
5. 测试类(HtmlToPdfMain.java)
package com.example.demo.pro;
import freemarker.template.TemplateException;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class HtmlToPdfMain {
//声明request变量
private MockHttpServletRequest request = new MockHttpServletRequest();
//run
@Test
public void runMain() throws IOException, TemplateException {
//创建request对象并设置字符编码
request.setCharacterEncoding("UTF-8");
//如果是linux系统的话将\\改为File.separator来代替
//生成html
//第一个参数 模板路径, 第二个参数 模板名称, 第三个参数 数据源, 第四个参数 保存路径, 第五个参数 保存名称
HtmlUtil.createHtml(ToPath(request),"model.ftl",DataMap(),"E:\\","model.html");
//将html转换为Pdf
HtmlToPdf.convert("E:\\model.html","E:\\model.pdf");
}
/**
* 获取路径
* @param request
* @return
*/
public static String ToPath(MockHttpServletRequest request){
//path1 根据class的地址获取项目编译后的路径
//如果这种获取的方法在linux环境上获取的地址不对,那就使用request的方法获取路径 具体写法请看path
String path = request.getServletContext().getRealPath("/templates/");
System.out.println("Request获取Path = " + path);
String path1 = (String.valueOf(HtmlToPdfMain.class.getResource("/templates/"))).replaceAll("file:/", "").replaceAll("%20", " ").trim();
System.out.println("类名获取Path1 = " + path1);
String path2 = (String.valueOf(HtmlToPdfMain.class.getResource(""))).replaceAll("file:/", "").replaceAll("%20", " ").trim();
System.out.println("获取当前类的所在路径Path2 = " + path2);
String path3 = (String.valueOf(HtmlToPdfMain.class.getResource("/"))).replaceAll("file:/", "").replaceAll("%20", " ").trim();
System.out.println("获取当前类的所在路径Path3 = " + path3);
return path;
}
/**
* 数据源
* @return
*/
public static Map DataMap(){
Map map = new HashMap();
map.put("jsxm", "甄士隐 ");
map.put("ssyx", "北京师范学院 ");
map.put("sksj", "2020-11-08 4444 ");
List<Map> user = new ArrayList<>();
Map a = new ConcurrentHashMap();
a.put("tmlx","教师教学状态(80分)");
a.put("tmmc","思政育人(10分)");
a.put("tmxx","A.在教学");
a.put("tmfz","10");//分数
a.put("sfxz","0");//0是没有选着 1是选着了
a.put("tmlxkhsl","4");//题目类型跨行数量
a.put("tmmckhsl","2");//题目名称跨行数量
user.add(a);
a = new ConcurrentHashMap();
a.put("tmlx","教师教学状态(80分)");
a.put("tmmc","思政育人(10分)");
a.put("tmxx","B.在教学");
a.put("tmfz","8");
a.put("sfxz","1");
a.put("tmlxkhsl","4");//题目类型跨行数量
a.put("tmmckhsl","2");//题目名称跨行数量
user.add(a);
a = new ConcurrentHashMap();
a.put("tmlx","教师教学状态(80分)");
a.put("tmmc","精神状态(10分)");
a.put("tmxx","A.精神饱满");
a.put("tmfz","10");
a.put("sfxz","0");
a.put("tmlxkhsl","4");//题目类型跨行数量
a.put("tmmckhsl","2");//题目名称跨行数量
user.add(a);
a = new ConcurrentHashMap();
a.put("tmlx","教师教学状态(80分)");
a.put("tmmc","精神状态(10分)");
a.put("tmxx","B.精神饱满");
a.put("tmfz","8");
a.put("sfxz","1");
a.put("tmlxkhsl","4");//题目类型跨行数量
a.put("tmmckhsl","2");//题目名称跨行数量
user.add(a);
map.put("userList",user);
return map;
}
}
6. ftl模板(model.ftl)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
}
</style>
</head>
<body style="text-align: center;">
<#-- 这里些相应的html就行-->
<#-- 这里我把freemarker经常用到的几个标签写在了下面-->
<#--assign 类似 c:set 一样 就是初始化一个变量-->
<#--list 类似 c:forEach 一样 循环list-->
<#--if 类似 c:if 一样 判断条件 -->
<#--在html中取值的话统一用${字段名} 在freemarker中取值是直接写字段名就行-->
<div class="tab-main">
<table border="1" style="margin: auto;">
<tr>
<td style="width:10%;">教师姓名</td>
<td style="width:15%;">${jsxm}</td>
<td style="width:10%;">所属院部</td>
<td style="width:35%;">${ssyx}</td>
<td style="width:10%;">上课时间</td>
<td style="width:20%;" colspan="2">${sksj}</td>
</tr>
<tr>
<td rowspan="2" colspan="2">评价内容</td>
<td colspan="6">评价结果</td>
</tr>
<tr>
<td colspan="3">评价标准</td>
<td>分值</td>
<td>评价</td>
</tr>
<#-- 循环开始-->
<#assign tmlx = '' tmmc = '' tmxx = '' tmfz = '' sfxz = '' tmlxkhsl ='' tmmckhsl=''/><#--这里定义了几个变量-->
<#list userList as user>
<tr>
<#if tmlx??><#--这个代表tmlx不等于空-->
<#if tmlx == user.tmlx >
<#else>
<#assign tmlx = user.tmlx/>
<td rowspan="${user.tmlxkhsl}">${user.tmlx}</td>
</#if>
<#else><#--否者 如果还有if的话就是 <#elseif 判断条件> -->
<#assign tmlx = user.tmlx/>
<td rowspan="${user.tmlxkhsl}">${user.tmlx}</td>
</#if>
<#if tmmc??>
<#if tmmc == user.tmmc >
<#else>
<#assign tmmc = user.tmmc/>
<td rowspan="${user.tmmckhsl}">${user.tmmc}</td>
</#if>
<#else>
<#assign tmmc = user.tmmc/>
<td rowspan="${user.tmmckhsl}">${user.tmmc}</td>
</#if>
<td colspan="3">${user.tmxx}</td>
<td >${user.tmfz}</td>
<#if user.sfxz?? && user.sfxz == '1' >
<td>√</td>
<#else>
<td></td>
</#if>
</tr>
</#list>
<#-- //结束-->
</table>
</div>
</body>
</html>