引入maven
<dependencies>
<!-- Thymeleaf -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.1.RELEASE</version> <!-- 或与 Spring Boot 匹配的版本 -->
</dependency>
<!-- Flying Saucer PDF 渲染(使用 OpenPDF 和 XHTMLRenderer) -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-openpdf</artifactId>
<version>9.1.22</version>
</dependency>
<!-- iText 2.x (lowagie) 用于合并 PDF 页等操作 -->
<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
<!-- Servlet API(如果你在非 Web 项目中操作 HttpServletResponse) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Lombok(用于注解 @Slf4j @SneakyThrows)-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
工具类
package com.tlzf.util;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfCopy;
import com.lowagie.text.pdf.PdfImportedPage;
import com.lowagie.text.pdf.PdfReader;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.w3c.dom.Document;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.util.List;
import java.util.Map;
/**
* pdf处理工具类
*
* @author gourd.hu
* @version 1.0findInstallationElevatorBlockVOById
*/
@Slf4j
@Component
public class PdfUtil {
private PdfUtil() {
}
/**
* 按模板和参数生成html字符串,再转换为flying-saucer识别的Document
*
* @param templateName freemarker模板名称x`
* @param variables freemarker模板参数
* @return Document
*/
private static Document generateDoc(TemplateEngine templateEngine, String templateName, Map<String, Object> variables) {
// 声明一个上下文对象,里面放入要存到模板里面的数据
final Context context = new Context();
context.setVariables(variables);
StringWriter stringWriter = new StringWriter();
try (BufferedWriter writer = new BufferedWriter(stringWriter)) {
templateEngine.process(templateName, context, writer);
writer.flush();
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
return builder.parse(new ByteArrayInputStream(stringWriter.toString().getBytes("UTF-8")));
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 核心: 根据freemarker模板生成pdf文档
*
* @param templateEngine 配置
* @param templateName 模板名称
* @param out 输出流
* @param listVars 模板参数
* @throws Exception 模板无法找到、模板语法错误、IO异常
*/
private static void generateAll(TemplateEngine templateEngine, String templateName, OutputStream out, List<Map<String, Object>> listVars) throws Exception {
if (CollectionUtils.isEmpty(listVars)) {
log.warn("警告:模板参数为空!");
return;
}
ITextRenderer renderer = new ITextRenderer();
//设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体"
ITextFontResolver fontResolver = renderer.getFontResolver();
//避免中文为空设置系统字体
fontResolver.addFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
fontResolver.addFont("static/fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
//根据参数集个数循环调用模板,追加到同一个pdf文档中
//(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容)
for (int i = 0; i < listVars.size(); i++) {
Document docAppend = generateDoc(templateEngine, templateName, listVars.get(i));
renderer.setDocument(docAppend, null);
renderer.getSharedContext().setBaseURL(null);
//展现和输出pdf
renderer.layout();
if (i == 0) {
renderer.createPDF(out, false);
} else {
//写下一个pdf页面
renderer.writeNextDocument();
}
}
renderer.finishPDF(); //完成pdf写入
}
private static void generateAll(TemplateEngine templateEngine, String templateName, OutputStream out, List<Map<String, Object>> listVars, String path) throws Exception {
if (CollectionUtils.isEmpty(listVars)) {
log.warn("警告:模板参数为空!");
return;
}
ITextRenderer renderer = new ITextRenderer();
//设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体"
ITextFontResolver fontResolver = renderer.getFontResolver();
//避免中文为空设置系统字体
fontResolver.addFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont("static/fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
String url = "File:///" + path + "/";
log.info("导出 文件地址 = {}", url);
//根据参数集个数循环调用模板,追加到同一个pdf文档中
//(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容)
for (int i = 0; i < listVars.size(); i++) {
Document docAppend = generateDoc(templateEngine, templateName, listVars.get(i));
renderer.setDocument(docAppend, null);
renderer.getSharedContext().setBaseURL(url);
//展现和输出pdf
renderer.layout();
if (i == 0) {
renderer.createPDF(out, false);
} else {
//写下一个pdf页面
renderer.writeNextDocument();
}
}
renderer.finishPDF(); //完成pdf写入
}
/**
* pdf下载
*
* @param templateEngine 配置
* @param templateName 模板名称(带后缀.ftl)
* @param listVars 模板参数集
* @param response HttpServletResponse
* @param fileName 下载文件名称(带文件扩展名后缀)
*/
public static void download(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response, String fileName) {
// 设置编码、文件ContentType类型、文件头、下载文件名
response.setCharacterEncoding("utf-8");
response.setContentType("multipart/form-data");
try {
response.setHeader("Content-Disposition", "attachment;fileName=" +
new String(fileName.getBytes("gb2312"), "ISO8859-1"));
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage(), e);
}
try (ServletOutputStream out = response.getOutputStream()) {
generateAll(templateEngine, templateName, out, listVars);
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public static void downloadWithImg(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response, String fileName, String path) {
// 设置编码、文件ContentType类型、文件头、下载文件名
response.setCharacterEncoding("utf-8");
response.setContentType("multipart/form-data");
try {
response.setHeader("Content-Disposition", "attachment;fileName=" +
new String(fileName.getBytes("gb2312"), "ISO8859-1"));
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage(), e);
}
try (ServletOutputStream out = response.getOutputStream()) {
generateAll(templateEngine, templateName, out, listVars, path);
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
/**
* pdf预览
*
* @param templateEngine 配置
* @param templateName 模板名称(带后缀.ftl)
* @param listVars 模板参数集
* @param response HttpServletResponse
*/
public static void preview(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response) {
try (ServletOutputStream out = response.getOutputStream()) {
generateAll(templateEngine, templateName, out, listVars);
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public static void previewWithImg(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response, String path) {
try (ServletOutputStream out = response.getOutputStream()) {
generateAll(templateEngine, templateName, out, listVars, path);
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
/**
* 生成pdf 指定文件绝对路径
*
* @param templateEngine 配置
* @param templateName 模板名称(带后缀.ftl)
* @param listVars 模板参数集
* @param filePath 文件路径
*/
public static void creatPdfFile(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, String filePath) {
FileOutputStream out = null;
try {
//新建文件
File file = new File(filePath);
// 创建复制路径
File parent = file.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
// 复制文件
if (file.exists()) {
file.createNewFile();
}
out = new FileOutputStream(file);
generateAll(templateEngine, templateName, out, listVars);
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 多个PDF合并功能 要捕获错误信息,所以不用try-catch
*
* @param files 多个PDF的路径
* @param savePath 生成的新PDF绝对路径
*/
@SneakyThrows
public static void mergePdfFiles(String[] files, String savePath) {
if (files.length > 0) {
// try {
com.lowagie.text.Document document = new com.lowagie.text.Document(new PdfReader(files[0]).getPageSize(1));
PdfCopy copy = new PdfCopy(document, new FileOutputStream(savePath));
document.open();
for (String file : files) {
PdfReader reader = new PdfReader(file);
int n = reader.getNumberOfPages();
for (int j = 1; j <= n; j++) {
document.newPage();
PdfImportedPage page = copy.getImportedPage(reader, j);
copy.addPage(page);
}
}
document.close();
// } catch (IOException | DocumentException e) {
// e.printStackTrace();
// }
}
}
}
pdf.html 模版
多页模版
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml" layout:decorator="layout">
<head lang="en">
<title></title>
<style>
@media print {
div.footer-content {
display: block;
font-family: SimSun;
font-size: 16px;
color: #000;
text-align: left;
margin-left: -490px;
position: running(footer-content);
}
}
@page {
size: A4; /*设置纸张大小:A4(210mm 297mm)、A3(297mm 420mm) 横向则反过来*/
margin: 1in;
padding: 1em;
}
.page {
page-break-after: always;
font-size: 16px;
font-family: SimSun;
}
/* 防止最后一页多空页 */
.page:last-child {
page-break-after: auto;
}
body {
font-family: 'SimSun'
}
h1 {
text-align: center;
line-height: 60px;
}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
padding: 3px;
font-size: 16px;
height: 10px;
}
.half-cell {
position: relative;
}
.footer {
position: fixed;
bottom: 10px;
left: 0;
width: 100%;
font-size: 12px;
margin-left: 10px;
margin-right: 10px; /* 右边也留 */
}
.footer .right {
text-align: right;
margin-top: 30px;
}
</style>
</head>
<!--这样配置不中文不会显示-->
<!--<body style="font-family: 宋体">-->
<body style="font-family: 'SimSun'">
<!--第一页-->
<div class="page">
<div style="width: 100%; overflow: hidden;">
<!-- 左 -->
<div style="float: left; width: 40%;">
<table style="width: 100%; border-collapse: collapse;text-align: center;">
<colgroup>
<col style="width: 50%;" />
<col style="width: 50%;" />
</colgroup>
<tr class="expertTitle">
<td>**</td>
<td th:text="${bjbh}"></td>
</tr>
<tr class="expertTitle">
<td>**</td>
<td th:text="${bdh}"></td>
</tr>
<tr>
<td>**</td>
<td th:text="${fbType}"></td>
</tr>
</table>
</div>
<!-- 右 -->
<div style="float: right; width: 30%; text-align: right;">
<img th:src="${photo}" alt="二维码" style="width: 100px; height: 100px;"/>
</div>
</div>
<h2 style="text-align: center;" data-pdf-bookmark-name="***">***</h2>
<div style="line-height: 20px;">
<!-- 继续下面内容 -->
<div style="margin-top: 20px; font-size: 16px; margin-left: 10px; line-height: 1.8;">
<span style="border-bottom: 1px solid #000; padding: 0 0px" th:text="${name}"></span> <span>:</span>
<p style="text-indent: 2em;">
***
</p>
</div>
<div>
<table style="text-align: center; width: 100%; border-collapse: collapse;" border="1">
<colgroup>
<!-- 设置表格宽度 -->
<col style="width: 16.66%;" />
<col style="width: 16.66%;" />
<col style="width: 16.66%;" />
<col style="width: 16.66%;" />
<col style="width: 16.66%;" />
<col style="width: 16.66%;" />
</colgroup>
<tr>
<td>**</td>
<!-- 设置表格合并 -->
<td colspan="5" th:text="${jsdd}"></td>
</tr>
<tr style="height: 150px;">
<td colspan="1">**</td>
<!-- 设置表格自动换行 -->
<td colspan="3" style="text-align: left; vertical-align: top; padding: 5px;">
<span th:text="${zbdljg}"></span>
</td>
</tr>
</table>
</div>
</div>
<div class="footer" >
<div>
附注:
</div>
<div>
 1. ****。
</div>
<div class="right">
<div>上****** 制  </div>
<div>2020 版  </div>
</div>
</div>
</div>
<div class="page">
<div style="width: 100%; overflow: hidden;">
<!-- 左 -->
<div style="float: left; width: 40%;text-align: center;">
<table style="width: 100%; border-collapse: collapse;text-align: center;">
<colgroup>
<col style="width: 50%;" />
<col style="width: 50%;" />
</colgroup>
<tr class="expertTitle">
<td>**</td>
<td th:text="${bjbh}"></td>
</tr>
<tr class="expertTitle">
<td>**</td>
<td th:text="${bdh}"></td>
</tr>
<tr>
<td>**</td>
<td th:text="${fbType}"></td>
</tr>
</table>
</div>
<!-- 右 -->
<div style="float: right; width: 30%; text-align: right;">
<img th:src="${photo}" alt="二维码" style="width: 100px; height: 100px;"/>
</div>
</div>
<h2 style="text-align: center;" data-pdf-bookmark-name="专业工程暂估价明细表">专业工程暂估价明细表</h2>
<table style="width: 100%; border-collapse: collapse; font-weight: normal;text-align: center;" >
<colgroup>
<col style="width: 20%;" />
<col style="width: 20%;" />
<col style="width: 20%;" />
<col style="width: 20%;" />
<col style="width: 20%;" />
</colgroup>
<tr>
<td>序号</td>
<td>专业工程名称</td>
<td>专业类别</td>
<td>招标主体</td>
<td>金额(万元)</td>
</tr>
<!-- 循环 -->
<tr th:each="item, iterStat : ${list}" >
<td th:text="${iterStat.count}"></td>
<td th:text="${item.projectName}"></td>
<td th:text="${item.projectType}"></td>
<td th:text="${item.bidEntity}"></td>
<td th:text="${item.amount}"></td>
</tr>
</table>
<div class="footer">
<div>
附注:
</div>
<div>
1. 本中标可通过二维码在上海市建筑业官方微信验证。
</div>
<div class="right" style=" margin-right: 30px;">
<div> *** 制  </div>
<div>2020 版  </div>
</div>
</div>
</div>
</body>
</html>