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…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...

【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
【Java学习笔记】Arrays类
Arrays 类 1. 导入包:import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序(自然排序和定制排序)Arrays.binarySearch()通过二分搜索法进行查找(前提:数组是…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习) 一、Aspose.PDF 简介二、说明(⚠️仅供学习与研究使用)三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...