目录
一、序言
一般正常来说,生成PDF的操作都是通过将HTML转成PDF,HTML动态渲染可以借助模板引擎,如常用的Thymeleaf
或者Freemarker
。
HTML转PDF可以通过flyingsaucer
来实现,可以参考之前博主写的一篇文章《flyingsaucer进行html文件转图片和pdf》,至于PDF样式,我们可以通过CSS打印样式来控制。
今天这篇文章主要分享模板引擎动态渲染以及结合flyingsaucer
通过CSS打印样式控制PDF的内容呈现,固定每页PDF的头和尾部。
二、CSS样式控制打印模板
在PrintCSS上有一篇文章: Running Headers and Footers ,里面会介绍CSS运行时元素以及如何控制打印PDF时的头部和尾部。
这里介绍一个在线工具:PrintCSS.live,里面可以在线预览pdf打印效果,如下:
三、代码示例
1、pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.22</version>
</dependency>
2、application.yml
spring:
# freemarker configuration
freemarker:
cache: true
suffix: .ftl
charset: UTF-8
template-loader-path: classpath:templates/
备注:
template-loader-path
为.ftl
模板加载路径,这里我们指定了类路径下的templates目录。
3、PdfGenerationController
import com.itextpdf.text.pdf.BaseFont;
import com.universe.wonderful.pojo.model.AccountProofModel;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
/**
* @author Nick Liu
* @date 2023/3/1
*/
@Slf4j
@RestController
@RequiredArgsConstructor
public class PdfGenerationController {
private final Configuration configuration;
@RequestMapping("/pdf/preview")
public ResponseEntity<byte[]> downloadPdfWithFixedHeaderAndFooter() {
AccountProofModel accountProofModel = AccountProofModel.builder()
.generationDate(LocalDate.now().toString())
.memberName("Nick Liu")
.memberAddress("Nanshan District, Shenzhen city, Guangdong Province")
.accountNo("88888888888888")
.bankName("ICBC")
.bankSwiftCode("ABCDEFG")
.bankAddress("Shenzhen city of Guangdong Province")
.countryName("China")
.build();
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
// 不建议直接创建Template实例,开销比较大,可以直接通过Configuration实例获取,有缓存机制
Template template = configuration.getTemplate("personalAccountProof.ftl");
String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, accountProofModel);
ITextRenderer renderer = new ITextRenderer();
// 如果内容有中文则需要添加支持中文的字体
renderer.getFontResolver().addFont("/fonts/calibri.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
renderer.setDocumentFromString(content);
renderer.layout();
renderer.createPDF(os);
renderer.finishPDF();
} catch (Exception e) {
log.error("Fail to generate pdf: {}", e.getMessage(), e);
return ResponseEntity.internalServerError().body(null);
}
HttpHeaders respHeaders = new HttpHeaders();
respHeaders.setContentType(MediaType.APPLICATION_PDF);
respHeaders.setContentDisposition(ContentDisposition.inline().filename("accountProof.pdf", StandardCharsets.UTF_8).build());
return new ResponseEntity<>(os.toByteArray(), respHeaders, HttpStatus.OK);
}
}
备注:字体会从类路径下加载,底层通过
ClassLoader#getResourceAsStream()
读取。
字体目录和freemarker模板目录如下图:
4、Freemarker模板内容
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Running Headers and Footers</title>
<style>
@page {
size: A4;
margin: 40mm 10mm 50mm 10mm;
@top-left {
content: element(headerLeft);
}
@bottom-center {
content: element(footerCenter);
}
}
* {
padding: 0;
margin: 0;
}
body {
font-family: Calibri, serif;
}
.headerLeft {
position: running(headerLeft);
}
.titleWrapper > div {
margin: 2px 0;
}
.footerCenter {
text-align: center;
position: running(footerCenter);
}
.footerTipsWrapper {
color: #C1A97D;
margin-top: 10px;
border-top: 2px solid #EFE7DA;
}
.footerTipsWrapper > div {
font-size: 12px;
margin-top: 12px;
}
.contentWrapper {
margin-top: -10px;
}
.paddingWrapper {
padding: 10px;
}
.accountIntroduction {
margin-top: 60px;
background-color: #EFE7DA;
border: 1px solid #EFE7DA;
border-radius: 10px;
}
.accountDetailsWrapper {
margin-top: 50px;
border: 3px solid #EFE7DA;
border-radius: 10px;
}
.subTitle {
font-weight: bold;
border-bottom: 2px solid #EFE7DA;
padding-bottom: 10px;
}
.accountDetails > div {
margin-top: 8px;
}
</style>
</head>
<body>
<div class="headerLeft paddingWrapper">
<img src="http://localhost:8080/images/proof/head_logo.png" />
</div>
<div class="footerCenter">
<div class="footerLogoWrapper"><img src="http://localhost:8080/images/proof/footer_logo.png" alt="logo" /></div>
<div class="footerTipsWrapper">
<div>www.aletaplanet.com | account@aletaplanet.com</div>
<div>MPHK Management Company Limited | Suite 615, 6/F, Ocean Centre, Harbour City, Tsim Sha Tsui, Tsim Sha Tsui, Kowloon |<br/>
License No.: 21-10-03068
</div>
</div>
</div>
<div class="contentWrapper">
<div class="titleWrapper paddingWrapper">
<div><b>Proof of Account Details</b></div>
<div>Generated on: ${generationDate}</div>
</div>
<div class="tips paddingWrapper">To whom it may concern,</div>
<div class="accountIntroduction paddingWrapper">
<div><b>Personal account of ${memberName}</b></div>
<div style="margin-top: 10px;word-break: break-word">
This letter confirms the below account details allow ${memberName} residing at ${memberAddress} to receive payments into his/ her AP-1 Account:
</div>
</div>
<div class="accountDetailsWrapper paddingWrapper">
<div class="subTitle">Business account details</div>
<div class="accountDetails">
<div>Account Name: ${memberName}</div>
<div>Account Number: ${accountNo}</div>
<div>Bank Name: ${bankName}</div>
<div>Bank SWIFT/BIC: ${bankSwiftCode}</div>
<div>Bank Country: ${countryName}</div>
<div>Bank Address: ${bankAddress}</div>
</div>
</div>
</div>
</body>
</html>
在@page{}
代码块中我们指定了打印页面的大小为A4、上下左右的边缘分别为40毫米、50毫米、10毫米、10毫米,同时在页面左上角指定了logo,以及在页面底部居中指定了logo和描述。
实际上@top-left
和@bottom-center
的效果类似于固定定位。
备注:关于
@page
、@top-left
、@bottom-center
的介绍可以参考:https://www.w3.org/TR/css-page-3/#margin-boxes。
四、展示效果
启动项目,打开浏览器,输入http://localhost:8080/pdf/preview
,可以预览生成的PDF,如下:
备注:如果有多页,头部和尾部的logo也会在同样的地方显示。