Easyexcel 结合POI 做图片读取

前言

需求是要对excel里,每行单元格内的图片进行解析

借鉴内容:https://blog.csdn.net/lost_gost/article/details/149895505

在原文的基础上,做了个自定义注解的封装

作为工具类记录下咯,以后还能用

正文

ExcelPicturesUtils 图片解析工具类

java 复制代码
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.XML;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.map.HashedMap;
import org.apache.commons.io.IOUtils;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xssf.usermodel.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;


/**
 * excel内的图片读取工具类
 *
 * @author Heng.Wei
 * @date 2025/10/20 09:52
 **/
@Slf4j
public class ExcelPicturesUtils {

    /**
     * 获取浮动图片,以 map 形式返回,键为行列格式 x-y。
     *
     * @param xssfSheet WPS 工作表
     * @return 浮动图片的 map
     */
    public static Map<String, XSSFPictureData> getFloatingPictures(XSSFSheet xssfSheet) {
        Map<String, XSSFPictureData> mapFloatingPictures = new HashMap<>(8);
        XSSFDrawing drawingPatriarch = xssfSheet.getDrawingPatriarch();
        if (drawingPatriarch != null) {
            List<XSSFShape> shapes = drawingPatriarch.getShapes();
            for (XSSFShape shape : shapes) {
                if (shape instanceof XSSFPicture) {
                    XSSFPicture picture = (XSSFPicture) shape;
                    XSSFClientAnchor anchor = (XSSFClientAnchor) picture.getAnchor();
                    XSSFPictureData pictureData = picture.getPictureData();
                    String key = anchor.getRow1() + "-" + anchor.getCol1();
                    mapFloatingPictures.put(key, pictureData);
                }
            }
        }
        return mapFloatingPictures;
    }

    /**
     * 处理 WPS 文件中的图片数据,返回图片信息 map。
     *
     * @param stream    输入流
     * @param mapConfig 配置映射
     * @return 图片信息的 map
     */
    private Map<String, XSSFPictureData> processPictures(ByteArrayInputStream stream, Map<String, String> mapConfig) throws IOException {
        Map<String, XSSFPictureData> mapPictures = new HashedMap<>();
        Workbook workbook = WorkbookFactory.create(stream);
        @SuppressWarnings("unchecked")
        List<XSSFPictureData> allPictures = (List<XSSFPictureData>) workbook.getAllPictures();
        for (XSSFPictureData pictureData : allPictures) {
            PackagePartName partName = pictureData.getPackagePart().getPartName();
            String uri = partName.getURI().toString();
            if (mapConfig.containsKey(uri)) {
                String strId = mapConfig.get(uri);
                mapPictures.put(strId, pictureData);
            }
        }
        return mapPictures;
    }

    /**
     * 获取 WPS 文档中的图片,包括嵌入式图片和浮动式图片。
     *
     * @param file 文件
     * @return 图片信息的 map
     */
    public Map<String, XSSFPictureData> getPictures(MultipartFile file) {
        try {
            byte[] data = file.getBytes();
            Map<String, String> mapConfig = processZipEntries(new ByteArrayInputStream(data));
            Map<String, XSSFPictureData> mapPictures = processPictures(new ByteArrayInputStream(data), mapConfig);
            Iterator<Sheet> sheetIterator = WorkbookFactory.create(new ByteArrayInputStream(data)).sheetIterator();
            while (sheetIterator.hasNext()) {
                XSSFSheet sheet = (XSSFSheet) sheetIterator.next();
                mapPictures.putAll(getFloatingPictures(sheet));
            }
            return mapPictures;
        } catch (IOException e) {
            return new HashedMap<>();
        }
    }

    /**
     * 处理 Zip 文件中的条目,更新图片配置信息。
     *
     * @param stream Zip 输入流
     * @return 配置信息的 map
     */
    private Map<String, String> processZipEntries(ByteArrayInputStream stream) throws IOException {
        Map<String, String> mapConfig = new HashedMap<>();
        ZipInputStream zipInputStream = new ZipInputStream(stream);
        ZipEntry zipEntry;
        while ((zipEntry = zipInputStream.getNextEntry()) != null) {
            try {
                final String fileName = zipEntry.getName();
                if ("xl/cellimages.xml".equals(fileName)) {
                    processCellImages(zipInputStream, mapConfig);
                } else if ("xl/_rels/cellimages.xml.rels".equals(fileName)) {
                    return processCellImagesRels(zipInputStream, mapConfig);
                }
            } finally {
                zipInputStream.closeEntry();
            }
        }
        return new HashedMap<>();
    }

    /**
     * 处理 Zip 文件中的 cellimages.xml 文件,更新图片配置信息。
     *
     * @param zipInputStream Zip 输入流
     * @param mapConfig      配置信息的 map
     */
    private void processCellImages(ZipInputStream zipInputStream, Map<String, String> mapConfig) throws IOException {
        String content = IOUtils.toString(zipInputStream, StandardCharsets.UTF_8);
        JSONObject jsonObject = XML.toJSONObject(content);
        if (jsonObject != null) {
            JSONObject cellImages = jsonObject.getJSONObject("etc:cellImages");
            if (cellImages != null) {
                JSONArray cellImageArray = null;
                Object cellImage = cellImages.get("etc:cellImage");
                if (cellImage instanceof JSONArray) {
                    cellImageArray = (JSONArray) cellImage;
                } else if (cellImage instanceof JSONObject) {
                    JSONObject cellImageObj = (JSONObject) cellImage;
                    cellImageArray = new JSONArray();
                    cellImageArray.add(cellImageObj);
                }
                if (cellImageArray != null) {
                    processImageItems(cellImageArray, mapConfig);
                }
            }
        }
    }

    /**
     * 处理 cellImageArray 中的图片项,更新图片配置信息。
     *
     * @param cellImageArray 图片项的 JSONArray
     * @param mapConfig      配置信息的 map
     */
    private void processImageItems(JSONArray cellImageArray, Map<String, String> mapConfig) {
        for (int i = 0; i < cellImageArray.size(); i++) {
            JSONObject imageItem = cellImageArray.getJSONObject(i);
            if (imageItem != null) {
                JSONObject pic = imageItem.getJSONObject("xdr:pic");
                if (pic != null) {
                    processPic(pic, mapConfig);
                }
            }
        }
    }

    /**
     * 处理 pic 中的图片信息,更新图片配置信息。
     *
     * @param pic       图片的 JSONObject
     * @param mapConfig 配置信息的 map
     */
    private void processPic(JSONObject pic, Map<String, String> mapConfig) {
        JSONObject nvPicPr = pic.getJSONObject("xdr:nvPicPr");
        if (nvPicPr != null) {
            JSONObject cNvPr = nvPicPr.getJSONObject("xdr:cNvPr");
            if (cNvPr != null) {
                String name = cNvPr.getStr("name");
                if (StrUtil.isNotEmpty(name)) {
                    String strImageEmbed = updateImageEmbed(pic);
                    if (strImageEmbed != null) {
                        mapConfig.put(strImageEmbed, name);
                    }
                }
            }
        }
    }

    /**
     * 获取嵌入式图片的 embed 信息。
     *
     * @param pic 图片的 JSONObject
     * @return embed 信息
     */
    private String updateImageEmbed(JSONObject pic) {
        JSONObject blipFill = pic.getJSONObject("xdr:blipFill");
        if (blipFill != null) {
            JSONObject blip = blipFill.getJSONObject("a:blip");
            if (blip != null) {
                return blip.getStr("r:embed");
            }
        }
        return null;
    }

    /**
     * 处理 Zip 文件中的 relationship 条目,更新配置信息。
     *
     * @param zipInputStream Zip 输入流
     * @param mapConfig      配置信息的 map
     * @return 配置信息的 map
     */
    private Map<String, String> processCellImagesRels(ZipInputStream zipInputStream, Map<String, String> mapConfig) throws IOException {
        String content = IOUtils.toString(zipInputStream, StandardCharsets.UTF_8);
        JSONObject jsonObject = XML.toJSONObject(content);
        JSONObject relationships = jsonObject.getJSONObject("Relationships");
        if (relationships != null) {
            JSONArray relationshipArray = null;
            Object relationship = relationships.get("Relationship");

            if (relationship instanceof JSONArray) {
                relationshipArray = (JSONArray) relationship;
            } else if (relationship instanceof JSONObject) {
                JSONObject relationshipObj = (JSONObject) relationship;
                relationshipArray = new JSONArray();
                relationshipArray.add(relationshipObj);
            }
            if (relationshipArray != null) {
                return processRelationships(relationshipArray, mapConfig);
            }
        }
        return null;
    }

    /**
     * 处理 relationshipArray 中的关系项,更新配置信息。
     *
     * @param relationshipArray 关系项的 JSONArray
     * @param mapConfig         配置信息的 map
     * @return 配置信息的 map
     */
    private Map<String, String> processRelationships(JSONArray relationshipArray, Map<String, String> mapConfig) {
        Map<String, String> mapRelationships = new HashedMap<>();
        for (int i = 0; i < relationshipArray.size(); i++) {
            JSONObject relaItem = relationshipArray.getJSONObject(i);
            if (relaItem != null) {
                String id = relaItem.getStr("Id");
                String value = "/xl/" + relaItem.getStr("Target");
                if (mapConfig.containsKey(id)) {
                    String strImageId = mapConfig.get(id);
                    mapRelationships.put(value, strImageId);
                }
            }
        }
        return mapRelationships;
    }

}

PersonExcelDTO

java 复制代码
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;

/**
 * 人员excel导入DTO
 *
 * @author Heng.Wei
 * @date 2025/10/18 13:59
 **/
@Data
@EqualsAndHashCode
@ExcelIgnoreUnannotated
public class PersonExcelDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    @ExcelProperty("序号")
    private Integer serialNumber;

    @ExcelProperty(value = "头像", converter = ExcelPicIdToByteConverter.class)
    private Byte[] avatar;

    /** 头像URL */
    private String avatarUrl;
    
    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("身份证号码")
    private String idCardNumber;
    
    @ExcelProperty("手机号码")
    private String phoneNumber;
    
    @ExcelProperty("民族")
    private String nation;
    
    @ExcelProperty("政治面貌")
    private String politicalStatus;
    
    @ExcelProperty("学历")
    private String education;
    
    @ExcelProperty("准入生效日期")
    private String validStartDate;
    
    @ExcelProperty("准入失效时间")
    private String validEndDate;
    
    @ExcelProperty("岗位")
    private String position;
}

ExcelPicIdToByteConverter 图片ID转Byte[]

java 复制代码
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.rtzh.server.persmgmt.core.listener.PersonExcelImportListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.usermodel.XSSFPictureData;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 自定义 - excel图片ID转图片内容的字节数组
 * 目前这个是用于WPS,因为office的excel 图片不能放在单元格里面、只能是悬浮状态. 如果我理解错了,请留言帮我纠正、感谢
 * - 如果客户非要用office,可以考虑使用身份号作为图片的文件名,然后去匹配图片?未尝试
 * @author Heng.Wei
 * @date 2025/10/20 10:23
 **/
@Slf4j
public class ExcelPicIdToByteConverter implements Converter<Byte[]> {

    private static final Pattern PIC_PATTERN = Pattern.compile("\"(ID_.+?)\"");

    @Override
    public Class<?> supportJavaTypeKey() {
        return Byte[].class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public Byte[] convertToJavaData(ReadConverterContext<?> context) {

        String cellValueString = context.getReadCellData().getStringValue();
        if (StrUtil.isBlank(cellValueString)) {
            return null;
        }

        Matcher matcher = PIC_PATTERN.matcher(cellValueString);
        if (!matcher.find()) {
            log.warn(">>> 未获取到图片ID cellValue[{}]", cellValueString);
            return null;
        }

        String avatarId = matcher.group(1);
        Map<String, XSSFPictureData> picIdToPicDataMap = PersonExcelImportListener.picturesThreadLocal.get();
        XSSFPictureData pictureData = picIdToPicDataMap.get(avatarId);
        if (pictureData != null) {
            byte[] data = pictureData.getData();
            return ArrayUtil.wrap(data);
        }
        return null;
    }
}

PersonExcelImportListener

java 复制代码
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import org.apache.poi.xssf.usermodel.XSSFPictureData;
import org.springframework.web.multipart.MultipartFile;

import java.util.Map;

/**
 * excel导入
 *
 * @author Heng.Wei
 * @date 2025/10/18 14:45
 **/
public class PersonExcelImportListener extends AnalysisEventListener<PersonExcelDTO> {

    public static ThreadLocal<Map<String, XSSFPictureData>> picturesThreadLocal = new ThreadLocal<>();

    public PersonExcelImportListener(MultipartFile file) {
        Map<String, XSSFPictureData> pictures = new ExcelPicturesUtils().getPictures(file);
        picturesThreadLocal.set(pictures);
    }

    @Override
    public void invoke(PersonExcelDTO dto, AnalysisContext analysisContext) {
        System.out.println(">>>>>> personExcelDTO:" + dto);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        picturesThreadLocal.remove();
    }

}

自测示例

java 复制代码
    @ApiOperation("人员导入")
    @PostMapping("/importExcel")
    public Response importExcel(@RequestParam("file") MultipartFile file) {

        InputStream inputStream;
        try {
            inputStream = file.getInputStream();
        } catch (IOException e) {
            log.error("<<< 人员导入异常", e);
            throw new RuntimeException(e);
        }

        PersonExcelImportListener personExcelImportListener = new PersonExcelImportListener(file);
        EasyExcel.read(inputStream, PersonExcelDTO.class, personExcelImportListener)
                // 跳过前两行标题栏,从第三行开始读取
                .headRowNumber(2)
                // 只读取第一个sheet
                .sheet(0)
                .doRead();
        return Response.success();
    }

亲测OK,这个byte[] 的图片是可以正常回写到本地磁盘正确展示的