最近项目中需要生成日报文件,日报文件的格式为pdf,且日报的样式相对而言比较复杂,存在多段文字,存在多个表格,且存在样式。目前想到的解决办法是先生成html文件,让后将html文件转换成pdf文件。通过网上搜索,发现openhtmltopdf可以实现我们的需求,此处记录一下。
1、html的生成,我们可以通过freemarker来实现。
2、html转pdf,通过openhtmltopdf来实现。
首先搭建一个简单的可运行的程序,可实现Freemarker渲染模板,然后生成pdf文件
| <dependencies> |
| <dependency> |
| <groupId>org.springframework.boot</groupId> |
| <artifactId>spring-boot-starter-web</artifactId> |
| <version>2.6.0</version> |
| </dependency> |
| <!-- 模板引擎,用于渲染html --> |
| <dependency> |
| <groupId>org.freemarker</groupId> |
| <artifactId>freemarker</artifactId> |
| <version>2.3.30</version> |
| </dependency> |
| <!-- 用于将html转换成pdf --> |
| <dependency> |
| <groupId>com.openhtmltopdf</groupId> |
| <artifactId>openhtmltopdf-pdfbox</artifactId> |
| <version>1.0.10</version> |
| </dependency> |
| <dependency> |
| <groupId>org.projectlombok</groupId> |
| <artifactId>lombok</artifactId> |
| <version>1.18.36</version> |
| </dependency> |
| </dependencies> |
加载程序中src/main/resources/templates/ftls目录下的模板文件,然后渲染成html内容。
| package com.huan.pdf.utils; |
| import freemarker.cache.ClassTemplateLoader; |
| import freemarker.template.Configuration; |
| import freemarker.template.Template; |
| import freemarker.template.TemplateExceptionHandler; |
| import lombok.extern.slf4j.Slf4j; |
| import java.io.StringWriter; |
| import java.util.Map; |
| /** |
| * freemarker 工具类 |
| * |
| * @author admin |
| */ |
| public class FreemarkerUtils { |
| /** |
| * 模板文件夹路径 |
| */ |
| private static final String TEMPLATE_DIR = "/templates/ftls"; |
| private static final Configuration CONFIGURATION; |
| static { |
| CONFIGURATION = new Configuration(Configuration.VERSION_2_3_30); |
| CONFIGURATION.setTemplateLoader(new ClassTemplateLoader(FreemarkerUtils.class, TEMPLATE_DIR)); |
| CONFIGURATION.setDefaultEncoding("UTF-8"); |
| CONFIGURATION.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); |
| CONFIGURATION.setLogTemplateExceptions(false); |
| CONFIGURATION.setWrapUncheckedExceptions(true); |
| } |
| /** |
| * 根据模板名称和数据模型生成字符串 |
| * |
| * @param templateName 模板名称 |
| * @param dataModel 数据模型 |
| * @return 生成的字符串 |
| */ |
| public static String processTemplate(String templateName, Map<String, Object> dataModel) { |
| try { |
| Template template = CONFIGURATION.getTemplate(templateName); |
| StringWriter writer = new StringWriter(); |
| template.process(dataModel, writer); |
| return writer.toString(); |
| } catch (Exception e) { |
| log.error("解析模板出现问题", e); |
| } |
| return ""; |
| } |
| } |
编写pdf工具类,用于将html内容渲染成pdf文件,此处只是简单实现,后期该类还需要修改
| package com.huan.pdf.utils; |
| import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; |
| import lombok.extern.slf4j.Slf4j; |
| import javax.servlet.http.HttpServletResponse; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.UUID; |
| /** |
| * pdf工具类 |
| * |
| * @author admin |
| */ |
| public class PdfUtils { |
| /** |
| * 生成pdf文件 |
| * |
| * @param pdfTemplate pdf模板 |
| * @param response http response |
| */ |
| public static void generatePdf(String pdfTemplate, HttpServletResponse response) { |
| // 设置响应头 |
| String fileName = UUID.randomUUID() + ".pdf"; |
| response.setContentType("application/pdf"); |
| response.setHeader("Content-Disposition", "attachment; filename=" + fileName); |
| try (OutputStream os = response.getOutputStream()) { |
| PdfRendererBuilder builder = new PdfRendererBuilder(); |
| builder.withHtmlContent(pdfTemplate, null); |
| builder.toStream(os); |
| builder.run(); |
| } catch (IOException e) { |
| log.error("生成pdf文件失败", e); |
| throw new RuntimeException("生成pdf文件失败", e); |
| } |
| } |
| } |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <title>生成pdf</title> |
| <style> |
| .main-title { text-align: center; font-size:25px; } |
| </style> |
| </head> |
| <body> |
| <div class="main-title">${mainTitle}</div> |
| </body> |
| </html> |
该模板中存在变量mainTitle,这个变量的值通过后台来赋值
| package com.huan.pdf.controller; |
| import com.huan.pdf.utils.FreemarkerUtils; |
| import com.huan.pdf.utils.PdfUtils; |
| import org.springframework.web.bind.annotation.GetMapping; |
| import org.springframework.web.bind.annotation.RestController; |
| import javax.servlet.http.HttpServletResponse; |
| import java.time.LocalDateTime; |
| import java.time.format.DateTimeFormatter; |
| import java.util.HashMap; |
| import java.util.Map; |
| /** |
| * pdf控制器 |
| * |
| * @author admin |
| */ |
| public class PdfController { |
| public void pdf(HttpServletResponse response) { |
| Map<String, Object> params = new HashMap<>(16); |
| params.put("mainTitle", "这是一个标题 - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); |
| // 渲染模板 |
| String htmlContent = FreemarkerUtils.processTemplate("pdf.ftl", params); |
| // 生成pdf |
| PdfUtils.generatePdf(htmlContent, response); |
| } |
| } |
注意:此处的mainTitle中存在中文,生产的Pdf会乱码待会儿在处理
可以看到可以正常的生成pdf了,但是中文乱码了。 至此我们一个简单的程序就搭建完成了,下面让我们来完善功能。
默认情况下生成的pdf,中文是乱码的,若需要解决这个问题,就需要引入中文字体。此处我们使用宋体。
在程序的src/main/resources/fonts目录下,引入宋体(simsun.ttf)
| builder.useFont(() -> PdfUtils.class.getClassLoader().getResourceAsStream("fonts/simsun.ttf"), "SimSun"); |
| <style> |
| body { font-family: "SimSun"; font-size: 16px; line-height: 1.5; color: #000;} |
| </style> |
从上图中可以看到,现在已经可以展示中文了。
此处实现将生成的pdf中的 这是一个标题-时间 这句话的字体修改成红色。
| .main-title { text-align: center; font-size:25px; color:#FF0000; } |
通过上图可知,样式已经生效了。
| <style> |
| table { border-collapse: collapse; } |
| td { border: 1px solid black; padding: 70px;} |
| </style> |
| <table> |
| <tr><td>序号</td></tr> |
| <tr><td>1</td></tr> |
| <tr><td>2</td></tr> |
| <tr><td>3</td></tr> |
| <tr><td>4</td></tr> |
| <tr><td>5</td></tr> |
| </table> |
从上图可以看到,生成的pdf,内容跨了2页,那么如何解决这个问题呢?通过css样式解决
| table { border-collapse: collapse; page-break-inside: auto;} |
| tr { page-break-inside: avoid;} |
通过css样式page-break-before:always开启新的一页pdf。
默认情况是A4 纵向,现在我想修改成A3 横向。这个指定对所有的页面都生效,不可只对某一个页面生效,若想对某一个页面生效,可以生成多个pdf文件,然后进行pdf文件的合并操作。
| @page{ size:A3 landscape; } |
从上图中可知 正好是A3横向
实现思路:通过pdfbox生成加密的密码,此处给默认密码a0nin13s
| /** |
| * 生成带密码的 PDF 文件(用户密码 a0min13s) |
| * |
| * @param pdfTemplate HTML 模板字符串 |
| * @param response HTTP 响应 |
| */ |
| public static void generatePdf(String pdfTemplate, HttpServletResponse response) { |
| String fileName = UUID.randomUUID() + ".pdf"; |
| response.setContentType("application/pdf"); |
| response.setHeader("Content-Disposition", "attachment; filename=" + fileName); |
| // 1. 先用 openhtmltopdf 生成未加密 PDF(内存) |
| ByteArrayOutputStream temp = new ByteArrayOutputStream(); |
| try { |
| PdfRendererBuilder builder = new PdfRendererBuilder(); |
| builder.useFont(() -> PdfUtils.class.getClassLoader().getResourceAsStream("fonts/simsun.ttf"), "SimSun"); |
| builder.withHtmlContent(pdfTemplate, null); |
| builder.toStream(temp); |
| // 完成渲染 |
| builder.run(); |
| } catch (IOException e) { |
| log.error("生成PDF失败", e); |
| throw new RuntimeException("生成PDF失败"); |
| } |
| // 用 PDFBox 加载并加密 |
| try (PDDocument doc = PDDocument.load(temp.toByteArray()); |
| OutputStream os = response.getOutputStream()) { |
| AccessPermission ap = new AccessPermission(); |
| // 可选:禁止打印、复制等 |
| ap.setCanPrint(false); |
| ap.setCanExtractContent(false); |
| // 用户密码,所有者密码一样即可(也可设不同) |
| StandardProtectionPolicy policy = |
| // ownerPwd userPwd |
| new StandardProtectionPolicy("a0min13s", "a0min13s", ap); |
| // 128 位 AES |
| policy.setEncryptionKeyLength(128); |
| policy.setPermissions(ap); |
| // 执行加密 |
| doc.protect(policy); |
| // 写给浏览器 |
| doc.save(os); |
| // 确保全部送出 |
| os.flush(); |
| } catch (IOException e) { |
| log.error("PDF加密输出失败", e); |
| throw new RuntimeException("PDF加密输出失败"); |
| } |
| } |
-----------------------------
客服QQ: 519751977
客服微信: qq2facai
服务时间: 9:00-18:00