一、序言
一般正常来说,生成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也会在同样的地方显示。