mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-04-08 17:27:34 +00:00
tif 优化多线程转换方法
This commit is contained in:
503
server/src/main/java/cn/keking/service/CadToPdfService.java
Normal file
503
server/src/main/java/cn/keking/service/CadToPdfService.java
Normal file
File diff suppressed because it is too large
Load Diff
399
server/src/main/java/cn/keking/service/PdfToJpgService.java
Normal file
399
server/src/main/java/cn/keking/service/PdfToJpgService.java
Normal file
@@ -0,0 +1,399 @@
|
||||
package cn.keking.service;
|
||||
|
||||
import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.model.FileAttribute;
|
||||
import cn.keking.service.cache.NotResourceCache;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.pdfbox.tools.imageio.ImageIOUtil;
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @author yudian-it
|
||||
*/
|
||||
@Component
|
||||
public class PdfToJpgService {
|
||||
private final FileHandlerService fileHandlerService;
|
||||
|
||||
// PDF转换专用线程池
|
||||
private ExecutorService pdfConversionPool;
|
||||
private ThreadPoolExecutor pdfThreadPoolExecutor;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PdfToJpgService.class);
|
||||
private static final String PDF_PASSWORD_MSG = "password";
|
||||
private static final String PDF2JPG_IMAGE_FORMAT = ".jpg";
|
||||
|
||||
// 最大并行页数阈值
|
||||
private static final int MAX_PARALLEL_PAGES = 20;
|
||||
|
||||
public PdfToJpgService(FileHandlerService fileHandlerService) {
|
||||
this.fileHandlerService = fileHandlerService;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
int threadCount = getPdfThreadPoolSize();
|
||||
int queueCapacity = threadCount * 10;
|
||||
|
||||
AtomicInteger threadNum = new AtomicInteger(1);
|
||||
pdfThreadPoolExecutor = new ThreadPoolExecutor(
|
||||
threadCount,
|
||||
threadCount,
|
||||
60L,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(queueCapacity),
|
||||
r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("pdf-convert-pool-" + threadNum.getAndIncrement());
|
||||
t.setUncaughtExceptionHandler((thread, throwable) ->
|
||||
logger.error("PDF转换线程未捕获异常: {}", thread.getName(), throwable));
|
||||
return t;
|
||||
},
|
||||
new ThreadPoolExecutor.CallerRunsPolicy()
|
||||
);
|
||||
|
||||
pdfThreadPoolExecutor.allowCoreThreadTimeOut(true);
|
||||
pdfConversionPool = pdfThreadPoolExecutor;
|
||||
|
||||
logger.info("PDF转换线程池初始化完成,线程数: {}, 队列容量: {}",
|
||||
threadCount, queueCapacity);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("PDF转换线程池初始化失败", e);
|
||||
int defaultThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
|
||||
pdfConversionPool = Executors.newFixedThreadPool(defaultThreads);
|
||||
logger.warn("使用默认PDF线程池配置,线程数: {}", defaultThreads);
|
||||
}
|
||||
}
|
||||
|
||||
private int getPdfThreadPoolSize() {
|
||||
try {
|
||||
String pdfThreadConfig = System.getProperty("pdf.thread.count");
|
||||
int threadCount;
|
||||
if (pdfThreadConfig != null) {
|
||||
threadCount = Integer.parseInt(pdfThreadConfig);
|
||||
} else {
|
||||
threadCount = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
|
||||
}
|
||||
|
||||
if (threadCount <= 0) {
|
||||
threadCount = Runtime.getRuntime().availableProcessors();
|
||||
logger.warn("PDF线程数配置无效,使用CPU核心数: {}", threadCount);
|
||||
}
|
||||
|
||||
int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
|
||||
if (threadCount > maxThreads) {
|
||||
logger.warn("PDF线程数配置过大({}),限制为: {}", threadCount, maxThreads);
|
||||
threadCount = maxThreads;
|
||||
}
|
||||
return threadCount;
|
||||
} catch (Exception e) {
|
||||
logger.error("获取PDF线程数配置失败", e);
|
||||
return Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
if (pdfConversionPool != null && !pdfConversionPool.isShutdown()) {
|
||||
gracefulShutdown(pdfConversionPool, getShutdownTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
private long getShutdownTimeout() {
|
||||
try {
|
||||
String pdfTimeout = System.getProperty("pdf.timeout");
|
||||
if (pdfTimeout != null) {
|
||||
return Long.parseLong(pdfTimeout);
|
||||
}
|
||||
return Long.parseLong(ConfigConstants.getCadTimeout());
|
||||
} catch (Exception e) {
|
||||
logger.warn("获取PDF关闭超时时间失败,使用默认值60秒", e);
|
||||
return 60L;
|
||||
}
|
||||
}
|
||||
|
||||
private void gracefulShutdown(ExecutorService executor, long timeoutSeconds) {
|
||||
logger.info("开始关闭{}...", "PDF转换线程池");
|
||||
executor.shutdown();
|
||||
|
||||
try {
|
||||
if (!executor.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) {
|
||||
logger.warn("{}超时未关闭,尝试强制关闭...", "PDF转换线程池");
|
||||
executor.shutdownNow();
|
||||
|
||||
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
logger.error("{}无法完全关闭", "PDF转换线程池");
|
||||
} else {
|
||||
logger.info("{}已强制关闭", "PDF转换线程池");
|
||||
}
|
||||
} else {
|
||||
logger.info("{}已正常关闭", "PDF转换线程池");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("{}关闭时被中断", "PDF转换线程池", e);
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> pdf2jpg(String fileNameFilePath, String pdfFilePath,
|
||||
String pdfName, FileAttribute fileAttribute) throws Exception {
|
||||
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
|
||||
String filePassword = fileAttribute.getFilePassword();
|
||||
|
||||
// 1. 检查缓存
|
||||
if (!forceUpdatedCache) {
|
||||
List<String> cacheResult = fileHandlerService.loadPdf2jpgCache(pdfFilePath);
|
||||
if (!CollectionUtils.isEmpty(cacheResult)) {
|
||||
return cacheResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 验证文件存在
|
||||
File pdfFile = new File(fileNameFilePath);
|
||||
if (!pdfFile.exists()) {
|
||||
logger.error("PDF文件不存在: {}", fileNameFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 创建输出目录
|
||||
int index = pdfFilePath.lastIndexOf(".");
|
||||
String folder = pdfFilePath.substring(0, index);
|
||||
File path = new File(folder);
|
||||
if (!path.exists() && !path.mkdirs()) {
|
||||
logger.error("创建转换文件目录失败: {}", folder);
|
||||
throw new IOException("无法创建输出目录");
|
||||
}
|
||||
|
||||
// 4. 加载PDF文档获取页数
|
||||
int pageCount = 0;
|
||||
try (PDDocument tempDoc = Loader.loadPDF(pdfFile, filePassword)) {
|
||||
pageCount = tempDoc.getNumberOfPages();
|
||||
logger.info("PDF文件总页数: {}, 文件: {}", pageCount, pdfFilePath);
|
||||
} catch (IOException e) {
|
||||
handlePdfLoadException(e, pdfFilePath);
|
||||
throw new Exception("PDF文件加载失败", e);
|
||||
}
|
||||
|
||||
// 5. 根据页数决定转换策略
|
||||
List<String> imageUrls;
|
||||
if (pageCount > MAX_PARALLEL_PAGES) {
|
||||
// 大文件使用新方案:每页独立加载PDF
|
||||
imageUrls = convertParallelIndependent(pdfFile, filePassword, pdfFilePath, folder, pageCount);
|
||||
} else {
|
||||
// 小文件使用串行处理(稳定)
|
||||
imageUrls = convertSequentially(pdfFile, filePassword, pdfFilePath, folder, pageCount);
|
||||
}
|
||||
|
||||
// 6. 缓存结果
|
||||
if (usePasswordCache || ObjectUtils.isEmpty(filePassword)) {
|
||||
fileHandlerService.addPdf2jpgCache(pdfFilePath, pageCount);
|
||||
}
|
||||
|
||||
logger.info("PDF转换完成,成功转换{}页,文件: {}", imageUrls.size(), pdfFilePath);
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理PDF加载异常
|
||||
*/
|
||||
private void handlePdfLoadException(Exception e, String pdfFilePath) throws Exception {
|
||||
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
||||
for (Throwable throwable : throwableArray) {
|
||||
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
||||
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
|
||||
logger.info("PDF文件需要密码: {}", pdfFilePath);
|
||||
throw new Exception(PDF_PASSWORD_MSG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.error("加载PDF文件异常, pdfFilePath:{}", pdfFilePath, e);
|
||||
throw new Exception("PDF文件加载失败", e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行转换(稳定方案)
|
||||
*/
|
||||
private List<String> convertSequentially(File pdfFile, String filePassword,
|
||||
String pdfFilePath, String folder, int pageCount) {
|
||||
List<String> imageUrls = new ArrayList<>(pageCount);
|
||||
|
||||
try (PDDocument doc = Loader.loadPDF(pdfFile, filePassword)) {
|
||||
doc.setResourceCache(new NotResourceCache());
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(doc);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
|
||||
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
|
||||
try {
|
||||
String imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(
|
||||
pageIndex,
|
||||
ConfigConstants.getPdf2JpgDpi(),
|
||||
ImageType.RGB
|
||||
);
|
||||
|
||||
ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
|
||||
image.flush();
|
||||
|
||||
String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex);
|
||||
imageUrls.add(imageUrl);
|
||||
|
||||
logger.debug("串行转换页 {} 完成", pageIndex);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("串行转换页 {} 失败: {}", pageIndex, e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("串行转换PDF失败", e);
|
||||
}
|
||||
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行转换 - 每个线程独立加载PDF(避免线程安全问题)
|
||||
*/
|
||||
private List<String> convertParallelIndependent(File pdfFile, String filePassword,
|
||||
String pdfFilePath, String folder, int pageCount) {
|
||||
List<String> imageUrls = Collections.synchronizedList(new ArrayList<>());
|
||||
List<Future<Boolean>> futures = new ArrayList<>();
|
||||
AtomicInteger successCount = new AtomicInteger(0);
|
||||
AtomicInteger errorCount = new AtomicInteger(0);
|
||||
|
||||
// 提交页面转换任务
|
||||
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
|
||||
final int currentPage = pageIndex;
|
||||
|
||||
Future<Boolean> future = pdfConversionPool.submit(() -> {
|
||||
try {
|
||||
// 每个任务独立加载PDF,确保线程安全
|
||||
try (PDDocument pageDoc = Loader.loadPDF(pdfFile, filePassword)) {
|
||||
pageDoc.setResourceCache(new NotResourceCache());
|
||||
PDFRenderer renderer = new PDFRenderer(pageDoc);
|
||||
renderer.setSubsamplingAllowed(true);
|
||||
|
||||
String imageFilePath = folder + File.separator + currentPage + PDF2JPG_IMAGE_FORMAT;
|
||||
BufferedImage image = renderer.renderImageWithDPI(
|
||||
currentPage,
|
||||
ConfigConstants.getPdf2JpgDpi(),
|
||||
ImageType.RGB
|
||||
);
|
||||
|
||||
ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
|
||||
image.flush();
|
||||
|
||||
String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, currentPage);
|
||||
synchronized (imageUrls) {
|
||||
imageUrls.add(imageUrl);
|
||||
}
|
||||
|
||||
successCount.incrementAndGet();
|
||||
logger.debug("并行转换页 {} 完成", currentPage);
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errorCount.incrementAndGet();
|
||||
logger.error("并行转换页 {} 失败: {}", currentPage, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
int timeout = calculateTimeout(pageCount);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
for (Future<Boolean> future : futures) {
|
||||
try {
|
||||
future.get(timeout, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
logger.warn("页面转换任务超时,取消剩余任务");
|
||||
for (Future<Boolean> f : futures) {
|
||||
if (!f.isDone()) {
|
||||
f.cancel(true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
logger.error("页面转换任务执行失败", e);
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
if (System.currentTimeMillis() - startTime > timeout * 1000L) {
|
||||
logger.warn("PDF转换整体超时,取消剩余任务");
|
||||
for (Future<Boolean> f : futures) {
|
||||
if (!f.isDone()) {
|
||||
f.cancel(true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||
logger.info("并行转换统计: 成功={}, 失败={}, 总页数={}, 耗时={}ms",
|
||||
successCount.get(), errorCount.get(), pageCount, elapsedTime);
|
||||
|
||||
// 按页码排序
|
||||
imageUrls.sort(Comparator.comparingInt(url -> {
|
||||
try {
|
||||
String pageStr = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'));
|
||||
return Integer.parseInt(pageStr);
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}));
|
||||
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算超时时间
|
||||
*/
|
||||
private int calculateTimeout(int pageCount) {
|
||||
if (pageCount <= 50) {
|
||||
return ConfigConstants.getPdfTimeout();
|
||||
} else if (pageCount <= 200) {
|
||||
return ConfigConstants.getPdfTimeout80();
|
||||
} else {
|
||||
return ConfigConstants.getPdfTimeout200();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监控线程池状态
|
||||
*/
|
||||
public void monitorThreadPoolStatus() {
|
||||
if (pdfThreadPoolExecutor != null) {
|
||||
logger.info("PDF线程池状态: 活跃线程={}, 队列大小={}, 完成任务={}, 线程总数={}",
|
||||
pdfThreadPoolExecutor.getActiveCount(),
|
||||
pdfThreadPoolExecutor.getQueue().size(),
|
||||
pdfThreadPoolExecutor.getCompletedTaskCount(),
|
||||
pdfThreadPoolExecutor.getPoolSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
426
server/src/main/java/cn/keking/service/TifToService.java
Normal file
426
server/src/main/java/cn/keking/service/TifToService.java
Normal file
@@ -0,0 +1,426 @@
|
||||
package cn.keking.service;
|
||||
|
||||
import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.utils.WebUtils;
|
||||
import cn.keking.web.filter.BaseUrlFilter;
|
||||
import com.itextpdf.text.Document;
|
||||
import com.itextpdf.text.DocumentException;
|
||||
import com.itextpdf.text.Image;
|
||||
import com.itextpdf.text.io.FileChannelRandomAccessSource;
|
||||
import com.itextpdf.text.pdf.PdfWriter;
|
||||
import com.itextpdf.text.pdf.RandomAccessFileOrArray;
|
||||
import com.itextpdf.text.pdf.codec.TiffImage;
|
||||
import org.apache.commons.imaging.Imaging;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class TifToService {
|
||||
|
||||
private static final int FIT_WIDTH = 500;
|
||||
private static final int FIT_HEIGHT = 900;
|
||||
private static final Logger logger = LoggerFactory.getLogger(TifToService.class);
|
||||
private static final String FILE_DIR = ConfigConstants.getFileDir();
|
||||
// 用于文档同步的锁对象
|
||||
private final Object documentLock = new Object();
|
||||
// 专用线程池用于TIF转换
|
||||
private static ExecutorService tifConversionPool;
|
||||
|
||||
static {
|
||||
initThreadPool();
|
||||
}
|
||||
|
||||
private static void initThreadPool() {
|
||||
int corePoolSize = getOptimalThreadCount();
|
||||
int maxPoolSize = corePoolSize * 2;
|
||||
long keepAliveTime = 60L;
|
||||
|
||||
tifConversionPool = new ThreadPoolExecutor(
|
||||
corePoolSize,
|
||||
maxPoolSize,
|
||||
keepAliveTime,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(100),
|
||||
new ThreadFactory() {
|
||||
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("tif-convert-thread-" + threadNumber.getAndIncrement());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
},
|
||||
new ThreadPoolExecutor.CallerRunsPolicy()
|
||||
);
|
||||
|
||||
logger.info("TIF转换线程池初始化完成,核心线程数: {}, 最大线程数: {}", corePoolSize, maxPoolSize);
|
||||
}
|
||||
|
||||
private static int getOptimalThreadCount() {
|
||||
int cpuCores = Runtime.getRuntime().availableProcessors();
|
||||
// 对于I/O密集型任务,可以设置更多的线程
|
||||
return Math.min(cpuCores * 2, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 RandomAccessFileOrArray 实例(修复已弃用的构造函数)
|
||||
*/
|
||||
private RandomAccessFileOrArray createRandomAccessFileOrArray(File file) throws IOException {
|
||||
RandomAccessFile aFile = new RandomAccessFile(file, "r");
|
||||
FileChannel inChannel = aFile.getChannel();
|
||||
FileChannelRandomAccessSource fcra = new FileChannelRandomAccessSource(inChannel);
|
||||
return new RandomAccessFileOrArray(fcra);
|
||||
}
|
||||
|
||||
/**
|
||||
* TIF转JPG - 支持多线程并行处理
|
||||
*/
|
||||
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,
|
||||
boolean forceUpdatedCache) throws Exception {
|
||||
return convertTif2Jpg(strInputFile, strOutputFile, forceUpdatedCache, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* TIF转JPG - 可选择是否启用并行处理
|
||||
* @param parallelProcessing 是否启用并行处理
|
||||
*/
|
||||
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,
|
||||
boolean forceUpdatedCache, boolean parallelProcessing) throws Exception {
|
||||
String baseUrl = BaseUrlFilter.getBaseUrl();
|
||||
String outputDirPath = strOutputFile.substring(0, strOutputFile.lastIndexOf('.'));
|
||||
|
||||
File tiffFile = new File(strInputFile);
|
||||
if (!tiffFile.exists()) {
|
||||
logger.error("找不到文件【{}】", strInputFile);
|
||||
throw new FileNotFoundException("文件不存在: " + strInputFile);
|
||||
}
|
||||
|
||||
File outputDir = new File(outputDirPath);
|
||||
if (!outputDir.exists() && !outputDir.mkdirs()) {
|
||||
throw new IOException("创建目录失败: " + outputDirPath);
|
||||
}
|
||||
|
||||
// 加载所有图片
|
||||
List<BufferedImage> images;
|
||||
try {
|
||||
images = Imaging.getAllBufferedImages(tiffFile);
|
||||
logger.info("TIF文件加载完成,共{}页,文件: {}", images.size(), strInputFile);
|
||||
} catch (IOException e) {
|
||||
handleImagingException(e, strInputFile);
|
||||
throw e;
|
||||
}
|
||||
|
||||
int pageCount = images.size();
|
||||
|
||||
// 根据页面数量决定是否使用并行处理
|
||||
boolean useParallel = parallelProcessing && pageCount > 5;
|
||||
|
||||
if (useParallel) {
|
||||
return convertParallel(images, outputDirPath, baseUrl, forceUpdatedCache);
|
||||
} else {
|
||||
return convertSequentially(images, outputDirPath, baseUrl, forceUpdatedCache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行转换
|
||||
*/
|
||||
private List<String> convertParallel(List<BufferedImage> images, String outputDirPath,
|
||||
String baseUrl, boolean forceUpdatedCache) {
|
||||
int pageCount = images.size();
|
||||
List<CompletableFuture<String>> futures = new ArrayList<>(pageCount);
|
||||
AtomicInteger successCount = new AtomicInteger(0);
|
||||
AtomicInteger skipCount = new AtomicInteger(0);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 提交所有页面转换任务
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
final int pageIndex = i;
|
||||
BufferedImage image = images.get(i);
|
||||
|
||||
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
String fileName = outputDirPath + File.separator + pageIndex + ".jpg";
|
||||
File outputFile = new File(fileName);
|
||||
|
||||
// 检查是否需要转换
|
||||
if (forceUpdatedCache || !outputFile.exists()) {
|
||||
// 使用PNG格式保持更好的质量,如果需要JPG可以调整
|
||||
boolean success = ImageIO.write(image, "png", outputFile);
|
||||
if (!success) {
|
||||
logger.error("无法写入图片格式,页号: {}", pageIndex);
|
||||
return null;
|
||||
}
|
||||
logger.debug("并行转换图片页 {} 完成", pageIndex);
|
||||
successCount.incrementAndGet();
|
||||
} else {
|
||||
logger.debug("使用缓存图片页 {}", pageIndex);
|
||||
skipCount.incrementAndGet();
|
||||
}
|
||||
|
||||
// 构建URL
|
||||
String relativePath = fileName.replace(FILE_DIR, "");
|
||||
return baseUrl + WebUtils.encodeFileName(relativePath);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("并行转换页 {} 失败: {}", pageIndex, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}, tifConversionPool);
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
// 等待所有任务完成并收集结果
|
||||
List<String> imageUrls = futures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
logger.info("TIF并行转换完成: 成功={}, 跳过={}, 总页数={}, 耗时={}ms",
|
||||
successCount.get(), skipCount.get(), pageCount, elapsedTime);
|
||||
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行转换
|
||||
*/
|
||||
private List<String> convertSequentially(List<BufferedImage> images, String outputDirPath,
|
||||
String baseUrl, boolean forceUpdatedCache) throws Exception {
|
||||
List<String> imageUrls = new ArrayList<>(images.size());
|
||||
|
||||
for (int i = 0; i < images.size(); i++) {
|
||||
String fileName = outputDirPath + File.separator + i + ".jpg";
|
||||
File outputFile = new File(fileName);
|
||||
|
||||
try {
|
||||
if (forceUpdatedCache || !outputFile.exists()) {
|
||||
BufferedImage image = images.get(i);
|
||||
boolean success = ImageIO.write(image, "png", outputFile);
|
||||
if (!success) {
|
||||
throw new IOException("无法写入JPG格式图片: " + fileName);
|
||||
}
|
||||
logger.debug("转换图片页 {} 完成", i);
|
||||
} else {
|
||||
logger.debug("使用缓存图片页 {}", i);
|
||||
}
|
||||
|
||||
String relativePath = fileName.replace(FILE_DIR, "");
|
||||
String url = baseUrl + WebUtils.encodeFileName(relativePath);
|
||||
imageUrls.add(url);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("转换页 {} 失败: {}", i, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将JPG图片转换为PDF - 优化版本
|
||||
*/
|
||||
public void convertTif2Pdf(String strJpgFile, String strPdfFile) throws Exception {
|
||||
convertJpg2Pdf(strJpgFile, strPdfFile, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将JPG图片转换为PDF - 支持并行处理图片加载
|
||||
*/
|
||||
public void convertJpg2Pdf(String strJpgFile, String strPdfFile, boolean parallelLoad) throws Exception {
|
||||
Document document = new Document();
|
||||
FileOutputStream outputStream = null;
|
||||
RandomAccessFileOrArray rafa = null;
|
||||
|
||||
try {
|
||||
File tiffFile = new File(strJpgFile);
|
||||
|
||||
// 修复:使用非弃用的方式创建 RandomAccessFileOrArray
|
||||
rafa = createRandomAccessFileOrArray(tiffFile);
|
||||
int pages = TiffImage.getNumberOfPages(rafa);
|
||||
logger.info("开始转换TIFF到PDF,总页数: {}", pages);
|
||||
|
||||
outputStream = new FileOutputStream(strPdfFile);
|
||||
PdfWriter.getInstance(document, outputStream);
|
||||
document.open();
|
||||
|
||||
// 修改为传入File对象而不是RandomAccessFileOrArray
|
||||
if (parallelLoad && pages > 10) {
|
||||
convertPagesParallel(document, tiffFile, pages);
|
||||
} else {
|
||||
convertPagesSequentially(document, tiffFile, pages);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
handlePdfConversionException(e, strPdfFile);
|
||||
throw e;
|
||||
} finally {
|
||||
// 修复:传入 rafa 以正确关闭资源
|
||||
closeResources(document, rafa, outputStream);
|
||||
}
|
||||
|
||||
logger.info("PDF转换完成: {}", strPdfFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行处理页面 - 修复版
|
||||
*/
|
||||
private void convertPagesSequentially(Document document, File tiffFile, int pages) throws IOException, DocumentException {
|
||||
RandomAccessFileOrArray rafa = null;
|
||||
try {
|
||||
// 修复:使用非弃用的方式创建 RandomAccessFileOrArray
|
||||
rafa = createRandomAccessFileOrArray(tiffFile);
|
||||
for (int i = 1; i <= pages; i++) {
|
||||
Image image = TiffImage.getTiffImage(rafa, i);
|
||||
image.scaleToFit(FIT_WIDTH, FIT_HEIGHT);
|
||||
document.add(image);
|
||||
|
||||
if (i % 10 == 0) {
|
||||
logger.debug("已处理 {} 页", i);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (rafa != null) {
|
||||
try {
|
||||
rafa.close();
|
||||
} catch (Exception e) {
|
||||
logger.warn("关闭RandomAccessFileOrArray失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行加载并添加图片到PDF - 修复版
|
||||
*/
|
||||
private void convertPagesParallel(Document document, File tiffFile, int pages) {
|
||||
List<CompletableFuture<Image>> futures = new ArrayList<>();
|
||||
|
||||
// 提交所有页面加载任务
|
||||
for (int i = 1; i <= pages; i++) {
|
||||
final int pageNum = i;
|
||||
CompletableFuture<Image> future = CompletableFuture.supplyAsync(() -> {
|
||||
RandomAccessFileOrArray localRafa = null;
|
||||
try {
|
||||
// 为每个线程创建独立的RandomAccessFileOrArray
|
||||
// 修复:使用非弃用的方式创建 RandomAccessFileOrArray
|
||||
localRafa = createRandomAccessFileOrArray(tiffFile);
|
||||
|
||||
Image image = TiffImage.getTiffImage(localRafa, pageNum);
|
||||
image.scaleToFit(FIT_WIDTH, FIT_HEIGHT);
|
||||
logger.debug("并行加载TIFF页 {}", pageNum);
|
||||
return image;
|
||||
} catch (Exception e) {
|
||||
logger.error("加载TIFF页 {} 失败", pageNum, e);
|
||||
return null;
|
||||
} finally {
|
||||
if (localRafa != null) {
|
||||
try {
|
||||
localRafa.close();
|
||||
} catch (Exception e) {
|
||||
logger.warn("关闭RandomAccessFileOrArray失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, tifConversionPool);
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
// 按顺序添加到文档(保持页面顺序)
|
||||
for (int i = 0; i < futures.size(); i++) {
|
||||
try {
|
||||
Image image = futures.get(i).get(30, TimeUnit.SECONDS);
|
||||
if (image != null) {
|
||||
// 使用专门的锁对象而不是同步document参数
|
||||
synchronized (documentLock) {
|
||||
document.add(image);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("添加页 {} 到PDF失败", i + 1, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常处理
|
||||
*/
|
||||
private void handleImagingException(IOException e, String filePath) {
|
||||
if (!e.getMessage().contains("Only sequential, baseline JPEGs are supported at the moment")) {
|
||||
logger.error("TIF转JPG异常,文件路径:{}", filePath, e);
|
||||
} else {
|
||||
logger.warn("不支持的非基线JPEG格式,文件:{}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePdfConversionException(IOException e, String filePath) {
|
||||
if (!e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)")) {
|
||||
logger.error("TIF转PDF异常,文件路径:{}", filePath, e);
|
||||
} else {
|
||||
logger.warn("TIFF文件字节顺序标记错误,文件:{}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源关闭
|
||||
*/
|
||||
private void closeResources(Document document, RandomAccessFileOrArray rafa, FileOutputStream outputStream) {
|
||||
try {
|
||||
if (document != null && document.isOpen()) {
|
||||
document.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("关闭Document失败", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (rafa != null) {
|
||||
rafa.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("关闭RandomAccessFileOrArray失败", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("关闭FileOutputStream失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优雅关闭
|
||||
*/
|
||||
public static void shutdown() {
|
||||
if (tifConversionPool != null && !tifConversionPool.isShutdown()) {
|
||||
tifConversionPool.shutdown();
|
||||
try {
|
||||
if (!tifConversionPool.awaitTermination(30, TimeUnit.SECONDS)) {
|
||||
tifConversionPool.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
tifConversionPool.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user