如何使用C#提取Excel单元格内的图片并获取其所属单元格/位置信息
如何使用C#提取Excel单元格内的图片并获取其所属单元格/位置信息
我完全理解你的困扰:用ClosedXML能轻松抓取浮在单元格上方的图片,但遇到嵌入到单元格内部的图片就失效了;而直接用OpenXml遍历包的方式虽然能拿到所有图片,却丢失了关键的位置信息。别担心,下面我给你一套基于OpenXml的完整解决方案,既能提取所有图片,又能精准获取每个图片所属的单元格位置。
核心原理说明
Excel里无论是嵌入单元格还是浮在单元格上的图片,都会存储在工作表的DrawingsPart部件中。每个图片通过OneCellAnchor(对应单个单元格嵌入的图片)或TwoCellAnchor(对应跨单元格/固定位置的图片)元素与单元格位置绑定。我们只需要把图片部件和这些锚点信息关联起来,就能同时拿到图片内容和所属位置。
完整可运行代码
下面是封装好的工具类,直接就能用:
using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; using DocumentFormat.OpenXml.Drawing.Spreadsheet; using System; using System.IO; using System.Linq; public class ExcelImageExtractor { public static void ExtractImagesWithPosition(string excelFilePath, string outputFolder) { // 确保输出目录存在 Directory.CreateDirectory(outputFolder); using (SpreadsheetDocument doc = SpreadsheetDocument.Open(excelFilePath, false)) { WorkbookPart workbookPart = doc.WorkbookPart; if (workbookPart == null) return; // 遍历所有工作表 foreach (WorksheetPart worksheetPart in workbookPart.WorksheetParts) { // 获取当前工作表的名称 string sheetName = workbookPart.Workbook.Descendants<Sheet>() .First(s => s.Id.Value == workbookPart.GetIdOfPart(worksheetPart)).Name; // 检查当前工作表是否有图片容器 DrawingsPart drawingsPart = worksheetPart.DrawingsPart; if (drawingsPart == null) continue; // 处理单个单元格嵌入的图片(OneCellAnchor) foreach (var anchor in drawingsPart.WorksheetDrawing.Descendants<OneCellAnchor>()) { ProcessAnchor(anchor, sheetName, drawingsPart, outputFolder); } // 处理跨单元格或固定位置的图片(TwoCellAnchor) foreach (var anchor in drawingsPart.WorksheetDrawing.Descendants<TwoCellAnchor>()) { ProcessAnchor(anchor, sheetName, drawingsPart, outputFolder); } } } } private static void ProcessAnchor(OpenXmlElement anchor, string sheetName, DrawingsPart drawingsPart, string outputFolder) { // 获取图片关联的起始单元格 Marker marker = anchor.Descendants<Marker>().FirstOrDefault(); if (marker == null) return; string cellReference = GetCellAddress(marker); // 获取图片对应的二进制部件 Blip blip = anchor.Descendants<Blip>().FirstOrDefault(); if (blip == null) return; ImagePart imagePart = (ImagePart)drawingsPart.GetPartById(blip.Embed.Value); // 生成带位置信息的文件名 string imageExtension = GetExtensionFromContentType(imagePart.ContentType); string fileName = $"{sheetName}_{cellReference}_{Guid.NewGuid()}{imageExtension}"; string outputPath = Path.Combine(outputFolder, fileName); // 保存图片 using (Stream imageStream = imagePart.GetStream()) using (FileStream outputStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write)) { imageStream.CopyTo(outputStream); } // 输出位置日志(也可以存入变量供后续使用) Console.WriteLine($"已提取图片:{fileName},所属位置:工作表【{sheetName}】的单元格【{cellReference}】"); } // 从锚点标记解析单元格地址(如A1、B5) private static string GetCellAddress(Marker marker) { uint rowNum = marker.RowId.Value; int columnIndex = (int)marker.ColumnId.Value; string columnName = ConvertColumnIndexToName(columnIndex); return $"{columnName}{rowNum}"; } // 将列索引转换为Excel列名(1→A,26→Z,27→AA) private static string ConvertColumnIndexToName(int index) { string columnName = string.Empty; while (index > 0) { int remainder = (index - 1) % 26; columnName = Convert.ToChar('A' + remainder) + columnName; index = (index - 1) / 26; } return columnName; } // 根据Content-Type获取图片扩展名 private static string GetExtensionFromContentType(string contentType) { return contentType switch { "image/png" => ".png", "image/jpeg" => ".jpg", "image/gif" => ".gif", "image/bmp" => ".bmp", _ => ".png" // 未知类型默认用PNG }; } } // 使用示例 // ExcelImageExtractor.ExtractImagesWithPosition(@"C:\test.xlsx", @"C:\ExtractedExcelImages");
代码关键点解析
锚点类型区分:
OneCellAnchor:对应完全嵌入到单个单元格的图片,是你当前问题的核心处理对象。TwoCellAnchor:对应跨多个单元格或浮在单元格上方的图片,这段代码也兼容这种场景。
位置解析:
从锚点的Marker元素中读取RowId和ColumnId,再将列索引转换为Excel的列名(如1→A),最终组合成标准的单元格地址。图片关联:
通过Blip元素的Embed属性,我们可以找到对应的ImagePart,从而获取图片的二进制流并保存到本地。
额外小贴士
- 代码会自动处理多工作表场景,保存的图片文件名包含工作表名和单元格位置,方便你快速对应。
- 如果需要对图片做进一步处理(如存入数据库),可以直接修改
ProcessAnchor方法中的保存逻辑,无需额外转换。
备注:内容来源于stack exchange,提问作者MilleB
相关产品推荐
相关产品推荐

