Bootstrap

libreoffice在Windows和Linux环境的安装和结合Springboot使用教程

前言:

        在公司做开发时,遇到一个需求,要求上传的文件有图片,也有word和pdf。预览信息时,既要求能水印展示出来,又要求能大图水印预览。思索许久,我决定采取全部打水印然后转成图片Base64,但是后面突然发现word不能直接转为图片,强制转换会有内容丢失,于是我打算先把word转为pdf,然后再把pdf转为图片Base64,思路堪称完美。

        思路有了,那该选什么工具好呢?网上的教程参差不齐,有的工具也能用,但是转出来的效果不尽人意,在寻找许久后我选择了document4j,这工具在转换方面堪称完美,但美中不足的是只能在Windows上使用,最后我选择了libreoffice。

        libreoffice既兼容Windows,同时又兼容Linux,转换出来的效果也不错,于是我的工具问题也就迎刃而解了。

环境:

IntelliJ IDEA 2024.1.4

Linux服务器

Windows笔记本

Oracle数据库(Mysql数据也行,我这里测试用的Mysql)

 LibreOffice 24.2.4 下载地址:下载 LibreOffice | LibreOffice 简体中文官方网站 - 自由免费的办公套件

1.安装教程:  

        我们需要下载两个LibreOffice 24.2.4,一个是Windows的,一个是Linux的。另外下载的话选择国内镜像下载,比较快:

        Window下载完成后安装在D盘即可(选择),安装没有特别需要注意的,安装完自己记得路径就行。对于Linux安装,安装如下:

1.1解压压缩包

        上传到linux指定目录下或者用wget直接下载到linux指定目录下后(我上传到的是/opt目录下),使用下面命令解压:

cd /opt
tar -xvf LibreOffice_24.2.4_Linux_x86-64_rpm.tar.gz

1.2安装libreoffice

        执行下面命令开始安装,等待安装完成即可:

sudo yum install ./LibreOffice_24.2.4_Linux_x86-64_rpm/RPMS/*.rpm

1.3启动libreoffice

        Windows的话cmd执行下面命令启动,其中D:\Program Files\LibreOffice就是安装的路径:

D:\Program Files\LibreOffice\program\soffice.exe --headless --invisible --nologo --nodefault --nofirststartwizard --accept="socket,host=127.0.0.1,port=8100;urp;"

        Linux的话执行下面命令启动,同样的/opt/libreoffice24.2就是安装的路径:

/opt/libreoffice24.2/program/soffice --headless --invisible --nologo --nodefault --nofirststartwizard --accept="socket,host=127.0.0.1,port=8100;urp;"

        Linux的话还可以使用docker部署,这样的话就可以不用安装并启动得那么麻烦,使用的镜像是libreoffice/online,但是要把这个安装的libreoffice映射出来,我这里就不详细介绍了,感兴趣的可以自己去试一试。

        到此,Windows的安装启动就结束了,但是Linux还没有结束,因为Linux缺少中文字体的缘故,导致转出来的中文会是如下的效果:

        为此,我们要给Linux装上中文字体:

1.4Linux安装中文字体

        首先安装fontconfig:

yum -y install fontconfig

        进入/usr/share目录下,执行ls会发现这两个目录,则说明安装成功:

cd /usr/share
ls

        接着,打开这个fonts文件夹,新建一个叫chinese文件夹:

cd fonts
mkdir chinese

        然后将Windows系统的字体文件全部拷进去(这里直接复制是不允许的,需要复制C:\Windows下的Fonts目录到桌面,然后再从桌面双击进入Fonts文件夹,Ctrl+A全选,上传到服务器的这个chinese目录下):

        接着给这个目录赋予权限,执行命令:

chmod -R 755 /usr/share/fonts/chinese

        安装ttmkfdir,编辑配置文件,执行命令:

yum -y install ttmkfdir
ttmkfdir -e /usr/share/X11/fonts/encodings/encodings.dir
vi /etc/fonts/fonts.conf

        在Font directory list配置项下,把这个中文字体目录加进去,添加如下代码:

<dir>/usr/share/fonts/chinese</dir>

        保存退出后,执行下面命令刷新一下缓存

fc-cache

        到此,中文字体安装完成。转出的效果也变成了如下图,中文字体也能正常显示了:

2.Springboot集成libreoffice

        环境安装好了,解下来,我们就开始写代码了。

2.1引入pom依赖

        首先创建完Springboot项目后,安装必要的依赖,其中重点就是jodconverter-spring-boot-starter和jodconverter-local,其他的看需要引入:

        <!--office转换工具-->
        <dependency>
            <groupId>org.jodconverter</groupId>
            <artifactId>jodconverter-spring-boot-starter</artifactId>
            <version>4.4.7</version>
        </dependency>
        <dependency>
            <groupId>org.jodconverter</groupId>
            <artifactId>jodconverter-local</artifactId>
            <version>4.4.7</version>
        </dependency>
        <!--pdfbox-->
        <dependency>
            <groupId>org.apache.pdfbox</groupId>
            <artifactId>pdfbox</artifactId>
            <version>2.0.24</version>
        </dependency>
        <!--hutool-->
        <dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.8.25</version>
		</dependency>
        <!--Apache Commons-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.16.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <!--Apache Poi-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.5</version>
        </dependency>
        <!--itextpdf-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13.4</version>
        </dependency>

2.2修改yaml配置文件

        修改application.yaml,添加如下配置:

spring:
  profiles:
    active: local


# 共享配置
jodconverter:
  local:
    enabled: true
    portNumbers: 8100
    maxTasksPerProcess: 100
    taskQueueTimeout: 60

file:
  path:
    windows: 'D:/temp/file'
    linux: '/file'

---
spring:
  config:
    activate:
      on-profile: local
jodconverter:
  local:
    officeHome: 'D:\Program Files\LibreOffice'
---
spring:
  config:
    activate:
      on-profile: server
jodconverter:
  local:
    officeHome: '/opt/libreoffice24.2'

其中我用spring: profiles: active: 来区分是Windows启动的项目还是Linux,若是Windows则改为local,若是Linux的话改成server即可

file:path:是文件上传后保存的目录。若是Linux环境确保启动该项目的用户对该目录有足够的读写权限,否则上传文件会报错500。

2.3control层代码

        FileControl.java完整代码如下:

package com.example.testdemo.test.control;

import com.example.testdemo.test.model.DownloadFile;
import com.example.testdemo.test.service.FileService;
import com.example.testdemo.test.utils.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;

/**
 * 项目名称: testDemo
 * 作者: zhaojs
 * 创建日期: 2024年07月04日
 * 文件说明: 文件控制层
 */
@RestController
public class FileControl {

    /**
     * 文件服务
     */
    @Resource
    private FileService fileService;

    /**
     * 文件上传
     * @param files
     * @return
     */
    @PostMapping("/file/upload")
    public Result uploadFile(@RequestParam("files") MultipartFile[] files) {
        return fileService.uploadFile(files);
    }

    /**
     * 获取文件列表
     * @param ids
     * @return
     */
    @GetMapping("/file/getFile/{ids}")
    public Result getFile(@PathVariable("ids") String ids) {
        return Result.success(fileService.getFile(ids));
    }

    /**
     * 附件转为图片的base64编码
     * 支持附件类型:doc、docx、pdf、png、jpeg
     * @param downloadFile
     */
    @PostMapping("/file/switchAttachment/imageBase64")
    public Result switchAttachmentImageBase64(@RequestBody DownloadFile downloadFile) {
        return fileService.switchAttachmentImageBase64(downloadFile);
    }
}

2.4service层代码

        FileService.java完整代码如下:

package com.example.testdemo.test.service;

import com.example.testdemo.test.model.Attachment;
import com.example.testdemo.test.model.DownloadFile;
import com.example.testdemo.test.utils.Result;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

/**
 * 项目名称: testDemo
 * 创建日期: 2024年07月04日
 * @author zhaojs
 * 文件说明: 见类描述
 */
public interface FileService {
    /**
     * 上传文件
     * @param files
     * @return
     */
    Result uploadFile(MultipartFile[] files);

    /**
     * 获取文件列表
     * @param ids
     * @return
     */
    List<Attachment> getFile(String ids);

    /**
     * 附件转为图片的base64编码
     * @param downloadFile
     * @return
     */
    Result switchAttachmentImageBase64(DownloadFile downloadFile);
}

        FileServiceImpl.java完整代码如下:

package com.example.testdemo.test.service.Impl;

import com.example.testdemo.test.dao.AttachmentDao;
import com.example.testdemo.test.model.Attachment;
import com.example.testdemo.test.model.DownloadFile;
import com.example.testdemo.test.service.FileService;
import com.example.testdemo.test.utils.Result;
import com.example.testdemo.test.utils.WordWaterMarker;
import com.itextpdf.text.Element;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfGState;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.jodconverter.core.DocumentConverter;
import org.jodconverter.core.document.DefaultDocumentFormatRegistry;
import org.jodconverter.core.document.DocumentFormat;
import org.jodconverter.core.office.OfficeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class FileServiceImpl implements FileService {

    
    /**
     * Windows文件路径
     */
    @Value("${file.path.windows}")
    private String filePathWindows;

    /**
     * Linux文件路径
     */
    @Value("${file.path.linux}")
    private String filePathLinux;

    /**
     * 文档转换器
     */
    @Resource
    private DocumentConverter documentConverter;

    /**
     * 附件表Dao
     */
    @Resource
    private AttachmentDao attachmentDao;

    /**
     * 日期时间格式
     */
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    /**
     * 并发处理任务的数量
     */
    private static final int MAX_CONCURRENT_TASKS = 5;

    /**
     * 日志
     */
    private static final Logger logger = LoggerFactory.getLogger(FileServiceImpl.class);


    /**
     * 上传文件
     *
     * @param files
     * @return
     */
    @Override
    public Result uploadFile(MultipartFile[] files) {
        try {
            // 遍历文件数组并处理每个文件
            for (MultipartFile file : files) {
                if (! file.isEmpty()) {
                    // 处理文件名
                    String originalFilename = file.getOriginalFilename();
                    String extension = FilenameUtils.getExtension(originalFilename);
                    String uniqueFilename = UUID.randomUUID().toString() + (extension != null && ! extension.isEmpty() ? "." + extension : "");
                    // 获取当前日期并格式化为年月目录格式
                    LocalDate now = LocalDate.now();
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM");
                    String yearMonthDir = now.format(formatter);
                    // 指定保存的根目录,对于Windows可以是D盘的某个路径,对于Linux通常是/home/user或特定的目录
                    String rootDir = System.getProperty("os.name").toLowerCase().startsWith("win") ? filePathWindows : filePathLinux;
                    // 构建保存文件的完整路径,使用Paths.get()自动处理路径分隔符
                    Path directoryPath = Paths.get(rootDir, yearMonthDir);
                    // 确保目录存在
                    Files.createDirectories(directoryPath);
                    // 文件保存路径
                    Path filePath = directoryPath.resolve(uniqueFilename);
                    // 将文件输入流中的数据复制到目标路径,如果目标文件已存在,则覆盖原有文件
                    Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
                    // 插入附件表
                    Attachment attachment = new Attachment();
                    attachment.setAttachmentId(UUID.randomUUID().toString());
                    attachment.setFileId(uniqueFilename);
                    attachment.setFileName(originalFilename);
                    attachment.setFileType(file.getContentType());
                    attachment.setFileSize(Double.valueOf(file.getSize()));
                    attachment.setFilePath(filePath.toString());
                    attachment.setCreateTime(LocalDateTime.now());
                    attachmentDao.insert(attachment);
                } else {
                    return Result.fail("文件为空");
                }
            }
            return Result.success("文件上传成功");
        } catch (IOException e) {
            return Result.fail("文件上传失败");
        }
    }

    /**
     * 获取文件列表
     *
     * @param ids
     * @return
     */
    @Override
    public List<Attachment> getFile(String ids) {
        if (StringUtils.isNotBlank(ids)) {
            String[] attachmentIds = ids.split(",");
            List<Attachment> attachmentList = attachmentDao.selectBatchIds(Arrays.asList(attachmentIds));
            return attachmentList;
        }
        return null;
    }

    /**
     * 附件转为图片的base64编码
     *
     * @param downloadFile
     * @return
     */
    @Override
    public Result switchAttachmentImageBase64(DownloadFile downloadFile) {
        List<Map<String, String>> fileList = new ArrayList<>();

        List<Attachment> attachmentList = getFile(downloadFile.getAttachmentIds());
        attachmentList.forEach(attachment -> {
            Map<String, String> map = new HashMap<>();
            map.put("fileName", attachment.getFileName());
            map.put("filePath", attachment.getFilePath());
            fileList.add(map);
        });

        //附件归类
        List<String> imageFilePathList = new ArrayList<>();
        List<String> pdfFilePathList = new ArrayList<>();
        List<String> wordFilePathList = new ArrayList<>();
        fileList.forEach(map -> {
            // 附件名称
            String fileName = MapUtils.getString(map, "fileName");
            // 判断附件类型
            String filePath = MapUtils.getString(map, "filePath");
            if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || fileName.endsWith(".png")) {
                imageFilePathList.add(filePath);
            } else if (fileName.endsWith(".pdf")) {
                pdfFilePathList.add(filePath);
            } else if (fileName.endsWith(".doc") || fileName.endsWith(".docx")) {
                wordFilePathList.add(filePath);
            }
        });

        List<String> imageBase64List = new ArrayList<>();
        //先把word转为图片base64编码
        if (CollectionUtils.isNotEmpty(wordFilePathList)) {
            List<String> imageBase64 = wordFileToImageBase64(wordFilePathList, downloadFile.getWatermarkText());
            if (imageBase64 != null && ! imageBase64.isEmpty()) {
                imageBase64List.addAll(imageBase64);
            }
        }
        //再把pdf转为图片base64编码
        if (CollectionUtils.isNotEmpty(pdfFilePathList)) {
            List<String> imageBase64 = pdfFileToImageBase64(pdfFilePathList, downloadFile.getWatermarkText());
            if (imageBase64 != null && ! imageBase64.isEmpty()) {
                imageBase64List.addAll(imageBase64);
            }
        }
        //最后把图片直接转为base64编码
        if (CollectionUtils.isNotEmpty(imageFilePathList)) {
            List<String> imageBase64 = imageFileToBase64(imageFilePathList, downloadFile.getWatermarkText());
            if (imageBase64 != null && ! imageBase64.isEmpty()) {
                imageBase64List.addAll(imageBase64);
            }
        }

        return Result.success(imageBase64List);
    }

    /**
     * word转为图片base64编码
     *
     * @param wordFilePathList
     * @param watermarkText
     * @return
     */
    public List<String> wordFileToImageBase64(List<String> wordFilePathList, String watermarkText) {
        List<String> imageBase64List = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_TASKS);

        try {
            List<Future<List<String>>> futures = new ArrayList<>();

            // 首先,将Word文件转换为PDF字节流,并为每个字节流创建一个转换任务
            for (String filePath : wordFilePathList) {
                // 将Word转换为PDF字节流
                ByteArrayOutputStream pdfByteStream = wordToPdf(filePath);
                // 将PDF字节流转换为图片Base64
                Callable<List<String>> task = () -> pdfByteToImageBase64Single(pdfByteStream.toByteArray(), watermarkText);
                futures.add(executor.submit(task));
            }

            // 收集所有任务的结果
            for (Future<List<String>> future : futures) {
                try {
                    // 合并每个任务返回的Base64编码图像字符串列表
                    imageBase64List.addAll(future.get());
                } catch (InterruptedException | ExecutionException e) {
                    Thread.currentThread().interrupt();
                    logger.error("处理PDF字节流时发生错误", e);
                }
            }
        } finally {
            executor.shutdown();
            try {
                if (! executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
                logger.error("关闭线程池时发生中断", e);
            }
        }

        return imageBase64List;
    }

    /**
     * word转pdf
     *
     * @param wordFilePath
     * @return
     * @throws Exception
     */
    public ByteArrayOutputStream wordToPdf(String wordFilePath) {
        ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
        try (InputStream docxInputStream = new FileInputStream(wordFilePath)) {
            DocumentFormat type = "docx".equals(getFileType(new File(wordFilePath))) ? DefaultDocumentFormatRegistry.DOCX : DefaultDocumentFormatRegistry.DOC;

            documentConverter.convert(docxInputStream)
                    .as(type)
                    .to(pdfOutputStream)
                    .as(DefaultDocumentFormatRegistry.PDF)
                    .execute();
        } catch (IOException | OfficeException e) {
            logger.error("转换Word文件为PDF时发生错误", e);
            throw new RuntimeException(e);
        }
        return pdfOutputStream;
    }

    /**
     * pdfBytes转图片Base64(单个字节流处理)
     *
     * @param pdfBytes
     * @param watermarkText
     * @return
     */
    private List<String> pdfByteToImageBase64Single(byte[] pdfBytes, String watermarkText) {
        List<String> imageBase64List = new ArrayList<>();

        try (PDDocument document = PDDocument.load(pdfBytes)) {
            PDFRenderer pdfRenderer = new PDFRenderer(document);

            for (int pageIndex = 0; pageIndex < document.getNumberOfPages(); pageIndex++) {
                BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300, ImageType.RGB);

                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ImageIO.write(image, "png", baos);
                baos.flush();
                byte[] imageBytes = baos.toByteArray();
                baos.close();
                if (StringUtils.isNotBlank(watermarkText)) {
                    try (InputStream watermarkedInputStream = addImageWaterMark(new ByteArrayInputStream(imageBytes), watermarkText)) {
                        byte[] watermarkedBytes = watermarkedInputStream.readAllBytes();
                        watermarkedInputStream.close();
                        String format = "png";
                        String fullEncodedImage = "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(watermarkedBytes);
                        imageBase64List.add(fullEncodedImage);
                    } catch (IOException e) {
                        logger.error("添加水印时发生错误", e);
                    }
                } else {
                    String format = "png";
                    String fullEncodedImage = "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
                    imageBase64List.add(fullEncodedImage);
                }
            }
        } catch (IOException e) {
            logger.error("处理PDF字节流时发生错误", e);
        }

        return imageBase64List;
    }

    /**
     * pdf转图片Base64
     *
     * @param pdfFilePathList
     * @param watermarkText
     * @return
     */
    public List<String> pdfFileToImageBase64(List<String> pdfFilePathList, String watermarkText) {
        List<String> imageBase64List = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_TASKS);

        try {
            // 提交任务到线程池
            List<Future<List<String>>> futures = new ArrayList<>();
            for (String filePath : pdfFilePathList) {
                Callable<List<String>> task = () -> convertSinglePdfToImageBase64(filePath, watermarkText);
                futures.add(executor.submit(task));
            }

            // 收集所有任务的结果
            for (Future<List<String>> future : futures) {
                try {
                    // 获取并合并每个任务的结果
                    imageBase64List.addAll(future.get());
                } catch (InterruptedException | ExecutionException e) {
                    Thread.currentThread().interrupt();
                    logger.error("处理PDF文件时发生错误", e);
                }
            }
        } finally {
            executor.shutdown();
            try {
                if (! executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
                logger.error("关闭线程池时发生中断", e);
            }
        }

        return imageBase64List;
    }

    /**
     * 单个PDF转图片Base64
     *
     * @param filePath
     * @param watermarkText
     * @return
     */
    private List<String> convertSinglePdfToImageBase64(String filePath, String watermarkText) {
        List<String> singleFileImageBase64List = new ArrayList<>();
        try (PDDocument document = PDDocument.load(new FileSystemResource(filePath).getFile())) {
            PDFRenderer pdfRenderer = new PDFRenderer(document);

            for (int pageIndex = 0; pageIndex < document.getNumberOfPages(); pageIndex++) {
                BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300, ImageType.RGB);

                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ImageIO.write(image, "png", baos);
                baos.flush();
                byte[] imageBytes = baos.toByteArray();
                baos.close();

                if (StringUtils.isNotBlank(watermarkText)) {
                    try (InputStream watermarkedInputStream = addImageWaterMark(new ByteArrayInputStream(imageBytes), watermarkText)) {
                        byte[] watermarkedBytes = watermarkedInputStream.readAllBytes();
                        String format = "png";
                        String fullEncodedImage = "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(watermarkedBytes);
                        singleFileImageBase64List.add(fullEncodedImage);
                    } catch (IOException e) {
                        logger.error("添加水印时发生错误", e);
                    }
                } else {
                    String format = "png";
                    String fullEncodedImage = "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
                    singleFileImageBase64List.add(fullEncodedImage);
                }
            }
        } catch (IOException e) {
            logger.error("加载PDF文档时发生错误", e);
        }

        return singleFileImageBase64List;
    }


    /**
     * 图片转为base64编码
     *
     * @param imageFilePathList
     * @param watermarkText
     * @return
     */
    public List<String> imageFileToBase64(List<String> imageFilePathList, String watermarkText) {
        List<String> imageBase64List = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_TASKS);

        try {
            // 提交任务到线程池
            List<Future<String>> futures = imageFilePathList.stream()
                    .map(path -> executor.submit(() -> convertSingleImageToBase64(path, watermarkText)))
                    .collect(Collectors.toList());

            // 收集所有任务的结果
            for (Future<String> future : futures) {
                try {
                    // 添加每个任务返回的Base64编码字符串
                    imageBase64List.add(future.get());
                } catch (InterruptedException | ExecutionException e) {
                    Thread.currentThread().interrupt();
                    logger.error("处理图像文件时发生错误", e);
                }
            }
        } finally {
            executor.shutdown();
            try {
                if (! executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
                logger.error("关闭线程池时发生中断", e);
            }
        }

        return imageBase64List;
    }

    /**
     * 单个图像文件转换为Base64字符串
     *
     * @param imagePath     图像文件路径
     * @param watermarkText
     * @return Base64编码的字符串
     */
    private String convertSingleImageToBase64(String imagePath, String watermarkText) {
        try {
            File file = new File(imagePath);
            InputStream originalInputStream = new FileInputStream(file);
            if (StringUtils.isNotBlank(watermarkText)) {
                try (InputStream watermarkedInputStream = addImageWaterMark(originalInputStream, watermarkText)) {
                    byte[] bytes = watermarkedInputStream.readAllBytes();
                    String format = getFileExtension(file.getName()).toLowerCase();
                    String fullEncodedImage = "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(bytes);
                    return fullEncodedImage;
                }
            } else {
                String format = getFileExtension(file.getName()).toLowerCase();
                String fullEncodedImage = "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(originalInputStream.readAllBytes());
                return fullEncodedImage;
            }
        } catch (Exception e) {
            logger.error("转换图像文件为Base64时出错: " + imagePath, e);
            return null;
        }
    }

    /**
     * 获取文件扩展名
     *
     * @param fileName
     * @return
     */
    private static String getFileExtension(String fileName) {
        int lastIndexOfDot = fileName.lastIndexOf('.');
        if (lastIndexOfDot == - 1) {
            // 没有找到扩展名的情况
            return "";
        }
        return fileName.substring(lastIndexOfDot + 1);
    }

    /**
     * 判断word是doc还是docx
     *
     * @param file
     * @return
     */
    public String getFileType(File file) {
        String fileName = file.getName();
        int dotIndex = fileName.lastIndexOf('.');
        if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
            String extension = fileName.substring(dotIndex + 1).toLowerCase();
            if ("docx".equals(extension)) {
                return "docx";
            } else if ("doc".equals(extension)) {
                return "doc";
            }
        }
        return null;
    }

    /**
     * 给照片添加水印
     *
     * @param inputStream
     * @param waterMarkText
     * @return
     */
    public InputStream addImageWaterMark(InputStream inputStream, String waterMarkText) throws IOException {
        if (waterMarkText == null || waterMarkText == "") {
            return inputStream;
        }
        BufferedImage image;
        try {
            image = ImageIO.read(inputStream);
        } catch (IOException e) {
            throw e;
        }

        int width = image.getWidth();
        int height = image.getHeight();
        BufferedImage watermarkedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = watermarkedImage.createGraphics();

        // 绘制原始图像到新图像
        g2d.drawImage(image, 0, 0, width, height, null);

        // 设置抗锯齿
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        // 设置水印的字体和颜色(包括透明度)
        Font font = new Font("STSong-Light", Font.BOLD, 30); // 可以调整字体大小
        g2d.setFont(font);
        g2d.setColor(new Color(0, 0, 0, 255)); // 红色,透明度为 25%
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.2f));

        // 创建旋转变换
        AffineTransform originalTransform = g2d.getTransform();
        AffineTransform rotateTransform = new AffineTransform();
        rotateTransform.rotate(Math.toRadians(- 30), 0, 0);
        g2d.setTransform(rotateTransform);

        // 获取字体度量信息
        FontMetrics fontMetrics = g2d.getFontMetrics();
        int watermarkWidth = fontMetrics.stringWidth(waterMarkText);
        int watermarkHeight = fontMetrics.getHeight();

        // 将图像用水印铺满
        for (int x = - width; x < width * 2; x += watermarkWidth + 100) {
            for (int y = - height; y < height * 2; y += watermarkHeight + 100) {
                g2d.drawString(waterMarkText, x, y);
            }
        }

        // 恢复原始变换
        g2d.setTransform(originalTransform);
        g2d.dispose();

        // 将带有水印的图像写入ByteArrayOutputStream
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try (ImageOutputStream ios = ImageIO.createImageOutputStream(os)) {
            if (! ImageIO.write(watermarkedImage, "jpg", ios)) {
                throw new IOException("Failed to write watermarked image to output stream");
            }
        } catch (IOException e) {
            throw e;
        } finally {
            try {
                os.close();
            } catch (IOException e) {
                throw e;
            }
        }

        // 返回处理后的图像流
        return new ByteArrayInputStream(os.toByteArray());
    }

    /**
     * 给PDF添加水印
     *
     * @param inputStream
     * @param waterMarkText
     * @return
     */
    public InputStream addPdfWaterMark(InputStream inputStream, String waterMarkText) {
        if (waterMarkText == null || waterMarkText == "") {
            return inputStream;
        }
        OutputStream outputStream = null;
        PdfReader pdfReader = null;
        PdfStamper pdfStamper = null;
        try {
            // 水印的高和宽
            int waterMarkHeight = 30;
            int watermarkWeight = 60;
            // 水印间隔距离
            int waterMarkInterval = 100;
            outputStream = new ByteArrayOutputStream();
            // 读取PDF文件流
            pdfReader = new PdfReader(inputStream);
            // 创建PDF文件的模板,可以对模板的内容修改,重新生成新PDF文件
            pdfStamper = new PdfStamper(pdfReader, outputStream);
            // 设置水印字体
            BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
            // 设置PDF内容的Graphic State 图形状态
            PdfGState pdfGraPhicState = new PdfGState();
            // 填充透明度
            pdfGraPhicState.setFillOpacity(0.2f);
            // 轮廓不透明度
            pdfGraPhicState.setStrokeOpacity(0.4f);
            // PDF页数
            int pdfPageNum = pdfReader.getNumberOfPages() + 1;
            // PDF文件内容字节
            PdfContentByte pdfContent;
            // PDF页面矩形区域
            com.itextpdf.text.Rectangle pageRectangle;
            for (int i = 1; i < pdfPageNum; i++) {
                // 获取当前页面矩形区域
                pageRectangle = pdfReader.getPageSizeWithRotation(i);
                // 获取当前页内容,getOverContent表示之后会在页面内容的上方加水印
                pdfContent = pdfStamper.getOverContent(i);
                // 获取当前页内容,getOverContent表示之后会在页面内容的下方加水印
                // pdfContent = pdfStamper.getUnderContent(i);
                pdfContent.saveState();
                // 设置水印透明度
                pdfContent.setGState(pdfGraPhicState);
                // 开启写入文本
                pdfContent.beginText();
                // 设置字体
                pdfContent.setFontAndSize(baseFont, 15);
                // 在高度和宽度维度每隔waterMarkInterval距离添加一个水印
                for (int height = waterMarkHeight; height < pageRectangle.getHeight(); height = height + waterMarkInterval) {
                    for (int width = watermarkWeight; width < pageRectangle.getWidth() + watermarkWeight;
                         width = width + waterMarkInterval) {
                        // 添加水印文字并旋转30度角
                        pdfContent.showTextAligned(Element.ALIGN_LEFT, waterMarkText, width - watermarkWeight,
                                height - waterMarkHeight, 30);
                    }
                }
                // 停止写入文本
                pdfContent.endText();
            }
            pdfStamper.close();
            pdfReader.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (pdfReader != null) {
                    pdfReader.close();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.flush();
                    outputStream.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return new ByteArrayInputStream(((ByteArrayOutputStream) outputStream).toByteArray());
    }

    /**
     * 给word添加水印
     *
     * @param inputStream
     * @param waterMarkText
     * @return
     */
    public InputStream addWordWaterMark(InputStream inputStream, String waterMarkText) {
        if (waterMarkText == null || waterMarkText == "") {
            return inputStream;
        }
        try {
            XWPFDocument doc = new XWPFDocument(inputStream);
            WordWaterMarker.makeFullWaterMarkByWordArt(doc, waterMarkText, "#d8d8d8", "0.5pt", "-30");
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            doc.write(os);
            return new ByteArrayInputStream(os.toByteArray());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

2.5model层代码

        DownloadFile.java完整代码如下:

package com.example.testdemo.test.model;

import lombok.Data;
import java.util.List;

/**
 * 项目名称: testDemo
 * 作者: zhaojs
 * 创建日期: 2024年07月04日
 * 文件说明: 见类描述
 */
@Data
public class DownloadFile {
    /**
     * 附件id,多个用','隔开
     */
    private String attachmentIds;
    /**
     * 文件压缩包名
     */
    private String fileZipName;
    /**
     * 水印文字
     */
    private String watermarkText;
}

        Attachment.java完整代码如下:

package com.example.testdemo.test.model;

import java.time.LocalDateTime;
import java.util.Date;
import java.io.Serializable;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;


/**
 * 附件表实体类
 * @author zhaojs
 * @date 2024-07-04
 */
@Data
@TableName("ATTACHMENT")
public class Attachment implements Serializable {
    /**
     * 序列化变量
     */
    private static final long serialVersionUID = 697303854047803428L;
    /**
     * 主键id
     */
    @TableId
    private String attachmentId;
    /**
     * 文件id
     */
    private String fileId;
    /**
     * 文件名
     */
    private String fileName;
    /**
     * 文件类型
     */
    private String fileType;
    /**
     * 文件大小
     */
    private Double fileSize;
    /**
     * 文件路径
     */
    private String filePath;
    /**
     * 创建人id
     */
    private String createUserId;
    /**
     * 创建人姓名
     */
    private Integer createUserName;
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
    /**
     * 更新人id
     */
    private String updateUserId;
    /**
     * 更新人姓名
     */
    private String updateUserName;
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
    /**
     * 文件过期时间
     */
    private LocalDateTime expireTime;
}

2.6dao层代码

        AttachmentDao.java完整代码如下,对应的mapper文件自己映射就好,这里就不给出了:

package com.example.testdemo.test.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.testdemo.test.model.Attachment;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

/**
 * 附件表Dao接口
 * @author zhaojs
 * @date 2024-07-04
 */
public interface AttachmentDao extends BaseMapper<Attachment> {
}

2.6说明

        (1)代码内的数据库表自己创建,参考Attachment.java即可。

        (2)到此,就可以使用了,Linux和Windows都可以,启动项目前记得把libreoffice也启起来,也就是1.3启动libreoffice

        (3)若是转换过程发现报错Cause: java.sql.SQLException: Incorrect string value: '\xE6\xB5\x8B\xE8\xAF\x95...' for column 'FILE_NAME' at row 1
; uncategorized SQLException; SQL state [HY000]; error code [1366]; Incorrect string value: '\xE6\xB5\x8B\xE8\xAF\x95...' for column 'FILE_NAME' at row 1; nested exception is java.sql.SQLException: Incorrect string value: '\xE6\xB5\x8B\xE8\xAF\x95...' for column 'FILE_NAME' at row 1] with root cause

 则是字符集编码引起的,执行下面命令修改mysql字符集即可:

ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

ALTER DATABASE 数据库名 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

;