当前位置: 首页 > news >正文

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

背景:

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

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

一:工具代码

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

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)*/@Overridepublic String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {try {// 检查单元格是否包含DISPIMG公式if (cellData.getStringValue() != null) {String stringValue = cellData.getStringValue();if (stringValue.contains("DISPIMG")) {// 提取图片IDString 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;}}
@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文件记录图片的位置和显示方式。图片并不直接嵌入单元格内,而是作为工作表对象的一部分,随单元格的变化调整显示。

http://www.dtcms.com/a/596318.html

相关文章:

  • 福清建设银行网站网红营销的作用
  • 34节点配电网牛顿-拉夫逊潮流计算 + 分布式电源(DG)多场景分析的 MATLAB
  • 分布式专题——53 ElasticSearch高可用集群架构实战
  • 电子商务网站建设与设计网站常州建设
  • 学习编程好么 | 编程的好处与学习路径分析
  • 从中间件的历史来看移动App开发的未来
  • Faster-Whisper:更快更好的开源Asr模型
  • ubuntu部署whisper+speaker_large+qwen【gradio界面版】
  • 阿里云通过中国信通院首批安全可信中间件评估
  • 正点原子【第四期】Linux之驱动开发学习笔记-12.1 Linux 阻塞和非阻塞 IO 实验
  • 做网站fjfzwl门户wordpress主题下载
  • Elasticsearch的用法
  • LLMChain for Chat Models in LangChain
  • 【JAVA进阶】SpringBoot启动流程深度解析:从main方法到应用就绪的完整旅程
  • 昆明建设厅培训网站创意设计文案
  • 如何为虚拟机配置多渠道、可聚合、更智能的告警通知机制?
  • 分布式容器镜像自动同步系统 设计方案
  • 智联无界,术教相融:分布式医疗示教系统重构医疗教学新生态
  • 【深度学习新浪潮】三维数字孪生核心算法深度解析
  • Clustering vs Classification|聚类vs分类
  • Java-小林coding八股文(1)
  • Selenium详细教程
  • 门户网站开发介绍上海华谊集团建设有限公司网站
  • 8. Linux-riscv内存管理35-40问
  • Vue3响应式系统中,对象新增属性、数组改索引、原始值代理的问题如何解决?
  • HTTP接口和Dubbo接口区别
  • K8S中ETCD高可用机制详解
  • jmeter发送数据到sasl加密的kafka
  • 【MATLAB代码】二维平面的TOA定位,GDOP(几何精度因子)和CRLB(克拉美罗下界)计算与输出
  • 【Hadoop】Hadoop核心基础——YARN 框架架构与运行机制(Hadoop 集群的 “资源管家”)