WPS Excel 内嵌图片的程序化导入导出技术

前言

在企业应用中,经常需要处理包含图片的Excel文件。WPS和Microsoft Excel都支持在单元格中嵌入图片(通过DISPIMG函数),但这种图片的存储方式与传统的浮动图片不同,给程序化处理带来了一定挑战。

本文将深入分析WPS内嵌图片的存储原理,并介绍如何正确实现导入和导出功能。


一、XLSX文件结构解析

1.1 XLSX本质是ZIP压缩包

XLSX文件本质上是一个ZIP压缩包,解压后可以看到如下结构:

bash 复制代码
xlsx文件/
├── [Content_Types].xml
├── _rels/
├── docProps/
└── xl/
    ├── workbook.xml          # 工作簿定义
    ├── worksheets/           # 工作表数据
    │   └── sheet1.xml
    ├── cellimages.xml        # 【关键】单元格图片定义
    ├── _rels/
    │   └── cellimages.xml.rels  # 【关键】图片资源映射
    └── media/                # 实际图片文件
        ├── image1.png
        └── image2.png

1.2 DISPIMG函数

WPS/Excel使用DISPIMG函数在单元格中嵌入图片。当你在单元格中看到图片时,实际上单元格的公式是:

bash 复制代码
=_xlfn.DISPIMG("ID_XXXXX",1)

其中ID_XXXXX是图片的唯一标识符。


二、导入逻辑分析

2.1 传统方案的问题

最初的实现方案是:

  1. 扫描所有DISPIMG公式,按顺序收集不同的图片ID
  2. 获取工作簿中的所有图片
  3. 按顺序一一对应
java 复制代码
// 伪代码:按顺序映射
for (int i = 0; i < imageIds.size(); i++) {
    imageIdMap.put(imageIds.get(i), allPictures.get(i));
}

问题 :当两行使用相同的图片时(即使是复制粘贴同一张图片),WPS会进行去重优化 ------只在media/目录存储一份图片,但为每个单元格生成不同的DISPIMG ID。

例如:

  • 第1行公式:DISPIMG("ID_001", 1)
  • 第2行公式:DISPIMG("ID_002", 1) ← 不同的ID!

allPictures只有1张图片(因为内容相同被去重),导致ID_002无法找到对应图片。

2.2 正确的解决方案

正确的做法是解析Excel内部的元数据文件,获取精确的映射关系。

核心文件1:xl/cellimages.xml

定义了图片ID与资源引用ID(rId)的对应关系:

xml 复制代码
<etc:cellImages>
  <etc:cellImage>
    <xdr:pic>
      <xdr:nvPicPr>
        <xdr:cNvPr name="ID_001"/>  <!-- 图片ID -->
      </xdr:nvPicPr>
      <xdr:blipFill>
        <a:blip r:embed="rId1"/>    <!-- 资源引用ID -->
      </xdr:blipFill>
    </xdr:pic>
  </etc:cellImage>
  <etc:cellImage>
    <xdr:nvPicPr>
      <xdr:cNvPr name="ID_002"/>    <!-- 另一个图片ID -->
    </xdr:nvPicPr>
    <xdr:blipFill>
      <a:blip r:embed="rId1"/>      <!-- 指向同一个rId!-->
    </xdr:blipFill>
  </etc:cellImage>
</etc:cellImages>
核心文件2:xl/_rels/cellimages.xml.rels

定义了rId与实际图片文件的对应关系:

xml 复制代码
<Relationships>
  <Relationship Id="rId1" Target="../media/image1.png"/>
</Relationships>
映射链路
bash 复制代码
DISPIMG("ID_001") → rId1 → media/image1.png → 图片数据
DISPIMG("ID_002") → rId1 → media/image1.png → 图片数据(同一张)

2.3 实现流程

java 复制代码
public static Map<String, PictureData> extractImageMapping(InputStream xlsxStream, XSSFWorkbook workbook) {
    Map<String, PictureData> result = new HashMap<>();
    
    // 1. 构建 图片路径 → 图片数据 的映射
    Map<String, PictureData> pathToPictureMap = buildPathToPictureMap(workbook);
    
    // 2. 解析ZIP,读取元数据文件
    ZipInputStream zipStream = new ZipInputStream(xlsxStream);
    ZipEntry entry;
    
    Map<String, String> nameToRidMap = new HashMap<>();  // ID_xxx → rId
    Map<String, String> ridToPathMap = new HashMap<>();  // rId → media/image.png
    
    while ((entry = zipStream.getNextEntry()) != null) {
        if ("xl/cellimages.xml".equals(entry.getName())) {
            // 解析:图片名称 → rId
            nameToRidMap = parseCellImagesXml(zipStream);
        } else if ("xl/_rels/cellimages.xml.rels".equals(entry.getName())) {
            // 解析:rId → 图片路径
            ridToPathMap = parseCellImagesRels(zipStream);
        }
    }
    
    // 3. 组合映射链:图片ID → rId → 图片路径 → 图片数据
    for (Map.Entry<String, String> e : nameToRidMap.entrySet()) {
        String imageId = e.getKey();
        String rId = e.getValue();
        String path = ridToPathMap.get(rId);
        PictureData picture = pathToPictureMap.get(path);
        
        if (picture != null) {
            result.put(imageId, picture);
        }
    }
    
    return result;
}

三、导出逻辑分析

3.1 需求背景

数据库中存储的是图片的URL或本地路径,导出Excel时希望:

  • 图片直接嵌入单元格显示
  • 而不是显示为文本链接

3.2 实现思路

  1. 下载/读取图片:根据URL或本地路径获取图片字节数据
  2. 添加图片到工作簿 :使用POI的addPicture方法
  3. 创建锚点定位 :使用ClientAnchor将图片定位到指定单元格
  4. 插入图片 :使用Drawing.createPicture创建图片

3.3 关键代码

java 复制代码
private static void insertImage(XSSFWorkbook workbook, XSSFSheet sheet, 
        XSSFDrawing drawing, String imageUrl, int rowIndex, int colIndex) {
    
    // 1. 下载图片
    byte[] imageBytes = downloadImage(imageUrl);
    
    // 2. 判断图片类型
    int pictureType = getPictureType(imageUrl, imageBytes);
    
    // 3. 添加图片到工作簿,获取图片索引
    int pictureIndex = workbook.addPicture(imageBytes, pictureType);
    
    // 4. 创建锚点,定位图片位置
    XSSFClientAnchor anchor = new XSSFClientAnchor();
    anchor.setCol1(colIndex);      // 起始列
    anchor.setRow1(rowIndex);      // 起始行
    anchor.setCol2(colIndex + 1);  // 结束列
    anchor.setRow2(rowIndex + 1);  // 结束行
    
    // 设置边距(单位:EMU,1像素 ≈ 9525 EMU)
    int margin = 5 * 9525;
    anchor.setDx1(margin);
    anchor.setDy1(margin);
    
    // 5. 设置锚点类型:随单元格移动和调整大小
    anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
    
    // 6. 创建图片
    drawing.createPicture(anchor, pictureIndex);
}

3.4 图片类型判断

通过文件扩展名或文件头魔数判断:

java 复制代码
private static int getPictureType(String imageUrl, byte[] imageBytes) {
    // 优先根据扩展名判断
    if (imageUrl.toLowerCase().endsWith(".png")) {
        return Workbook.PICTURE_TYPE_PNG;
    } else if (imageUrl.toLowerCase().endsWith(".jpg")) {
        return Workbook.PICTURE_TYPE_JPEG;
    }
    
    // 根据文件头魔数判断
    if (imageBytes[0] == (byte)0x89 && imageBytes[1] == (byte)0x50) {
        return Workbook.PICTURE_TYPE_PNG;  // PNG魔数:89 50 4E 47
    }
    if (imageBytes[0] == (byte)0xFF && imageBytes[1] == (byte)0xD8) {
        return Workbook.PICTURE_TYPE_JPEG; // JPEG魔数:FF D8 FF
    }
    
    return Workbook.PICTURE_TYPE_PNG; // 默认
}

四、总结

导入关键点

问题 解决方案
WPS对相同图片去重存储 解析cellimages.xml元数据获取精确映射
图片ID与图片数据顺序不对应 通过ID → rId → Path → Data链路映射

导出关键点

问题 解决方案
图片定位到单元格 使用ClientAnchor设置行列范围
图片自适应单元格大小 使用MOVE_AND_RESIZE锚点类型
支持多种图片格式 根据扩展名或魔数判断类型

核心依赖

xml 复制代码
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.3</version>
</dependency>

参考资料

  • Apache POI官方文档
  • OOXML规范(ECMA-376)
  • WPS开发者文档
相关推荐
百事牛科技12 小时前
Excel打开密码怎么设置?一篇讲清楚
windows·excel
玩泥巴的12 小时前
基于.NET操作Excel COM组件生成数据透视报表
c#·.net·excel·二次开发·com互操作
2301_8002561112 小时前
【数据库】查找距离最近的电影院 pgSQL 存储过程片段
大数据·数据库·excel
城数派13 小时前
我国省市县三级逐日、逐月和逐年降水数据(Shp/Excel格式)1960-2024年
大数据·数据分析·excel
WKP941813 小时前
POI操作excel示例
java·开发语言·excel
用一个不重复的昵称14 小时前
excel 去除特殊字符,仅保留中英文字符和数字
excel
fensnote14 小时前
QT使用QtXlsxWriter读取excel文件
开发语言·qt·excel
高山莫衣14 小时前
读取手机通信录vCard文件(后缀vcf)文件并导出为excel表格
智能手机·excel
jogging14 小时前
mysql导出表结构信息到excel【DBeaver】
mysql·oracle·excel·导出·表结构
未来之窗软件服务14 小时前
万象EXCEL应用(二十二) Excel火锅店物资采购自动统计台账报表——东方仙盟炼气期
excel·仙盟创梦ide·东方仙盟·万象excel