功能实现——使用 OpenPDF 将 HTML 转换为 PDF,并将其上传到 FTP 服务器

news2024/11/15 22:42:23

目录

  • 1.需求分析
  • 2.项目环境搭建
  • 3.将 HTML 转换为 PDF
    • 3.1.代码实现
      • mail.html
      • HtmlToPDFController.java
      • PDFConverterService.java
      • PDFConverterServiceImpl.java
    • 3.2.测试
    • 3.3.注意事项
  • 4.将生成的 PDF 上传到 FTP 服务器
    • 4.1.搭建 FTP 服务器
    • 4.2.配置文件
    • 4.3.代码实现
      • FtpUtil.java
      • FTPController.java
    • 4.4.测试

1.需求分析

使用 OpenPDF 将 HTML 文件转换为 PDF,并将其上传到 FTP 服务器。

2.项目环境搭建

(1)在 IDEA 中创建一个 Spring Boot 项目,具体可以参考【环境搭建】使用IDEA创建SpringBoot项目详细步骤这篇文章。

(2)pom.xml 中添加如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.16.2</version>
</dependency>

<dependency>
    <groupId>com.github.librepdf</groupId>
    <artifactId>openpdf</artifactId>
    <version>1.3.32</version>
</dependency>

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf-openpdf</artifactId>
    <version>9.3.1</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
</dependency>

<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.6</version>
</dependency>

3.将 HTML 转换为 PDF

3.1.代码实现

mail.html

邮件模板 mail.html 如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>入职欢迎邮件</title>

    <style>
        body {
            font-family: SimHei;
        }
    </style>

</head>
<body>
  欢迎 <span th:text="${name}"></span> 加入 XXXX 大家庭,您的入职信息如下:
  <table border="1">
      <tr>
          <td>姓名</td>
          <td th:text="${name}"></td>
      </tr>
      <tr>
          <td>职位</td>
          <td th:text="${posName}"></td>
      </tr>
      <tr>
          <td>职称</td>
          <td th:text="${jobLevelName}"></td>
      </tr>
      <tr>
          <td>部门</td>
          <td th:text="${departmentName}"></td>
      </tr>
  </table>
  <p>我们公司的工作忠旨是严格,创新,诚信,您的加入将为我们带来新鲜的血液,带来创新的思维,
    以及为我们树立良好的公司形象!希望在以后的工作中我们能够齐心协力,与时俱进,团结协作!
    同时也祝您在本公司,工作愉快,实现自己的人生价值!希望在未来的日子里,携手共进!</p>
</body>
</html>

HtmlToPDFController.java

package com.example.htmltopdf.controller;

import com.example.htmltopdf.service.PDFConverterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/htp")
public class HtmlToPDFController {

    @Autowired
    private PDFConverterService pdfConverterService;

    @PostMapping("/converter")
    public String htmlToPDF() {
        pdfConverterService.convertHtmlToPDF();
        return "success";
    }

}

PDFConverterService.java

package com.example.htmltopdf.service;

public interface PDFConverterService {

    void convertHtmlToPDF();

}

PDFConverterServiceImpl.java

package com.example.htmltopdf.service.impl;

import com.example.htmltopdf.service.PDFConverterService;
import com.lowagie.text.pdf.BaseFont;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
public class PDFConverterServiceImpl implements PDFConverterService {

    @Autowired
    private TemplateEngine templateEngine;

    @Override
    public void convertHtmlToPDF() {
        Context context = new Context();
        context.setVariables(assembleParameters());
        System.out.println("Processing template...");
        String htmlContent = templateEngine.process("mail", context);
        
        Document doc = Jsoup.parse(htmlContent, "utf-8");
        //默认是以 html 的方式
        doc.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
        //需改用 xml 的方式格式,否则用 openPdf 转化时 PDF 时可能排版错乱
        byte[] pdfBytes = html2PDF(doc.outerHtml());

        //文件保存本地路径
        String filePath = "output.pdf";
        try (FileOutputStream fos = new FileOutputStream(filePath)) {
            //将 byte[] 数据写入文件
            fos.write(pdfBytes);
            log.info("PDF 文件保存成功:{}", filePath);
        } catch (IOException e) {
            log.info("保存 PDF 文件时出现错误:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    public Map<String, Object> assembleParameters() {
        HashMap<String, Object> map = new HashMap<>();
        map.put("name", "Tom");
        map.put("posName", "software");
        map.put("jobLevelName", "高级");
        map.put("departmentName", "软件部");
        return map;
    }

    public byte[] html2PDF(String html) {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            ITextRenderer renderer=new ITextRenderer();

            // 加载字体文件(SimHei为示例,请根据实际字体文件替换路径)
            String fontFile = "/static/fonts/SimHei.ttf";
            renderer.getFontResolver().addFont(fontFile, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

            renderer.setDocumentFromString(html);
            renderer.layout();
            renderer.createPDF(outputStream);
            return outputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new byte[0];
    }

}

3.2.测试

启动项目后,在 Postman 中进行接口测试(注意是 POST 请求):

http://localhost:8080/htp/converter

运行成功后会发现已经生成了如下 PDF 文件:

在这里插入图片描述

PDF 中的内容如下:

在这里插入图片描述

3.3.注意事项

在 HTML 转为 PDF 时,如果页面存在中文字符,可能会出现转换后中文字符不显示的情况!本文的解决办法如下:

(1)在 HTML 页面中设置字体系列(下面以黑体 SimHei 为例):

<style>
    body {
        font-family: SimHei;
    }
</style>

(2)使用 ITextRenderer 转换时加载对应字体(具体见 html2PDF 方法):

// 加载字体文件(SimHei为示例,请根据实际字体文件替换路径)
String fontFile = "/static/fonts/SimHei.ttf";
renderer.getFontResolver().addFont(fontFile, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

4.将生成的 PDF 上传到 FTP 服务器

相关链接:
Java实现文件上传到ftp服务器
Java从ftp服务器上传与下载文件

4.1.搭建 FTP 服务器

下面所使用的 TFP 服务器搭建在 CentOS 7 上,具体搭建过程可见 Linux - 搭建 FTP 服务器这篇文章。

4.2.配置文件

application.yml 中的内容如下:

server:
  port: 8080

ftp:
  server:
    host: 192.168.101.65
    port: 21
    username: sc
    password: 123
    remoteDir: /home/sc

4.3.代码实现

FtpUtil.java

package com.example.htmltopdf.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;

import java.io.*;
import java.nio.charset.StandardCharsets;

@Slf4j
public class FTPUtil {

    /**
     * 获取一个 FTP 连接
     *
     * @param host     ip 地址
     * @param port     端口
     * @param username 用户名
     * @param password 密码
     * @return 返回 FTP 连接对象
     * @throws Exception 连接 FTP 时发生的各种异常
     */
    public static FTPClient getFtpClient(String host, Integer port, String username, String password) throws Exception {
        FTPClient ftpClient = new FTPClient();

        //连接服务器
        ftpClient.connect(host, port);

        int reply = ftpClient.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            log.error("无法连接至 ftp 服务器,host:{},port:{}", host, port);
            ftpClient.disconnect();
            return null;
        }

        //登入服务器
        boolean login = ftpClient.login(username, password);
        if (!login) {
            log.error("登录失败, 用户名或密码错误");
            ftpClient.logout();
            ftpClient.disconnect();
            return null;
        }

        //连接并且成功登陆 FTP 服务器
        log.info("login success ftp server, host: {}, port: {}, user: {}", host, port, username);

        //设置通道字符集, 要与服务端设置一致
        ftpClient.setControlEncoding("UTF-8");
        //设置文件传输编码类型, 字节传输:BINARY_FILE_TYPE, 文本传输:ASCII_FILE_TYPE, 建议使用BINARY_FILE_TYPE进行文件传输
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        //主动模式: enterLocalActiveMode(),被动模式: enterLocalPassiveMode(),一般选择被动模式
        ftpClient.enterLocalPassiveMode();
        //切换目录
        //ftpClient.changeWorkingDirectory("xxxx");

        return ftpClient;
    }

    /**
     * 断开 FTP 连接
     *
     * @param ftpClient FTP 连接客户端
     */
    public static void disConnect(FTPClient ftpClient) {
        if (ftpClient == null) {
            return;
        }
        try {
            log.info("断开 FTP 连接,host: {},port: {}", ftpClient.getPassiveHost(), ftpClient.getPassivePort());
            ftpClient.logout();
            ftpClient.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
            log.error("FTP 连接断开异常,请检查!");
        }

    }

    /**
     * 文件下载
     *
     * @param ftpClient FTP 连接客户端
     * @param path      文件路径
     * @param downPath  文件名称
     */
    public static void download(FTPClient ftpClient, String path, String downPath) throws Exception {
        if (ftpClient == null || path == null || downPath == null) {
            return;
        }

        //中文目录处理存在问题, 转化为 FTP 能够识别中文的字符集
        String remotePath;
        try {
            remotePath = new String(path.getBytes(StandardCharsets.UTF_8), FTP.DEFAULT_CONTROL_ENCODING);
        } catch (UnsupportedEncodingException e) {
            remotePath = path;
        }

        InputStream inputStream = ftpClient.retrieveFileStream(remotePath);
        if (inputStream == null) {
            log.error("{} 在 TFP 服务器中不存在,请检查", path);
            return;
        }

        FileOutputStream outputStream = new FileOutputStream(downPath);
        BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
        try {
            byte[] buffer = new byte[2048];
            int i;
            while ((i = bufferedInputStream.read(buffer)) != -1) {
                bufferedOutputStream.write(buffer, 0, i);
                bufferedOutputStream.flush();
            }
        } catch (Exception e) {
            log.error("文件下载异常", e);
            log.error("{} 下载异常,请检查!", path);
        }

        inputStream.close();
        outputStream.close();
        bufferedInputStream.close();
        bufferedOutputStream.close();

        //关闭流之后必须执行,否则下一个文件导致流为空
        boolean complete = ftpClient.completePendingCommand();
        if (complete) {
            log.info("文件 {} 下载完成", remotePath);
        } else {
            log.error("文件 {} 下载失败", remotePath);
        }
    }


    /**
     * 上传文件
     *
     * @param ftpClient  FTP 连接客户端
     * @param sourcePath 源地址
     */
    public static void upload(FTPClient ftpClient, String sourcePath, String remoteDir) throws Exception {
        if (ftpClient == null || sourcePath == null) {
            return;
        }

        File file = new File(sourcePath);
        if (!file.exists() || !file.isFile()) {
            return;
        }

        //中文目录处理存在问题,转化为 TFP 能够识别中文的字符集
        String remotePath = new String((remoteDir + "/" + file.getName())
                    .getBytes(StandardCharsets.UTF_8), FTP.DEFAULT_CONTROL_ENCODING);
        try (
                InputStream inputStream = new FileInputStream(file);
                OutputStream outputStream = ftpClient.storeFileStream(remotePath);
        ) {
            byte[] buffer = new byte[2048];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
                outputStream.flush();
            }
        } catch (Exception e) {
            log.error("文件上传异常", e);
        }

        // 关闭流之后必须执行,否则下一个文件导致流为空
        boolean complete = ftpClient.completePendingCommand();
        if (complete) {
            log.info("文件 {} 上传完成", remotePath);
        } else {
            log.error("文件 {} 上传失败", remotePath);
        }
    }
}

FTPController.java

package com.example.htmltopdf.controller;

import com.example.htmltopdf.utils.FTPUtil;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ftp")
public class FTPController {

    @Value("${ftp.server.host}")
    private String FTP_HOST;

    @Value("${ftp.server.port}")
    private int FTP_PORT;

    @Value("${ftp.server.username}")
    private String FTP_USERNAME;

    @Value("${ftp.server.password}")
    private String FTP_PASSWORD;

    @Value("${ftp.server.remoteDir}")
    private String FTP_REMOTE_DIR;

    @PostMapping("/upload")
    public String uploadFile() throws Exception {
        System.out.println();
        FTPClient ftpClient = FTPUtil.getFtpClient(FTP_HOST, FTP_PORT, FTP_USERNAME, FTP_PASSWORD);

        // 展示文件夹
        assert ftpClient != null;
        FTPFile[] ftpFiles = ftpClient.listDirectories();
        for (FTPFile file : ftpFiles) {
            System.out.println(file.getName());
        }

        //上传文件
        FTPUtil.upload(ftpClient, "output.pdf", FTP_REMOTE_DIR);

        //下载文件
        FTPUtil.download(ftpClient, FTP_REMOTE_DIR + "/output.pdf", "E:\\output.pdf");

        FTPUtil.disConnect(ftpClient);

        return "success";
    }

}

4.4.测试

启动项目后,在 Postman 中进行接口测试(注意是 POST 请求):

http://localhost:8080/ftp/upload

运行成功后会发现 FTP 服务器对应目录中已经有了该 PDF 文件:

在这里插入图片描述

并且下载功能也是正常的,在本地的 E 盘中也出现该 PDF 文件:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1956295.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PostgreSQL性能优化之体系结构

本文介绍 PostgreSQL 数据库的体系结构&#xff0c;包括实例结构&#xff08;进程与内存&#xff09;、存储结构&#xff08;物理与逻辑&#xff09;以及插件式存储引擎。 实例与数据库聚簇 PostgreSQL 使用典型的客户端/服务器&#xff08;Client/Server&#xff09;架构&am…

【Android】Fragment的添加

上一篇文章学到了碎片的创建与生命周期&#xff0c;接下来学习碎片的常用操作&#xff0c;其中会用到上一篇文章的三个碎片&#xff0c;就做一个简单的说明吧&#xff1a;LeftFragment&#xff08;包含一个按钮&#xff09;、RightFragment4&#xff08;以粉色为背景的文本&…

【人工智能】穿越科技迷雾:解锁人工智能、机器学习与深度学习的奥秘之旅

文章目录 前言一、人工智能1. 人工智能概述a.人工智能、机器学习和深度学习b.人工智能发展必备三要素c.小案例 2.人工智能发展历程a.人工智能的起源b.发展历程 3.人工智能的主要分支 二、机器学习1.机器学习工作流程a.什么是机器学习b.机器学习工作流程c.特征工程 2.机器学习算…

动手学深度学习V2每日笔记(模型选择+过拟合和欠拟合)

本文主要参考沐神的视频教程 https://www.bilibili.com/video/BV1K64y1Q7wu/?spm_id_from333.788.recommend_more_video.0&vd_sourcec7bfc6ce0ea0cbe43aa288ba2713e56d 文档教程 https://zh-v2.d2l.ai/ 本文的主要内容对沐神提供的代码中个人不太理解的内容进行笔记记录&a…

Java之归并排序

归并排序 归并排序(Merge Sort)算法&#xff0c;使用的是分治思想。分治&#xff0c;顾名思义&#xff0c;就是分而治之&#xff0c;将一个大问题分解成小的子问题来解决。小的子问题解决了&#xff0c;大问题也就解决了。 核心源码: mergeSort(m->n) merge(mergeSort(m-&g…

对于500强企业来说,有比FTP好用的传输工具吗?

500强企业在进行文件传输时&#xff0c;会根据其业务需求、数据安全性要求以及技术架构的不同&#xff0c;选择多种文件传输方式&#xff0c;最常见的便是FTP。然而FTP在使用却存在较多的问题&#xff1a; 1&#xff09;安全性问题 缺乏安全策略&#xff1a;FTP本身不提供加密…

「百年孤独」

引言 《百年孤独》是加西亚马尔克斯创作的魔幻现实主义经典小说&#xff0c;刻画了布恩迪亚家族七代人的跌宕起伏和马孔多小镇的兴衰。是拉丁美洲文学中一部不朽的杰作。 故事概述 小说从布恩迪亚家族的始祖荷塞阿卡迪奥布恩迪亚和妻子乌尔苏拉开始&#xff0c;讲述了七代人…

DeiT III(Meta)论文解读

paper&#xff1a;DeiT III: Revenge of the ViT official implementation&#xff1a;https://github.com/facebookresearch/deit 出发点 本研究旨在重新审视ViT的监督训练方法&#xff0c;并提出一种基于ResNet-50训练方法的简化版新训练策略。与现有的自动数据增强方法不…

C++从入门到起飞之——友元内部类匿名对象对象拷贝时的编译器优化 全方位剖析!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C从入门到起飞 &#x1f516;克心守己&#xff0c;律己则安 目录 1、友元 2、内部类 3、 匿名对象 4、对象拷⻉时的编译器优化 5、完结散花 1、友元 • 友元提供…

springAOP理解及事务

AOP&#xff1a; springAOP是什么&#xff1a; AOP&#xff1a;Aspect Oriented Programming&#xff08;面向切面编程、面向方面编程&#xff09;&#xff0c;其实就是面向特定方法编程。 使用场景&#xff1a; 比如你想统计业务中每个方法的执行耗时&#xff0c;那我们最…

基于bert的自动对对联系统

目录 概述 演示效果 核心逻辑 使用方式 1.裁剪数据集 根据自己的需要选择 2.用couplet数据集训练模型 模型存储在model文件夹中 3.将模型转换为ONNX格式 4.打开index.html就可以在前端使用此自动对对联系统了。 本文所涉及所有资源均在传知代码平台可获取。 概述 这个生成器利用…

什么是婚恋聊天交友源码?今天大家讲解一下。源码交付,支持二开,可打包APP小程序H5。

婚恋交友APP开发前景 对于现代的年轻人来说&#xff0c;社恐已经是深入骨子里不可别除的&#xff0c;除了每天上班下班&#xff0c;许多人宁愿宅在家里&#xff0c;面对线下的相亲机构&#xff0c;家里长辈介绍的会都是饭度抗柜的。而这几年疫情的影响更是大大的限制了正常的社…

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

7. 运行时数据区-栈

栈的分类 栈分为Java虚拟机栈还有本地方法栈&#xff1a; Java虚拟机栈&#xff1a;用于保存Java中的方法相关的内容本地方法栈&#xff1a;用于保存在Java中使用native 标记的用C来实现方法 由于hotspot的作者发现使用一个栈就可以保存以上两个部分的内容&#xff0c;所以在…

图像生成中图像质量评估指标—PSNR的详细介绍

文章目录 1. 背景介绍2. 实际应用3. 总结和讨论 1. 背景介绍 峰值信噪比&#xff08;Peak Signal-to-Noise Ratio&#xff0c;简称PSNR&#xff09;是一种广泛应用于图像和视频处理领域的客观图像质量评价指标。它主要用于衡量图像的噪声水平和图像质量&#xff0c;可以用来评…

HttpClient初学

介绍&#xff1a; HttpClient 是Apache Jakarta Common 下的子项目&#xff0c;可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包&#xff0c;并且它支持 HTTP 协议最新的版本和建议。 这里阿里云的oss依赖底层是httpclient&#xff0c;所以这里不再重…

用f-string+sys.stdout.write定制“自己的writer”

f-stringsys.stdout.write&#xff0c;在python中“随意”我的输出。 (笔记模板由python脚本于2024年07月29日 08:09:35创建&#xff0c;本篇笔记适合喜欢python并有一定基础的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&am…

dotnet开发编译之争:Ahead-of-Time(AOT) vs Just-in-Time(JIT)谁才是未来最佳编译选择?

1. 前言 编译技术的选择对于现代应用程序的性能至关重要。在.Net开发平台下&#xff0c;选择合适的编译策略对于提升应用程序的响应速度、资源利用率以及最终用户体验有着不可忽视的影响。其中&#xff0c;Ahead-of-Time (AOT) 编译和 Just-in-Time (JIT) 编译是两种广泛采用的…

【编程工具使用技巧】VS如何显示行号

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《编程工具与技巧探索》 期待您的关注 目录 引言 一、VS编译器行号显示的基本步骤 1.打开VS与项目 2.进入选项设置 3.找到并…

Open3D 计算点到平面的距离

目录 一、概述 1.1原理 1.2实现步骤 1.3原理 二、代码实现 1.1关键函数 1.2完整代码 三、实现效果 3.1原始点云 3.2计算距离后赋色的点云 Open3D点云算法汇总及实战案例汇总的目录地址&#xff1a; Open3D点云算法与点云深度学习案例汇总&#xff08;长期更新&#…