最近,我们的系统新增了一个客服模块,其中一个重要功能是能够以PDF格式导出客服与用户之间的聊天记录。这些聊天记录包含文字、图片和文件等多种内容。为了实现这一功能,我们首先使用了itextpdf 5.x版本制作了一个Demo。今天,我将与家人们分享一下这项进展。

# iTextPDF 介绍

iTextPDF 是一个用于创建和操作 PDF(Portable Document Format)文档的流行的 Java 库。它提供了一套全面的功能,用于处理 PDF 文件,包括创建新文档、修改现有文档以及提取信息。以下是 iTextPDF 的一些关键方面的简要概述:

  • 文档创建:

iTextPDF 允许您从头开始创建新的 PDF 文档。

您可以向文档添加段落、表格、图像和其他元素。

  • 文本操作:

该库提供了格式化和处理文本的方法。

  • 页面布局:

您可以定义页面的布局,包括页面尺寸、边距等。

  • 字体和颜色:

iTextPDF 允许您选择字体和颜色,以定制文档的外观。

  • 表格:

通过 iTextPDF,您可以创建包含表格的文档,设置表格的列数、行数和单元格内容。

  • 图像处理:

您可以将图像插入到文档中,并设置图像的大小和位置。

  • 文档安全性:

iTextPDF 提供了对文档进行加密和数字签名的功能,以增强文档的安全性。

  • 文档解析:

除了创建文档,iTextPDF 还允许您解析现有的 PDF 文档,提取文本、图像等信息。

# 代码示例

我们此处使用的 iTextPDF 5.x的版本实现的

# 添加依赖

在pom文件中添加如下依赖

<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>itextpdf</artifactId>
  <version>5.5.13.2</version> 
</dependency>

# 代码编写

service代码

import cn.xj.xjdoc.system.entity.Message;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.itextpdf.text.*;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.ColumnText;
import com.itextpdf.text.pdf.PdfAction;
import com.itextpdf.text.pdf.PdfWriter;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

@Service
@Slf4j
public class PdfService {

    public void export(HttpServletResponse response) throws IOException, DocumentException {

        List<Message> messageList = getMsgList();
        // 创建 PDF document
        Document document = new Document();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PdfWriter pdfWriter = PdfWriter.getInstance(document, baos);

        //获取系统字体,如果是中文,则需注意linux中不存在windows字体,中文乱码或者写不进去
        FontFactory.registerDirectories();

        Font chineseFont = FontFactory.getFont("SimSun", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

        document.open();

        // 发送人字体
        Font senderFont = new Font(chineseFont.getBaseFont(), 16, Font.BOLD, BaseColor.ORANGE);
        // 发送时间字体
        Font senderTimeFont = new Font(chineseFont.getBaseFont(), 12, Font.UNDEFINED, BaseColor.BLUE);
        // 内容字体
        Font defaultFont = new Font(chineseFont.getBaseFont(), 12);

        // 将信息写入pdf中
        for (Message msg : messageList) {

            String senderText = String.format("%s  ", msg.getSendUser());
            String timeText = msg.getSendTime();
            String messageText = msg.getContent();
            Integer type = msg.getType();

            Paragraph paragraph = new Paragraph();

            ColumnText columnText = new ColumnText(pdfWriter.getDirectContent());
            columnText.addElement(paragraph);
            columnText.setSimpleColumn(20, document.bottom(), document.right() - 20, document.top(), 0, Element.ALIGN_LEFT);
            paragraph.add(new Chunk(senderText, senderFont));
            paragraph.add(new Chunk(timeText + "\n", senderTimeFont));

            float proportion = 1f;

            if(type == 1){
                //文字
                paragraph.add(new Chunk(messageText + "\n", defaultFont));
            }else if(type == 2){
                //图片
                // 创建Image对象
                Image image = Image.getInstance(new URL(msg.getContent()));

                //等比缩小图片
                if(image.getWidth() > 150){
                    proportion = image.getWidth()/150;
                    image.scaleToFit(image.getWidth()/proportion, image.getHeight()/proportion);
                }
                //加一空行
                paragraph.add(new Chunk(Chunk.NEWLINE));
                // 判断当前页内容是否已满
                if ((pdfWriter.getVerticalPosition(true) - image.getHeight()/proportion) < document.bottom()) {
                    document.newPage(); // Start a new page
                    columnText.setYLine(document.top());
                }
                paragraph.add(image);
            }else{
                //文件
                // 创建Chunk,设置文件连接,点击下载
                Chunk chunk = new Chunk(msg.getFileName(),defaultFont);
                chunk.setAction(new PdfAction(new URL(msg.getContent()).toExternalForm()));
                paragraph.add(chunk);
            }

            paragraph.setIndentationLeft(20f);
            paragraph.setIndentationRight(20f);
            paragraph.setSpacingAfter(10f);

            // 判断当前页内容是否已满
            if (ColumnText.hasMoreText(columnText.go())) {
                document.newPage();
            }
            document.add(paragraph);

        }
        document.close();

        //返回pdf
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "inline; filename=chat_export.pdf");
        response.getOutputStream().write(baos.toByteArray());
        response.getOutputStream().flush();
    }


    /**
     * 获取数据的方法
     * @return
     */
    public List<Message>  getMsgList(){
        String  jsonArrayString ="[{\"type\": 1, \"content\": \"嘿,听说最近有点八卦,你有什么料吗?\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:46:12\", \"sendUser\": \"大雄\"}, {\"type\": 1, \"content\": \"当然啦!你知道吗,最近有个超级智能机器人在公司里开始工作了,听说能做我们的工作,让人有点担心啊。\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:47:17\", \"sendUser\": \"修己\"}, {\"type\": 1, \"content\": \" 哇,真的吗?那它是怎么工作的?会不会抢我们的饭碗?\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:48:22\", \"sendUser\": \"大雄\"}, {\"type\": 1, \"content\": \" 别慌,听说它只是一个AI助手,能够处理一些重复性的任务,让我们有更多时间专注于创造性的工作。\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:49:12\", \"sendUser\": \"修己\"}, {\"type\": 1, \"content\": \" 哦,原来如此。不过,你觉得这个机器人有没有潜在的危险性啊?\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:50:18\", \"sendUser\": \"大雄\"}, {\"type\": 1, \"content\": \"哈哈,我也有点担心,但听说它的设计是为了帮助我们,而不是取代我们。还有,它可不会参与任何八卦。\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:51:42\", \"sendUser\": \"修己\"}, {\"type\": 1, \"content\": \" 哈哈,说到八卦,你有听说最近公司里有什么有趣的事情吗?\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:52:12\", \"sendUser\": \"大雄\"}, {\"type\": 1, \"content\": \"对呀,听说某某老板最近秘密约会了某某同事,整个公司都在传。你觉得是真的吗?照片都爆出来了,给你发下\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:53:12\", \"sendUser\": \"修己\"}, {\"type\": 2, \"content\": \"http://chevereto.xiuji.mynatapp.cc/images/2023/06/27/z_3b2fa0f1285d8071229631addebf3087.png\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:54:12\", \"sendUser\": \"修己\"}, {\"type\": 2, \"content\": \"http://chevereto.xiuji.mynatapp.cc/images/2023/06/23/_20230623212425.png\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:55:12\", \"sendUser\": \"修己\"}, {\"type\": 2, \"content\": \"http://chevereto.xiuji.mynatapp.cc/images/2023/06/27/z_73bc1ab1a92d68b15e9e1babc3d15afb.png\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:56:12\", \"sendUser\": \"修己\"}, {\"type\": 2, \"content\": \"http://chevereto.xiuji.mynatapp.cc/images/2023/06/23/_20230623212445.png\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:57:12\", \"sendUser\": \"修己\"}, {\"type\": 1, \"content\": \"哇,这可真是八卦大爆炸啊!我倒是没听说,但如果是真的,那可真是太有趣了。\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:58:12\", \"sendUser\": \"大雄\"}, {\"type\": 1, \"content\": \"是啊,公司里的八卦总是让人忍不住想知道更多。有时候我觉得,我们也是一群活在八卦世界里的AI助手。\", \"fileName\": null, \"sendTime\": \"2024-01-25 21:59:12\", \"sendUser\": \"修己\"}, {\"type\": 1, \"content\": \"哈哈,说得对!我们也需要一些八卦来调剂一下生活。不过,我还是觉得那个超级智能机器人有点神秘呢。\", \"fileName\": null, \"sendTime\": \"2024-01-25 22:02:36\", \"sendUser\": \"大雄\"}, {\"type\": 1, \"content\": \"是啊,或许我们也能通过它得知更多关于公司内幕的事情。要不要试试向它搭讪?\", \"fileName\": null, \"sendTime\": \"2024-01-25 22:05:36\", \"sendUser\": \"修己\"}, {\"type\": 1, \"content\": \" 哈哈,说不定它能告诉我们更多关于那对神秘约会的内幕。不过,我们还是小心点为好,免得被当成八卦机器人。\", \"fileName\": null, \"sendTime\": \"2024-01-25 22:08:36\", \"sendUser\": \"大雄\"}, {\"type\": 1, \"content\": \"顺便给你在发个我最近吃的瓜的pdf\", \"fileName\": null, \"sendTime\": \"2024-01-25 22:12:36\", \"sendUser\": \"修己\"}, {\"type\": 4, \"content\": \"http://chevereto.xiuji.mynatapp.cc/images/2023/06/23/西安外国语大学会计ACCA2001班丁玉婕出轨情况说明.pdf\", \"fileName\": \"西安外国语大学会计ACCA2001班丁玉婕出轨情况说明.pdf\", \"sendTime\": \"2024-01-25 22:18:36\", \"sendUser\": \"修己\"}]";
        // 使用Fastjson将JsonArray字符串解析为JSONArray对象
        JSONArray jsonArray = JSONArray.parseArray(jsonArrayString);
        List<Message> messageList = new ArrayList<>();
        // 遍历JsonArray,将每个JSON对象转换为Message对象并添加到List中
        for (Object jsonMessage : jsonArray) {
            JSONObject jsonObject = (JSONObject) jsonMessage;
            Message message = jsonObject.toJavaObject(Message.class);
            messageList.add(message);
        }
        return messageList;
    }
}

controller代码

import cn.xj.xjdoc.system.service.PdfService;
import com.itextpdf.text.DocumentException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@Slf4j
public class PdfController {

    @Resource
    private PdfService pdfService;
    
    @GetMapping("/export")
    public void export(HttpServletResponse response) throws DocumentException, IOException {
        pdfService.export(response);
    }
}

代码到这就完了,我们可以启动服务,查看下展示的效果:

_20240126070304.jpg

_20240126070314.jpg

_20240126070325.jpg

到这儿,如果不出意外的话肯定要出意外了,请继续阅读文章。

# Linux上解决中文没写入或者乱码问题

如果我们将服务部署到Linux服务器上,可能会遇到中文未正确写入或乱码的问题。这是由于Linux系统上的字体库与Windows系统不同。为了解决这个问题,我们可以在代码中直接将所需字体的ttf文件复制到项目目录下,并使用itextpdf加载这些字体。此前,我们成功为服务器添加了Windows字体库,因此我们可以直接从系统中获取字体。接下来,我们将介绍在Linux中添加Windows字体的操作步骤。

windows字体库的位置:C:\Users\Administrator\AppData\Local\Microsoft\Windows\Fonts

# Linux 中添加windows字体库

  • ubuntu

将windows的字体库Fonts 复制到目录 /usr/share/fonts 下,执行如下权限命令:

sudo chmod -R 777 Fonts

然后执行以下命令使字体生效

sudo fc-cache -fv
  • centos

将windows的字体库Fonts下的文件 复制到目录 /usr/share/fonts 下,依次执行如下命令:

yum install -y mkfontscale

yum install -y fontconfig

cd /usr/share/fonts/

mkfontscale

mkfontdir

fc-cache

fc-list

# 构建具有windows字体库的docker镜像

  • ubuntu

Dockerfile

# 基于哪个镜像
FROM ubuntu:20.10

# 维护者
MAINTAINER xj

# 拷贝文件到容器
ADD Fonts /usr/share/fonts/chinese_font/
RUN chmod -R 755 /usr/share/fonts/chinese_font
RUN fc-cache -fv
  • centos

Dockerfile

# 基于哪个镜像
FROM centos:centos7.1.1503
# 维护者
MAINTAINER xj
ADD Fonts/* /usr/share/fonts/
RUN yum install -y mkfontscale
RUN yum install -y fontconfig
RUN cd /usr/share/fonts/
RUN mkfontscale
RUN mkfontdir
RUN fc-cache
RUN fc-list

# 总结

这个例子演示了如何使用Spring Boot和iTextPDF创建动态的、个性化的PDF文档。你可以根据实际需求扩展生成的PDF内容,包括图表、表格等,以满足项目的特定要求。希望这篇文章对你有所帮助!如果有任何问题或建议,请随时提出。