最近在做项目,有个需求为根据订单的详情生成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>