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";