前言
需求是要对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[] 的图片是可以正常回写到本地磁盘正确展示的