
优化版本(自定义 ThreadPoolExecutor + 有界队列 + 超时 + 异常处理)更适合生产,更健壮
javapackage top.zhoudeshui.utils;
import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.ComThread;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.util.concurrent.*;
/**
* WPS文档转换工具类(使用Jacob调用WPS Office进行格式转换)
* 通过线程池队列机制保证并发安全,避免WPS进程冲突
*
* @author zhoudeshui
* @date 2025-03-20
*/
public class WPSConvertUtils {
private static final Logger logger = LoggerFactory.getLogger(WPSConvertUtils.class);
// 转换常量
private static final int WD_FORMAT_PDF = 17; // Word 转 PDF 格式
private static final int XL_TYPE_PDF = 0; // Excel 转 PDF 格式
private static final int PP_SAVE_AS_PDF = 32; // PPT 转 PDF 格式
/**
* 线程池核心线程数
*/
private static final int CORE_POOL_SIZE = 1;
/**
* 线程池最大线程数
*/
private static final int MAX_POOL_SIZE = 3;
/**
* 空闲线程存活时间(秒)
*/
private static final long KEEP_ALIVE_TIME = 60L;
/**
* 任务队列容量
*/
private static final int QUEUE_CAPACITY = 50;
/**
* 转换任务超时时间(秒)
*/
private static final long CONVERSION_TIMEOUT = 120L;
/**
* CompletableFuture 等待结果的超时时间
*/
private static final long FUTURE_TIMEOUT = 130L;
/**
* 单线程线程池用于串行执行WPS转换任务,避免多实例冲突
*/
private static final ExecutorService CONVERSION_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
r -> new Thread(r, "WPS-Conversion-Thread"),
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行
);
/**
* 将文件转换为PDF(支持Word、Excel、PPT、图片、PDF复制)
*
* @param inputFilePath 输入文件路径
* @param outputDirOrFilePath 输出目录或完整PDF路径
* @return CompletableFuture<String> 转换成功返回PDF路径,失败返回null
*/
public static CompletableFuture<String> convertToPDF(String inputFilePath, String outputDirOrFilePath) {
try {
// 异步提交任务
return CompletableFuture.supplyAsync(() -> {
try {
return processConversionTask(inputFilePath, outputDirOrFilePath);
} catch (Exception e) {
logger.error("转换任务执行异常: {}", inputFilePath, e);
return null;
}
}, CONVERSION_EXECUTOR)
.orTimeout(FUTURE_TIMEOUT, TimeUnit.SECONDS)
.exceptionally(throwable -> {
logger.error("转换任务超时或异常: {}", inputFilePath, throwable);
return null;
});
} catch (Exception e) {
logger.error("提交转换任务失败: {}", inputFilePath, e);
CompletableFuture<String> future = new CompletableFuture<>();
future.complete(null);
return future;
}
}
/**
* 处理具体的转换任务
*
* @param inputFilePath 输入文件路径
* @param outputDirOrFilePath 输出路径
* @return PDF文件路径,失败返回null
*/
private static String processConversionTask(String inputFilePath, String outputDirOrFilePath) {
File inputFile = new File(inputFilePath);
if (!inputFile.exists()) {
logger.warn("原文件不存在: {}", inputFilePath);
return null;
}
String kind = getFileSuffix(inputFilePath).toLowerCase();
if ("pdf".equalsIgnoreCase(kind)) {
return copyPdfFile(inputFilePath, outputDirOrFilePath);
}
String baseName = getBaseName(inputFile.getName());
String outputFilePath = determineOutputPath(outputDirOrFilePath, baseName, ".pdf");
if (outputFilePath == null) {
logger.error("无法确定输出路径: {}", outputDirOrFilePath);
return null;
}
// 创建输出目录
File outputFile = new File(outputFilePath);
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
logger.error("创建输出目录失败: {}", outputFile.getParentFile().getAbsolutePath());
return null;
}
// 执行转换
boolean success;
switch (kind) {
case "doc":
case "docx":
case "txt":
success = wordToPDF(inputFilePath, outputFilePath);
break;
case "xls":
case "xlsx":
success = exToPDF(inputFilePath, outputFilePath);
break;
case "ppt":
case "pptx":
case "pptm":
case "ppsx":
success = pptToPDF(inputFilePath, outputFilePath);
break;
case "png":
case "jpg":
case "jpeg":
case "gif":
success = imageToPDF(inputFilePath, outputFilePath);
break;
default:
logger.warn("不支持的文件格式: {}", kind);
return null;
}
return success ? outputFilePath : null;
}
/**
* 复制PDF文件(不做转换)
*
* @param inputFilePath 源PDF路径
* @param outputDirOrFilePath 输出路径
* @return 目标PDF路径,失败返回null
*/
private static String copyPdfFile(String inputFilePath, String outputDirOrFilePath) {
try {
Path source = Path.of(inputFilePath);
Path target = Path.of(outputDirOrFilePath);
if (!Files.exists(source)) {
logger.error("源文件不存在: {}", source);
return null;
}
// 如果目标是目录,则拼接文件名
if (Files.isDirectory(target)) {
target = target.resolve(source.getFileName());
}
// 确保父目录存在
Path parent = target.getParent();
if (parent != null && !Files.exists(parent)) {
Files.createDirectories(parent);
}
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
logger.info("PDF 文件已复制: {} -> {}", source, target);
return target.toString();
} catch (IOException e) {
logger.error("复制PDF文件失败", e);
return null;
}
}
/**
* 确定输出文件路径
*
* @param outputDirOrFilePath 输出参数(目录或完整路径)
* @param baseName 文件基础名
* @param extension 扩展名
* @return 完整输出路径,失败返回null
*/
private static String determineOutputPath(String outputDirOrFilePath, String baseName, String extension) {
File output = new File(outputDirOrFilePath);
if (outputDirOrFilePath.toLowerCase().endsWith(extension)) {
return outputDirOrFilePath;
} else {
if (!output.exists() && !output.mkdirs()) {
logger.error("创建输出目录失败: {}", output.getAbsolutePath());
return null;
}
return new File(output, baseName + extension).getAbsolutePath();
}
}
/**
* 获取文件扩展名
*
* @param fileName 文件名
* @return 扩展名(不含点)
*/
private static String getFileSuffix(String fileName) {
int lastDot = fileName.lastIndexOf('.');
return (lastDot == -1) ? "" : fileName.substring(lastDot + 1).toLowerCase();
}
/**
* 获取文件基础名(不含扩展名)
*
* @param fileName 文件名
* @return 基础名
*/
private static String getBaseName(String fileName) {
int lastDot = fileName.lastIndexOf('.');
return (lastDot == -1) ? fileName : fileName.substring(0, lastDot);
}
/**
* Word 转 PDF
*
* @param inputFile 输入文件路径
* @param pdfFile 输出PDF路径
* @return 是否成功
*/
private static boolean wordToPDF(String inputFile, String pdfFile) {
ActiveXComponent app = null;
try {
ComThread.InitSTA();
app = new ActiveXComponent("KWPS.Application");
app.setProperty("Visible", new Variant(false));
app.setProperty("AutomationSecurity", new Variant(3)); // 禁用宏
Dispatch docs = app.getProperty("Documents").toDispatch();
Dispatch doc = Dispatch.call(docs, "Open", inputFile, false, true).toDispatch();
Dispatch.call(doc, "ExportAsFixedFormat", pdfFile, WD_FORMAT_PDF);
Dispatch.call(doc, "Close", false);
logger.info("Word 转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (Exception e) {
logger.error("Word 转换失败: {}", inputFile, e);
return false;
} finally {
releaseWPSApp(app);
}
}
/**
* Excel 转 PDF
*
* @param inputFile 输入文件路径
* @param pdfFile 输出PDF路径
* @return 是否成功
*/
private static boolean exToPDF(String inputFile, String pdfFile) {
ActiveXComponent app = null;
try {
ComThread.InitSTA();
app = new ActiveXComponent("KET.Application");
app.setProperty("Visible", new Variant(false));
app.setProperty("AutomationSecurity", new Variant(3)); // 禁用宏
Dispatch workbooks = app.getProperty("Workbooks").toDispatch();
Dispatch workbook = Dispatch.invoke(workbooks, "Open", Dispatch.Method,
new Object[]{inputFile, new Variant(false), new Variant(false)}, new int[9]).toDispatch();
Dispatch.invoke(workbook, "ExportAsFixedFormat", Dispatch.Method,
new Object[]{new Variant(XL_TYPE_PDF), pdfFile}, new int[1]);
Dispatch.call(workbook, "Close", new Variant(false));
logger.info("Excel 转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (Exception e) {
logger.error("Excel 转换失败: {}", inputFile, e);
return false;
} finally {
releaseWPSApp(app);
}
}
/**
* PPT 转 PDF
*
* @param inputFile 输入文件路径
* @param pdfFile 输出PDF路径
* @return 是否成功
*/
private static boolean pptToPDF(String inputFile, String pdfFile) {
ActiveXComponent app = null;
try {
ComThread.InitSTA();
app = new ActiveXComponent("KWPP.Application");
app.setProperty("Visible", new Variant(false));
app.setProperty("AutomationSecurity", new Variant(3)); // 禁用宏
Dispatch presentations = app.getProperty("Presentations").toDispatch();
Dispatch ppt = Dispatch.call(presentations, "Open", inputFile, true, false).toDispatch();
Dispatch.invoke(ppt, "SaveAs", Dispatch.Method,
new Object[]{pdfFile, new Variant(PP_SAVE_AS_PDF)}, new int[1]);
Dispatch.call(ppt, "Close");
logger.info("PPT 转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (Exception e) {
logger.error("PPT 转换失败: {}", inputFile, e);
return false;
} finally {
releaseWPSApp(app);
}
}
/**
* 图片转 PDF(使用PDFBox)
*
* @param inputFile 输入图片路径
* @param pdfFile 输出PDF路径
* @return 是否成功
*/
private static boolean imageToPDF(String inputFile, String pdfFile) {
try (PDDocument document = new PDDocument()) {
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
BufferedImage image = ImageIO.read(new File(inputFile));
PDImageXObject pdImage = LosslessFactory.createFromImage(document, image);
float scaleX = PDRectangle.A4.getWidth() / pdImage.getWidth();
float scaleY = PDRectangle.A4.getHeight() / pdImage.getHeight();
float scale = Math.min(scaleX, scaleY);
float x = (PDRectangle.A4.getWidth() - pdImage.getWidth() * scale) / 2;
float y = (PDRectangle.A4.getHeight() - pdImage.getHeight() * scale) / 2;
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(pdImage, x, y, pdImage.getWidth() * scale, pdImage.getHeight() * scale);
}
document.save(pdfFile);
logger.info("图片转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (IOException e) {
logger.error("图片转换失败: {}", inputFile, e);
return false;
}
}
/**
* 安全释放WPS应用实例
*
* @param app ActiveXComponent 实例
*/
private static void releaseWPSApp(ActiveXComponent app) {
if (app != null) {
try {
app.invoke("Quit");
} catch (Exception e) {
logger.warn("WPS Quit调用异常", e);
} finally {
app.safeRelease();
}
}
ComThread.Release();
}
/**
* 关闭转换线程池(应用关闭时调用)
*/
public static void shutdown() {
if (!CONVERSION_EXECUTOR.isShutdown()) {
CONVERSION_EXECUTOR.shutdown();
try {
if (!CONVERSION_EXECUTOR.awaitTermination(CONVERSION_TIMEOUT, TimeUnit.SECONDS)) {
CONVERSION_EXECUTOR.shutdownNow();
}
} catch (InterruptedException e) {
CONVERSION_EXECUTOR.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
(使用 Executors.newSingleThreadExecutor() 无界队列)极易在高并发下造成 OOM
javapackage top.zhoudeshui.utils;
import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.ComThread;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.*;
public class WPSConvertUtils {
private static final Logger logger = LoggerFactory.getLogger(WPSConvertUtils.class);
private static final int WD_FORMAT_PDF = 17;
private static final int XL_TYPE_PDF = 0;
private static final int PP_SAVE_AS_PDF = 32;
private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
public static CompletableFuture<String> convertToPDF(String inputFilePath, String outputDirOrFilePath) {
File inputFile = new File(inputFilePath);
if (!inputFile.exists()) {
logger.warn("原文件不存在: {}", inputFilePath);
return CompletableFuture.completedFuture(null);
}
String kind = getFileSuffix(inputFile.getName());
// 检查文件是否为PDF
if ("pdf".equalsIgnoreCase(kind)) {
try {
Path source = new File(inputFilePath).toPath();
Path target = new File(outputDirOrFilePath).toPath();
// 检查源文件是否存在
if (!Files.exists(source)) {
logger.error("Source file does not exist: {}", source);
return CompletableFuture.completedFuture(null);
}
// 如果目标是目录,创建一个以源文件名命名的新文件
if (Files.isDirectory(target)) {
target = target.resolve(source.getFileName());
}
// 确保目标目录存在
Path targetParent = target.getParent();
if (targetParent != null && !Files.exists(targetParent)) {
Files.createDirectories(targetParent);
}
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
logger.info("PDF 文件已复制: {} -> {}", source, target);
return CompletableFuture.completedFuture(target.toString());
} catch (IOException e) {
logger.error("Error copying PDF file", e);
return CompletableFuture.completedFuture(null);
}
}
String baseName = inputFile.getName().substring(0, inputFile.getName().lastIndexOf("."));
File outputDirOrFile = new File(outputDirOrFilePath);
String outputFilePath;
// 检查输出路径是否包含.pdf
if (outputDirOrFilePath.toLowerCase().endsWith(".pdf")) {
// 输出路径已经包含.pdf,直接使用该文件名
File pdfFile = new File(outputDirOrFilePath);
// 确保文件的父目录存在,如果不存在则创建
if (!pdfFile.getParentFile().exists() && !pdfFile.getParentFile().mkdirs()) {
logger.error("创建目录失败,请检查目录权限!");
return CompletableFuture.completedFuture(null);
}
outputFilePath = outputDirOrFilePath;
} else {
// 输出路径不包含.pdf,使用源文件的基本名称作为输出文件名
if (!outputDirOrFile.exists() && !outputDirOrFile.mkdirs()) {
logger.error("创建目录失败,请检查目录权限!");
return CompletableFuture.completedFuture(null);
}
outputFilePath = new File(outputDirOrFile, baseName + ".pdf").getAbsolutePath();
}
// 提交任务到线程池并返回 CompletableFuture
return CompletableFuture.supplyAsync(() -> processTask(inputFilePath, outputFilePath, kind), executorService)
.thenApply(result -> {
if (result) {
logger.info("转换成功: {} -> {}", inputFilePath, outputFilePath);
return outputFilePath;
} else {
logger.error("转换失败: {}", inputFilePath);
return null;
}
});
}
private static boolean processTask(String inputFilePath, String outputFilePath, String kind) {
switch (kind.toLowerCase()) {
case "doc":
case "docx":
case "txt":
return wordToPDF(inputFilePath, outputFilePath);
case "ppt":
case "pptx":
case "pptm":
case "ppsx":
return pptToPDF(inputFilePath, outputFilePath);
case "xls":
case "xlsx":
return exToPDF(inputFilePath, outputFilePath);
case "png":
case "jpg":
case "jpeg":
case "gif":
return imageToPDF(inputFilePath, outputFilePath);
default:
logger.warn("不支持的文件格式: {}", kind);
return false;
}
}
private static String getFileSuffix(String fileName) {
int splitIndex = fileName.lastIndexOf(".");
return splitIndex == -1 ? "" : fileName.substring(splitIndex + 1);
}
private static boolean wordToPDF(String inputFile, String pdfFile) {
ActiveXComponent app = null;
try {
ComThread.InitSTA();
app = new ActiveXComponent("KWPS.Application");
app.setProperty("Visible", new Variant(false));
app.setProperty("AutomationSecurity", new Variant(3)); // 禁用宏
Dispatch docs = app.getProperty("Documents").toDispatch();
Dispatch doc = Dispatch.call(docs, "Open", inputFile, false, true).toDispatch();
Dispatch.call(doc, "ExportAsFixedFormat", pdfFile, WD_FORMAT_PDF);
Dispatch.call(doc, "Close", false);
logger.info("Word 文件转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (Exception e) {
logger.error("Word 文件转换失败: {}", inputFile, e);
return false;
} finally {
if (app != null) {
app.invoke("Quit", 0);
app.safeRelease(); // 释放 ActiveXComponent 资源
}
ComThread.Release();
}
}
private static boolean exToPDF(String inputFile, String pdfFile) {
ActiveXComponent app = null;
try {
ComThread.InitSTA();
app = new ActiveXComponent("KET.Application");
app.setProperty("Visible", new Variant(false));
app.setProperty("AutomationSecurity", new Variant(3)); // 禁用宏
Dispatch excels = app.getProperty("Workbooks").toDispatch();
Dispatch excel = Dispatch.invoke(excels, "Open", Dispatch.Method, new Object[]{inputFile, new Variant(false), new Variant(false)}, new int[9]).toDispatch();
Dispatch.invoke(excel, "ExportAsFixedFormat", Dispatch.Method, new Object[]{new Variant(XL_TYPE_PDF), pdfFile}, new int[1]);
Dispatch.call(excel, "Close", new Variant(false));
logger.info("Excel 文件转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (Exception e) {
logger.error("Excel 文件转换失败: {}", inputFile, e);
return false;
} finally {
if (app != null) {
app.invoke("Quit");
app.safeRelease(); // 释放 ActiveXComponent 资源
}
ComThread.Release();
}
}
private static boolean pptToPDF(String inputFile, String pdfFile) {
ActiveXComponent app = null;
try {
ComThread.InitSTA();
app = new ActiveXComponent("KWPP.Application");
Dispatch ppts = app.getProperty("Presentations").toDispatch();
Dispatch ppt = Dispatch.call(ppts, "Open", inputFile, true, false).toDispatch();
Dispatch.invoke(ppt, "SaveAs", Dispatch.Method, new Object[]{pdfFile, new Variant(PP_SAVE_AS_PDF)}, new int[1]);
Dispatch.call(ppt, "Close");
logger.info("PPT 文件转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (Exception e) {
logger.error("PPT 文件转换失败: {}", inputFile, e);
return false;
} finally {
if (app != null) {
app.invoke("Quit");
app.safeRelease(); // 释放 ActiveXComponent 资源
}
ComThread.Release();
}
}
/**
* 将图片转换为PDF
*
* @param inputFile 图片文件路径
* @param pdfFile 输出PDF文件路径
* @return 是否转换成功
*/
public static boolean imageToPDF(String inputFile, String pdfFile) {
try (PDDocument document = new PDDocument()) {
PDPage page = new PDPage(PDRectangle.A4); // 创建一个A4页面
document.addPage(page);
// 加载图片并创建一个PDF图像对象
File file = new File(inputFile);
BufferedImage bufferedImage = ImageIO.read(file);
PDImageXObject pdImage = LosslessFactory.createFromImage(document, bufferedImage);
// 计算图片缩放比例以适应页面
float originalWidth = pdImage.getWidth();
float originalHeight = pdImage.getHeight();
float scaleX = PDRectangle.A4.getWidth() / originalWidth;
float scaleY = PDRectangle.A4.getHeight() / originalHeight;
float scale = Math.min(scaleX, scaleY); // 取较小的缩放比例以完全适应页面
// 开始在页面上绘制内容
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
// 将图片绘制到页面上,居中并按比例缩放
float x = (PDRectangle.A4.getWidth() - originalWidth * scale) / 2;
float y = (PDRectangle.A4.getHeight() - originalHeight * scale) / 2;
contentStream.drawImage(pdImage, x, y, originalWidth * scale, originalHeight * scale);
}
// 保存PDF文件
document.save(pdfFile);
logger.info("图片转换成功: {} -> {}", inputFile, pdfFile);
return true;
} catch (IOException e) {
logger.error("图片转换失败: {}", inputFile, e);
return false;
}
}
}
本文作者:周得水
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!