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天前
|
安全 Go
Golang深入浅出之-Go语言模板(text/template):动态生成HTML
【4月更文挑战第24天】Go语言标准库中的`text/template`包用于动态生成HTML和文本,但不熟悉其用法可能导致错误。本文探讨了三个常见问题:1) 忽视模板执行错误,应确保正确处理错误;2) 忽视模板安全,应使用`html/template`包防止XSS攻击;3) 模板结构不合理,应合理组织模板以提高可维护性。理解并运用这些最佳实践,能提升Go语言模板编程的效率和安全性,助力构建稳健的Web应用。
24 0
|
2天前
|
移动开发 前端开发 HTML5
HTML5实现酷炫个人产品推广、工具推广、信息推广、个人主页、个人介绍、酷炫官网、门户网站模板源码
HTML5实现酷炫个人产品推广、工具推广、信息推广、个人主页、个人介绍、酷炫官网、门户网站模板源码
|
2天前
|
前端开发 文件存储 Python
python之xhtml2pdf: HTML转PDF工具示例详解
python之xhtml2pdf: HTML转PDF工具示例详解
11 0
|
2天前
|
JavaScript 前端开发 UED
html渲染优先级
html渲染优先级
13 1
|
2天前
|
移动开发 前端开发 JavaScript
【专栏:HTML与CSS实战项目篇】使用HTML5与CSS3制作一个动态表单验证页面
【4月更文挑战第30天】本文介绍了使用HTML5和CSS3创建动态表单验证页面的方法。首先,简述HTML5用于构建网页内容,CSS3用于描述样式。接着,分四步展示实现过程:1) 设计包含输入框和提示信息的表单结构;2) 使用CSS3创建样式,增强视觉效果;3) 使用JavaScript监听输入事件,动态验证表单并显示错误信息;4) 测试和调试确保跨平台兼容性。通过学习,开发者能掌握创建带验证功能的表单,提升用户体验。
|
2天前
|
编解码 前端开发 JavaScript
【专栏:HTML与CSS实战项目篇】打造一个动态新闻网站
【4月更文挑战第30天】构建动态新闻网站,运用HTML和CSS提升编程技能和网页设计理解。项目包括首页、新闻列表页和详情页,设计简洁易用,包含顶部导航、轮播图和新闻列表。页面布局注重吸引力和易用性,色彩搭配选用冷色调为主,辅以亮色点缀。字体选择清晰易读,布局保持整洁。交互效果如轮播图、导航栏高亮和响应式设计增强用户体验。本文提供基础新闻网站构建指南,为进一步功能扩展和优化打下基础。
|
2天前
|
移动开发 JavaScript 前端开发
【专栏:HTML进阶篇】HTML模板与Web组件:可复用的网页元素
【4月更文挑战第30天】HTML模板和Web组件提升网页开发效率和可维护性。HTML模板,如&lt;template&gt;元素和服务器端模板引擎,用于创建可复用的HTML结构。Web组件是自定义的HTML元素,结合影子DOM和模板,实现封装的可重用组件。两者助力构建高效、现代的网页和网站。
|
2天前
|
安全 Go 开发者
Golang深入浅出之-Go语言模板(text/template):动态生成HTML
【4月更文挑战第25天】Go语言的`text/template`和`html/template`库提供动态HTML生成。本文介绍了模板基础,如基本语法和数据绑定,以及常见问题和易错点,如忘记转义、未初始化变量、复杂逻辑处理和错误处理。建议使用`html/template`防止XSS攻击,初始化数据结构,分离业务逻辑,并严谨处理错误。示例展示了条件判断和循环结构。通过遵循最佳实践,开发者能更安全、高效地生成HTML。
25 0
|
2天前
科技感十足的动态HTML源码
科技感十足的动态HTML源码,源码由HTML+CSS+JS组成,记事本打开源码文件可以进行内容文字之类的修改,双击html文件可以本地运行效果,也可以上传到服务器里面
20 0
科技感十足的动态HTML源码
|
2天前
|
存储 移动开发 前端开发
如何写html邮件 —— 参考主流outook、gmail、qq邮箱渲染邮件过程
如何写html邮件 —— 参考主流outook、gmail、qq邮箱渲染邮件过程
26 1

热门文章

最新文章