Freemarker动态模板渲染&flyingsaucer将html转PDF(多页固定头尾)

一、序言

一般正常来说,生成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 | [email protected]</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也会在同样的地方显示。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lingbomanbu_lyl/article/details/129283495