From deb3113cc6452298760838e38799e609eace78de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E6=B2=9B=20=E7=8E=8B?= <1016423262@qq.com> Date: Sun, 29 Jun 2025 19:57:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20MiniExcel=20=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=20Excel=20=E6=97=B6=EF=BC=8C=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=8F=92=E5=85=A5=E6=96=B9=E5=BC=8F=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B0=86=E5=9B=BE=E7=89=87=E5=B5=8C=E5=85=A5?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 解决了使用 `addpicture` 函数插入图片时,图片默认以浮动模式插入的问题 - 为图片插入增加了单元格嵌入模式的支持,确保图片直接嵌入到单元格中 - 新增了对应的测试用例,验证了嵌入模式下图片的插入功能 - 343 个测试用例全部通过,验证功能修改未引入其他问题 --- samples/xlsx/TestImageType.xlsx | Bin 0 -> 11119 bytes src/MiniExcel/Enums/XlsxImgType.cs | 24 ++ src/MiniExcel/Picture/MiniExcelPicture.cs | 10 +- .../Picture/MiniExcelPictureImplement.cs | 227 ++++++++++++++++-- tests/MiniExcelTests/MiniExcelIssueTests.cs | 12 +- .../SaveByTemplate/MiniExcelTemplateTests.cs | 96 +++++++- 6 files changed, 345 insertions(+), 24 deletions(-) create mode 100644 samples/xlsx/TestImageType.xlsx create mode 100644 src/MiniExcel/Enums/XlsxImgType.cs diff --git a/samples/xlsx/TestImageType.xlsx b/samples/xlsx/TestImageType.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4a86ee3ec14d01cb85f6611839695d15bc37bf1b GIT binary patch literal 11119 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 literal 0 HcmV?d00001 diff --git a/src/MiniExcel/Enums/XlsxImgType.cs b/src/MiniExcel/Enums/XlsxImgType.cs new file mode 100644 index 0000000..9e08ef6 --- /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 1d6b67c..aca5aae 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 f503169..0cf8df6 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 4e64d1e..9181a16 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 5cca02a..4ea31a5 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"; -- Gitee