JAVA 使用IText7 + Freemarker 动态数据生成PDF实现案例
技术方案:IText7 + Freemarker
技术文档
- Itext 官网:https://itextpdf.com/
- itext API文档:https://api.itextpdf.com/iText7/java/7.1.14/
- FreeMarker API文档:英文:https://freemarker.apache.org/docs/index.html ;中文:http://freemarker.foofun.cn/ref_builtins_loop_var.html
- CSS 文档:https://www.runoob.com/css/css-tutorial.html
- HTML文档:https://www.runoob.com/html/html-tutorial.html
使用maven导入相关依赖
<properties>
<itext.version>7.1.15</itext.version>
</properties>
<dependencies>
<!-- itext7 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>forms</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>pdfa</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>pdftest</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.18</version>
</dependency>
<!--itext7 html转pdf用到的包-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
</dependencies>
实现方案
- 配置Freemarker引擎
首先,我们需要配置Freemarker引擎,指定模板文件所在的路径并设置默认编码。我们可以使用以下代码来完成配置:
Configuration config = new Configuration(Configuration.getVersion());
config.setTemplateLoader(new ClassTemplateLoader(PdfGenerator.class, templatesPath)); config.setDefaultEncoding("UTF-8");
这里的templatesPath
变量表示我们存放模板文件的相对路径。由于我们是在Java中运行程序,所以要使用ClassTemplateLoader
类来加载模板文件。
- 加载模板文件并填充数据
一旦我们配置好了Freemarker引擎,就可以加载模板文件并将要填充的数据传递给它。可以使用以下代码来完成这一步骤:
Template template = config.getTemplate(templatesName);
StringWriter out = new StringWriter();
template.process(data, out); out.flush();
这里的templatesName
变量表示我们要加载的模板文件的名称。数据通过process
方法传递给模板引擎,填充模板并生成HTML代码。
- 将HTML代码转换为PDF文件
一旦我们有了HTML代码,就可以使用IText7将其转换为PDF文件。使用以下代码将HTML代码转换为PDF文件:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf, PageSize.A4);
document.setMargins(2, 4, 2, 2);
FontSet fontSet = new FontSet();
fontSet.addFont(fontsPath);
FontProvider fontProvider = new FontProvider(fontSet);
ConverterProperties converterProps = new ConverterProperties(); converterProps.setFontProvider(fontProvider);
HtmlConverter.convertToPdf(htmlContent, pdf, converterProps);
pdf.close();
byte[] bytes = outputStream.toByteArray();
这里的fontsPath
变量表示我们使用的中文字体的路径。我们使用了IText7提供的HtmlConverter
类来将HTML代码转换为PDF文件,并将字体设置为中文字体。
在完成上述步骤后,我们便能够成功实现使用IText7和Freemarker引擎生成PDF文件的功能。
附工具类完成代码
import com.alibaba.fastjson.JSONObject;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.layout.font.FontSet;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
/**
* @date: 2023年5月23日, 0023 下午 08:21
* @version: 1.0.0
* @Description:PDF工具类
* @Time: 2023-05-23 20:21
*/
public class PdfGenerator {
private static final String templatesPath = "/templates/";
private static final String fontsPath = "/fonts/simhei.ttf";
private static final String templatesName = "template.html";
/**
* 使用Freemarker引擎加载HTML模板文件并填充变量值,并将HTML字符串转换为PDF文件
*
* @param data 模板要填充的数据
* @throws Exception
*/
public static byte[] generatePDF(Map<String, Object> data) throws Exception {
Configuration config = new Configuration(Configuration.getVersion());
// 设置Freemarker引擎的模板路径
config.setTemplateLoader(new ClassTemplateLoader(PdfGenerator.class, templatesPath));
config.setDefaultEncoding("UTF-8");
Template template = config.getTemplate(templatesName);
StringWriter out = new StringWriter();
template.process(data, out);
out.flush();
// 使用Freemarker引擎加载HTML模板文件并填充变量值
String htmlContent = out.toString();
byte[] bytes = convertHtmlToPdf(htmlContent);
return bytes;
}
/**
* 使用iText 7将HTML字符串转换为PDF文件,并返回PDF文件的二进制数据
*
* @param htmlString 待转换的HTML字符串
* @return 返回生成的PDF文件内容
* @throws IOException
*/
private static byte[] convertHtmlToPdf(String htmlString) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf, PageSize.A4);
// 设置左、右、上、下四个边距的值,以点(pt)为单位
document.setMargins(2, 4, 2, 2);
// 设置中文字体
FontSet fontSet = new FontSet();
fontSet.addFont(fontsPath);
FontProvider fontProvider = new FontProvider(fontSet);
ConverterProperties converterProps = new ConverterProperties();
converterProps.setFontProvider(fontProvider);
// 调用HtmlConverter类的convertToPdf函数,将HTML字符串转换为PDF文件
HtmlConverter.convertToPdf(htmlString, pdf, converterProps);
pdf.close();
// 将PDF文件转换为字节数组并返回
return outputStream.toByteArray();
}
}
注:templatesPath,fontsPath,templatesName在src/main/resources下
附HTML模板文件代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<style type="text/css">
@page {
size: A4;
margin: 5mm 10mm; /* 上下和左右两个方向的边距分别为 10mm 和 20mm */
}
body {
width: 180mm;
height: 297mm;
}
.item_content {
width: 95%;
height: 100%;
}
.item_content .title {
overflow: hidden; /* 清除浮动 */
box-sizing: border-box; /* 盒模型以边框为界计算宽度 */
display: flex;
height: 20pt;
width: 100%;
align-items: center;
justify-content: center;
}
.item_content .title .circle {
float: left;
width: 3%;
padding-right: 0.5px; /* 右侧间距 */
align-self: center;
justify-self: center;
margin-right: -1pt;
margin-top: 2pt;
}
.item_content .title .title_text {
float: left;
width: 12%;
color: #409EFF;
text-align: center;
position: relative;
font-size: 15pt;
padding-right: 0.5px; /* 右侧间距 */
align-self: center;
justify-self: center;
}
.item_content .title .line {
float: right;
width: 85%;
border-top: 0.75pt solid black;
margin-top: 10pt;
}
.item_content .section {
overflow: hidden; /* 清除浮动 */
box-sizing: border-box; /* 盒模型以边框为界计算宽度 */
display: flex;
height: 100%; /* 高度设为100% */
margin: auto;
}
.item_content .section .section_photo {
float: left;
width: 14.5%;
text-align: center;
}
.item_content .section .section_info {
float: right;
width: 84.5%;
align-self: center;
justify-self: center;
text-align: left;
}
.item_content .section .section_info .name {
color: #409EFF;
font-size: 21pt;
}
.item_content .section .section_info .address {
width: 100%;
height: 40px;
line-height: 40px;
background: #5195db;
color: white;
font-size: 15pt;
padding: 0 15pt;
margin: 10px 0 0;
-webkit-print-color-adjust: exact;
}
.item_content .section .section_info .divider {
width: 100%;
margin: 15pt 0;
border-top: 0.75pt solid #ccc;
}
.item_content .section .section_info .info-container {
overflow: hidden;/* 清除浮动后的高度问题 */
margin-top: 1%;
margin-bottom: 1%;
font-size: 11.25pt;
gap: 4%;
}
.item_content .section .section_info .info-container .left-container{
float: left;
width: 48%;
mmargin-top: 1%;
margin-bottom: 1%;
}
.item_content .section .section_info .info-container .right-container{
float: right;
width: 48%;
margin-top: 1%;
margin-bottom: 1%;
}
.item_content .section .section_info .info-container .left-container .id-card-photo-container{
overflow: hidden;/* 清除浮动后的高度问题 */
}
.item_content .section .section_info .info-container .left-container .id-card-photo-container .left-id-card-photo{
float: left;
width: 40%;/* 左半部分的宽度为 40% */
}
.item_content .section .section_info .info-container .left-container .id-card-photo-container .right-id-card-photo{
float: right;
width: 60%;/* 右半部分的宽度为 60% */
}
.item_content .section .section_info .record-container {
overflow: hidden;/* 清除浮动后的高度问题 */
margin-top: 1%;
margin-bottom: 1%;
font-size: 11.25pt;
gap: 4%;
}
.item_content .section .section_info .record-container .left-record {
float: left;
width: 48%;/* 左半部分的宽度为 50% */
mmargin-top: 1%;
margin-bottom: 1%;
text-align: left;
}
.item_content .section .section_info .record-container .right-record {
float: right;
width: 48%;/* 右半部分的宽度为 50% */
margin-top: 1%;
margin-bottom: 1%;
text-align: left;
}
.item_content .experience {
width: 90%;
margin: 15px auto 20px;
background: #f5f5f5;
-webkit-print-color-adjust: exact;
padding: 10px 20px;
}
.item_content .experience .experience-info{
overflow: hidden;/* 清除浮动后的高度问题 */
font-size: 11.25pt;
}
.item_content .experience .experience-info .left-experience{
float: left; /* 左浮动 */
width: 33.33%; /* 固定宽度 */
box-sizing: border-box; /* 盒模型以边框为界计算宽度 */
padding-right: 7.5pt; /* 右侧间距 */
text-align: left;
}
.item_content .experience .experience-info .middle-experience{
float: left; /* 左浮动 */
width: 33.33%; /* 固定宽度 */
box-sizing: border-box; /* 盒模型以边框为界计算宽度 */
padding-right: 7.5px; /* 右侧间距 */
text-align: left;
}
.item_content .experience .experience-info .right-experience{
float: left; /* 左浮动 */
width: 33.33%; /* 固定宽度 */
box-sizing: border-box; /* 盒模型以边框为界计算宽度 */
text-align: left;
}
.item_content .table_style {
border-collapse: collapse;
width: 100%;
}
.item_content .table_style td {
text-align: center;
height: 11.25pt;
padding: 3.75pt 7.5pt;
min-width: 97.5pt;
max-width: none;
}
.item_content .table_style thead td {
font-weight: 600;
background: #f3f3f3;
font-size: 11.25pt;
}
.item_content .table_style thead th {
font-weight: 600;
background: #f3f3f3;
font-size: 13pt;
}
</style>
</head>
<body style="text-align: center">
<div>
<div>
<div class="scroll-show">
<div style="margin-right:3%;margin-left:5%;">
<div class="item_content">
<div class="title">
<div class="circle"><img src="https://egongban.oss-cn-shenzhen.aliyuncs.com/2023/05/29/b506e8108f4d9ee742370f4301e170a.png" style="width: 14pt; height: 14pt;"></div>
<div class="title_text">个人信息</div>
<div class="line"></div>
</div>
<div class="section">
<div class="section_photo">
<#if projectWorker.avatarUrl?has_content>
<#assign imgUrl = projectWorker.avatarUrl>
<#assign imgAlt = "Image description">
<img src="${imgUrl}" alt="${imgAlt}"
style="width: 75pt; height: 75pt;margin-top: 110pt;"></img>
</#if>
</div>
<div class="section_info">
<div class="name">${projectWorker.name}</div>
<div class="address">所属省市:${projectWorker.address}</div>
<div class="info-container">
<div class="left-container">
<div class="id-card-photo-container">
<div class="left-id-card-photo">身份证照片:</div>
<div class="right-id-card-photo">
<#if projectWorker.idCardFrontUrl?has_content>
<#assign imgUrl = projectWorker.idCardFrontUrl>
<#assign imgAlt = "Image description">
<img src="${imgUrl}" alt="${imgAlt}"
style="width: 37.5pt; height: 37.5pt;margin-right: 3.75pt;">
</#if>
<#if projectWorker.idCardBackUrl?has_content>
<#assign imgUrl = projectWorker.idCardBackUrl>
<#assign imgAlt = "Image description">
<img src="${imgUrl}" alt="${imgAlt}"
style="width: 37.5pt; height: 37.5pt;">
</#if>
</div>
</div>
<div>姓名:${projectWorker.name}</div>
<div>性别:<#if projectWorker.gender?has_content>${projectWorker.gender}</#if></div>
<div>民族:<#if projectWorker.nationality?has_content>${projectWorker.nationality}</#if></div>
<div>身份证有效日期:<#if projectWorker.dateIssue?has_content && projectWorker.expiryDate?has_content>${projectWorker.dateIssue?substring(0, 10)} — ${projectWorker.expiryDate?substring(0, 10)}</#if></div>
</div>
<div class="right-container">
<div style="height: 37.5pt">身份证:<#if projectWorker.idCard?has_content>${projectWorker.idCard}</#if></div>
<div>年龄:<#if projectWorker.age?has_content>${projectWorker.age}</#if></div>
<div>住址:<#if projectWorker.address?has_content>${projectWorker.address}</#if></div>
<div>发证机关:<#if projectWorker.issuingAuthority?has_content>${projectWorker.issuingAuthority}</#if></div>
</div>
</div>
<div class="divider"></div>
<div class="info-container">
<div class="left-container">
<div>联系电话:<#if projectWorker.phone?has_content>${projectWorker.phone}</#if></div>
<div>家庭电话:<#if projectWorker.familyPhone?has_content>${projectWorker.familyPhone}</#if></div>
<div>微信号码:<#if projectWorker.wechat?has_content>${projectWorker.wechat}</#if></div>
<div>联系地址:<#if projectWorker.contactAddress?has_content>${projectWorker.contactAddress}</#if></div>
</div>
<div class="right-container">
<div>工人工种:<#if projectWorker.workTypeName?has_content>${projectWorker.workTypeName}</#if></div>
<div>紧急联系人:<#if projectWorker.emergencyContactPerson?has_content>${projectWorker.emergencyContactPerson}</#if></div>
<div>紧急联系人关系:<#if projectWorker.emergencyContactRelation?has_content>${projectWorker.emergencyContactRelation}</#if></div>
<div>紧急联系人电话:<#if projectWorker.emergencyContactPhone?has_content>${projectWorker.emergencyContactPhone}</#if></div>
</div>
</div>
<div class="divider"></div>
<div class="record-container">
<#if (projectWorker.projectWorkerBankCardList?has_content && projectWorker.projectWorkerBankCardList?size > 0)>
<#list projectWorker.projectWorkerBankCardList as record>
<#if record?index % 2 == 0>
<div class="left-record">
<div>工资银行卡:${record.bankName}</div>
<div>工资卡开户支行:${record.branchBankName}</div>
<div>工资卡卡号:${record.bankCard}</div>
</div>
<#else>
<div class="right-record">
<div>工资银行卡:${record.bankName}</div>
<div>工资卡开户支行:${record.branchBankName}</div>
<div>工资卡卡号:${record.bankCard}</div>
</div>
</#if>
</#list>
</#if>
</div>
<div class="divider"></div>
<div class="info-container">
<div class="left-container">
<div>政治面貌:<#if projectWorker.politicalAffiliationName?has_content>${projectWorker.politicalAffiliationName}</#if></div>
<div>文化水平:<#if projectWorker.educationalLevelName?has_content>${projectWorker.educationalLevelName}</#if></div>
<div>是否服兵役:<#if projectWorker.militaryService?has_content>${projectWorker.militaryService?string('是', '否')}</#if></div>
<div>服役时间:<#if projectWorker.enlistmentBeginTime?has_content && projectWorker.enlistmentEndTime?has_content>${projectWorker.enlistmentBeginTime?substring(0, 10)} — ${projectWorker.enlistmentEndTime?substring(0, 10)}</#if></div>
<div>婚姻状况:<#if projectWorker.maritalStatus?has_content>${projectWorker.maritalStatus?string('已婚', '未婚')}</#if></div>
<div>家庭成员:<#if projectWorker.familyMember?has_content>${projectWorker.familyMember}</#if></div>
<div>从业时间:<#if projectWorker.employmentBeginTime?has_content && projectWorker.employmentEndTime?has_content>${projectWorker.employmentBeginTime?substring(0, 10)} — ${projectWorker.employmentEndTime?substring(0, 10)}</#if></div>
</div>
<div class="right-container">
<div>专业技能:<#if projectWorker.professionalSkill?has_content>${projectWorker.professionalSkill}</#if></div>
<div>专业证书:
<#if projectWorker.emergencyContactPerson?has_content>
${projectWorker.emergencyContactPerson}
</#if>
</div>
<div>有无困难:<#if projectWorker.difficulty?has_content>${projectWorker.difficulty}</#if></div>
<div>对企业的希望:<#if projectWorker.enterpriseExpectation?has_content>${projectWorker.enterpriseExpectation}</#if></div>
<div>对自身的职业规划:<#if projectWorker.oneselfOccupationalPlan?has_content>${projectWorker.oneselfOccupationalPlan}</#if></div>
</div>
</div>
</div>
</div>
<div class="title">
<div class="circle"><img src="https://egongban.oss-cn-shenzhen.aliyuncs.com/2023/05/29/b506e8108f4d9ee742370f4301e170a.png" style="width: 14pt; height: 14pt;"></div>
<div class="title_text" id="experience">从业经历</div>
<div class="line"></div>
</div>
<div class="experience">
<#if (workingExperienceList?has_content && workingExperienceList?size> 0)>
<#list workingExperienceList as record>
<#if record?has_content>
<div class="experience-info">
<div class="left-experience">
<div>项目名称:<#if record.projectName?has_content>${record.projectName}</#if></div>
<div>合同名称:<#if record.contractName?has_content>${record.contractName}</#if></div>
<div>合伙项名称:<#if record.contractItemName?has_content>${record.contractItemName}</#if></div>
<div>项目工种:<#if record.workTypeName?has_content>${record.workTypeName}</#if></div>
<div>评分:<#if record.score?has_content>${record.score}</#if></div>
<#if (record.userAEvalRecordsVo?has_content)>
<#list record.userAEvalRecordsVo as vo>
<#if vo?has_content && (vo?index == 0 || vo?index % 3 == 0)>
<div>${vo.indicatorName}: ${vo.avgScore}</div>
</#if>
</#list>
</#if>
</div>
<div class="middle-experience">
<div>所属公司:<#if record.companyName?has_content>${record.companyName}</#if></div>
<div>所属小组:<#if record.teamName?has_content>${record.teamName}</#if></div>
<div>记工方式:<#if record.payTypeName?has_content>${record.payTypeName}</#if></div>
<div>进场日期:<#if record.approachTime?has_content>${record.approachTime}</#if></div>
<br>
<#if (record.userAEvalRecordsVo?has_content)>
<#list record.userAEvalRecordsVo as vo>
<#if vo?has_content && (vo?index == 1 || vo?index % 3 == 1)>
<div>${vo.indicatorName}: ${vo.avgScore}</div>
</#if>
</#list>
</#if>
</div>
<div class="right-experience">
<div>退场日期:<#if record.exitTime?has_content>${record.exitTime}</#if></div>
<div>角色:<#if record.workerTypeName?has_content>${record.workerTypeName}</#if><#if record.partnerTypeName?has_content>${record.partnerTypeName}</#if></div>
<br>
<br>
<br>
<#if (record.userAEvalRecordsVo?has_content)>
<#list record.userAEvalRecordsVo as vo>
<#if vo?has_content && (vo?index == 2 || vo?index % 3 == 2)>
<div>${vo.indicatorName}: ${vo.avgScore}</div>
</#if>
</#list>
</#if>
</div>
</div>
</#if>
</#list>
</#if>
</div>
<div class="title">
<div class="circle"><img src="https://egongban.oss-cn-shenzhen.aliyuncs.com/2023/05/29/b506e8108f4d9ee742370f4301e170a.png" style="width: 14pt; height: 14pt;"></div>
<div class="title_text" id="training">安全培训</div>
<div class="line"></div>
</div>
<div style="width: 90%;margin: 15px auto;">
<table class="table_style">
<thead>
<tr>
<th style="width: 33%;">培训视频</th>
<th style="width: 33%;">状态</th>
<th style="width: 33%;">观看日期</th>
</tr>
</thead>
<tbody>
<#if (workerTrainingRecordList?has_content)>
<#list workerTrainingRecordList as record>
<#if record?has_content>
<tr>
<td>
<#if record.title?has_content>${record.title}</#if>
</td>
<td>
<#if record.statusName?has_content>${record.statusName}</#if>
</td>
<td>
<#if record.date?has_content>${record.date}</#if>
</td>
</tr>
</#if>
</#list>
</#if>
</tbody>
</table>
</div>
<div class="title">
<div class="circle"><img src="https://egongban.oss-cn-shenzhen.aliyuncs.com/2023/05/29/b506e8108f4d9ee742370f4301e170a.png" style="width: 14pt; height: 14pt;"></div>
<div class="title_text" id="record">奖惩记录</div>
<div class="line"></div>
</div>
<div style="width: 90%;margin: 15px auto;">
<table class="table_style">
<thead>
<tr>
<th style="width: 25%;font-size: 16px;">项目名称</th>
<th style="width: 25%;">类型</th>
<th style="width: 25%;">金额</th>
<th style="width: 25%;">备注</th>
</tr>
</thead>
<tbody>
<#if (workerRewardPunishRecordList?has_content)>
<#list workerRewardPunishRecordList as record>
<#if record?has_content>
<tr>
<td>
<#if record.projectName??>${record.projectName}</#if>
</td>
<td>
<#if record.typeName??>${record.typeName}</#if>
</td>
<td>
<#if record.amount??>${record.amount}</#if>
</td>
<td>
<#if record.remark??>${record.remark}</#if>
</td>
</tr>
</#if>
</#list>
</#if>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战