合并Pdf、excel、图片、word为单个Pdf文件的工具类(拿来即用版)

一、概述

技术点选择与解析版:合并Pdf、excel、图片、word为单个Pdf文件的工具类(技术点的选择与深度解析)2.1 流的重置机制:选择 ge - 掘金

基于三个核心Java工具类,实现多格式文件合并为PDF。这三个类分别是:

  1. FileTypeDetector - 文件类型检测工具类
  2. MergeFilesToPDFUtil - 文件合并为PDF工具类
  3. Word2PdfUtils - Word转PDF工具类

1.1 类功能简介

FileTypeDetector(文件类型检测工具)

FileTypeDetector 是一个基于文件头字节(Magic Number)的文件类型识别工具。它通过读取文件的前几个字节来判断文件类型,而不是依赖文件扩展名,这种方式更加可靠和安全。

核心方法:

java 复制代码
public static String detectFileType(FileInputStream inputStream) throws IOException

使用示例:

java 复制代码
FileInputStream fis = new FileInputStream("document.docx");
String fileType = FileTypeDetector.detectFileType(fis);
// 返回: "docx"

MergeFilesToPDFUtil(文件合并工具)

MergeFilesToPDFUtil 提供了将多种格式文件(PDF、Word、Excel、图片等)合并为单个PDF文档的功能。支持两种使用方式:

  • 基于文件路径列表合并
  • 基于文件输入流列表合并

核心方法:

java 复制代码
// 基于文件路径
public static ByteArrayOutputStream generatePdf(List<String> fileNames, String outputFile)

// 基于文件流
public static ByteArrayOutputStream generatePdfFromFileStream(
    List<FileInputStream> fileInputStreams, String outputFile)

使用示例:

java 复制代码
List<String> files = Arrays.asList("doc1.docx", "excel1.xlsx", "image1.jpg");
ByteArrayOutputStream pdfStream = MergeFilesToPDFUtil.generatePdf(files, null);
// 返回合并后的PDF字节流

Word2PdfUtils(Word转PDF工具)

Word2PdfUtils 专门负责将Word文档(.doc、.docx)转换为PDF格式,并具备自动删除空白页的功能。

核心方法:

java 复制代码
public static void wordToPdfStream(InputStream inputStream, OutputStream outputStream)

使用示例:

java 复制代码
FileInputStream wordFile = new FileInputStream("document.docx");
FileOutputStream pdfFile = new FileOutputStream("output.pdf");
Word2PdfUtils.wordToPdfStream(wordFile, pdfFile);

特殊说明:实现word转pdf和excel转pdf使用了商用aspose-words、aspose-cells,必须下载相关jar包,实现无水印转pdf 需要修改jar包,需要的话参考文章:blog.csdn.net/qq_40965901...

二、类源码

word2PdfUtils

java 复制代码
import com.aspose.words.Document;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import lombok.SneakyThrows;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Word转pdf工具类
 * @Version 1.0
 */
public class Word2PdfUtils {

    /**
     * 加载license 用于破解 不生成水印
     */
    @SneakyThrows
    private static void getLicense() {
        try (InputStream is = Word2PdfUtils.class.getClassLoader().getResourceAsStream("License.xml")) {
            License license = new License();
            license.setLicense(is);
        }
    }

    /**
     * word转pdf
     *
     * @param inputStream word文件保存的路径
     * @param response  转换后pdf文件保存的路径
     */
    @SneakyThrows
    public static void wordToPdf(InputStream inputStream, HttpServletResponse response) {
        getLicense();
        // 先转换为PDF到内存中
        ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
        Document doc = new Document(inputStream);
        doc.updatePageLayout();
        doc.save(pdfOutputStream, SaveFormat.PDF);
        
        // 删除PDF中的空白页
        byte[] pdfBytes = removeEmptyPagesFromPdf(pdfOutputStream.toByteArray());
        
        // 输出到response
        try (OutputStream outputStream = response.getOutputStream()) {
            outputStream.write(pdfBytes);
            outputStream.flush();
        }
    }

    @SneakyThrows
    public static void wordToPdfStream(InputStream inputStream, OutputStream outputStream) {
        getLicense();
        // 先转换为PDF到内存中
        ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
        Document doc = new Document(inputStream);
        doc.updatePageLayout();
        doc.save(pdfOutputStream, SaveFormat.PDF);
        
        // 删除PDF中的空白页
        byte[] pdfBytes = removeEmptyPagesFromPdf(pdfOutputStream.toByteArray());
        
        // 输出到目标流
        outputStream.write(pdfBytes);
        outputStream.flush();
    }

    /**
     * 从PDF中删除空白页
     * 
     * @param pdfBytes PDF文件的字节数组
     * @return 删除空白页后的PDF字节数组
     */
    @SneakyThrows
    private static byte[] removeEmptyPagesFromPdf(byte[] pdfBytes) {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(pdfBytes);
        PdfReader reader = new PdfReader(inputStream);
        
        // 检查哪些页面是空白页
        java.util.List<Integer> pagesToKeep = new java.util.ArrayList<>();
        for (int i = 1; i <= reader.getNumberOfPages(); i++) {
            if (!isPageEmpty(reader, i)) {
                pagesToKeep.add(i);
            }
        }
        
        // 如果没有页面需要保留,返回空PDF
        if (pagesToKeep.isEmpty()) {
            reader.close();
            return new byte[0];
        }
        
        // 创建新的PDF,只包含非空白页
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        
        // 创建新的PdfReader,只包含要保留的页面
        PdfReader newReader = new PdfReader(pdfBytes);
        newReader.selectPages(pagesToKeep);
        
        PdfStamper stamper = new PdfStamper(newReader, outputStream);
        stamper.close();
        newReader.close();
        reader.close();
        
        return outputStream.toByteArray();
    }

    /**
     * 判断PDF页面是否为空
     * 
     * @param reader PDF读取器
     * @param pageNumber 页面编号
     * @return 是否为空页面
     */
    @SneakyThrows
    private static boolean isPageEmpty(PdfReader reader, int pageNumber) {
        // 获取页面内容
        String pageContent = com.itextpdf.text.pdf.parser.PdfTextExtractor.getTextFromPage(reader, pageNumber);
        
        // 如果页面内容为空或只包含空白字符,则认为是空白页
        return pageContent == null || pageContent.trim().isEmpty();
    }
}

MergeFilesToPDFUtil

java 复制代码
import com.aspose.cells.License;
import com.aspose.cells.SaveFormat;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import jdk.internal.util.xml.impl.Input;
import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
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.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.io.IOUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.*;
import java.util.ArrayList;
import java.util.List;


/**
 * @ClassName MergeFilesToPDFUtil
 * @Description
 **/
public class MergeFilesToPDFUtil {
    private static final String url = "D:\\testMeta\\tempFile\\tempp";

    public static ByteArrayOutputStream generatePdf(List<String> fileNames) throws IOException{
        return generatePdf(fileNames,null);
    }


    /**
     *
     * @param fileNames 需要转换的文件列表(需带文件路径)
     * @param outputFile 输出文件路径 如:/test/output.pdf
     */
    public static ByteArrayOutputStream generatePdf(List<String> fileNames, String outputFile) throws IOException{
        //合成过程的temp pdfDocument
        List<PDDocument> pdfdocuments = new ArrayList<>();
        String num = System.currentTimeMillis()+"";
        String realFileName = num + ".pdf";
        String tempPath = url+"/"+num;
        PDDocument document = null;
        //生成临时文件的路径
        //在输出pdf存在的时候应该删除,防止旧的数据污染(待定)
        try {
            if(outputFile!=null){
                File file = new File(outputFile+"/"+realFileName);
                if (file.exists()) {
                    // 删除已存在的文件
                    file.delete();
                } else {
                    // 如果文件不存在,确保父目录存在
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }
                }
                // 创建新文件
                file.createNewFile();
            }


            // 创建一个空的pdf文档
            document = new PDDocument();
            // 依次读取要合并的各个文件,并将其内容添加到pdf文档中
            for (String fileName : fileNames) {
                String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
                FileInputStream fis = new FileInputStream(fileName);
                //读取jpg、jpeg、png格式的文件
                if (fileExt.equalsIgnoreCase("jpg") || fileExt.equalsIgnoreCase("jpeg")
                        || fileExt.equalsIgnoreCase("png")) {
                    imageToPdf(fis, document);
                    fis.close();
                } else if (fileExt.equalsIgnoreCase("pdf")) {
                   // PDDocument pdf = PDDocument.load(new File(fileName));
                    PDDocument pdf = PDDocument.load(fis);
                    for (PDPage page : pdf.getPages()) {
                        document.addPage(page);
                    }
                    pdfdocuments.add(pdf);
                    fis.close();
                } else if (fileExt.equalsIgnoreCase("docx") || fileExt.equalsIgnoreCase("doc")) {

                    wordToPdf(fis,tempPath, document, pdfdocuments);
                    fis.close();
                } else if (fileExt.equalsIgnoreCase("xlsx") || fileExt.equalsIgnoreCase("xls")
                || fileExt.equalsIgnoreCase("xml")) {
                    excelToPdf(fis,tempPath, document, pdfdocuments);
                    fis.close();
                }
            }
            // 将pdf文档保存到本地
            if(outputFile!=null){
                document.save(outputFile+"/"+realFileName);
            }
            ByteArrayOutputStream pdfMemoryStream = new ByteArrayOutputStream();
            document.save(pdfMemoryStream);
            return pdfMemoryStream;

        } catch (FileNotFoundException e) {
            throw new RuntimeException("文件不存在!",e);
        } catch (IOException e) {
            throw new RuntimeException("系统异常,请联系管理员!",e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if(document!=null){
                document.close();
            }
            closeIo( pdfdocuments);
            FileUtils.forceDelete(new File(tempPath));
        }
    }
}

    /**
     *  合并多个文件为pdf文件
     * @param fileInputStreams
     * @return PDF字节流
     * @throws IOException
     */
    public static ByteArrayOutputStream generatePdfFromFileStream(List<FileInputStream> fileInputStreams) throws IOException{
        return generatePdfFromFileStream(fileInputStreams,null);
    }
    /**
     * 合并多个文件为pdf文件
     * @param fileInputStreams pdf文件输入流列表
     * @param outputFile 合并后的pdf文件夹路径
     * @return ByteArrayOutputStream PDf字节流
     * @throws IOException
     */

    public static ByteArrayOutputStream generatePdfFromFileStream(List<FileInputStream> fileInputStreams, String outputFile) throws IOException{
        if(fileInputStreams == null ||fileInputStreams.isEmpty()){
            throw new RuntimeException("文件流列表不能为空!");
        }
        PDDocument document = null;
        List<PDDocument> pdfdocuments = new ArrayList<>();
        String num = System.currentTimeMillis()+ "";
        String realFileName = num + ".pdf";
        String tempPath = url+"/"+num;
        //String url = "D:\\testMeta\\tempFile";
        //在输出pdf存在的时候应该删除,防止旧的数据污染(待定)
        try {
            if(outputFile != null){
                File file = new File(outputFile+"/"+realFileName);
                if (file.exists()) {
                    // 删除已存在的文件
                    file.delete();
                } else {
                    // 如果文件不存在,确保父目录存在
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }
                }
                // 创建新文件
                file.createNewFile();
            }

            // 创建一个空的pdf文档
            document = new PDDocument();
            // 依次读取要合并的各个文件,并将其内容添加到pdf文档中
            for (FileInputStream fis : fileInputStreams) {
                //通过文件流获取文件类型
                String fileExt = FileTypeDetector.detectFileType(fis);
                if(fileExt == null){
                    fis.close();
                    continue;
                }
                if (fileExt.equalsIgnoreCase("jpg") || fileExt.equalsIgnoreCase("jpeg")
                        || fileExt.equalsIgnoreCase("png")) {
                    imageToPdf(fis, document);
                    fis.close();
                } else if (fileExt.equalsIgnoreCase("pdf")) {
                    pdfToPdf(fis,document, pdfdocuments);
                    fis.close();
                } else if (fileExt.equalsIgnoreCase("docx") || fileExt.equalsIgnoreCase("doc")) {
                    wordToPdf(fis,tempPath, document, pdfdocuments);
                    fis.close();
                } else if (fileExt.equalsIgnoreCase("xlsx") || fileExt.equalsIgnoreCase("xls")
                            || fileExt.equalsIgnoreCase("xml")) {
                    excelToPdf(fis,tempPath, document, pdfdocuments);
                    fis.close();
                }
            }
            // 将pdf文档保存到传入位置
            if(outputFile != null){
                document.save(outputFile+"/"+realFileName);
            }
            ByteArrayOutputStream pdfMemoryStream = new ByteArrayOutputStream();
            document.save(pdfMemoryStream);
            return pdfMemoryStream;
        } catch (FileNotFoundException e) {
            throw new RuntimeException("文件不存在!",e);
        } catch (IOException e) {
            throw new RuntimeException("系统异常,请联系管理员!",e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if(document != null){
                document.close();
            }
            //关闭pdf合并过程中产生的临时pdDocuments
            closeIo(pdfdocuments);
            //删除临时目录
            FileUtils.forceDelete(new File(tempPath));
        }
    }
    /**
     * 关闭所有的io流
     * @param pdfdocuments
     */
    public static void closeIo(List<PDDocument> pdfdocuments){
        try {
            //延迟处理等待文件关闭
            for (PDDocument pdfdocument : pdfdocuments) {
                pdfdocument.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
/**
     * 通过pdf文件流将pdf加入输出pdf文档
     * @param fis 文件流
     * @param document 输出pdf文档
     * @param pdfdocuments 临时pdf文档list
     */
    public static void pdfToPdf(FileInputStream fis ,PDDocument document, List<PDDocument> pdfdocuments ) throws IOException {
        PDDocument pdf = PDDocument.load(fis);
        for (PDPage page : pdf.getPages()) {
            document.addPage(page);
        }
        pdfdocuments.add(pdf);
    }
/**
     * 根据图片文件地址转pdf
     * @param fileName
     * @param document
     */
    private static void imageToPdf(String fileName, PDDocument document){
        try {
            FileInputStream fis = new FileInputStream(fileName);
            imageToPdf(fis, document);
        }catch (IOException e){
            throw new RuntimeException(e);
        }
    }
    /**
     * 图片文件流转pdf
     * @param is 图片文件流
     * @param document 输出pdf文档
     */
    private static void imageToPdf(FileInputStream is, PDDocument document ) throws IOException {
        // 1. 将 FileInputStream 转换为字节数组(PDImageXObject 支持从字节数组创建图片)
        byte[] imageBytes = streamToByteArray(is);

        // 2. 从字节数组创建 PDImageXObject(第二个参数为图片名称,可自定义)
        PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "image");

        // 3. 创建与图片尺寸一致的 PDF 页面(避免图片拉伸/压缩)
        PDPage page = new PDPage(new PDRectangle(image.getWidth(), image.getHeight()));
        document.addPage(page);

        // 4. 将图片绘制到 PDF 页面(坐标 (0,0) 表示左上角对齐)
        try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
            contentStream.drawImage(image, 0, 0); // 图片尺寸与页面一致,直接填充
        }
    }
    /**
     * doc、docx文件转pdf
     * @param is doc、docx文件输入流
     * @param tempPath 临时文件保存路径
     * @param document 输出pdf的文档
     * @param pdDocuments 待关闭流list
     */

    private static void wordToPdf(FileInputStream is,String tempPath, PDDocument document, List<PDDocument> pdDocuments) throws IOException {
        String pdfFile = System.currentTimeMillis() + ".pdf";
        String tempPdfUrl = tempPath + "/" + pdfFile;
        // 确保临时目录存在
        File tempDir = new File(tempPath);
        if (!tempDir.exists()) {
            tempDir.mkdirs();
        }
        FileOutputStream os = new FileOutputStream(tempPdfUrl);
        Word2PdfUtils.wordToPdfStream(is,os);
        os.close();
        //这里是将转完的pdf添加到新的pdf页面之后
        PDDocument load = PDDocument.load(new File(tempPdfUrl));
        for (PDPage page : load.getPages()) {
            document.addPage(page);
        }
        //将生成的pdf临时文件删除掉
        //FileUtils.forceDelete(new File(tempPdfUrl));
        pdDocuments.add(load);
    }

    /**
     * xlsx、xls、xml文件转pdf
     * @param is 文件流
     * @param tempPath 临时文件保存路径
     * @param document 输出pdf的文档
     * @param pdDocuments 待关闭流list
     */
    private static void excelToPdf(FileInputStream is,String tempPath, PDDocument document, List<PDDocument> pdDocuments){
        String pdfFile = System.currentTimeMillis() + ".pdf";
        String tempPdfUrl = tempPath + "/" + pdfFile;
        // 确保临时目录存在
        File tempDir = new File(tempPath);
        if (!tempDir.exists()) {
            tempDir.mkdirs();
        }
        //转换Excel文件为pdfAspose
        try {
            convertExcelToPdfByAspose(is, tempPdfUrl);
            PDDocument load = PDDocument.load(new File(tempPdfUrl));

            for (PDPage page : load.getPages()) {
                document.addPage(page);
            }
            //将生成的pdf临时文件删除掉
            //FileUtils.forceDelete(new File(tempPdfUrl));
            pdDocuments.add(load);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        //这里是将转完的pdf添加到新的pdf页面之后

    }
    /**
     * FileInputStream 转换为字节数组
     * @param stream 输入流
     * @return 字节数组
     * @throws IOException 流读取异常
     */
    private static byte[] streamToByteArray(FileInputStream stream) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024]; // 缓冲区大小,可根据图片大小调整(如 4096)
        int len;
        // 循环读取流数据到缓冲区,再写入字节输出流
        while ((len = stream.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
        }
        bos.flush(); // 确保所有数据写入
        return bos.toByteArray();
    }

   private static void convertExcelToPdfByAspose(FileInputStream is, String pdfFilePath) throws Exception {
        FileOutputStream os = null;
        InputStream resourceAsStream = null;
        com.aspose.cells.Workbook workbook = null;
        try{
            resourceAsStream = MergeFilesToPDFUtil.class.getClassLoader().getResourceAsStream("License.xml");
            com.aspose.cells.License license = new com.aspose.cells.License();
            license.setLicense(resourceAsStream);
            os = new FileOutputStream(pdfFilePath);
            workbook = new com.aspose.cells.Workbook(is);
            workbook.save(os, SaveFormat.PDF);//设置转换文件类型并转换
        }catch (Exception e){
            throw new RuntimeException("Excel转换为PDF失败: " + e.getMessage(), e);
        }finally {
            if(os != null){
                os.close();
            }
            if(resourceAsStream != null){
                resourceAsStream.close();
            }
            if(workbook != null){
                //特有的关闭非托管资源
                workbook.dispose();
            }
        }


    }

FileTypeDetector

java 复制代码
import cn.hutool.core.io.FileTypeUtil;
import com.baomidou.mybatisplus.generator.config.rules.FileType;
import lombok.Getter;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;

import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * 文件类型判断工具(基于文件头字节)
 *   @ClassName MergeFilesToPDFUtil
 *   @Description
 * **/
public class FileTypeDetector {
    // 常见文件的"文件头字节"和对应类型(扩展可添加更多格式)
    private static final List<FileType> FILE_TYPES = Arrays.asList(
            // PDF: 前4字节是 %PDF (十六进制:25 50 44 46)
            new FileType("pdf", new byte[]{0x25, 0x50, 0x44, 0x46}),
            // Word (docx): 同ZIP格式,前4字节 PK
            new FileType("docx", new byte[]{0x50, 0x4B, 0x03, 0x04}),
            // Word (doc): 前2字节是 D0 CF (OLE格式)
            new FileType("doc", new byte[]{(byte) 0xD0, (byte) 0xCF}),
            // Excel (xlsx/xlsm): 前4字节是 PK (ZIP格式,因为xlsx是压缩包),十六进制:50 4B 03 04
            new FileType("xlsx", new byte[]{0x50, 0x4B, 0x03, 0x04}),
            // Excel (xls): 前8字节是 58 4C 53 48 (BIFF8格式)
            new FileType("xls", new byte[]{0x58, 0x4C, 0x53, 0x48, 0x00, 0x00, 0x00, 0x00}),
            // Excel (xls): 前4字节是 D0 CF (OLE格式)
            new FileType("xls", new byte[]{(byte) 0xD0, (byte) 0xCF}),
            // 图片 (PNG): 前8字节是 89 50 4E 47 0D 0A 1A 0A
            new FileType("png", new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}),
            // 图片 (JPG): 前2字节是 FF D8
            new FileType("jpg", new byte[]{(byte) 0xFF, (byte) 0xD8}),
            // 图片 (GIF): 前3字节是 47 49 46
            new FileType("gif", new byte[]{0x47, 0x49, 0x46}),
            // XML (带UTF-8 BOM): 前3字节 EF BB BF + 第4字节 3C (<)
            new FileType("xml", new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF, 0x3C}),
            // XML (无BOM): 前1字节 3C (<),覆盖大多数纯XML文件(如<?xml version="1.0"?>)
            new FileType("xml", new byte[]{0x3C}),
            // 图片 (JPEG): 前2字节是 FF D8(与JPG核心头一致,单独标识格式)
            new FileType("jpeg", new byte[]{(byte) 0xFF, (byte) 0xD8}),
            // 图片 (JPEG EXIF版): 前4字节是 FF D8 FF E1(带EXIF信息的JPEG,如相机照片,提升识别精准度)
            new FileType("jpeg", new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE1})
    );

    // 读取流的前N字节(用于判断文件头,避免读取整个流)
    private static final int READ_BYTES_LENGTH = 20; // 足够覆盖常见文件头长度

    // 读取 ZIP 条目的最大字节限制(防止恶意文件,仅读前 10KB)
    private static final int ZIP_SCAN_LIMIT = 1024 * 10;
    private static final String XLSX_FEATURE = "xl/workbook.xml";
    private static final String DOCX_FEATURE = "word/document.xml";
    private static final String XLS_FEATURE = "Workbook";
    private static final String DOC_FEATURE = "WordDocument";

    /**
     * 通过输入流判断文件类型
     * @param inputStream 输入流(不会关闭流,需调用方自行处理)
     * @return 文件类型枚举(如PDF、EXCEL_XLSX),未知类型返回 UNKNOWN
     * @throws IOException 流读取异常
     */
    public static String detectFileType(FileInputStream inputStream) throws IOException {
        if (inputStream == null) {
            return null;
        }
        try {
            //读取前20个字节,用于判断文件类型
            byte[] fileHeader = new byte[READ_BYTES_LENGTH];
            int readLen = inputStream.read(fileHeader);
            if (readLen <= 0) {
                return null;
            }
            // 匹配文件头字节,返回对应的文件类型
            for (FileType fileType : FILE_TYPES) {
                byte[] magicNumber = fileType.magicNumber;
                if (readLen >= magicNumber.length) {
                    byte[] actualHeader = Arrays.copyOfRange(fileHeader, 0, magicNumber.length);
                    if (Arrays.equals(actualHeader, magicNumber)) {
                        String typeName = fileType.getTypeName();
                        boolean check = true;
                        //xlsx、docx文件头都是OOXML格式,xls、doc文件头都是OLE格式,需要进一步判断
                        if(typeName.equals("xlsx") ){
                            check = isZipFeatureExists(inputStream,XLSX_FEATURE);
                        }else if(typeName.equals("docx")){
                            check = isZipFeatureExists(inputStream,DOCX_FEATURE);
                        }else if(typeName.equals("xls")){
                            check = isOleFeatureExists(inputStream,XLS_FEATURE);
                        }else if(typeName.equals("doc")){
                            check = isOleFeatureExists(inputStream,DOC_FEATURE);
                        }
                        if(check){
                            return typeName;
                        }

                    }
                }
            }
            return null;
        }catch (IOException e){
            throw new IOException("系统异常,请联系管理员!",e);
        }
        finally {
            inputStream.getChannel().position(0);
        }
    }


    private static boolean isZipFeatureExists(FileInputStream fis,String feature) throws IOException {
        // 重置流到起始位置(确保 ZipInputStream 从开头读取)
        fis.getChannel().position(0);
        ZipInputStream zipIs = null;
        try {
            //匿名包装流:重写 close() 为空,避免关闭包装流的同时关闭底层流
            InputStream noCloseFis = new FilterInputStream(fis) {
                @Override
                public void close() throws IOException {}
            };
            zipIs = new ZipInputStream(noCloseFis);
            ZipEntry entry;
            // 遍历 ZIP 条目,查找特征文件(找到即返回,性能极高)
            while ((entry = zipIs.getNextEntry()) != null) {
                // 统一路径分隔符(避免 Windows/Linux 差异)
                String entryPath = entry.getName().replace("\\", "/");
//                System.out.println("辨别:"+feature+"---"+entryPath);
                // 关闭当前条目,释放资源(不关闭会导致内存泄漏)
                zipIs.closeEntry();
                // 忽略大小写(部分特殊文件可能大小写不一致)
                if (entryPath.equalsIgnoreCase(feature)) {
                    return true;
                }

                // 超过扫描限制,直接退出(防止恶意文件无限遍历)
//                if (zipIs.available() <= 0) {
//                    break;
//                }
            }
            return false;
        } catch (IOException e) {
            // 解析 ZIP 失败(非 ZIP 格式),返回异常
            throw new IOException("系统异常,请联系管理员!",e);
        }finally {
            if(zipIs != null){
                zipIs.close();
            }
        }
//        return false;
    }


    /**
     * 验证 OLE 容器内是否存在指定流(区分 xls/doc)
     */
    private static boolean isOleFeatureExists(FileInputStream is, String featureStream) throws IOException {
        //is.reset();
        is.getChannel().position(0);
        POIFSFileSystem poifs = null;
        try {
            // 使用不关闭底层流的包装
            InputStream nonClosingStream = new FilterInputStream(is) {
                @Override
                public void close() throws IOException {
                    // 不执行任何操作,防止流被关闭
                }
            };
            poifs = new POIFSFileSystem(nonClosingStream);
            DirectoryEntry root = poifs.getRoot();
            // 遍历 OLE 根目录下的流,查找特征流名称
            for (Entry entry : root) {
                if (entry.getName().equals(featureStream)) {
                    return true;
                }
            }
            return false;
        } catch (IOException e) {
            // 解析 OLE 失败(非 OLE 格式)
            throw new IOException("系统异常,请联系管理员!",e);
        }finally {
            if(poifs != null){
                poifs.close();
            }
        }
    }
    // 内部静态类:存储文件类型名称和对应的文件头字节
    @Getter
    public static class FileType {
        String typeName; // 类型名称(需与 FileTypeEnum 枚举值一致)
        byte[] magicNumber; // 文件头字节(Magic Number)

        FileType(String typeName, byte[] magicNumber) {
            this.typeName = typeName;
            this.magicNumber = magicNumber;
        }
    }
}

相关推荐
bcbnb1 小时前
手机崩溃日志导出的工程化方法,构建多工具协同的跨平台日志获取与分析体系(iOS/Android 全场景 2025 进阶版)
后端
Java水解2 小时前
为何最终我放弃了 Go 的 sync.Pool
后端·go
oliveira-time2 小时前
原型模式中的深浅拷贝
java·开发语言·原型模式
二川bro2 小时前
第41节:第三阶段总结:打造一个AR家具摆放应用
后端·restful
进阶的猿猴2 小时前
easyExcel实现单元格合并
java·excel
aiopencode2 小时前
苹果应用商店上架全流程 从证书体系到 IPA 上传的跨平台方法
后端
小许学java2 小时前
MySQL-触发器
java·数据库·mysql·存储过程·触发器
JEECG低代码平台2 小时前
【2025/11】GitHub本月热度排名前十的开源Java项目
java·开源·github
百***86052 小时前
Spring BOOT 启动参数
java·spring boot·后端