Java 基于 Apache POI 提取 Excel 单元格内图片的技术实现
应用背景
在企业级开发中,经常需要处理 Excel 数据的导入,其中一种复杂场景是读取 Excel 文件中嵌入的图片并将其关联到对应的业务数据行。由于图片在 Excel 中通常作为浮动对象存储,而非直接包含在单元格的值中,因此需要利用 Apache POI 提供的底层绘图对象来定位并提取这些图片。

环境配置
首先,在项目的 pom.xml 中引入 Apache POI 的核心依赖。建议使用 4.x 或更高版本以获得更好的 OOXML 支持。
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
临时存储目录生成
在处理文件上传时,通常需要一个临时目录来缓冲提取出来的图片流,处理完成后再统一迁移至文件服务器或云存储。
/**
* 创建基于任务ID的临时存储路径
* @param taskId 导入任务唯一标识
* @return 临时目录路径
*/
public static String acquireWorkPath(String taskId) {
String dateLabel = new SimpleDateFormat("yyyyMMdd").format(new Date());
String subPath = String.format("temp/%s_%s", dateLabel, taskId);
File dir = new File(subPath);
if (!dir.exists()) {
dir.mkdirs();
}
return subPath;
}
解析 Excel 中的图片资源
实现思路是遍历 Sheet 中的绘图层(Drawing),获取所有的形状(Shape),识别出图片类型并根据其锚点(Anchor)信息确定图片所在的行索引。
/**
* 提取指定 Sheet 中的图片并返回行号映射关系
* @param targetSheet 目标工作表
* @param jobToken 任务令牌
* @return Map<行索引, 图片文件名列表>
*/
public static Map<Integer, List<String>> fetchImagesFromSheet(Sheet targetSheet, String jobToken) throws Exception {
if (jobToken == null || jobToken.isEmpty()) {
return Collections.emptyMap();
}
Map<Integer, List<String>> rowImageMap = new HashMap<>();
String storagePath = acquireWorkPath(jobToken);
Workbook wb = targetSheet.getWorkbook();
// 获取或创建绘图管理器
Drawing<?> drawing = targetSheet.createDrawingPatriarch();
if (wb instanceof XSSFWorkbook) {
processXSSFImages((XSSFDrawing) drawing, storagePath, jobToken, rowImageMap);
} else if (wb instanceof HSSFWorkbook) {
processHSSFImages((HSSFPatriarch) drawing, storagePath, jobToken, rowImageMap);
}
return rowImageMap;
}
private static void processXSSFImages(XSSFDrawing drawing, String path, String token, Map<Integer, List<String>> map) throws IOException {
int seq = 0;
for (XSSFShape shape : drawing.getShapes()) {
if (shape instanceof XSSFPicture) {
XSSFPicture pic = (XSSFPicture) shape;
savePictureData(pic, path, token, ++seq, map);
}
}
}
private static void processHSSFImages(HSSFPatriarch drawing, String path, String token, Map<Integer, List<String>> map) throws IOException {
int seq = 0;
for (HSSFShape shape : drawing.getChildren()) {
if (shape instanceof HSSFPicture) {
HSSFPicture pic = (HSSFPicture) shape;
savePictureData(pic, path, token, ++seq, map);
}
}
}
图片持久化与坐标匹配
在提取图片时,ClientAnchor 是关键对象。它记录了图片左上角和右下角所在的行列坐标。通常我们以起始行(Row1)作为该图片所属的数据行。
private static void savePictureData(Picture pic, String dir, String token, int index, Map<Integer, List<String>> map) throws IOException {
PictureData data = pic.getPictureData();
ClientAnchor anchor = pic.getClientAnchor();
// 获取图片定位行
int targetRow = anchor.getRow1();
String extension = data.suggestFileExtension();
// 构造唯一文件名
String fileName = String.format("img_%s_R%d_%d.%s", token, targetRow, index, extension);
File targetFile = new File(dir, fileName);
// 写入文件系统
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
fos.write(data.getData());
}
// 更新结果集
map.computeIfAbsent(targetRow, k -> new ArrayList<>()).add(fileName);
}
技术要点与优化建议
处理 Excel 图片时,内存管理至关重要。尤其是 .xlsx 格式的大型文件,图片数据会显著增加 JVM 堆内存占用。
- 内存释放:
pictureData.getData()会在内存中创建字节数组副本。在完成 IO 写入后,应确保引用被及时置空,以便 GC 回收。 - 异常处理: 对于非图片格式(如 Excel 中嵌入的图标或图表),
suggestFileExtension可能会返回非预期结果。建议增加文件头校验或扩展名过滤。 - 大文件流式处理: 如果 Excel 文件极大且包含数千张图片,建议考虑使用
SXSSF模式的变种处理逻辑,或者通过解压.xlsx(实质是 XML 压缩包) 直接读取xl/media目录下的原始文件,以降低内存压力。 - 清理机制: 任务结束后,务必通过
File.delete()或文件清理工具类移除临时文件夹,防止磁盘空间溢出。