最近在做项目,有个需求为根据订单的详情生成pdf并能够支持pdf的预览功能。为了能够简单的实现功能,就使用了浏览器自带的预览pdf功能实现。
环境准备
具体的环境如下:
- jdk 8
- maven 3.6.3
- idea
pom 依赖
为了能够快速的启动项目,则使用spring-boot作为依赖,则对应的pom文件如下:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-openpdf</artifactId>
<version>9.1.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.9</version>
</dependency>
</dependencies>
相关配置
在实例中主要使用了以下框架:
- freemarker用于生成html文本
- flying-saucer-pdf-openpdf 用于生成pdf文档
freemarker
freemarker主要配置了模板的路径信息,主要配置如下:
spring:
freemarker:
enabled: true
template-loader-path: classpath:/templates/
在对应的订单详情生成模板:
<html>
<head>
<title>${title}</title>
<style>
body {
MARGIN: AUTO;
width: 690px;
font-size: 12px;
font-family: SimSun;
color: #222;
}
</style>
</head>
<body>
<table>
<tbody>
<tr>
<td>订单编号:</td>
<td>${orderNo}</td>
<td>订单时间:</td>
<td>${orderDate}</td>
</tr>
<tr>
<td>收货地址:</td>
<td>${address}</td>
<td>联系人:</td>
<td>${userName}</td>
</tr>
</tbody>
</table>
</body>
</html>
业务逻辑
业务实体Order
order中主要定义了订单相关信息,
package org.spring.learn.pdf.entity;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class Order {
private String orderNo;
private String title;
private String userName;
private String address;
private String orderDate;
}
业务逻辑
业务逻辑主要根据订单相关信息填充模板,并生成html, 并根据html生成对应的pdf信息,则对应业务代码如下;
package org.spring.learn.pdf.service;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import com.lowagie.text.pdf.BaseFont;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.spring.learn.pdf.entity.Order;
import org.spring.learn.pdf.util.StreamUtil;
import org.springframework.stereotype.Service;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.*;
import java.util.Date;
@Service
public class OrderService {
public static final String ORDER_TPL = "order.ftl";
private Configuration configuration;
public OrderService(Configuration configuration) {
this.configuration = configuration;
}
public File previewPdf(String orderNo) throws IOException, TemplateException {
// 模拟创建订单
Order order = Order.builder()
.orderDate(DateUtil.formatDate(new Date()))
.orderNo(orderNo)
.title("订单: " + orderNo)
.address("收货地址")
.userName("测试用户")
.build();
// 写出pdf到临时文件
String pathPrefix = OrderService.class.getResource("/").getPath();
String filePath = pathPrefix + File.separator + "/temp/" + UUID.randomUUID().toString() + ".pdf";
File file = new File(filePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
FileOutputStream fios = new FileOutputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Template template = configuration.getTemplate(ORDER_TPL);
template.process(order, new OutputStreamWriter(baos));
// 获取html信息
String htmlStr = new String(baos.toByteArray());
// 将html转转为pdf
ITextRenderer iTextRenderer = new ITextRenderer();
SharedContext sharedContext = iTextRenderer.getSharedContext();
sharedContext.setPrint(true);
sharedContext.setInteractive(false);
ITextFontResolver fontResolver = iTextRenderer.getFontResolver();
String fontPath = OrderService.class.getResource("/simsun.ttc").getPath();
fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
iTextRenderer.setDocumentFromString(htmlStr);
iTextRenderer.layout();
// 写出
iTextRenderer.createPDF(fios);
StreamUtil.close(fios);
return file;
}
}
Controller实现
Controller主要接收请求,写出数据,则对应代码如下:
package org.spring.learn.pdf.controller;
import lombok.extern.slf4j.Slf4j;
import org.spring.learn.pdf.service.OrderService;
import org.spring.learn.pdf.util.StreamUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderPdfPreviewController {
private OrderService orderService;
public OrderPdfPreviewController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/pdf/preview/{orderNo}")
public void previewPdf(@PathVariable("orderNo") String orderNo, HttpServletResponse response) {
FileInputStream fis = null;
try {
File f = orderService.previewPdf(orderNo);
response.setContentType("application/pdf");
// 获取文件输入流
fis = new FileInputStream(f);
FileChannel fileChannel = fis.getChannel();
fileChannel.transferTo(0, fileChannel.size(), Channels.newChannel(response.getOutputStream()));
} catch (Exception e) {
log.error("previewPdf: 预览pdf失败, 原因: {}", e.getMessage(), e);
} finally {
StreamUtil.close(fis);
}
}
}
启动类
spring-boot启动类如下:
package org.spring.learn.pdf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PdfApplication {
public static void main(String[] args) {
SpringApplication.run(PdfApplication.class, args);
}
}
我们启动后端的服务,并访问http://localhost:8080/order/pdf/preview/234, 则可以看到浏览器打开了pdf文档,如图:

问题
1.如何控制PDF分页
当我们希望单个订单单独显示为一个PDF页的时候,由于每个订单的长度不一致,所以需要控制PDF分页信息,这时,我们可以只修改html的样式接口实现,例如:
<html>
<head>
<title>${title}</title>
<style>
body {
MARGIN: AUTO;
width: 690px;
font-size: 12px;
font-family: SimSun;
color: #222;
}
.pdfpage {
width: 100%;
page-break-inside: avoid;
}
</style>
</head>
<body>
<#list orders as order>
<table class="pdfpage">
<tbody>
<tr>
<td>订单编号:</td>
<td>${order.orderNo}</td>
<td>订单时间:</td>
<td>${order.orderDate}</td>
</tr>
<tr>
<td>收货地址:</td>
<td>${order.address}</td>
<td>联系人:</td>
<td>${order.userName}</td>
</tr>
</tbody>
</table>
</#list>
</body>
</html>
主要通过page-break-inside控制分页的参数信息,问题的到解决。
2.设置PDF页方向
有时我们生成的PDF因为内容比较长,可能会导致部分内容被截取,这个时候我们可以设置PDF的页为横向,展示超过的部分内容。只需要在html中加入@page样式即可
/*横向*/
@page{
size: 297mm 210mm;
}
/*纵向*/
@page {
size: 210mm 297mm;
}
3. 默认生成pdf中文不展示
这个是因为flying-saucer-pdf-openpdf默认不支持中文,需要我们用自己的字体替代,我们可以在C:/windows/fonts中选择宋体,并拷贝到项目的resources中,则可以通过代码的方式加入的框架,并在html中使用字体即可。
在代码中可以加入如下代码:
ITextFontResolver fontResolver = iTextRenderer.getFontResolver();
String fontPath = OrderService.class.getResource("/simsun.ttc").getPath();
fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
在html模板中使用引入的字体,字体需要区分大小写:
<style>
body {
MARGIN: AUTO;
width: 690px;
font-size: 12px;
font-family: SimSun;
color: #222;
}
</style>