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

news2024/9/28 9:21:45

目录

  • 一、序言
  • 二、CSS样式控制打印模板
  • 三、代码示例
    • 1、pom.xml
    • 2、application.yml
    • 3、PdfGenerationController
    • 4、Freemarker模板内容
  • 四、展示效果

一、序言

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/381783.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从外行到外包,从手工测试到知名互联大厂测开,我经历了什么...

本人本科就读于某普通一本院校&#xff08;非985&#xff0c;211&#xff09;&#xff0c;经管类专业&#xff0c;从大四实习到15年毕业后前两年一直在从事自己专业相关的工作。17年时决定想要转业从事计算机相关领域工作&#xff0c;在17年9月的一个机遇大跨度转行到测试行业&…

vue子组件监听父组件数据变化并作出改变(亲测有效)

vue子组件监听父组件数据变化并作出改变&#xff08;亲测有效&#xff09; 1. 问题 1.1 封装组件时经常会遇到子组件需要根据父组件数据变化并执行对应的操作逻辑 1.2 监听方法中加了deep、immediate 等参数监听数组/对象还是没有生效 1.3 类型table组件需要根据父组件数据…

Java多线程学习——线程的创建、Thread类以及多线程状态

文章目录学习目标一、认识线程1、线程是什么&#xff1f;2、为什么要有线程3、进程和线程的区别二、Thread类以及常见方法1.创建线程的几种方式2、Thread类属性及方法2.1、Thread的常见构造方法2.2、Thread的常见属性3、线程的中断-interrupt()中断一个线程&#xff1a;4、等待…

前端面试题 —— 浏览器原理(一)

目录 一、进程与线程的概念 二、如何实现浏览器内多个标签页之间的通信? 三、浏览器资源缓存的位置有哪些&#xff1f; 四、对浏览器内核的理解 五、常见的浏览器内核比较 六、浏览器的主要组成部分 七、渲染过程中遇到 JS 文件如何处理&#xff1f; 八、什么情况会阻塞…

【C语言】动态内存管理

我们之前开辟的空间&#xff0c;大小固定&#xff0c;且在申明数组的时候&#xff0c;必须指定数组的长度。但是有时候我们需要的空间大小在程序运行的时候才知道&#xff0c;这就得动态内存开辟出马了。 目录 1.malloc和free 2.calloc 3.realloc 4.常见动态内存错误 5.经…

TCP 握手过程 三次 四次

蛋老师视频 SYN 同步 ACK 确认 FIN 结束 核心机制是确定哪些请求或响应需要丢弃 SYN、ACK、FIN 通过 1/0 设置开启/关闭 开启SYN后&#xff0c;报文中会随机生成 Sequence序号 用于校验 &#xff08;应用可能发起多个会话&#xff0c;可以区分&#xff09; 服务器的同步序…

2023版D盾防火墙v2.1.7.2,主动防御保护,以内外保护的方式 防止网站和服务器给入侵。限制了常见的入侵方法,让服务器更安全

v2.1.7.2 (20230107) 2023-1-7 1.修正PHP一处文件检测的bug。 2.修正某些情况下无法文件加白问题。 v2.1.7.2 2022-10-13 1.针对aspx的样本加入了新的识别。 2.针对上传 doc格式文件提示“上传格式不符” 的修正。 3.工具“HTTPS安全”,把 TLS 1.1 和 TLS 1.0 设置为默认不选中…

杰理AD16N简介

一、概述&#xff1a; AD16N是杰理新出的一个MP3解码芯片&#xff0c;是高集成度的 32 位通用音频 SOC&#xff0c; 集成 40KByte SRAM&#xff0c; 时钟源可选内部 RC 或外部12MHz 晶振&#xff0c; 最高主频可达 160MHz&#xff1b; 主要是替代AC109N系列和AC608N、AC104N系列…

Python爬虫书写时遇到的问题汇总

文章目录python的xpath插件需要的库下载出现问题懒加载python 爬取图片,网址都正确但是下不下来的原因:爬取下来的文字包含Windows不能识别的特殊字符selenium的find_element_by_id()出现的问题爬虫信息写入mysql时的1045号错误python的xpath插件需要的库下载出现问题 ERROR: C…

MySQL特殊语法insert into ... on duplicate key update ...

一、前言 在日常开发中&#xff0c;经常会遇到这样的需求&#xff1a;查看某条记录是否存在&#xff0c;不存在的话创建一条新记录&#xff0c;存在的话更新某些字段。 比如下列伪代码&#xff1a; $row mysql_query($result);if($row){mysql_execute(update ...);}else{my…

MongoDB复习

目录 1.docker安装 2.mondo概念解析 3.数据库操作 4.基本数据类型 5. 适合使用场景 6.对集合操作 7.常用操作 1.docker安装 docker pull mongo:latest docker run -d --restartalways -p 27017:27017 --name mymongo -v /data/db:/data/db -d mongo docker exec -it m…

【SpringBoot高级篇】SpringBoot集成Sharding-JDBC分库分表

【SpringBoot高级篇】SpringBoot集成Sharding-JDBC分库分表Apache ShardingSphere分库分表分库分表的方式垂直切分垂直分表垂直分库水平切分水平分库水平分表分库分表带来的问题分库分表中间件Sharding-JDBCsharding-jdbc实现水平分表sharding-jdbc实现水平分库sharding-jdbc实…

数据结构-考研难点代码突破(查找算法 - 散列表(哈希表)C++实现除留余数法拉链法哈希)

文章目录1. 哈希表与解决哈希冲突的方法2. C实现除留余数法拉链法哈希1. 哈希表与解决哈希冲突的方法 散列表(Hash Table)&#xff0c;又称哈希表。是一种数据结构。 特点&#xff1a;数据元素的关键字与其存储地址直接相关。 关键字通过散列函数&#xff08;哈希函数&#…

Vue3.0文档整理:2、创建单页面应用程序

2.1&#xff1a;创建步骤 2.1.1&#xff1a;vue-cli 安装并执行create-vue:npm init vuelatest 它是Vue官方的项目脚手架工具 选择项目功能 除了第一项的项目名字外&#xff0c;其他可以暂时默认回撤或者选择No 切换到项目目录:cd <your-project-name> 安装项目依赖&…

山寨APP频出?安全工程师和黑灰产在较量

在山寨这个领域&#xff0c;没有人比黑灰产更懂模仿。 据安全从业者介绍&#xff0c;一般而言&#xff0c;对于成熟的山寨开发者来说&#xff0c;几天时间内就可以做出一套前端框架。服务器、源代码、域名、服务商这些内容的创建&#xff0c;通过网上租赁的方式就可以解决。 比…

【面试题】2023前端vue面试题及答案

Vue3.0 为什么要用 proxy&#xff1f;在 Vue2 中&#xff0c; 0bject.defineProperty 会改变原始数据&#xff0c;而 Proxy 是创建对象的虚拟表示&#xff0c;并提供 set 、get 和 deleteProperty 等处理器&#xff0c;这些处理器可在访问或修改原始对象上的属性时进行拦截&…

Window问题详解(下)

建议先看一下 Window问题详解(上) 思路② 既然会超时,那该怎么办呢? 显然需要一个更快速的方法来解决这个问题! 我们先来观察一下图片: 我们发现,每一次选中的数都会增加下一个。 !!!!! 因此,我们可以根据此特性优化时间!! 第一次先求出前 k − 1 k-1 k−

hdfs file system shell的简单使用

文章目录1、背景2、hdfs file system shell命令有哪些3、确定shell操作的是哪个文件系统4、本地准备如下文件5、hdfs file system shell5.1 mkdir创建目录5.2 put上传文件5.3 ls查看目录或文件5.4 cat 查看文件内容5.5 head 查看文件前1000字节内容5.6 tail 查看文件后1000字节…

Kubernetes12:k8s集群安全机制 ***与证书生成***

Kubernetes12&#xff1a;k8s集群安全机制 1、概述 1&#xff09;访问一个k8s集群的时候&#xff0c;需要经过以下三个步骤才能完成具体操作 第一步&#xff1a;认证操作第二部&#xff1a;鉴权操作&#xff08;授权&#xff09;第三部&#xff1a;准入控制操作 2&#xff…

小白晋升大牛的13个项目

入门到放弃 “C/C真的太难学了,我准备放弃了!” 很多初学者在学完C和C的基本语法后&#xff0c;就停滞不前了&#xff0c;最终走向“从入门到放弃”。其实&#xff0c;我们初学者最需要的不是HelloWorld&#xff0c;也不是语法知识的堆砌&#xff0c;需要的只是实战项目的磨砺…