【Excel导入】读取WPS格式嵌入单元格内的图片

背景:

读取WPS的自定义格式DISPIMG的图片格式。

由于POI本身不支持图片单元格内嵌,所以需要根据Excel的底层实现解析。(如果是POI格式悬浮的图片可以直接通过锚点的方式获取)

一:工具代码

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Excel导入处理器
 * @author c
 * date: 2025-11-05 09:19:31
 * description: 用于处理Excel文件中单元格的嵌入图片数据。
 */
@Slf4j
public class ExcelDispImgExtractorUtil {
     /**
     * 解析单元格图片映射
     * key-图片所在单元格显示的ID value-图片转数据(直接使用中可将它通过Base64编码转换成字符串使用更节省内存)
     * key在实际使用中可以通过EasyExcel的Convert进行转换直接得到(这个具体可根据个人情况使用)
     */
    public static Map<String, byte[]> importLocalExcel(MultipartFile file) {
        Map<String, byte[]> cellImageMap = new LinkedHashMap<>();

        try (InputStream inputStream = file.getInputStream();
             ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(inputStream.readAllBytes());
             ZipInputStream zis = new ZipInputStream(byteArrayInputStream)) {

            // 解析ZIP文件内容
            ZipParseResult zipResult = parseZipEntries(zis);

            // 解析关系映射
            Map<String, String> relsMap = parseRelsMapping(zipResult.getCellImagesRelsXml());

            // 解析单元格图片映射
            parseCellImages(zipResult.getCellImagesXml(), relsMap, zipResult.getImagesData(), cellImageMap);

            return cellImageMap;

        } catch (Exception e) {
            log.error("导入Excel文件异常: {}", e.getMessage(), e);
            return Collections.emptyMap();
        }
    }

    /**
     * 解析ZIP条目
     */
    private static ZipParseResult parseZipEntries(ZipInputStream zis) throws IOException {
        ZipParseResult result = new ZipParseResult();
        ZipEntry zipEntry;

        while ((zipEntry = zis.getNextEntry()) != null) {
            String entryName = zipEntry.getName();
            byte[] entryData = IOUtils.toByteArray(zis);

            switch (entryName) {
                case "xl/cellimages.xml" -> result.setCellImagesXml(new String(entryData, StandardCharsets.UTF_8));
                case "xl/_rels/cellimages.xml.rels" ->
                        result.setCellImagesRelsXml(new String(entryData, StandardCharsets.UTF_8));
                default -> {
                    if (entryName.startsWith("xl/media/")) {
                        String imageName = entryName.substring("xl/media/".length());
                        result.getImagesData().put(imageName, entryData);
                    }
                }
            }
            zis.closeEntry();
        }

        return result;
    }

    /**
     * 解析关系映射
     */
    private static Map<String, String> parseRelsMapping(String cellImagesRelsXml)
            throws ParserConfigurationException, SAXException, IOException {

        Map<String, String> relsMap = new HashMap<>();

        if (StringUtils.isEmpty(cellImagesRelsXml)) {
            return relsMap;
        }

        DocumentBuilderFactory factory = createSecureDocumentBuilderFactory();
        DocumentBuilder builder = factory.newDocumentBuilder();

        try (ByteArrayInputStream bais = new ByteArrayInputStream(
                cellImagesRelsXml.getBytes(StandardCharsets.UTF_8))) {

            Document relsDoc = builder.parse(bais);
            NodeList relNodes = relsDoc.getElementsByTagName("Relationship");

            for (int i = 0; i < relNodes.getLength(); i++) {
                Element relElement = (Element) relNodes.item(i);
                String rId = relElement.getAttribute("Id");
                String target = relElement.getAttribute("Target");

                if (target.startsWith("media/")) {
                    relsMap.put(rId, target.substring("media/".length()));
                } else {
                    log.warn("发现不符合预期的target格式: {}", target);
                }
            }
        }

        return relsMap;
    }

    /**
     * 解析单元格图片
     */
    private static void parseCellImages(String cellImagesXml, Map<String, String> relsMap,
                                        Map<String, byte[]> imagesData, Map<String, byte[]> cellImageMap)
            throws ParserConfigurationException, SAXException, IOException {

        if (StringUtils.isEmpty(cellImagesXml)) {
            return;
        }

        DocumentBuilderFactory factory = createSecureDocumentBuilderFactory();
        DocumentBuilder builder = factory.newDocumentBuilder();

        try (ByteArrayInputStream bais = new ByteArrayInputStream(
                cellImagesXml.getBytes(StandardCharsets.UTF_8))) {

            Document cellImagesDoc = builder.parse(bais);
            NodeList cellImageNodes = cellImagesDoc.getElementsByTagName("etc:cellImage");

            for (int i = 0; i < cellImageNodes.getLength(); i++) {
                Element cellImageElement = (Element) cellImageNodes.item(i);
                processSingleCellImage(cellImageElement, relsMap, imagesData, cellImageMap);
            }
        }
    }

    /**
     * 处理单个单元格图片
     */
    private static void processSingleCellImage(Element cellImageElement, Map<String, String> relsMap,
                                               Map<String, byte[]> imagesData, Map<String, byte[]> cellImageMap) {
        try {
            Element picElement = (Element) cellImageElement.getElementsByTagName("xdr:pic").item(0);
            if (picElement == null) return;

            Element cNvPr = (Element) picElement.getElementsByTagName("xdr:cNvPr").item(0);
            if (cNvPr == null) return;

            String imageName = cNvPr.getAttribute("name");
            if (StringUtils.isEmpty(imageName)) return;

            Element blipFill = (Element) picElement.getElementsByTagName("xdr:blipFill").item(0);
            if (blipFill == null) return;

            Element blip = (Element) blipFill.getElementsByTagName("a:blip").item(0);
            if (blip == null) return;

            String rId = blip.getAttribute("r:embed");
            if (StringUtils.isEmpty(rId)) return;

            String imageFileName = relsMap.get(rId);
            if (StringUtils.isEmpty(imageFileName)) {
                cellImageMap.put(imageName, null);
                return;
            }

            byte[] imageBytes = imagesData.get(imageFileName);
            cellImageMap.put(imageName, imageBytes);

        } catch (Exception e) {
            log.warn("处理单元格图片时发生异常: {}", e.getMessage(), e);
        }
    }

    /**
     * 创建安全的DocumentBuilderFactory
     */
    private static DocumentBuilderFactory createSecureDocumentBuilderFactory() throws ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        // 防止XXE攻击
        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        factory.setXIncludeAware(false);
        factory.setExpandEntityReferences(false);
        return factory;
    }

    /**
     * ZIP文件解析结果容器
     */
    private static class ZipParseResult {
        private String cellImagesXml;
        private String cellImagesRelsXml;
        private final Map<String, byte[]> imagesData = new HashMap<>();

        public String getCellImagesXml() {
            return cellImagesXml;
        }

        public void setCellImagesXml(String cellImagesXml) {
            this.cellImagesXml = cellImagesXml;
        }

        public String getCellImagesRelsXml() {
            return cellImagesRelsXml;
        }

        public void setCellImagesRelsXml(String cellImagesRelsXml) {
            this.cellImagesRelsXml = cellImagesRelsXml;
        }

        public Map<String, byte[]> getImagesData() {
            return imagesData;
        }
    }

}

扩展:提供基于EasyExcel的Convert

java 复制代码
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;

/**
 * @author c
 * date: 2025-11-11 15:16:46
 * description: 将Excel单元格数据转换为字符串(图片ID)
 */
@Slf4j
public class ByteGetIdConverter implements Converter<String> {

    /**
     * 将Excel单元格数据转换为字符串(图片ID)
     */
    @Override
    public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
        try {
            // 检查单元格是否包含DISPIMG公式
            if (cellData.getStringValue() != null) {
                String stringValue = cellData.getStringValue();
                if (stringValue.contains("DISPIMG")) {
                    // 提取图片ID
                    String imageId = extractImageIdFromFormula(stringValue);
                    if (imageId != null) {
                        return imageId;
                    }
                }
            }
            // 如果没有公式,返回单元格的字符串值
            return cellData.getStringValue();
        } catch (Exception e) {
            log.warn("转换图片ID时发生异常: {}", e.getMessage());
            return cellData.getStringValue();
        }
    }

    /**
     * 从DISPIMG公式中提取图片ID
     */
    private String extractImageIdFromFormula(String formula) {
        try {
            // 公式格式: _xlfn.DISPIMG("ID_XXXX",1)
            int startIndex = formula.indexOf("\"");
            int endIndex = formula.lastIndexOf("\"");
            if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
                return formula.substring(startIndex + 1, endIndex);
            }
        } catch (Exception e) {
            log.warn("从公式中提取图片ID失败: {}", formula);
        }
        return null;
    }

}
java 复制代码
@Data
public class ImportData {

    @ExcelProperty(value="测试")
    private String test;

    @ExcelProperty(value="详情")
    private String checkRole;

    @ExcelProperty(value="图片", converter = ByteGetIdConverter.class)
    private String image;

}

二:读取原理

Excel文档(例如 .xlsx 文件)是由多个组成部分构成的。它实际上是一个压缩文件,内部包含了许多不同类型的数据、样式和资源,而这些资源和数据通过不同的文件格式组合在一起。我们可以通过详细了解它的组成部分,来解释为什么Excel文档能够存储图片。

2.1. Excel文档的组成结构

.xlsx 文件本质上是一个 压缩文件包 ,使用了 ZIP 格式进行压缩。这意味着,它不是一个单一的文件,而是包含多个文件和文件夹的集合,使用ZIP压缩格式存储。你可以通过更改文件扩展名将 .xlsx 改为 .zip,然后解压缩,看到以下几个主要的组成部分:

  • [xl/contents.xml]:包含工作簿的主要内容,如工作表数据、单元格内容等。
  • [xl/worksheets/]:保存每个工作表的数据(如单元格的值、公式、格式等)。
  • [xl/drawings/]:保存与图形、形状和图片等对象相关的资源。
  • [xl/theme/]:包含文档的主题和样式信息。
  • [xl/styles.xml]:包含工作簿的样式设置,如字体、颜色、单元格边框等。
  • [xl/media/]:保存文档中嵌入的媒体文件(例如图片、视频等)。
  • [docProps/]:保存文件的元数据,如作者、创建日期、最后修改日期等。

2.2. Excel中存储图片的机制

在Excel文件中,图片通常存储在xl/media/文件夹中。以下是图片如何被存储在Excel中的细节:

  • 图片作为独立的文件 : Excel文档中的图片会以嵌入的文件 的形式存储。它们通常是图像文件(如 .jpg.png.bmp 等格式)的一部分,而这些图片被存储在xl/media/文件夹内。

  • 图片与工作表内容关联 : 图片并不直接存储在单元格中,而是作为一个独立对象 存储在工作簿中。每张图片都会有一个唯一的ID,这个ID会在Excel的XML内容中引用,指向xl/media/中的实际图片文件。Excel中的工作表(通常是xl/worksheets/sheet1.xml)会记录图片的位置信息(例如,图片在工作表中的锚点、大小、位置等)。

  • 锚点和定位: 图片在Excel工作表中通常被称为"对象",这些图片通过**锚点(Anchor)**来定位。锚点可以指示图片应该与哪个单元格关联。Excel使用锚点机制来控制图片在工作表中的位置,确保图片能够随单元格大小的调整而调整(例如,通过"随单元格调整大小"设置)。

  • 图片的压缩存储: 图片通常会被压缩存储,以减少文件的大小,尤其是在使用现代Excel格式(.xlsx)时。这是因为ZIP压缩格式可以有效地减少存储空间。

2.3. Excel文档存储图片的具体过程

  1. 图片插入

    • 当你插入一张图片时,Excel将该图片保存为一个图像文件,并将其放入xl/media/目录中。
  2. 图片引用

    • Excel会生成一个XML文件(通常在xl/worksheets/sheetX.xml中),并在其中记录图片的位置信息(锚点、大小、层次等),以便在打开工作表时,图片能够被正确显示在预定的位置。
  3. 图片的显示

    • 当你打开Excel文件时,Excel会通过这些XML文件读取位置信息,并根据存储的图片文件将图片显示在工作表中。图片显示时会遵循锚点设置(比如:图片是否随单元格调整大小)。

2.4. Excel如何将图片与单元格关联

Excel通过**锚点(anchor)**机制来将图片与工作表中的特定单元格关联。锚点定义了图片的位置和大小。图片可以设置为:

  • 固定位置:图片位置不随单元格的调整而变化。
  • 随单元格调整大小:图片大小会根据单元格的大小自动调整。

这种机制让Excel可以灵活地将图片与单元格关联,确保图片能够正确显示和跟随单元格大小变化。

总结:

  • Excel文档是一个ZIP格式的压缩文件,包含多个子文件和文件夹。
  • 图片 被存储在xl/media/文件夹中,以图像文件的形式存储。
  • Excel通过XML文件记录图片的锚点 和位置,并引用存储在xl/media/文件夹中的实际图片文件。
  • 图片在Excel工作表中作为对象 存在,通过锚点机制与单元格相关联。

因此,Excel能够存储图片,主要是因为它允许将图片文件嵌入到文件结构中,并通过XML文件记录图片的位置和显示方式。图片并不直接嵌入单元格内,而是作为工作表对象的一部分,随单元格的变化调整显示。

相关推荐
IoT智慧学堂2 小时前
C语言流程控制:if判断语句全解析
c语言·开发语言
楼田莉子2 小时前
C++/Linux小项目:自主shell命令解释器
linux·服务器·开发语言·c++·后端·学习
用户298698530142 小时前
Java: 为PDF批量添加图片水印实用指南
java·后端·api
EXtreme352 小时前
C语言指针深度剖析(2):从“数组名陷阱”到“二级指针操控”的进阶指南
c语言·开发语言·算法
q***31832 小时前
微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
java·spring cloud·微服务
大头an2 小时前
Spring事务在微服务架构中的实践与挑战
java
BugShare2 小时前
嘿嘿,一个简单ElasticSearch小实现
java·大数据·spring boot·elasticsearch
程序员大雄学编程2 小时前
定积分的几何应用(一):平面图形面积计算详解
开发语言·python·数学·平面·微积分
Evand J2 小时前
【MATLAB例程】二维平面的TOA定位,几何精度因子GDOP和克拉美罗下界CRLB计算与输出
开发语言·matlab·平面·crlb·gdop