Java中实现html转pdf

目录

  • 1、背景
  • 2、需求
  • 3、思路
  • 4、实现步骤
    • [4.1 搭建一个简单的工程](#4.1 搭建一个简单的工程)
      • [4.1.1 引入依赖](#4.1.1 引入依赖)
      • [4.1.2 编写Freemarker工具类](#4.1.2 编写Freemarker工具类)
      • [4.1.3 编写pdf工具类](#4.1.3 编写pdf工具类)
      • [4.1.4 增加一个模板](#4.1.4 增加一个模板)
      • [4.1.5 增加一个控制层](#4.1.5 增加一个控制层)
      • [4.1.6 运行](#4.1.6 运行)
    • [4.2 功能完善](#4.2 功能完善)
      • [4.2.1 生成的pdf需要支持中文](#4.2.1 生成的pdf需要支持中文)
        • [4.2.1.1 程序中引入宋体](#4.2.1.1 程序中引入宋体)
        • [4.2.1.2 pdf工具类中增加使用中文字体](#4.2.1.2 pdf工具类中增加使用中文字体)
        • [4.2.1.3 freemarker模板中使用中文字体](#4.2.1.3 freemarker模板中使用中文字体)
        • [4.2.1.4 运行](#4.2.1.4 运行)
      • [4.2.2 生成的pdf支持简单的样式](#4.2.2 生成的pdf支持简单的样式)
        • [4.2.2.1 freemarker模板中使用css样式](#4.2.2.1 freemarker模板中使用css样式)
        • [4.2.2.2 运行](#4.2.2.2 运行)
      • [4.2.3 表格的某一行不要出现跨页](#4.2.3 表格的某一行不要出现跨页)
        • [4.2.3.1 freemarker模板中增加一个表格](#4.2.3.1 freemarker模板中增加一个表格)
        • [4.2.3.2 查看效果](#4.2.3.2 查看效果)
        • [4.3.2.3 css解决](#4.3.2.3 css解决)
        • [4.3.2.4 查看效果](#4.3.2.4 查看效果)
      • [4.2.4 单独开启一页pdf](#4.2.4 单独开启一页pdf)
        • [4.2.4.1 freemarker模板修改](#4.2.4.1 freemarker模板修改)
        • [4.2.4.2 查看效果](#4.2.4.2 查看效果)
      • [4.2.5 指定pdf页面的规格](#4.2.5 指定pdf页面的规格)
        • [4.2.5.1 css样式指定页面规则](#4.2.5.1 css样式指定页面规则)
        • [4.2.5.2 查看效果](#4.2.5.2 查看效果)
      • [4.2.6 pdf 加密](#4.2.6 pdf 加密)
        • [4.2.6.1 修改pdf生成的工具类](#4.2.6.1 修改pdf生成的工具类)
        • [4.2.6.2 查看效果](#4.2.6.2 查看效果)
  • 5、完整代码

1、背景

最近项目中需要生成日报文件,日报文件的格式为pdf,且日报的样式相对而言比较复杂,存在多段文字,存在多个表格,且存在样式。目前想到的解决办法是
先生成html文件,让后将html文件转换成pdf文件。通过网上搜索,发现openhtmltopdf可以实现我们的需求,此处记录一下。

2、需求

  1. 生成的pdf需要支持中文。
  2. 生成的pdf支持简单的样式。(此处可以使用css样式来解决,但不是所有的css样式都支持)
  3. 生成的pdf存在表格,每行应完整地出现在同一页,不要一半在上一页、一半在下一页。
  4. 生成的pdf可以自己指定到分页,比如某个表格的数据渲染完之后,需要单独开启一页。
  5. 生成的pdf支持密码加密。
  6. 生成的pdf可以支持纸张规格,比如是A3还是A4,并且还可设置横向还是纵向。

3、思路

1、html的生成,我们可以通过freemarker来实现。

2、html转pdf,通过openhtmltopdf来实现。

4、实现步骤

4.1 搭建一个简单的工程

首先搭建一个简单的可运行的程序,可实现Freemarker渲染模板,然后生成pdf文件

4.1.1 引入依赖

xml 复制代码
<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>

4.1.2 编写Freemarker工具类

加载程序中src/main/resources/templates/ftls目录下的模板文件,然后渲染成html内容。

java 复制代码
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
 */
@Slf4j
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 "";
    }
}

4.1.3 编写pdf工具类

编写pdf工具类,用于将html内容渲染成pdf文件,此处只是简单实现,后期该类还需要修改

java 复制代码
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
 */
@Slf4j
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);
        }
    }
}

4.1.4 增加一个模板

html 复制代码
<!DOCTYPE html>
<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,这个变量的值通过后台来赋值

4.1.5 增加一个控制层

java 复制代码
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
 */
@RestController
public class PdfController {

    @GetMapping("pdf")
    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会乱码待会儿在处理

4.1.6 运行

可以看到可以正常的生成pdf了,但是中文乱码了。 至此我们一个简单的程序就搭建完成了,下面让我们来完善功能。

4.2 功能完善

4.2.1 生成的pdf需要支持中文

默认情况下生成的pdf,中文是乱码的,若需要解决这个问题,就需要引入中文字体。此处我们使用宋体

4.2.1.1 程序中引入宋体

在程序的src/main/resources/fonts目录下,引入宋体(simsun.ttf)

4.2.1.2 pdf工具类中增加使用中文字体

java 复制代码
builder.useFont(() -> PdfUtils.class.getClassLoader().getResourceAsStream("fonts/simsun.ttf"), "SimSun");

4.2.1.3 freemarker模板中使用中文字体

css 复制代码
<style>
   body { font-family: "SimSun";  font-size: 16px; line-height: 1.5; color: #000;}
</style>

4.2.1.4 运行

从上图中可以看到,现在已经可以展示中文了。

4.2.2 生成的pdf支持简单的样式

此处实现将生成的pdf中的 这是一个标题-时间 这句话的字体修改成红色

4.2.2.1 freemarker模板中使用css样式

css 复制代码
.main-title { text-align: center; font-size:25px; color:#FF0000; }

4.2.2.2 运行

通过上图可知,样式已经生效了。

4.2.3 表格的某一行不要出现跨页

4.2.3.1 freemarker模板中增加一个表格

html 复制代码
<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>

4.2.3.2 查看效果

从上图可以看到,生成的pdf,内容跨了2页,那么如何解决这个问题呢?通过css样式解决

4.3.2.3 css解决

css 复制代码
 table { border-collapse: collapse; page-break-inside: auto;}
 tr { page-break-inside: avoid;}

4.3.2.4 查看效果

4.2.4 单独开启一页pdf

4.2.4.1 freemarker模板修改

通过css样式page-break-before:always开启新的一页pdf。

4.2.4.2 查看效果

4.2.5 指定pdf页面的规格

默认情况是A4 纵向,现在我想修改成A3 横向。这个指定对所有的页面都生效,不可只对某一个页面生效,若想对某一个页面生效,可以生成多个pdf文件,然后进行pdf文件的合并操作。

4.2.5.1 css样式指定页面规则

css 复制代码
@page{ size:A3 landscape; }

4.2.5.2 查看效果

从上图中可知 正好是A3横向

4.2.6 pdf 加密

实现思路:通过pdfbox生成加密的密码,此处给默认密码a0nin13s

4.2.6.1 修改pdf生成的工具类

java 复制代码
/**
 * 生成带密码的 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加密输出失败");
    }
}

4.2.6.2 查看效果

5、完整代码

https://gitee.com/huan1993/spring-cloud-parent/tree/master/pdf/openhtmltopdf

相关推荐
野犬寒鸦3 小时前
从零起步学习Redis || 第二章:Redis中数据类型的深层剖析讲解(下)
java·redis·后端·算法·哈希算法
王者鳜錸3 小时前
方言普通话识别大模型,支持中英+202种方言识别
java·vue·语音识别
haokan_Jia3 小时前
【springboot的分页功能TableDataInfo,有时候需要复杂的分页实现,怎么办呢?】
java·spring boot·后端
Terio_my3 小时前
Java 后端面试技术文档(参考)
java·开发语言·面试
南部余额4 小时前
Maven 依赖管理与版本优化
java·maven
create174 小时前
IntelliJ IDEA 等软件如何与 AI 编程工具(Cursor、Windsurf、Qoder等)实现互相跳转
java·ide·人工智能·intellij-idea
望获linux4 小时前
论文解读:利用中断隔离技术的 Linux 亚微秒响应性能优化
java·linux·运维·前端·arm开发·数据库·性能优化
龙茶清欢4 小时前
7、微服务中 DTO、VO、PO、BO 的设计规范
java·spring boot·spring cloud
ToneChip4 小时前
配合AI自动生成时序图--最详细在VS Code中使用PlantUML绘图
java