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 FilesLibreOffice就是安装的路径:
D:Program FilesLibreOfficeprogramsoffice.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: trueportNumbers: 8100maxTasksPerProcess: 100taskQueueTimeout: 60file:path:windows: 'D:/temp/file'linux: '/file'---
spring:config:activate:on-profile: local
jodconverter:local:officeHome: 'D:Program FilesLibreOffice'
---
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 {/*** 文件服务*/@Resourceprivate 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;/*** 文档转换器*/@Resourceprivate DocumentConverter documentConverter;/*** 附件表Dao*/@Resourceprivate 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*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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字节流转换为图片Base64Callable<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();// 将带有水印的图像写入ByteArrayOutputStreamByteArrayOutputStream 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*/@TableIdprivate 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: ‘?μ?èˉ?..’ for column ‘FILE_NAME’ at row 1
; uncategorized SQLException; SQL state [HY000]; error code [1366]; Incorrect string value: ‘?μ?èˉ?..’ for column ‘FILE_NAME’ at row 1; nested exception is java.sql.SQLException: Incorrect string value: ‘?μ?èˉ?..’ 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;
相关文章:

libreoffice在Windows和Linux环境的安装和结合Springboot使用教程
前言: 在公司做开发时,遇到一个需求,要求上传的文件有图片,也有word和pdf。预览信息时,既要求能水印展示出来,又要求能大图水印预览。思索许久,我决定采取全部打水印然后转成图片Base64&#x…...

前端开发 -- 自动回复机器人【附完整源码】
一:效果展示 本项目实现了一个简单的网页聊天界面,用户可以在输入框中输入消息,并点击发送按钮或按下回车键来发送消息。机器人会根据用户发送的消息内容,通过关键字匹配来生成自动回复。 二:源代码分享 <!DOCTYP…...

vue+echarts实现疫情折线图
效果: 代码: <<template><div><div id"left1" style "height:800px;width:100%"></div></div> </template><script> //疫情数据//export default {data() {return {data:{//疫情数据…...
服务器nfs文件共享
1. 配置 NFS 服务器(NFS Server) 在 Ubuntu/Debian 上: sudo apt update sudo apt install nfs-kernel-server在 CentOS/RHEL 上: sudo yum install nfs-utils1.2 创建共享目录 选择一个要共享的目录,并确保该目录的权限正确设置。例如,假设我们要共享 /srv/nfs 目录…...

基于Vue+SSM+SpringCloudAlibaba的科目课程管理系统
功能1:科目列表 功能2:条件查询 功能3:分页查询 功能4:excel批量导入 功能5:修改 功能6:删除...
vue3配置caddy作为静态服务器,在浏览器地址栏刷新出现404
vue3配置caddy作为静态服务器,在浏览器地址栏刷新出现404 1 情况描述2 原因3 配置 1 情况描述 在vue打包之后,形成dist文件,采用caddy作为静态资源服务器。在浏览器中输入域名时可以访问网站,但是,进过路由导航栏内部…...
深入理解委托:C# 编程中的强大工具
在面向对象编程中,委托(Delegate) 是一个非常强大且灵活的概念,特别是在 C# 编程语言中。它不仅仅是函数指针的替代品,还提供了更高层次的抽象,使得代码更加简洁、灵活和可维护。在这篇博客中,我…...

【Java 数据结构】合并两个有序链表
🔥博客主页🔥:【 坊钰_CSDN博客 】 欢迎各位点赞👍评论✍收藏⭐ 目录 1. 题目 2. 解析 3. 代码实现 4. 小结 1. 题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示…...

基于微信小程序的校园访客登记系统
基于微信小程序的校园访客登记系统 功能列表 用户端功能 注册与登录 :支持用户通过手机号短信验证码注册和登录。个人资料管理 :允许用户编辑和更新个人信息及其密码。站内信消息通知:通知公告。来访预约:提交来访预约支持车牌…...

uniapp 判断多选、选中取消选中的逻辑处理
一、效果展示 二、代码 1.父组件: :id=“this.id” : 给子组件传递参数【id】 @callParentMethod=“takeIndexFun” :给子组件传递方法,这样可以在子组件直接调用父组件的方法 <view @click="$refs.member.open()"...
php8.0版本更新了哪些内容
PHP 8.0版本是PHP语言的一个重要更新,它引入了许多新特性和改进,旨在提高性能、增强代码的可读性和可维护性。以下是PHP 8.0版本更新的主要内容: 一、性能提升 JIT编译器:PHP 8.0引入了Just-In-Time(JIT)…...

Browser Use:AI智能体自动化操作浏览器的开源工具
Browser Use:AI智能体自动化操作浏览器的开源工具 Browser Use 简介1. 安装所需依赖2. 生成openai密钥3. 编写代码4. 运行代码5. 部署与优化5.1 部署AI代理5.2 优化与扩展总结Browser Use 简介 browser-use是一个Python库,它能够帮助我们将AI代理与浏览器自动化操作结合起来;…...

Android笔记(四十):ViewPager2嵌套RecyclerView滑动冲突进一步解决
背景 ViewPager2内嵌套横向滑动的RecyclerView,会有滑动冲突的情况,引入官方提供的NestedScrollableHost类可以解决冲突问题,但是有一些瑕疵,滑动横向RecyclerView到顶部,按住它不放手继续往左拖再往右拖,这…...
POS系统即销售点系统 文档与数据库设计
POS系统即销售点系统,是一种用于商业交易的软硬件集成系统,主要用于管理销售、库存、客户信息等,以下是其详细介绍: 1. 系统组成 硬件部分 : 收银终端:包括传统的台式收银机、平板电脑、智能手机等设备&a…...

安全合规遇 AI 强援:深度驱动行业发展新引擎 | 倍孜网络CEO聂子尧出席ICT深度观察报告会!
12月24日,2025中国信通院深度观察报告会科技伦理与合规发展分论坛在北京举办。本次分论坛主题为“伦理先行,合规致远”,聚焦互联网广告合规治理、移动终端应用生态治理、短视频平台责任限度等前沿话题进行分享与探讨。工业和信息化部领导&…...

算法进阶:贪心算法
贪心算法是一种简单而直观的算法思想,它在每一步选择中都采取在当前状态下最优的选择,以期望最终得到全局最优解。贪心算法通常适用于一些具有最优子结构的问题,即问题的最优解可以通过一系列局部最优解的选择得到。 贪心算法的基本思路是&a…...
C++ 设计模式:工厂方法(Factory Method)
链接:C 设计模式 链接:C 设计模式 - 抽象工厂 链接:C 设计模式 - 原型模式 链接:C 设计模式 - 建造者模式 工厂方法(Factory Method)是创建型设计模式之一,它提供了一种创建对象的接口…...
手机联系人 查询 添加操作
Android——添加联系人_android 添加联系人-CSDN博客 上面连接添加联系人已测试 是可以 Android : 获取、添加、手机联系人-ContentResolver简单应用_contentresolver 添加联系人-CSDN博客...

【LeetCode】2506、统计相似字符串对的数目
【LeetCode】2506、统计相似字符串对的数目 文章目录 一、哈希表位运算1.1 哈希表位运算 二、多语言解法 一、哈希表位运算 1.1 哈希表位运算 每个字符串, 可用一个 int 表示. (每个字符 是 int 的一个位) 哈希表记录各 字符组合 出现的次数 步骤: 遇到一个字符串, 得到 ma…...

金仓数据库对象访问权限的管理
基础知识 对象的分类 数据库的表、索引、视图、缺省值、规则、触发器等等,都称为数据库对象,对象分为如下两类: 模式(SCHEMA)对象:可以理解为一个存储目录,包含视图、索引、数据类型、函数和操作符等。非模式对象:其他的数据库对象&#x…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
Android Wi-Fi 连接失败日志分析
1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分: 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析: CTR…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...

人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...

接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...

【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...