一、背景

如果有条件的可以使用Aspose商业库来实现,会简单很多。如果不想付费购买,可以使用我这个方案。
注意:本方案是基于SpringBoot+python,利用Office原生功能来实现的文档转换,因此必须运行在
window机器中。

二、环境安装

1、JDK安装

  • JDK推荐1.8

2、Python安装

  • python版本推荐3.11

三、Python脚本编写

import win32com.client
import os
import sys
import json
import threading

# 全局锁,确保Excel COM对象的线程安全
excel_lock = threading.Lock()

def excel_to_pdf(input_excel_path, output_pdf_path):
    """
    Excel转PDF函数,支持并发安全

    Args:
        input_excel_path: 输入Excel文件路径
        output_pdf_path: 输出PDF文件路径

    Returns:
        dict: 包含成功状态和消息的字典
    """
    result = {
        "success": False,
        "message": "",
        "input_path": input_excel_path,
        "output_path": output_pdf_path
    }

    # 验证输入文件
    if not os.path.exists(input_excel_path):
        result["message"] = f"输入文件不存在: {input_excel_path}"
        return result

    # 验证输入文件格式
    valid_extensions = ['.xlsx', '.xls', '.xlsm', '.xlsb']
    if not any(input_excel_path.lower().endswith(ext) for ext in valid_extensions):
        result["message"] = f"不支持的文件格式,支持的格式: {', '.join(valid_extensions)}"
        return result

    # 确保输出目录存在
    output_dir = os.path.dirname(output_pdf_path)
    if output_dir and not os.path.exists(output_dir):
        try:
            os.makedirs(output_dir, exist_ok=True)
        except Exception as e:
            result["message"] = f"创建输出目录失败: {str(e)}"
            return result

    excel = None
    workbook = None

    # 使用锁确保Excel COM对象的线程安全
    with excel_lock:
        try:
            # 创建Excel应用对象
            excel = win32com.client.Dispatch("Excel.Application")
            excel.Visible = False  # 不显示Excel界面
            excel.DisplayAlerts = False  # 关闭警告提示
            excel.ScreenUpdating = False  # 关闭屏幕更新以提高性能

            # 打开工作簿
            workbook = excel.Workbooks.Open(os.path.abspath(input_excel_path))

            # 转换设置(核心代码)
            workbook.ExportAsFixedFormat(
                0,  # 类型: 0=PDF, 1=XPS
                os.path.abspath(output_pdf_path),
                0,  # 质量: 0=标准质量, 1=最小文件大小
                True,  # 包含文档属性
                True,  # 忽略打印区域
                1,  # 从第1页开始
                1,  # 到第1页结束(0表示所有页)
                False  # 打开PDF后不显示
            )

            result["success"] = True
            result["message"] = f"转换成功: {output_pdf_path}"

        except Exception as e:
            result["message"] = f"转换失败: {str(e)}"

        finally:
            # 确保关闭工作簿和Excel进程
            try:
                if workbook:
                    workbook.Close(False)  # 不保存更改
                if excel:
                    excel.Quit()

                # 释放COM对象
                if workbook:
                    del workbook
                if excel:
                    del excel

            except Exception as cleanup_error:
                # 记录清理错误,但不影响主要结果
                if result["success"]:
                    result["message"] += f" (清理警告: {str(cleanup_error)})"
                else:
                    result["message"] += f" (清理错误: {str(cleanup_error)})"

    return result

def main():
    """
    主函数,支持命令行调用
    用法: python cover.py <input_excel_path> <output_pdf_path>
    """
    if len(sys.argv) != 3:
        print("用法: python cover.py <input_excel_path> <output_pdf_path>")
        print("示例: python cover.py input.xlsx output.pdf")
        sys.exit(1)

    input_path = sys.argv[1]
    output_path = sys.argv[2]

    # 执行转换
    result = excel_to_pdf(input_path, output_path)

    # 输出JSON格式结果,便于Java程序解析
    print(json.dumps(result, ensure_ascii=False, indent=2))

    # 根据结果设置退出码
    sys.exit(0 if result["success"] else 1)

# 使用示例
if __name__ == "__main__":
    # 如果没有命令行参数,使用默认示例
    if len(sys.argv) == 1:
        print("Excel转PDF工具")
        print("用法: python cover.py <input_excel_path> <output_pdf_path>")
        print("\n示例转换:")
        input_path = r"D:\test\1.xlsx"  # 替换为你的Excel文件路径
        output_path = r"D:\test\1.pdf"    # 替换为输出PDF路径

        if os.path.exists(input_path):
            result = excel_to_pdf(input_path, output_path)
            print(json.dumps(result, ensure_ascii=False, indent=2))
        else:
            print(f"示例文件不存在: {input_path}")
    else:
        main()

可以在你的D:\test\1.xlsx 目录下创建一个1.xlsx,然后执行python cover.py来看一下转换是否成功,验证环境问题。
如果提示缺少包,则执行pip install pywin32

四、springboot调用层编写

不多说废话,直接上代码:

4.1 创建ExcelToPdfService类,代码全部如下:
package com.dhc.minboot.common;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Excel转PDF服务类 --高级版
 * 
 * @author CGH
 * @version 1.0.0
 */
@Service
public class ExcelToPdfService {

    private static final Logger logger = LoggerFactory.getLogger(ExcelToPdfService.class);
    

    private static final String tempDir = System.getProperty("java.io.tmpdir") + "excel2pdf-conversion\\";
    
    @Value("${app.file.upload.max-size:10485760}") // 10MB
    private long maxFileSize;
    
    //@Value("${app.excel-to-pdf.python-script:C:\\Users\\Administrator\\Documents\\augment-projects\\cgh-tools\\cover.py}")
    @Value("${app.excel-to-pdf.python-script:C:\\Users\\Administrator\\Desktop\\excel2pdf\\cover.py}")
    private String pythonScript;

    
    @Value("${app.excel-to-pdf.timeout:300}") // 5分钟超时
    private int timeoutSeconds;
    
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 上传Excel文件并转换为PDF
     */
    public Map<String, Object> convertExcelToPdf(MultipartFile file) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            // 验证文件
            Map<String, Object> validation = validateFile(file);
            if (!(Boolean) validation.get("success")) {
                return validation;
            }
            
            // 生成唯一文件名
            String fileId = UUID.randomUUID().toString();
            String originalFilename = file.getOriginalFilename();
            String fileExtension = getFileExtension(originalFilename);
            
            // 创建临时目录
            Path tempDirPath = Paths.get(tempDir);
            if (!Files.exists(tempDirPath)) {
                Files.createDirectories(tempDirPath);
            }
            
            // 保存上传的Excel文件
            String inputFileName = fileId + "_input" + fileExtension;
            String outputFileName = fileId + "_output.pdf";
            Path inputFilePath = tempDirPath.resolve(inputFileName);
            Path outputFilePath = tempDirPath.resolve(outputFileName);
            
            // 保存文件
            file.transferTo(inputFilePath.toFile());
            logger.info("Excel文件已保存: {}", inputFilePath);
            
            // 调用Python脚本进行转换
            Map<String, Object> conversionResult = callPythonScript(
                inputFilePath.toString(), 
                outputFilePath.toString()
            );
            
            if ((Boolean) conversionResult.get("success")) {
                result.put("success", true);
                result.put("message", "转换成功");
                result.put("fileId", fileId);
                result.put("originalFilename", originalFilename);
                result.put("outputFilename", outputFileName);
                result.put("inputPath", inputFilePath.toString());
                result.put("outputPath", outputFilePath.toString());
                
                // 检查输出文件是否存在
                if (!Files.exists(outputFilePath)) {
                    result.put("success", false);
                    result.put("error", "PDF文件生成失败");
                } else {
                    result.put("fileSize", Files.size(outputFilePath));
                }
            } else {
                result.put("success", false);
                result.put("error", "转换失败: " + conversionResult.get("message"));
                
                // 清理输入文件
                cleanupFile(inputFilePath);
            }
            
        } catch (Exception e) {
            logger.error("Excel转PDF过程中发生错误", e);
            result.put("success", false);
            result.put("error", "转换过程中发生错误: " + e.getMessage());
        }
        
        return result;
    }

    /**
     * 获取转换后的PDF文件
     */
    public File getPdfFile(String fileId) throws IOException {
        String outputFileName = fileId + "_output.pdf";
        Path outputFilePath = Paths.get(tempDir).resolve(outputFileName);
        
        if (!Files.exists(outputFilePath)) {
            throw new FileNotFoundException("PDF文件不存在: " + outputFileName);
        }
        
        return outputFilePath.toFile();
    }

    /**
     * 清理临时文件
     */
    public void cleanupFiles(String fileId) {
        try {
            Path tempDirPath = Paths.get(tempDir);
            String inputFileName = fileId + "_input";
            String outputFileName = fileId + "_output.pdf";
            
            // 清理输入文件(可能有不同扩展名)
            Files.list(tempDirPath)
                .filter(path -> path.getFileName().toString().startsWith(inputFileName))
                .forEach(this::cleanupFile);
            
            // 清理输出文件
            Path outputFilePath = tempDirPath.resolve(outputFileName);
            cleanupFile(outputFilePath);
            
            logger.info("已清理临时文件: {}", fileId);
            
        } catch (Exception e) {
            logger.warn("清理临时文件失败: {}", fileId, e);
        }
    }

    /**
     * 验证上传的文件
     */
    private Map<String, Object> validateFile(MultipartFile file) {
        Map<String, Object> result = new HashMap<>();
        
        if (file == null || file.isEmpty()) {
            result.put("success", false);
            result.put("error", "请选择要上传的文件");
            return result;
        }
        
        // 检查文件大小
        if (file.getSize() > maxFileSize) {
            result.put("success", false);
            result.put("error", String.format("文件大小超过限制,最大允许 %d MB", maxFileSize / 1024 / 1024));
            return result;
        }
        
        // 检查文件类型
        String filename = file.getOriginalFilename();
        if (StringUtils.isEmpty(filename)) {
            result.put("success", false);
            result.put("error", "文件名不能为空");
            return result;
        }
        
        String extension = getFileExtension(filename).toLowerCase();
        if (!isValidExcelFile(extension)) {
            result.put("success", false);
            result.put("error", "不支持的文件格式,请上传Excel文件 (.xlsx, .xls, .xlsm, .xlsb)");
            return result;
        }
        
        result.put("success", true);
        return result;
    }

    /**
     * 调用Python脚本进行转换
     */
    private Map<String, Object> callPythonScript(String inputPath, String outputPath) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            // 构建命令
            ProcessBuilder processBuilder = new ProcessBuilder(
                "python", pythonScript, inputPath, outputPath
            );
            processBuilder.directory(new File(System.getProperty("user.dir")));
            processBuilder.redirectErrorStream(true);
            
            logger.info("执行Python脚本: python {} {} {}", pythonScript, inputPath, outputPath);
            
            // 启动进程
            Process process = processBuilder.start();
            
            // 读取输出
            StringBuilder output = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream(), "UTF-8"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                }
            }
            
            // 等待进程完成
            boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
            
            if (!finished) {
                process.destroyForcibly();
                result.put("success", false);
                result.put("message", "转换超时");
                return result;
            }
            
            int exitCode = process.exitValue();
            String outputStr = output.toString().trim();
            
            logger.info("Python脚本执行完成,退出码: {}, 输出: {}", exitCode, outputStr);
            
            if (exitCode == 0) {
                // 尝试解析JSON输出
                try {
                    JsonNode jsonResult = objectMapper.readTree(outputStr);
                    result.put("success", jsonResult.get("success").asBoolean());
                    result.put("message", jsonResult.get("message").asText());
                } catch (Exception e) {
                    // 如果不是JSON格式,直接使用输出文本
                    result.put("success", true);
                    result.put("message", outputStr);
                }
            } else {
                result.put("success", false);
                result.put("message", outputStr);
            }
            
        } catch (Exception e) {
            logger.error("调用Python脚本失败", e);
            result.put("success", false);
            result.put("message", "调用转换脚本失败: " + e.getMessage());
        }
        
        return result;
    }

    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String filename) {
        if (StringUtils.isEmpty(filename)) {
            return "";
        }
        int lastDotIndex = filename.lastIndexOf('.');
        return lastDotIndex > 0 ? filename.substring(lastDotIndex) : "";
    }

    /**
     * 检查是否为有效的Excel文件
     */
    private boolean isValidExcelFile(String extension) {
        return extension.equals(".xlsx") || extension.equals(".xls") || 
               extension.equals(".xlsm") || extension.equals(".xlsb");
    }

    /**
     * 清理单个文件
     */
    private void cleanupFile(Path filePath) {
        try {
            if (Files.exists(filePath)) {
                Files.delete(filePath);
                logger.debug("已删除文件: {}", filePath);
            }
        } catch (Exception e) {
            logger.warn("删除文件失败: {}", filePath, e);
        }
    }
}

4.2 Controller层代码如下
/**
     * 适用于Postman等工具直接调用下载
     */
    @PostMapping("/test-excel2pdf2")
    public ResponseEntity<Resource> convertExcelToPdfDirect(@RequestParam("file") MultipartFile file) {
        try {
            // 转换文件
            Map<String, Object> result = excelToPdfService.convertExcelToPdf(file);

            if (!(Boolean) result.get("success")) {
                // 转换失败,返回错误信息
                return ResponseEntity.badRequest()
                        .header("X-Error-Message", (String) result.get("error"))
                        .build();
            }

            // 转换成功,获取PDF文件
            String fileId = (String) result.get("fileId");
            String originalFilename = (String) result.get("originalFilename");
            File pdfFile = excelToPdfService.getPdfFile(fileId);
            Resource resource = new FileSystemResource(pdfFile);

            // 生成PDF文件名
            String pdfFilename = originalFilename.replaceAll("\\.[^.]+$", ".pdf");
            String encodedFilename = URLEncoder.encode(pdfFilename, StandardCharsets.UTF_8.toString());

            // 异步清理临时文件(延迟5秒)
            new Thread(() -> {
                try {
                    Thread.sleep(5000); // 等待5秒确保文件下载完成
                    excelToPdfService.cleanupFiles(fileId);
                } catch (Exception e) {
                    // 忽略清理错误
                }
            }).start();

            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION,
                            "attachment; filename=\"" + pdfFilename + "\"; filename*=UTF-8''" + encodedFilename)
                    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PDF_VALUE)
                    .header("X-Original-Filename", originalFilename)
                    .header("X-Converted-Filename", pdfFilename)
                    .contentLength(pdfFile.length())
                    .body(resource);

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

完结撒花,大家可以拿来直接用,无需任何改动哦!o( ̄▽ ̄)ブ

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐