diff --git a/samples/xlsx/TestImageType.xlsx b/samples/xlsx/TestImageType.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..4a86ee3ec14d01cb85f6611839695d15bc37bf1b
Binary files /dev/null and b/samples/xlsx/TestImageType.xlsx differ
diff --git a/src/MiniExcel/Enums/XlsxImgType.cs b/src/MiniExcel/Enums/XlsxImgType.cs
new file mode 100644
index 0000000000000000000000000000000000000000..9e08ef6cda33c65ebf69a1bcc36519184ba2a5f3
--- /dev/null
+++ b/src/MiniExcel/Enums/XlsxImgType.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace MiniExcelLibs.Enums;
+
+///
+/// Excel 图片展示方式(是否随单元格对齐/缩放)。
+///
+public enum XlsxImgType
+{
+ ///
+ /// 图片随单元格移动但不缩放(OneCellAnchor)。
+ /// 通常用于图片只绑定一个起点单元格。
+ ///
+ OneCellAnchor,
+ ///
+ /// 图片浮动在表格上,固定位置不随单元格变化(AbsoluteAnchor)。
+ ///
+ AbsoluteAnchor,
+ ///
+ /// 图片嵌入单元格中,随单元格移动和缩放(TwoCellAnchor)。
+ ///
+ TwoCellAnchor,
+
+}
diff --git a/src/MiniExcel/Picture/MiniExcelPicture.cs b/src/MiniExcel/Picture/MiniExcelPicture.cs
index 1d6b67c688550ff576e258675345aef812934dcf..aca5aae0f876481b0985f9ac466ebaf6a485913b 100644
--- a/src/MiniExcel/Picture/MiniExcelPicture.cs
+++ b/src/MiniExcel/Picture/MiniExcelPicture.cs
@@ -1,4 +1,6 @@
-using MiniExcelLibs.Utils;
+using MiniExcelLibs.Enums;
+using MiniExcelLibs.Utils;
+using System.Drawing;
namespace MiniExcelLibs.Picture;
@@ -8,7 +10,11 @@ public class MiniExcelPicture
public string? SheetName { get; set; }
public string? PictureType { get; set; }
public string? CellAddress { get; set; }
-
+ ///
+ /// 只有当图片处于AbsoluteAnchor浮动才会生效
+ ///
+ public Point Location { get; set; }
+ public XlsxImgType ImgType { get; set; }
internal int ColumnNumber => ReferenceHelper.ConvertCellToXY(CellAddress).Item1 -1;
internal int RowNumber => ReferenceHelper.ConvertCellToXY(CellAddress).Item2 - 1;
diff --git a/src/MiniExcel/Picture/MiniExcelPictureImplement.cs b/src/MiniExcel/Picture/MiniExcelPictureImplement.cs
index f503169276bab434b170c50b417393db619efc11..0cf8df643caf7a315ead9af8d61400949a43481c 100644
--- a/src/MiniExcel/Picture/MiniExcelPictureImplement.cs
+++ b/src/MiniExcel/Picture/MiniExcelPictureImplement.cs
@@ -1,12 +1,14 @@
-using System;
+using MiniExcelLibs.Enums;
+using MiniExcelLibs.OpenXml;
+using MiniExcelLibs.Zip;
+using System;
+using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
-using MiniExcelLibs.OpenXml;
-using MiniExcelLibs.Zip;
using Zomp.SyncMethodGenerator;
namespace MiniExcelLibs.Picture;
@@ -140,9 +142,10 @@ public static async Task AddPictureAsync(Stream excelStream, CancellationToken c
var row = image.RowNumber;
var widthPx = image.WidthPx;
var heightPx = image.HeightPx;
-
- // Step 1: Add image to /xl/media/
- var imageName = $"image{Guid.NewGuid():N}.png";
+ var imgtype = image.ImgType;
+ var location = image.Location;
+ // Step 1: Add image to /xl/media/
+ var imageName = $"image{Guid.NewGuid():N}.png";
var imagePath = $"xl/media/{imageName}";
var imageEntry = archive.CreateEntry(imagePath);
@@ -167,7 +170,7 @@ public static async Task AddPictureAsync(Stream excelStream, CancellationToken c
// Step 3: Add anchor to drawing XML
var relId = $"rId{Guid.NewGuid():N}";
- drawingDoc = CreateDrawingXml(drawingDoc, col, row, widthPx, heightPx, relId);
+ drawingDoc = CreateDrawingXml(drawingDoc, col, row, widthPx, heightPx, relId, imgtype,location);
// Step 4: Add image relationship to drawing rels
var relNode = drawingRelsDoc.CreateElement("Relationship", drawingRelsDoc.DocumentElement.NamespaceURI);
@@ -288,6 +291,8 @@ public static void AddPicture(Stream excelStream, params MiniExcelPicture[] imag
var row = image.RowNumber;
var widthPx = image.WidthPx;
var heightPx = image.HeightPx;
+ var imgtype = image.ImgType;
+ var location = image.Location;
// Step 1: Add image to /xl/media/
var imageName = $"image{Guid.NewGuid():N}.png";
var imagePath = $"xl/media/{imageName}";
@@ -312,7 +317,7 @@ public static void AddPicture(Stream excelStream, params MiniExcelPicture[] imag
// Step 3: Add anchor to drawing XML
var relId = $"rId{Guid.NewGuid():N}";
- drawingDoc = CreateDrawingXml(drawingDoc, col, row, widthPx, heightPx, relId);
+ drawingDoc = CreateDrawingXml(drawingDoc, col, row, widthPx, heightPx, relId, imgtype,location);
// Step 4: Add image relationship to drawing rels
var relNode = drawingRelsDoc.CreateElement("Relationship", drawingRelsDoc.DocumentElement.NamespaceURI);
@@ -364,12 +369,16 @@ private static XmlNamespaceManager GetNamespaceManager(XmlDocument doc)
return nsmgr;
}
- private static XmlDocument CreateDrawingXml(XmlDocument existingDoc, int col, int row, int widthPx, int heightPx, string relId)
- {
- return DrawingXmlHelper.CreateOrUpdateDrawingXml(existingDoc, col, row, widthPx, heightPx, relId);
- }
+ private static XmlDocument CreateDrawingXml(XmlDocument existingDoc, int col, int row, int widthPx, int heightPx, string relId)
+ {
+ return DrawingXmlHelper.CreateOrUpdateDrawingXml(existingDoc, col, row, widthPx, heightPx, relId);
+ }
+ private static XmlDocument CreateDrawingXml(XmlDocument existingDoc, int col, int row, int widthPx, int heightPx, string relId,XlsxImgType imgtype,Point location)
+ {
+ return DrawingXmlHelper.CreateOrUpdateDrawingXml(existingDoc, col, row, widthPx, heightPx, relId,imgtype, location);
+ }
- public class DrawingXmlHelper
+ public class DrawingXmlHelper
{
private const string XdrNamespace = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing";
private const string ANamespace = "http://schemas.openxmlformats.org/drawingml/2006/main";
@@ -389,8 +398,198 @@ private static string GetColumnName(int colIndex)
return columnName;
}
+ public static XmlDocument CreateOrUpdateDrawingXml(
+ XmlDocument? existingDoc,
+ int col, int row,
+ int widthPx, int heightPx,
+ string relId,
+ XlsxImgType imgType,
+ Point Location
+)
+ {
+ var doc = existingDoc ?? new XmlDocument();
+ var ns = new XmlNamespaceManager(doc.NameTable);
+ ns.AddNamespace("xdr", XdrNamespace);
+ ns.AddNamespace("a", ANamespace);
+ ns.AddNamespace("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
+
+ // Root
+ XmlElement wsDr;
+ if (existingDoc is null)
+ {
+ wsDr = doc.CreateElement("xdr", "wsDr", XdrNamespace);
+ wsDr.SetAttribute("xmlns:xdr", XdrNamespace);
+ wsDr.SetAttribute("xmlns:a", ANamespace);
+ doc.AppendChild(wsDr);
+ }
+ else
+ {
+ wsDr = doc.DocumentElement!;
+ }
+
+ XmlNodeList anchors = wsDr.SelectNodes("//xdr:oneCellAnchor | //xdr:twoCellAnchor | //xdr:absoluteAnchor", ns);
+ int imageCount = anchors?.Count ?? 0;
+ int nextId = imageCount + 2;
+
+ string anchorType = imgType switch
+ {
+ XlsxImgType.AbsoluteAnchor => "absoluteAnchor",
+ XlsxImgType.TwoCellAnchor => "twoCellAnchor",
+ XlsxImgType.OneCellAnchor => "oneCellAnchor",
+ _ => "oneCellAnchor"
+ };
+
+ var anchor = doc.CreateElement("xdr", anchorType, XdrNamespace);
+ if (imgType == XlsxImgType.TwoCellAnchor)
+ anchor.SetAttribute("editAs", "twoCell");
+
+ if (imgType == XlsxImgType.AbsoluteAnchor)
+ {
+
+
+ var pos = doc.CreateElement("xdr", "pos", XdrNamespace);
+ pos.SetAttribute("x", PixelsToEmu(Location.X).ToString()); // 使用实际列宽
+ pos.SetAttribute("y", PixelsToEmu(Location.Y).ToString()); // 使用实际行高
+
+ var ext = doc.CreateElement("xdr", "ext", XdrNamespace);
+ ext.SetAttribute("cx", PixelsToEmu(widthPx).ToString());
+ ext.SetAttribute("cy", PixelsToEmu(heightPx).ToString());
+
+ anchor.AppendChild(pos);
+ anchor.AppendChild(ext);
+
+ }
+ else if (imgType == XlsxImgType.TwoCellAnchor)
+ {
+ var from = doc.CreateElement("xdr", "from", XdrNamespace);
+ AppendXmlElement(doc, from, "xdr", "col", col.ToString());
+ AppendXmlElement(doc, from, "xdr", "colOff", "0");
+ AppendXmlElement(doc, from, "xdr", "row", row.ToString());
+ AppendXmlElement(doc, from, "xdr", "rowOff", "0");
+ var to = doc.CreateElement("xdr", "to", XdrNamespace);
+ AppendXmlElement(doc, to, "xdr", "col", (col + 1).ToString());
+ AppendXmlElement(doc, to, "xdr", "colOff", "0");
+ AppendXmlElement(doc, to, "xdr", "row", (row + 1).ToString());
+ AppendXmlElement(doc, to, "xdr", "rowOff", "0");
+
+ anchor.AppendChild(from);
+ anchor.AppendChild(to);
+ }
+ else // OneCellAnchor
+ {
+ var from = doc.CreateElement("xdr", "from", XdrNamespace);
+ AppendXmlElement(doc, from, "xdr", "col", col.ToString());
+ AppendXmlElement(doc, from, "xdr", "colOff", "0");
+ AppendXmlElement(doc, from, "xdr", "row", row.ToString());
+ AppendXmlElement(doc, from, "xdr", "rowOff", "0");
+ var to = doc.CreateElement("xdr", "to", XdrNamespace);
+ AppendXmlElement(doc, to, "xdr", "col", (col ).ToString()); // Adjust the column and row for size
+ AppendXmlElement(doc, to, "xdr", "colOff", "0");
+ AppendXmlElement(doc, to, "xdr", "row", (row ).ToString());
+ AppendXmlElement(doc, to, "xdr", "rowOff", "0");
+
+ var ext = doc.CreateElement("xdr", "ext", XdrNamespace);
+ ext.SetAttribute("cx", PixelsToEmu(widthPx).ToString());
+ ext.SetAttribute("cy", PixelsToEmu(heightPx).ToString());
+
+ anchor.AppendChild(from);
+ anchor.AppendChild(ext);
+ }
+
+ // -------- Image Content --------
+ //
+ var pic = doc.CreateElement("xdr", "pic", XdrNamespace);
+
+ //
+ var nvPicPr = doc.CreateElement("xdr", "nvPicPr", XdrNamespace);
+ var cNvPr = doc.CreateElement("xdr", "cNvPr", XdrNamespace);
+ cNvPr.SetAttribute("id", nextId.ToString());
+ cNvPr.SetAttribute("name", $"ImageAt{GetColumnName(col)}{row + 1}");
+
+ // ...
+ var extLst = doc.CreateElement("a", "extLst", ANamespace);
+ var extNode = doc.CreateElement("a", "ext", ANamespace);
+ extNode.SetAttribute("uri", "{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}");
+
+ var creationId = doc.CreateElement("a16", "creationId", "http://schemas.microsoft.com/office/drawing/2014/main");
+ creationId.SetAttribute("id", "http://schemas.microsoft.com/office/drawing/2014/main", $"{{00000000-0008-0000-0000-0000{nextId:D6}000000}}");
+
+ extNode.AppendChild(creationId);
+ extLst.AppendChild(extNode);
+ cNvPr.AppendChild(extLst);
+
+ //
+ var cNvPicPr = doc.CreateElement("xdr", "cNvPicPr", XdrNamespace);
+ var picLocks = doc.CreateElement("a", "picLocks", ANamespace);
+ picLocks.SetAttribute("noChangeAspect", "1");
+ cNvPicPr.AppendChild(picLocks);
+
+ nvPicPr.AppendChild(cNvPr);
+ nvPicPr.AppendChild(cNvPicPr);
+ pic.AppendChild(nvPicPr);
+
+ //
+ var blipFill = doc.CreateElement("xdr", "blipFill", XdrNamespace);
+ var blip = doc.CreateElement("a", "blip", ANamespace);
+
+ blip.SetAttribute("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
+ blip.SetAttribute("embed", ns.LookupNamespace("r"), relId);
+ blip.SetAttribute("cstate", "print");
+
+ var stretch = doc.CreateElement("a", "stretch", ANamespace);
+ var fillRect = doc.CreateElement("a", "fillRect", ANamespace);
+ stretch.AppendChild(fillRect);
+
+ blipFill.AppendChild(blip);
+ blipFill.AppendChild(stretch);
+ pic.AppendChild(blipFill);
+
+ //
+ var spPr = doc.CreateElement("xdr", "spPr", XdrNamespace);
+ var xfrm = doc.CreateElement("a", "xfrm", ANamespace);
+
+ var off = doc.CreateElement("a", "off", ANamespace);
+ off.SetAttribute("x", "0");
+ off.SetAttribute("y", "0");
+
+ //var spExt = doc.CreateElement("a", "ext", ANamespace);
+ //spExt.SetAttribute("cx", "0");
+ //spExt.SetAttribute("cy", "0");
+
+ xfrm.AppendChild(off);
+ //xfrm.AppendChild(spExt);
+
+ var prstGeom = doc.CreateElement("a", "prstGeom", ANamespace);
+ prstGeom.SetAttribute("prst", "rect");
+
+ var avLst = doc.CreateElement("a", "avLst", ANamespace);
+ prstGeom.AppendChild(avLst);
+
+ spPr.AppendChild(xfrm);
+ spPr.AppendChild(prstGeom);
+
+ pic.AppendChild(spPr);
+
+ //
+ var clientData = doc.CreateElement("xdr", "clientData", XdrNamespace);
+
+ //oneCellAnchor.AppendChild(from);
+ //oneCellAnchor.AppendChild(ext);
+ //oneCellAnchor.AppendChild(pic);
+ //oneCellAnchor.AppendChild(clientData);
+
+ //wsDr.AppendChild(oneCellAnchor);
+ //var pic = CreatePictureNode(doc, col, row, widthPx, heightPx, relId, nextId);
+ // var clientData = doc.CreateElement("xdr", "clientData", XdrNamespace);
+
+ anchor.AppendChild(pic);
+ anchor.AppendChild(clientData);
+ wsDr.AppendChild(anchor);
- public static XmlDocument CreateOrUpdateDrawingXml(
+ return doc;
+ }
+
+ public static XmlDocument CreateOrUpdateDrawingXml(
XmlDocument? existingDoc,
int col, int row,
int widthPx, int heightPx,
diff --git a/tests/MiniExcelTests/MiniExcelIssueTests.cs b/tests/MiniExcelTests/MiniExcelIssueTests.cs
index 4e64d1e304ccdfc1e4ac9e035dbab527b62fa898..9181a16e953aba20084566061ef3c8dfc975070d 100644
--- a/tests/MiniExcelTests/MiniExcelIssueTests.cs
+++ b/tests/MiniExcelTests/MiniExcelIssueTests.cs
@@ -21,6 +21,7 @@
using MiniExcelLibs.Picture;
using TableStyles = MiniExcelLibs.OpenXml.TableStyles;
using System.Threading.Tasks;
+using LicenseContext = OfficeOpenXml.LicenseContext;
namespace MiniExcelLibs.Tests;
@@ -4428,9 +4429,8 @@ public void TestIssue814()
MiniExcel.AddPicture(path.FilePath, images);
using var package = new ExcelPackage(new FileInfo(path.FilePath));
-
- // Check picture in the first sheet (C3)
- var firstSheet = package.Workbook.Worksheets[0];
+ // Check picture in the first sheet (C3)
+ var firstSheet = package.Workbook.Worksheets[0];
var pictureInC3 = firstSheet.Drawings.OfType().FirstOrDefault(p => p.From.Column == 2 && p.From.Row == 2);
Assert.NotNull(pictureInC3);
@@ -4479,8 +4479,9 @@ public void TestIssue815()
];
MiniExcel.AddPicture(path.FilePath, images);
+ //ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
- using (var package = new ExcelPackage(new FileInfo(path.FilePath)))
+ using (var package = new ExcelPackage(new FileInfo(path.FilePath)))
{
// Check picture in the first sheet (C3)
var firstSheet = package.Workbook.Worksheets[0];
@@ -4538,8 +4539,9 @@ public void TestIssue816()
];
MiniExcel.AddPicture(path.FilePath, images);
+ //ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
- using var package = new ExcelPackage(new FileInfo(path.FilePath));
+ using var package = new ExcelPackage(new FileInfo(path.FilePath));
// Check picture in the first sheet (C3)
var firstSheet = package.Workbook.Worksheets[0];
diff --git a/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateTests.cs b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateTests.cs
index 5cca02a0044c8a39b9d12aa7e5572881c1d67ca1..4ea31a51a1e06cb478dff9b8c92d3cf46ca00d71 100644
--- a/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateTests.cs
+++ b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateTests.cs
@@ -1,13 +1,103 @@
-using System.Data;
-using Dapper;
+using Dapper;
+using DocumentFormat.OpenXml.Office2013.ExcelAc;
+using MiniExcelLibs.Enums;
+using MiniExcelLibs.Picture;
using MiniExcelLibs.Tests.Utils;
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing;
+using System.Data;
+using System.IO.Compression;
using Xunit;
namespace MiniExcelLibs.Tests.SaveByTemplate;
public class MiniExcelTemplateTests
{
- [Fact]
+ [Fact]
+ public void TestImageType()
+ {
+ const string templatePath = "../../../../../samples/xlsx/TestImageType.xlsx";
+ {
+ string absolutePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, templatePath));
+
+ using var path = AutoDeletingPath.Create();
+ File.Copy(absolutePath, path.FilePath, overwrite: true); // 拷贝模板文件
+
+ var img1Bytes = File.ReadAllBytes("../../../../../samples/images/TestIssue327.png"); // 使用你本地的图片
+ var img2Bytes = File.ReadAllBytes("../../../../../samples/images/TestIssue327.png"); // 使用你本地的图片
+ var img3Bytes = File.ReadAllBytes("../../../../../samples/images/TestIssue327.png"); // 使用你本地的图片
+
+ var pictures = new[]
+ {
+ new MiniExcelPicture
+ {
+ CellAddress = "B2",
+ ImageBytes = img1Bytes,
+ PictureType = "png",
+ ImgType = XlsxImgType.AbsoluteAnchor,
+ Location = new System.Drawing.Point(255,255),
+
+ WidthPx = 1920,
+ HeightPx = 1032
+ },
+ new MiniExcelPicture
+ {
+ CellAddress = "D4",
+ ImageBytes = img2Bytes,
+ PictureType = "png",
+ ImgType = XlsxImgType.TwoCellAnchor,
+ WidthPx = 1920,
+ HeightPx = 1032
+ },
+ new MiniExcelPicture
+ {
+ CellAddress = "F6",
+ ImageBytes = img3Bytes,
+ PictureType = "png",
+ ImgType = XlsxImgType.OneCellAnchor,
+ WidthPx = 1920,
+ HeightPx = 1032
+ }
+ };
+
+ // Act
+ MiniExcel.AddPicture(path.ToString(), pictures);
+
+ // Assert
+ using var zip = ZipFile.OpenRead(path.FilePath);
+ var mediaEntries = zip.Entries.Where(x => x.FullName.StartsWith("xl/media/")).ToList();
+ // ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
+ Assert.Equal(pictures.Length, mediaEntries.Count);
+
+ // Assert(使用 EPPlus 验证图片是否正确插入)
+ using (var package = new ExcelPackage(new FileInfo(path.FilePath)))
+ {
+ var sheet = package.Workbook.Worksheets[0];
+ var picB2 = sheet.Drawings.OfType()
+ .FirstOrDefault(p => p.EditAs == eEditAs.Absolute);
+
+ Assert.NotNull(picB2);
+ Assert.Equal(1920 * 9525, picB2.Size.Width);
+ Assert.Equal(1032 * 9525, picB2.Size.Height);
+ Console.WriteLine("✅ AbsoluteAnchor 图片存在,并且尺寸符合预期(1920x1032)");
+
+ Console.WriteLine("✅ 图片插入成功(B2 - AbsoluteAnchor)");
+
+ // 验证 D4 的图片(ImgType.TwoCellAnchor)
+ var picD4 = sheet.Drawings.OfType()
+ .FirstOrDefault(p => p.EditAs == eEditAs.TwoCell && p.From != null && p.From.Column == 3 && p.From.Row == 3);
+ Assert.NotNull(picD4);
+ Console.WriteLine("✅ 图片插入成功(D4 - TwoCellAnchor)");
+
+ // 验证 F6 的图片(ImgType.OneCellAnchor)
+ var picF6 = sheet.Drawings.OfType()
+ .FirstOrDefault(p => p.EditAs == eEditAs.OneCell && p.From != null && p.From.Column == 5 && p.From.Row == 5);
+ Assert.NotNull(picF6);
+ Console.WriteLine("✅ 图片插入成功(F6 - OneCellAnchor)");
+ }
+ }
+ }
+ [Fact]
public void DatatableTemptyRowTest()
{
const string templatePath = "../../../../../samples/xlsx/TestTemplateComplex.xlsx";