SpringBoot 生成PDF
Thymeleaf企业级真实应用:将HTML界面数据转换为PDF输出
参考: https://blog.51cto.com/u_13146445/6190475
https://blog.csdn.net/qq_27242695/article/details/115654447
0. 需求
- 后端渲染pdf生成 (thymeleaf根据已有的html模板,springboot进行渲染后,生成pdf)
- pdf中可放置图片(图片非引入文件方式,而是采用Base64方式)
- 注意:html文件需单独开发,css样式需内嵌,不允许外置css文件
- 暂时未整理Echarts图表部分,如需可参考顶部url参考部分
1. Thymeleaf说明
# 什么是Thymeleaf
- Thymeleaf是一种现代化的服务器端Java模板引擎,可以用于Web和独立环境中的HTML、XML、JavaScript、CSS和文本。在实际开发中,Thymeleaf可以用于生成动态的HTML页面,支持将数据与模板进行绑定,生成最终的HTML内容。它是一个开源的软件,采用Apache许可证2.0进行发布。
# Thymeleaf具有特点
- 与其他服务器端Java模板引擎相比,Thymeleaf具有以下特点:
- 语法简单易懂,支持自然的HTML标签
- 支持HTML5的规范和特性
- 支持CSS样式的绑定和操作
- 支持表达式语言(Expression Language,简称EL)和Spring表达式语言(Spring Expression Language,简称SpEL)
- 支持标准和Spring MVC的多种模板渲染方式
- 支持多种模板缓存策略
- 支持可扩展的引擎架构
- 在实际开发中,Thymeleaf可以用于生成动态的HTML页面,支持将数据与模板进行绑定,生成最终的HTML内容。它可以作为Web应用程序的模板引擎,也可以作为其他应用程序的模板引擎。由于其简单易用的语法和强大的功能,Thymeleaf已经成为Java领域中最受欢迎的模板引擎之一。
2. 将HTML界面数据转换为PDF输出逻辑说明
# 中心思想
- 使用模板引擎的模板文件和数据模型。模板文件定义了最终输出的PDF页面的结构和样式,而数据模型则提供了模板中要填充的动态数据。
- 具体来说,Thymeleaf使用Java对象作为数据模型,可以通过Spring的控制器将数据注入到数据模型中。然后,Thymeleaf将数据模型与模板文件结合起来,生成HTML内容。最后,使用PDF生成库将HTML内容转换为PDF输出。
# 操作说明
- 在实现PDF输出功能时,可以使用Spring Boot提供的`spring-boot-starter-thymeleaf`依赖,该依赖包含了Thymeleaf、PDF生成库以及其他必需的依赖项。可以在控制器中使用Thymeleaf的`TemplateEngine`对象将数据模型和模板文件合并,生成HTML内容。然后,可以使用PDF生成库将HTML内容转换为PDF格式。
- 需要注意的是,PDF输出可能需要一些特定的CSS样式和HTML标记,以便正确呈现和格式化PDF页面。因此,在生成PDF输出之前,可能需要对模板文件进行调整和优化,以确保输出的PDF页面具有所需的外观和布局。
# 具体步骤
- 定义HTML模板,需要输出的数据以HTML格式创建一个模板,生成.HTML文件
- 引入Thymeleaf中TemplateEngine->生成文本输出的Java模板引擎框架、Context->Web应用程序的上下文对象。生成html 模板渲染工具。处理上边我们定义的模板。得到一个String类的结果
- 读取这个结果byte[],将byte数组 转换为 Base64字符串
- 最后将Base64字符串转换为PDF格式的数据,输出路径
3. 具体实现
3.1. 依赖
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- html 转 pdf 需要用的jar -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.6</version>
</dependency>
3.2. 定义好html模板
需要转成thymeleaf格式,如头部标签
@page为pdf的尺寸
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello World!</title>
<style>
.setAa {
background-color: red;
}
@page{
size:297mm 210mm;
}
</style>
</head>
<body>
<h1 class="setAa" th:text="'Hello, ' + ${name} + '!'"></h1>
<p th:text="'You are ' + ${age} + ' years old.'"></p>
<img alt="" th:src="${imgSrc}" style="margin:0 auto;"/>
</body>
</html>
3.3. html 模板渲染工具类
import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.BaseFont;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.annotation.Resource;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
/**
* @Author:wangdi
* @Date:2023/6/9 14:58
* @Des: HtmlToPdfUtil 转换pdf工具类
*/
@Component
public class HtmlToPDFUtil {
@Resource
private TemplateEngine templateEngine;
/**
* 使用 Thymeleaf 渲染 HTML
*
* @param template HTML模板路径
* @param params 渲染的参数
* @return 返回渲染后的html代码
* @throws Exception
*/
public String render(String template, Map<String, Object> params) {
Context context = new Context();
if (params.size() > 0) {
context.setVariables(params);
}
//将数据填充到模板里,开始处理模板
return templateEngine.process(template, context);
}
/**
* 根据html生成pdf的base64格式
*
* @param html
* @return
*/
public static String getPDFBase64ByHtml(String html) throws DocumentException, IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();//构建字节输出流
ITextRenderer renderer = new ITextRenderer();
ITextFontResolver fontResolver = renderer.getFontResolver();
//指定文件字体添加到PDF库,指定字体不作为内部字体,而是外部字体被加载
fontResolver.addFont("font/SimSun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
renderer.setDocumentFromString(html);
renderer.layout();
renderer.createPDF(baos);
return new BASE64Encoder().encode(baos.toByteArray());
}
/**
* 根据pdf的base64格式和路径生成pdf文件
*
* @param base64 pdf的base64格式
* @param path 生成pdf的路径
* @return
*/
public static String base64ToPDF(String base64, String path) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String fileAdd = sdf.format(new Date());
//先判断文件是否存在
path = path + "/" + fileAdd;
String fileName = path + "/" + System.currentTimeMillis() + ".pdf";//新的文件名
BufferedInputStream bin = null;
FileOutputStream fout = null;
BufferedOutputStream bout = null;
BASE64Decoder decoder = new BASE64Decoder();
try {
byte[] bytes = decoder.decodeBuffer(base64);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 创建从底层输入流中读取数据的缓冲输入流对象
bin = new BufferedInputStream(bais);
//获取文件夹路径
File file = new File(path);
//如果文件夹不存在则创建
if (!file.exists() && !file.isDirectory()) {
file.mkdirs();
}
// 创建到指定文件的输出流
fout = new FileOutputStream(fileName);
// 为文件输出流对接缓冲输出流对象
bout = new BufferedOutputStream(fout);
byte[] buffers = new byte[1024];
int len = bin.read(buffers);
while (len != -1) {
bout.write(buffers, 0, len);
len = bin.read(buffers);
}
// 刷新此输出流并强制写出所有缓冲的输出字节,必须这行代码,否则有可能有问题
bout.flush();
//返回存储的路径
return fileName;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bin.close();
fout.close();
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return "";
}
}
字体
SimSun.ttf 自行搜索下载
HTML中的字体,pdf不能识别,需添加字体文件
注意,在添加字体文件时,需要确保字体文件的路径正确,并且字体文件能够被读取到。此外,还需要确保字体文件的格式正确,可以使用BaseFont.IDENTITY_H指定字体编码,使用BaseFont.NOT_EMBEDDED指定字体文件是否嵌入到PDF文件中。
ITextRenderer说明
ITextRenderer是一个基于iText库的Java库,它可以将HTML、XHTML或XML等文档渲染成为PDF、XLS、PNG、JPEG等格式的文件。
ITextRenderer库提供了一个
ITextRenderer
类,该类提供了丰富的API,用于将HTML、XHTML或XML文档转换成为PDF等格式的文件。该类内部使用了iText库的PDF生成和操作功能,同时也支持使用Flying Saucer库对文档进行渲染和布局。使用ITextRenderer库进行PDF输出的基本流程如下:
1)创建一个ITextRenderer对象;
2)使用setDocument()方法将要转换的文档设置到渲染器中;
3)使用layout()方法对文档进行排版布局;
4)使用createPDF()方法将文档渲染为PDF,并输出到输出流或文件中。
ITextFontResolver说明
ITextFontResolver是ITextRenderer库中的一个类,它用于管理和解析字体文件,为PDF生成提供字体支持。
在ITextRenderer库中,当使用HTML文档生成PDF时,由于PDF不支持HTML中使用的所有字体,因此需要在生成PDF之前将HTML中的字体替换为PDF支持的字体。ITextFontResolver提供了一个addFont()方法,该方法用于将字体文件添加到ITextFontResolver中进行管理,以便在PDF生成时使用。
转换为Base64说明
Base64是一种用于将二进制数据转换成文本数据的编码方式,通过Base64编码可以将图片、音频、视频等二进制数据转换成文本数据,从而方便在网络上传输。
3.4. 图片转Base64 工具类
import org.apache.commons.lang.StringUtils;
import javax.xml.bind.DatatypeConverter;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* @Author:wangdi
* @Date:2023/6/9 15:34
* @Des: ImgBase64Util 图片转Base64 工具类
*/
public class ImgBase64Util {
public final static String IMG_PRE = "data:image/png;base64,";
public static void main(String[] args) throws Exception {
//本地图片地址
String url = "D:/Soft/IDEA/IDEA_PLUGINS/Img/727eee40a3202c6799fffe55c9d6a026.jpg";
//在线图片地址
String string = "https://devpress.csdnimg.cn/489fad64a62648818eaaebc28e5c8659.jpg";
String str = ImageToBase64ByLocal(url);
System.out.println(str);
String ste = ImageToBase64ByOnline(string);
// Base64Utils.Base64ToImage(str,"C:/Users/Administrator/Desktop/test1.jpg");
// Base64Utils.Base64ToImage(ste, "C:/Users/Administrator/Desktop/test2.jpg");
}
/**
* 本地图片转换成base64字符串
*
* @param imgFile 图片本地路径
* @return
*/
public static String ImageToBase64ByLocal(String imgFile) {// 将图片文件转化为字节数组字符串,并对其进行Base64编码处理
InputStream in = null;
byte[] data = null;
// 读取图片字节数组
try {
in = new FileInputStream(imgFile);
data = new byte[in.available()];
in.read(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return DatatypeConverter.printBase64Binary(data);
}
/**
* 在线图片转换成base64字符串
*
* @param imgURL 图片线上路径
* @return
*/
public static String ImageToBase64ByOnline(String imgURL) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
InputStream is = null;
try {
// 创建URL
URL url = new URL(imgURL);
byte[] by = new byte[1024];
// 创建链接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
is = conn.getInputStream();
// 将内容读取内存中
int len = -1;
while ((len = is.read(by)) != -1) {
data.write(by, 0, len);
}
// 关闭流
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return DatatypeConverter.printBase64Binary(data.toByteArray());
// // 对字节数组Base64编码
// BASE64Encoder encoder = new BASE64Encoder();
// return encoder.encode(data.toByteArray());
}
/**
* base64字符串转换成图片
*
* @param imgStr base64字符串
* @param imgFilePath 图片存放路径
* @return
*/
public static boolean Base64ToImage(String imgStr, String imgFilePath) { // 对字节数组字符串进行Base64解码并生成图片
if (StringUtils.isEmpty(imgStr)) // 图像数据为空
return false;
OutputStream out = null;
try {
byte[] b = DatatypeConverter.parseBase64Binary(imgStr);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {// 调整异常数据
b[i] += 256;
}
}
out = new FileOutputStream(imgFilePath);
out.write(b);
out.flush();
return true;
} catch (Exception e) {
return false;
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3.5. 测试使用
@Autowired
private HtmlToPDFUtil htmlToPDFUtil;
/*
* 渲染pdf注意:
* 要生成一个独立的html文件
* 其中css样式必须内嵌,不可以单独是css文件,否则会渲染不成功
* 图片的处理采用base64方式进行渲染,可以将图片全部保存到项目里面,采用代码转换base64进行塞参数渲染,或者直接在html中图片就使用base64格式
* */
@Test
public void changeTaskReport() throws Exception {
Map<String, Object> data = new HashMap();
data.put("name", "Alice");
data.put("age", 20);
// 此处的图片可以用相对路径,配合Thread.currentThread().getContextClassLoader().getResource("").getPath(); 获取路径使用,注意测试类启动的和SpringBoot启动的路径地址不一致
data.put("imgSrc", ImgBase64Util.IMG_PRE + ImgBase64Util.ImageToBase64ByLocal("C:\\Users\\wangdi13\\Desktop\\Snipaste_2023-06-09_15-43-25.png"));
String html = htmlToPDFUtil.render("test.html", data);
System.out.println(html);
String base64 = HtmlToPDFUtil.getPDFBase64ByHtml(html);
String pdfAdd = HtmlToPDFUtil.base64ToPDF(base64, "D:\\______________________________________WorkSpace\\demo-wy-test\\src\\main\\resources\\");
System.out.println(pdfAdd);
}