PDF或Word转图片(多线程+aspose+函数式接口)
业务需求
有个业务需要把用户上传的PDF或者Word文件转成图片,用于前端展示
实现思路
这里直接用aspose实现即可,由于这种转换是非常慢的,所以还需要用到多线程
具体实现
引入aspose依赖
<!--aspose对应的word和pdf的依赖 -->
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-pdf</artifactId>
<version>17.3.0</version>
</dependency>
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>24.1</version>
<classifier>jdk17</classifier>
</dependency>
<!--File类转成MultipartFile类需要的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.20</version>
</dependency>
核心类
package com.smart.technology.utils;
import com.aspose.pdf.*;
import com.aspose.pdf.devices.PngDevice;
import com.aspose.pdf.devices.Resolution;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* PDF转图片工具类
* 使用Aspose.PDF将PDF文档转换为图片
* @author: daizhongliang
* @date: 2025/10/28
*/
public class PDFToImageUtils {
/**
* 将PDF文档转换为图片
* @param inputPath PDF文档的输入路径
* @param outputDir 输出图片的目录
* @return 转换后的图片文件列表
* @throws Exception 如果转换过程中发生错误
*/
public static List<File> pdfToImg(String inputPath, String outputDir) throws Exception {
List<File> imageFiles = new ArrayList<>();
try {
// 加载PDF文档
Document pdfDocument = new Document(inputPath);
System.out.println("文档页数: " + pdfDocument.getPages().size());
// 创建输出目录
File outputDirectory = new File(outputDir);
if (!outputDirectory.exists()) {
outputDirectory.mkdirs();
}
// 创建固定大小的线程池用于并行处理
int processors = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(processors);
// 创建CompletableFuture列表用于并行处理每一页
List<CompletableFuture<File>> futures = new ArrayList<>();
// 并行处理每一页
for (int i = 1; i <= pdfDocument.getPages().size(); i++) {
final int pageIndex = i;
CompletableFuture<File> future = CompletableFuture.supplyAsync(() -> {
try {
// 获取当前页
Page page = pdfDocument.getPages().get_Item(pageIndex);
// 设置图片保存选项
Resolution resolution = new Resolution(300); // 设置分辨率
PngDevice pngDevice = new PngDevice(resolution);
// 设置渲染选项以提高图片质量
RenderingOptions options = new RenderingOptions();
options.setUseNewImagingEngine(true);
pngDevice.setRenderingOptions(options);
// 生成输出文件名
String outputPath = outputDir + "page_" + pageIndex + ".png";
// 保存为图片
pngDevice.process(page, outputPath);
File imageFile = new File(outputPath);
System.out.println("已生成图片: " + outputPath);
return imageFile;
} catch (Exception e) {
throw new RuntimeException("页面转换失败: " + e.getMessage(), e);
}
}, executor);
futures.add(future);
}
// 等待所有任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allFutures.join();
// 收集结果
for (CompletableFuture<File> future : futures) {
imageFiles.add(future.get());
}
// 关闭线程池
executor.shutdown();
return imageFiles;
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
/**
* 测试main方法:将本地PDF文档转换为多张图片
*/
public static void main(String[] args) {
try {
// 输入PDF文件路径
String inputPath = "D:\\test\\pdf\\sample.pdf";
// 输出图片目录
String outputDir = "D:\\test\\pdf\\output_images\\";
System.out.println("开始转换PDF文档到图片...");
System.out.println("输入文件: " + inputPath);
System.out.println("输出目录: " + outputDir);
// 执行转换
List<File> imageFiles = pdfToImg(inputPath, outputDir);
System.out.println("转换完成!");
System.out.println("共生成 " + imageFiles.size() + " 张图片");
} catch (Exception e) {
System.err.println("转换失败: " + e.getMessage());
e.printStackTrace();
}
}
}
package com.smart.technology.utils;
import com.aspose.words.Document;
import com.aspose.words.ImageSaveOptions;
import com.aspose.words.PageSet;
import com.aspose.words.SaveFormat;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Word转图片工具类
* 使用Aspose.Words将Word文档转换为图片
* @author: daizhongliang
* @date: 2025/10/28
*/
public class WordToImageUtils {
/**
* 将Word文档转换为图片
* @param inputPath Word文档的输入路径
* @param outputDir 输出图片的目录
* @return 转换后的图片文件列表
* @throws Exception 如果转换过程中发生错误
*/
public static List<File> wordToImg(String inputPath, String outputDir) throws Exception {
List<File> imageFiles = new ArrayList<>();
try {
// 加载Word文档
Document doc = new Document(inputPath);
System.out.println("文档页数: " + doc.getPageCount());
// 创建输出目录
File outputDirectory = new File(outputDir);
if (!outputDirectory.exists()) {
outputDirectory.mkdirs();
}
// 创建固定大小的线程池用于并行处理
int processors = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(processors);
// 创建CompletableFuture列表用于并行处理每一页
List<CompletableFuture<File>> futures = new ArrayList<>();
// 并行处理每一页
for (int i = 0; i < doc.getPageCount(); i++) {
final int pageIndex = i;
CompletableFuture<File> future = CompletableFuture.supplyAsync(() -> {
try {
ImageSaveOptions options = new ImageSaveOptions(SaveFormat.PNG);
options.setPrettyFormat(true);
options.setUseAntiAliasing(true);
options.setUseHighQualityRendering(true);
options.setPageSet(new PageSet(pageIndex)); // 设置当前页
// 生成输出文件名
String outputPath = outputDir + "page_" + (pageIndex + 1) + ".png";
doc.save(outputPath, options);
File imageFile = new File(outputPath);
System.out.println("已生成图片: " + outputPath);
return imageFile;
} catch (Exception e) {
throw new RuntimeException("页面转换失败: " + e.getMessage(), e);
}
}, executor);
futures.add(future);
}
// 等待所有任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allFutures.join();
// 收集结果
for (CompletableFuture<File> future : futures) {
imageFiles.add(future.get());
}
// 关闭线程池
executor.shutdown();
return imageFiles;
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
/**
* 测试main方法:将本地Word文档转换为多张图片
*/
public static void main(String[] args) {
try {
// 输入Word文件路径
String inputPath = "D:\\test\\1.docx";
// 输出图片目录
String outputDir = "D:\\test\\output_images\\";
System.out.println("开始转换Word文档到图片...");
System.out.println("输入文件: " + inputPath);
System.out.println("输出目录: " + outputDir);
// 执行转换
List<File> imageFiles = wordToImg(inputPath, outputDir);
System.out.println("转换完成!");
System.out.println("共生成 " + imageFiles.size() + " 张图片");
} catch (Exception e) {
System.err.println("转换失败: " + e.getMessage());
e.printStackTrace();
}
}
}
线程池配置类
package com.smart.technology.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步线程池配置类
* 用于优化文件转换等耗时操作的性能
*/
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
/**
* 文件转换专用线程池
* 核心线程数: 10
* 最大线程数: 20
* 队列容量: 100
* 线程空闲时间: 60秒
* 拒绝策略: 调用者运行策略
*
* @return Executor 线程池执行器
*/
@Bean("fileConvertTaskExecutor")
public Executor fileConvertTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int processors = Runtime.getRuntime().availableProcessors();
// 核心线程数
executor.setCorePoolSize(processors);
// 最大线程数
executor.setMaxPoolSize(processors * 2);
// 队列容量
executor.setQueueCapacity(100);
// 线程空闲时间
executor.setKeepAliveSeconds(60);
// 线程名称前缀
executor.setThreadNamePrefix("file-convert-");
// 拒绝策略: 当线程池达到最大容量且队列已满时,新任务将在调用者线程中执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化线程池
executor.initialize();
return executor;
}
}
业务层具体引用
@Autowired
private Executor fileConvertTaskExecutor;
/**
* 将Word/PDF文件转换为图片并上传
* @param file 上传的文件
* @param packageId 课程包ID
* @return 转换结果,包含原始文件URL和图片列表
*/
@Override
public Result<FileImagesUploadDTO> uploadWord2Img(MultipartFile file, Long packageId) {
// 验证参数
if (file == null || file.isEmpty()) {
return Result.fail("文件不能为空");
}
if (packageId == null) {
return Result.fail("课程包ID不能为空");
}
// 获取文件后缀
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
return Result.fail("文件名不能为空");
}
// 获取文件扩展名
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
// word或者pdf目录名称
String dirName = "";
// word或者pdf转换后的图片目录名称
String dirImagesName = "";
if (fileExtension.contains(".doc") || fileExtension.contains(".docx")) {
dirName = "Word";
dirImagesName = "WordImage";
} else if (fileExtension.contains(".pdf")) {
dirName = "PDF";
dirImagesName = "PDFImage";
} else {
return Result.fail("不支持的文件类型");
}
String fileUUID = String.valueOf(UUID.randomUUID());
// 生成唯一文件名
String fileName = fileUUID + fileExtension;
// 构建文件路径
String filePath = String.format("/Course/default/%d/%s/", packageId, dirName);
// 1、上传word或pdf文件到COS云存储
FileUploadDTO fileInfo = cosService.uploadFile(file, filePath, fileName);
try {
// 2、根据文件类型调用对应的转换方法
List<String> imageUrls;
if ("Word".equals(dirName)) {
imageUrls = convertAndUploadImages(file, packageId, fileUUID, dirName, dirImagesName,
(tempFilePath, tempImagePath) -> WordToImageUtils.wordToImg(tempFilePath, tempImagePath));
log.info("Word文件转换为图片并上传完成,共{}张图片", imageUrls.size());
} else if ("PDF".equals(dirName)) {
imageUrls = convertAndUploadImages(file, packageId, fileUUID, dirName, dirImagesName,
(tempFilePath, tempImagePath) -> PDFToImageUtils.pdfToImg(tempFilePath, tempImagePath));
log.info("PDF文件转换为图片并上传完成,共{}张图片", imageUrls.size());
} else {
return Result.fail("不支持的文件类型");
}
// 创建返回对象
FileImagesUploadDTO result = new FileImagesUploadDTO();
result.setFileUrl(fileInfo.getFilePath());
result.setFileImages(imageUrls);
return Result.success(result);
} catch (Exception e) {
throw new RuntimeException("文件转换为图片失败: " + e.getMessage(), e);
}
}
/**
* 函数接口,用于定义文件转图片的转换方法
*/
@FunctionalInterface
private interface ImageConverter {
List<File> convert(String sourcePath, String targetPath) throws Exception;
}
/**
* 通用的文件转换和图片上传方法
* @param file 原始文件
* @param packageId 课程包ID
* @param fileUUID 文件UUID
* @param dirName 目录名称
* @param dirImagesName 图片目录名称
* @param converter 转换接口实现
* @return 图片URL列表
* @throws Exception 转换或上传过程中的异常
*/
private List<String> convertAndUploadImages(MultipartFile file, Long packageId, String fileUUID,
String dirName, String dirImagesName, ImageConverter converter) throws Exception {
// 创建临时目录
String tempDir = System.getProperty("java.io.tmpdir") + dirName + "ToImages" + File.separator + System.currentTimeMillis();
File tempDirFile = new File(tempDir);
tempDirFile.mkdirs();
try {
// 将上传的文件保存到临时路径
String tempFilePath = tempDir + File.separator + file.getOriginalFilename();
try (FileOutputStream fos = new FileOutputStream(tempFilePath)) {
IOUtils.copy(file.getInputStream(), fos);
}
// 临时存储转换后图片的路径
String tempImagePath = tempDir + File.separator + dirImagesName;
// 调用转换器将文件转换为图片(异步并行处理)
List<File> imageFiles = convertImagesAsync(tempFilePath, tempImagePath, converter);
// 上传转换后的图片到COS(异步并行上传)
List<String> imageUrls = uploadImagesAsync(imageFiles, packageId, fileUUID, dirImagesName);
return imageUrls;
} finally {
// 清理临时目录
tempDirFile.delete();
}
}
/**
* 异步并行转换文件为图片
* @param sourcePath 源文件路径
* @param targetPath 目标图片路径
* @param converter 转换器
* @return 图片文件列表
* @throws Exception 转换过程中的异常
*/
private List<File> convertImagesAsync(String sourcePath, String targetPath, ImageConverter converter) throws Exception {
// 调用转换器将文件转换为图片
return converter.convert(sourcePath, targetPath);
}
/**
* 异步并行上传图片到COS
* @param imageFiles 图片文件列表
* @param packageId 课程包ID
* @param fileUUID 文件UUID
* @param dirImagesName 图片目录名称
* @return 图片URL列表
* @throws Exception 上传过程中的异常
*/
private List<String> uploadImagesAsync(List<File> imageFiles, Long packageId, String fileUUID, String dirImagesName) throws Exception {
// 上传转换后的图片到COS
String imageDirPath = String.format("/Course/default/%d/%s/%s", packageId, dirImagesName, fileUUID);
List<String> imageUrls = new ArrayList<>();
// 创建CompletableFuture列表用于并行上传
List<CompletableFuture<FileUploadDTO>> futures = new ArrayList<>();
// 遍历图片文件列表,异步上传到COS
int imageIndex = 1;
for (File imageFile : imageFiles) {
final int index = imageIndex;
CompletableFuture<FileUploadDTO> future = CompletableFuture.supplyAsync(() -> {
try {
// 生成自定义文件名:uuid_1格式
String customFileName = UUID.randomUUID() + "_" + index + ".png";
// 将File对象转换为MultipartFile
MultipartFile multipartImageFile = new MockMultipartFile(
customFileName,
customFileName,
"image/png",
Files.newInputStream(imageFile.toPath())
);
// 上传到COS,使用指定的文件名
return cosService.uploadFile(multipartImageFile, imageDirPath, customFileName);
} catch (Exception e) {
log.error("图片上传失败: {}", imageFile.getName(), e);
throw new RuntimeException("图片上传失败: " + e.getMessage(), e);
}
}, fileConvertTaskExecutor);
futures.add(future);
imageIndex++;
}
// 等待所有上传任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allFutures.join();
// 收集上传结果
for (CompletableFuture<FileUploadDTO> future : futures) {
FileUploadDTO imageInfo = future.get();
imageUrls.add(imageInfo.getFilePath());
log.info("图片上传成功: {}, URL: {}", imageInfo.getFileName(), imageInfo.getFilePath());
}
return imageUrls;
}
骚戴解析:上面的逻辑其实很简单,首先把用户上传的pdf或者word上传到腾讯云cos云存储,然后把上传的pdf或者word文件复制到一个临时目录,再临时目录建立一个子目录用于存储转换后的图片,调用上面核心类的工具类去转换pdf或者word,把转换的List<File>转成MultipartFile然后调用腾讯云cos云存储的上传接口把转换后的图片上传到腾讯云cos云存储的桶上面去,最后把临时文件目录给删除掉。中间用了高级的东西,函数式接口来解耦和复用,因为pdf和word的整体逻辑其实差不多,只是调用的工具类不一样同时上传后的目录不一样而已