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

简介: 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打印效果,如下:


image.png


三、代码示例

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模板目录如下图:


image.png


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,如下:

image.png


备注:如果有多页,头部和尾部的logo也会在同样的地方显示。

相关文章
|
2月前
|
前端开发
html渲染优先级
html渲染优先级
32 0
|
3月前
|
JavaScript 前端开发 容器
用HTML DOM实现有条件地渲染网页元素(上)
用HTML DOM实现有条件地渲染网页元素(上)
|
3月前
|
存储 JavaScript 前端开发
用HTML DOM实现有条件地渲染网页元素(下)
用HTML DOM实现有条件地渲染网页元素(下)
|
2月前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
642 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
2月前
|
JSON 数据格式
LangChain-20 Document Loader 文件加载 加载MD DOCX EXCEL PPT PDF HTML JSON 等多种文件格式 后续可通过FAISS向量化 增强检索
LangChain-20 Document Loader 文件加载 加载MD DOCX EXCEL PPT PDF HTML JSON 等多种文件格式 后续可通过FAISS向量化 增强检索
141 2
|
2月前
|
JavaScript 前端开发 容器
Vue生成PDF文件攻略:html2canvas与jspdf联手,中文乱码与自动换行难题攻克
Vue生成PDF文件攻略:html2canvas与jspdf联手,中文乱码与自动换行难题攻克
242 0
|
4月前
|
前端开发
html渲染优先级
html渲染优先级
36 2
|
4月前
|
前端开发 JavaScript 开发者
React Server Component 使用问题之为什么选择使用 React 官方的 renderToString 来渲染 HTML,如何解决
React Server Component 使用问题之为什么选择使用 React 官方的 renderToString 来渲染 HTML,如何解决
|
8天前
|
人工智能 文字识别 数据挖掘
MarkItDown:微软开源的多格式转Markdown工具,支持将PDF、Word、图像和音频等文件转换为Markdown格式
MarkItDown 是微软开源的多功能文档转换工具,支持将 PDF、PPT、Word、Excel、图像、音频等多种格式的文件转换为 Markdown 格式,具备 OCR 文字识别、语音转文字和元数据提取等功能。
74 9
MarkItDown:微软开源的多格式转Markdown工具,支持将PDF、Word、图像和音频等文件转换为Markdown格式

热门文章

最新文章