“ 本文将详细介绍如何在若依框架中实现Excel浮动图片和WPS内嵌式图片的批量导入功能。”
01 实现浮动图片导入
经由对实体层以及 ExcelUtil.java 的革新,我们能够达成标准 Excel 图片与 WPS 嵌入式图片的导入。
若依框架自身携有浮动图片导入的功能,仅需于项目代码的实体层添加如下代码便可:
cellType = Excel.ColumnType.IMAGE
1.1 在实体层添加图片字段
在项目的实体类(例如`
PerformanceCategoryRuleSystem`)中,为需要导入图片的字段添加注解。例如:
/** 图示1 */
@Excel(name = "图示1", cellType = Excel.ColumnType.IMAGE)
private String image1;
/** 图示2 */
@Excel(name = "图示2", cellType = Excel.ColumnType.IMAGE)
private String image2;
通过以上配置,框架会自动识别这些字段为图片类型,并在导入时处理。
02 实现支持WPS内嵌式图片
2.1.修改ExcelUtil.java工具类
对于WPS内嵌式图片需要对ExcelUtil.java进行改造,保证能够读取Excel文件中嵌入式图片函数“=DISPIMG("ID_C185A741AD754C2C883DA9D8B97156E7",1)”,增加对cellimages.xml和cellimages.xml.rels文件的解析功能。
2.2.导入依赖
在ExcelUtil.java顶部添加以下依赖:
import java.io.*;
import java.util.zip.*;
import org.dom4j.*;
import org.dom4j.io.SAXReader;
import com.ruoyi.common.utils.uuid.IdUtils;
2.3.修改`importExcel`方法
修改importExcel方法以支持Excel文件流处理和图片解析,代码如下:
2.3.1.在ExcelUtil.java顶部申明变量
/**
* 文件对象
*/
private File file;
/**
* key 函数ID value Rid
*/
private Map<String, String> imageIdMappingMap = new HashMap<>();
/**
* key :Rid value 图片信息
*/
private Map<String, String> imageMap = new HashMap<>();
2.3.2.修改importExcel方法
public List
importExcel(String sheetName, InputStream is, int titleNum) throws Exception {
this.type = Type.IMPORT;
// 将Excel流转换为文件
this.file = new File(VasConfig.getImportPath() + "/" + IdUtils.fastUUID() + ".xlsx");
FileUtils.inputStreamToFile(is, this.file);
// 解析cellimages.xml文件中单元格函数ID与RID的对应关系
ridWithIDRelationShip();
// 解析cellimages.xml.rels文件中RID与图片路径的对应关系
ridWithImagePathRelationShip();
this.wb = WorkbookFactory.create(new FileInputStream(this.file));
List
list = new ArrayList
();
// 如果指定sheet名,则取指定sheet中的内容,否则默认指向第1个sheet
Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
if (sheet == null) {
throw new IOException("文件sheet不存在");
}
// ... 后续逻辑
}
2.4.处理浮动图片和WPS嵌入式图片
在importExcel方法中,添加对浮动图片和WPS嵌入式图片的处理逻辑:
else if (ColumnType.IMAGE == attr.cellType()) {
// 先尝试处理标准Excel图片
boolean processedStandardImage = false;
if (StringUtils.isNotEmpty(pictures)) {
PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey());
if (image != null) {
byte[] data = image.getData();
val = FileUtils.splitImportBytes(data);
log.info("成功导入标准Excel图片: {} @ {},{}", field.getName(), row.getRowNum(), entry.getKey());
processedStandardImage = true;
}
}
// 如果没有处理标准图片,则检查并处理WPS嵌入图片
if (!processedStandardImage) {
// 获取单元格原始值作为字符串
Cell rawCell = row.getCell(entry.getKey());
String cellValue = "";
if (rawCell != null) {
if (rawCell.getCellType() == CellType.FORMULA) {
cellValue = rawCell.getCellFormula();
} else {
try {
cellValue = rawCell.getStringCellValue();
} catch (Exception e) {
cellValue = this.getCellValue(row, entry.getKey()).toString();
}
}
}
log.info("检查WPS嵌入图片,单元格值: {}", cellValue);
// 检查是否包含WPS嵌入图片的特征
if (isWPSEmbeddedImage(cellValue)) {
log.info("发现WPS嵌入图片函数: {}", cellValue);
// 提取图片ID
String imageId = null;
for (String keyId : imageIdMappingMap.keySet()) {
if (cellValue.contains(keyId)) {
imageId = keyId;
break;
}
}
// 如果没找到精确匹配,尝试根据字符串提取ID
if (imageId == null) {
imageId = subImageId(cellValue);
}
if (imageId != null) {
log.info("提取的图片ID: {}", imageId);
String picPath = getImplantPicById(imageId);
if (picPath != null) {
String fullPath = "xl/" + picPath;
log.info("尝试读取图片文件: {}", fullPath);
try {
InputStream picInputStream = openFile(fullPath);
if (picInputStream != null) {
byte[] imageData = IOUtils.toByteArray(picInputStream);
val = FileUtils.splitImportBytes(imageData);
log.info("成功导入WPS嵌入图片: {} -> {}", imageId, val);
picInputStream.close();
} else {
log.error("无法读取图片文件: {}", fullPath);
}
} catch (Exception e) {
log.error("处理WPS嵌入图片失败: {}", e.getMessage(), e);
}
} else {
log.error("未找到图片路径对应的图片: {}", imageId);
}
} else {
log.error("无法从单元格值中提取图片ID: {}", cellValue);
}
}
}
}
2.4. 新增解析`cellimages.xml`中单元格函数ID与RID的对应关系
在ExcelUtil.java中新增以下方法:
/**
* 解析cellimages.xml文件中excel中单元格函数ID对应的RID对应关系
* @author:
* @date: 2025/5/14
* @param: []
* @return: void
*/
private void ridWithIDRelationShip() throws IOException {
InputStream inputStream = null;
try {
inputStream = openFile("xl/cellimages.xml");
if (inputStream == null) {
log.error("无法打开cellimages.xml文件");
return;
}
SAXReader reader = new SAXReader();
Map
nsMap = new HashMap<>();
nsMap.put("etc", "http://www.wps.cn/officeDocument/2017/etCustomData");
nsMap.put("xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing");
nsMap.put("a", "http://schemas.openxmlformats.org/drawingml/2006/main");
nsMap.put("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
reader.getDocumentFactory().setXPathNamespaceURIs(nsMap);
Document dc = reader.read(inputStream);
Element rootElement = dc.getRootElement();
List
cellImageList = rootElement.elements(QName.get("cellImage", Namespace.get("etc", "http://www.wps.cn/officeDocument/2017/etCustomData")));
log.info("找到 {} 个嵌入图片", cellImageList.size());
for (Element cellImage : cellImageList) {
try {
Element pic = cellImage.element(QName.get("pic", Namespace.get("xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing")));
Element nvPicPr = pic.element(QName.get("nvPicPr", Namespace.get("xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing")));
Element cNvPr = nvPicPr.element(QName.get("cNvPr", Namespace.get("xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing")));
String imageId = cNvPr.attributeValue("name");
Element blipFill = pic.element(QName.get("blipFill", Namespace.get("xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing")));
Element blip = blipFill.element(QName.get("blip", Namespace.get("a", "http://schemas.openxmlformats.org/drawingml/2006/main")));
String imageRid = blip.attributeValue(QName.get("embed", Namespace.get("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships")));
imageIdMappingMap.put(imageId, imageRid);
log.info("映射图片ID: {} -> RID: {}", imageId, imageRid);
} catch (Exception e) {
log.error("处理图片节点时出错: {}", e.getMessage());
}
}
} catch (Exception e) {
log.error("解析cellimages.xml文件中excel中单元格函数ID对应的RID对应关系失败", e);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
2.5.新增解析`cellimages.xml.rels`中RID与图片路径的对应关系
/**
* 解析cellimages.xml.rels文件中cellimages.xml文件中的RID对应图片信息对应关系
* @author:
* @date: 2025/5/14
* @param: []
* @return: void
*/
private void ridWithImagePathRelationShip() throws IOException {
InputStream inputStream = null;
try {
inputStream = openFile("xl/_rels/cellimages.xml.rels");
if (inputStream == null) {
log.error("无法打开cellimages.xml.rels文件");
return;
}
SAXReader reader = new SAXReader();
Map
nsMap = new HashMap<>();
nsMap.put("", "http://schemas.openxmlformats.org/package/2006/relationships");
reader.getDocumentFactory().setXPathNamespaceURIs(nsMap);
Document dc = reader.read(inputStream);
Element rootElement = dc.getRootElement();
List
imageRelationshipList = rootElement.elements("Relationship");
log.info("找到 {} 个图片关系定义", imageRelationshipList.size());
for (Element imageRelationship : imageRelationshipList) {
try {
String imageRid = imageRelationship.attributeValue("Id");
String imagePath = imageRelationship.attributeValue("Target");
this.imageMap.put(imageRid, imagePath);
log.info("图片路径映射: {} -> {}", imageRid, imagePath);
} catch (Exception e) {
log.error("处理图片关系时出错: {}", e.getMessage());
}
}
} catch (Exception e) {
log.error("处理xml中rid和图片路径映射关系失败: {}", e.getMessage(), e);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
2.6.新增打开ZIP文件并获取指定文件流
/**
* @author:
* @date: 2025/5/14
* @param: [filePath]
* @return: java.io.InputStream
*/
private InputStream openFile(String filePath) {
try {
ZipFile zipFile = new ZipFile(this.file);
ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(this.file));
ZipEntry nextEntry = null;
while ((nextEntry = zipInputStream.getNextEntry()) != null) {
String name = nextEntry.getName();
if (name.equalsIgnoreCase(filePath)) {
log.info("在ZIP中找到文件: {}", filePath);
InputStream stream = zipFile.getInputStream(nextEntry);
zipInputStream.close();
return stream;
}
}
zipInputStream.close();
log.error("在ZIP中未找到文件: {}", filePath);
} catch (Exception e) {
log.error("解析ZIP文件失败: {}", e.getMessage(), e);
}
return null;
}
2.7.新增提取图片ID
/**
* 提取图片ID
* @author:
* @date: 2025/5/14
* @param: [imageId]
* @return: String
*/
private String subImageId(String imageId) {
try {
int startIndex = imageId.indexOf("ID_");
if (startIndex == -1) {
log.error("单元格值中未找到ID_前缀: {}", imageId);
return null;
}
int endIndex = -1;
for (int i = startIndex; i < imageId.length(); i++) {
char c = imageId.charAt(i);
if (c == '"' || c == ',' || c == ')' || c == ' ') {
endIndex = i;
break;
}
}
if (endIndex == -1) {
endIndex = imageId.length();
}
String extractedId = imageId.substring(startIndex, endIndex);
log.info("成功提取图片ID: {}", extractedId);
return extractedId;
} catch (Exception e) {
log.error("提取图片ID时出错: {}", e.getMessage(), e);
return null;
}
}
2.8.新增获取嵌入图片路径
/**
* 根据图片ID获取嵌入图片路径
* @author:
* @date: 2025/5/14
* @param: [imageId]
* @return: String
*/
private String getImplantPicById(String imageId) throws IOException {
try {
String imageRid = imageIdMappingMap.get(imageId);
if (imageRid == null) {
log.error("未找到图片ID对应的RID: {}", imageId);
return null;
}
String imagePath = imageMap.get(imageRid);
if (imagePath == null) {
log.error("未找到RID对应的图片路径: {}", imageRid);
return null;
}
log.info("成功获取图片路径: {} -> {} -> {}", imageId, imageRid, imagePath);
return imagePath;
} catch (Exception e) {
log.error("获取图片路径时出错: {}", e.getMessage());
return null;
}
}
2.9.新增检测WPS嵌入图片
/**
* 检测单元格是否包含WPS嵌入图片
* @author:
* @date: 2025/5/14
* @param: [cellValue]
* @return: boolean
*/
private boolean isWPSEmbeddedImage(String cellValue) {
return cellValue != null &&
(cellValue.contains("DISPIMG") ||
cellValue.contains("ID_") &&
(cellValue.contains("image") || cellValue.contains("media")));
}
03 修改FileUtils.java方法
3.1.在FileUtils.java中新增将输入流写入文件的方法
/**
* 将输入流写入到文件
* @param in 输入流
* @param file 目标文件
*/
public static void inputStreamToFile(InputStream in, File file) throws IOException {
try {
Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
throw new IOException(e.getMessage(), e);
} finally {
IOUtils.closeQuietly(in);
}
}
总结
通过以上步骤,我们在若依框架中实现了Excel浮动图片和WPS内嵌式图片的批量导入功能。核心在于:
- 利用若依框架自带的图片导入功能,配置实体层字段。
- 改造ExcelUtil.java,增加对WPS嵌入式图片的解析逻辑。
- 新增解析cellimages.xml和cellimages.xml.rels的方法,处理图片ID与路径的映射关系。
- 增加文件流处理工具方法,确保Excel文件的正确解析。
这些改造能够有效处理标准Excel图片和WPS特有的内嵌图片,满足复杂的Excel导入需求。
注意:确保在开发环境中测试以上代码,并根据实际需求调整日志输出和异常处理逻辑。