Maven依赖
xml
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.16</version>
</dependency>
自定义ftl模板
该模板是由html页面直接后缀而成,模版名称定为template-01.ftl
注意事项
中文乱码问题
需要在模板中添加font-family: SimSun, serif;
标签,可解决中文乱码问题
html
body {
/*解决中文乱码*/
font-family: SimSun, serif;
/*自动换行*/
word-break: break-all;
}
页眉和页脚
其实页眉和页脚可以通过定义的ftl模板来实现
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
<style>
/*页眉的上下左右边距*/
@page {
margin: 30mm 20mm 30mm 20mm;
}
@page {
/*页眉*/
@top-center {
content: element(header)
}
/*页脚*/
@bottom-center {
content: element(footer)
}
}
/*页眉*/
#header {
position: running(header);
margin-top: 10mm;
}
/*页脚*/
#footer {
position: running(footer);
}
/*分页*/
#page-number:before {
content: counter(page);
}
/*分页*/
#page-count:before {
content: counter(pages);
}
</style>
</head>
<body>
<!--页眉-->
<div id="header">
深圳市xxx有限公司
<hr/>
</div>
<!--页脚-->
<div id="footer">
页码<span id="page-number"></span>/<span id="page-count"></span>
</div>
</body>
</html>
完整ftl模板页面
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
<style>
/*页眉的上下左右边距*/
@page {
margin: 30mm 20mm 30mm 20mm;
}
@page {
/*页眉*/
@top-center {
content: element(header)
}
/*页脚*/
@bottom-center {
content: element(footer)
}
}
/*页眉*/
#header {
position: running(header);
margin-top: 10mm;
}
/*页脚*/
#footer {
position: running(footer);
}
/*分页*/
#page-number:before {
content: counter(page);
}
/*分页*/
#page-count:before {
content: counter(pages);
}
* {
padding: 0;
margin: 0;
}
body {
/*解决中文乱码*/
font-family: SimSun, serif;
/*自动换行*/
word-break: break-all;
}
.main {
width: 100%;
height: auto;
margin: 0 auto;
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
}
td, th {
line-height: 20px;
padding: 7px 5px;
border: 1px solid #999999;
}
</style>
</head>
<body>
<!--页眉-->
<div id="header">
深圳市xxx有限公司
<hr/>
</div>
<!--页脚-->
<div id="footer">
页码<span id="page-number"></span>/<span id="page-count"></span>
</div>
<div class="main">
<h1>深圳市xxx有限公司</h1>
<p style="margin: 30px 0 50px 0;text-align: left;">
人在世俗的世界中行走着,在慢慢流逝的时间里静静等待着成年那一刻的全速奔跑。可漫长的等待过后却发现,形形色色的欲望与世俗观念像橡皮泥一样粘在身上,越积越重,最后竟无限膨胀,束缚了我们的双腿,减缓了我们的步伐。我们不能轻松上路,也不能全速奔跑。它们甚至遮蔽住我们的双眼,遮掩住我们纯真的心,让我们的脚步开始凌乱,旋转在灯红酒绿的花花世界里......
</p>
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
</tr>
</thead>
<tbody>
<#if !data?? || (data?size==0)>
<tr>
<td colspan="3">无</td>
</tr>
<#else>
<#list data as item>
<tr>
<td>${item.name}</td>
<td>${item.age}</td>
<td>${item.sex}</td>
</tr>
</#list>
</#if>
</tbody>
</table>
</div>
</body>
</html>
PDF工具类
字体包SimSun.ttc、ArialUni.ttf自行下载
java
package org.example;
import com.lowagie.text.pdf.BaseFont;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;
/**
* @author 苦瓜不苦
* @date 2023/11/28 18:33
**/
public class PDFUtil {
/**
* 模板生成器
*
* @param createFile 生成文件的路径
* @param ftlName 模板名称
* @param object 数据
*/
public static void processTemplate(String createFile, String ftlName, Object object) {
Configuration configuration = null;
StringWriter writer = null;
ByteArrayOutputStream outputStream = null;
try {
// 初始化模版
configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
writer = new StringWriter();
outputStream = new ByteArrayOutputStream();
// 加载模板目录
configuration.setClassForTemplateLoading(MainApi.class, "/module");
configuration.setClassicCompatible(true);
ITextRenderer renderer = new ITextRenderer();
// 设置字体
ITextFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont("fonts/SimSun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont("fonts/ArialUni.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
configuration.setEncoding(Locale.CHINA, "UTF-8");
// 读取模板文件
Template template = configuration.getTemplate(ftlName, "UTF-8");
// 写入数据到模板中
template.process(object, writer);
writer.flush();
// 获取填充好数据的html页面
String html = writer.toString();
renderer.setDocumentFromString(html);
renderer.layout();
// 通过html页面字符串转换成pdf文件
renderer.createPDF(outputStream);
renderer.finishPDF();
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (Objects.nonNull(outputStream)) {
outputStream.close();
}
if (Objects.nonNull(writer)) {
writer.close();
}
if (Objects.nonNull(configuration)) {
configuration.clone();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
调用测试
java
public class Main {
public static void main(String[] args) {
String fromFile = "./" + System.currentTimeMillis() + ".pdf";
String toFile = "template-01.ftl";
List<JSONObject> data = new ArrayList<>();
for (int i = 0; i < 60; i++) {
JSONObject object = new JSONObject();
object.set("name", "张三");
object.set("sex", "男");
object.set("age", "18");
data.add(object);
}
JSONObject object = new JSONObject();
object.set("data", data);
byte[] bytes = PDFUtil.processTemplate(toFile, object);
File file = FileUtil.writeBytes(bytes, fromFile);
System.err.println(file);
}
}
扩展情况
以上代码即可生成好一份PDF文档了,但是会存在一些问题,
表格的形式会被自动切割,出现以下情况
按照不同的需求,可以使用不同的方式来处理。
一是,当被分页时,每页都需要一个标题的存在。
二是,分页的头部和尾部需要闭合起来
还有扩展于图片水印或者文字水印的需求
图片水印方法
java
/**
* 添加图片水印
*
* @param bytes pdf字节
* @return
*/
public static byte[] appendImageWatermark(byte[] bytes) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
PdfReader reader = new PdfReader(bytes);
PdfStamper stamper = new PdfStamper(reader, byteArrayOutputStream);
// 加载水印图片
URL url = PDFUtil.class.getClassLoader().getResource("fonts/bg.png");
Image image = Image.getInstance(url);
// 设置等比缩放 图片大小
image.scalePercent(20);
// 自定义大小
// image.scaleAbsolute(200,100);
// 设置旋转弧度
image.setRotation(0);
// 设置旋转角度
image.setRotationDegrees(0);
// 创建PdfGState对象并设置透明度
PdfGState gState = new PdfGState();
// 填充透明度
gState.setFillOpacity(0.3f);
// 描边透明度
gState.setStrokeOpacity(0.3f);
// PDF总页数
int total = reader.getNumberOfPages() + 1;
for (int i = 1; i < total; i++) {
Rectangle pageRect = reader.getPageSizeWithRotation(i);
PdfContentByte content = stamper.getOverContent(i);
content.saveState();
content.setGState(gState);
// 设置图片水印
// 获取pdf每页的长宽
float width = pageRect.getWidth();
float top = pageRect.getTop();
// 获取缩放之后水印图片的长宽
float scaledWidth = image.getScaledWidth();
float scaledHeight = image.getScaledHeight();
// 通过计算将水印添加到中间
float x = (width - scaledWidth) / 2;
float y = (top - scaledHeight) / 2;
content.addImage(image, scaledWidth, 60, 0, scaledHeight, x, y);
content.restoreState();
}
stamper.close();
reader.close();
return byteArrayOutputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
文字水印方法
java
/**
* 添加文字水印
*
* @param bytes pdf字节
* @param text 水印文字
* @param size 文字大小
* @return
*/
public static byte[] appendTextWatermark(byte[] bytes, String text, Integer size) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
PdfReader reader = new PdfReader(bytes);
PdfStamper stamper = new PdfStamper(reader, byteArrayOutputStream);
// 创建PdfGState对象并设置透明度
PdfGState gState = new PdfGState();
// 填充透明度
gState.setFillOpacity(0.3f);
// 描边透明度
gState.setStrokeOpacity(0.3f);
// 加载字体
BaseFont baseFont = BaseFont.createFont("fonts/ArialUni.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// PDF总页数
int total = reader.getNumberOfPages() + 1;
for (int i = 1; i < total; i++) {
Rectangle pageRect = reader.getPageSizeWithRotation(i);
PdfContentByte content = stamper.getOverContent(i);
content.saveState();
// 获取pdf每页的长宽
float width = pageRect.getWidth();
float top = pageRect.getTop();
// 通过计算将水印添加到中间
float x = (width - (size * text.length())) / 2;
float y = (top - size) / 2;
// 设置字体水印
content.beginText();
content.setGState(gState);
// 字体
content.setFontAndSize(baseFont, size);
// 颜色
content.setColorFill(Color.BLACK);
// 水印位置
content.showTextAligned(Element.ALIGN_LEFT, text, x, y, 30);
content.endText();
content.restoreState();
}
stamper.close();
reader.close();
return byteArrayOutputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
表格分页被切割问题-方式一
在ftl模板中的style标签中添加css样式
html
tr {
page-break-inside: avoid;
page-break-after: auto;
}
同一个表格在分页时,会被自动添加上下边框
表格分页被切割问题-方式二
在ftl模板中的style标签中添加css样式,需要注意的是表格的标题需要使用thead标签包裹,表格其他行用tbody标签包裹
html
<style>
table {
page-break-inside: auto;
-fs-table-paginate: paginate;
border-spacing: 0;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
</style>
<body>
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
</tr>
</thead>
<tbody>
<#if !data?? || (data?size==0)>
<tr>
<td colspan="3">无</td>
</tr>
<#else>
<#list data as item>
<tr>
<td>${item.name}</td>
<td>${item.age}</td>
<td>${item.sex}</td>
</tr>
</#list>
</#if>
</tbody>
</table>
</body>
被分页时,表格的标题也会携带下来