> 技术文档 > libreoffice在Windows和Linux环境的安装和结合Springboot使用教程

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的。另外下载的话选择国内镜像下载,比较快:

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

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

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

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

1.1解压压缩包

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

cd /opttar -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缺少中文字体的缘故,导致转出来的中文会是如下的效果:

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

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

1.4Linux安装中文字体

        首先安装fontconfig:

yum -y install fontconfig

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

cd /usr/sharels

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

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

cd fontsmkdir chinese

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

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

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

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

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

yum -y install ttmkfdirttmkfdir -e /usr/share/X11/fonts/encodings/encodings.dirvi /etc/fonts/fonts.conf

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

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

/usr/share/fonts/chinese

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

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

fc-cache

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

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

 

2.Springboot集成libreoffice

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

2.1引入pom依赖

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

   org.jodconverter jodconverter-spring-boot-starter 4.4.7   org.jodconverter jodconverter-local 4.4.7    org.apache.pdfbox pdfbox 2.0.24   cn.hutoolhutool-all5.8.25   org.apache.commons commons-collections4 4.4   commons-io commons-io 2.16.1   org.apache.commons commons-lang3 3.12.0    org.apache.poi poi-ooxml 5.2.5    com.itextpdf itextpdf 5.5.13.4 

2.2修改yaml配置文件

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

spring: profiles: active: local# 共享配置jodconverter: local: enabled: true portNumbers: 8100 maxTasksPerProcess: 100 taskQueueTimeout: 60file: path: windows: \'D:/temp/file\' linux: \'/file\'---spring: config: activate: on-profile: localjodconverter: local: officeHome: \'D:\\Program Files\\LibreOffice\'---spring: config: activate: on-profile: serverjodconverter: 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日 * 文件说明: 文件控制层 */@RestControllerpublic 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 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;@Servicepublic 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 getFile(String ids) { if (StringUtils.isNotBlank(ids)) { String[] attachmentIds = ids.split(\",\"); List attachmentList = attachmentDao.selectBatchIds(Arrays.asList(attachmentIds)); return attachmentList; } return null; } /** * 附件转为图片的base64编码 * * @param downloadFile * @return */ @Override public Result switchAttachmentImageBase64(DownloadFile downloadFile) { List<Map> fileList = new ArrayList(); List attachmentList = getFile(downloadFile.getAttachmentIds()); attachmentList.forEach(attachment -> { Map map = new HashMap(); map.put(\"fileName\", attachment.getFileName()); map.put(\"filePath\", attachment.getFilePath()); fileList.add(map); }); //附件归类 List imageFilePathList = new ArrayList(); List pdfFilePathList = new ArrayList(); List 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 imageBase64List = new ArrayList(); //先把word转为图片base64编码 if (CollectionUtils.isNotEmpty(wordFilePathList)) { List imageBase64 = wordFileToImageBase64(wordFilePathList, downloadFile.getWatermarkText()); if (imageBase64 != null && ! imageBase64.isEmpty()) { imageBase64List.addAll(imageBase64); } } //再把pdf转为图片base64编码 if (CollectionUtils.isNotEmpty(pdfFilePathList)) { List imageBase64 = pdfFileToImageBase64(pdfFilePathList, downloadFile.getWatermarkText()); if (imageBase64 != null && ! imageBase64.isEmpty()) { imageBase64List.addAll(imageBase64); } } //最后把图片直接转为base64编码 if (CollectionUtils.isNotEmpty(imageFilePathList)) { List 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 wordFileToImageBase64(List wordFilePathList, String watermarkText) { List imageBase64List = new ArrayList(); ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_TASKS); try { List<Future<List>> futures = new ArrayList(); // 首先,将Word文件转换为PDF字节流,并为每个字节流创建一个转换任务 for (String filePath : wordFilePathList) { // 将Word转换为PDF字节流 ByteArrayOutputStream pdfByteStream = wordToPdf(filePath); // 将PDF字节流转换为图片Base64 Callable<List> task = () -> pdfByteToImageBase64Single(pdfByteStream.toByteArray(), watermarkText); futures.add(executor.submit(task)); } // 收集所有任务的结果 for (Future<List> 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 pdfByteToImageBase64Single(byte[] pdfBytes, String watermarkText) { List 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 pdfFileToImageBase64(List pdfFilePathList, String watermarkText) { List imageBase64List = new ArrayList(); ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_TASKS); try { // 提交任务到线程池 List<Future<List>> futures = new ArrayList(); for (String filePath : pdfFilePathList) { Callable<List> task = () -> convertSinglePdfToImageBase64(filePath, watermarkText); futures.add(executor.submit(task)); } // 收集所有任务的结果 for (Future<List> 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 convertSinglePdfToImageBase64(String filePath, String watermarkText) { List 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 imageFileToBase64(List imageFilePathList, String watermarkText) { List imageBase64List = new ArrayList(); ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_TASKS); try { // 提交任务到线程池 List<Future> futures = imageFilePathList.stream()  .map(path -> executor.submit(() -> convertSingleImageToBase64(path, watermarkText)))  .collect(Collectors.toList()); // 收集所有任务的结果 for (Future 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日 * 文件说明: 见类描述 */@Datapublic 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 {}

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;