diff --git a/src/EPPlus/Drawing/ExcelDrawing.cs b/src/EPPlus/Drawing/ExcelDrawing.cs
index 1778afb66..1147ea1e9 100644
--- a/src/EPPlus/Drawing/ExcelDrawing.cs
+++ b/src/EPPlus/Drawing/ExcelDrawing.cs
@@ -16,7 +16,10 @@ Date Author Change
using OfficeOpenXml.Drawing.OleObject;
using OfficeOpenXml.Drawing.Slicer;
using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using OfficeOpenXml.Packaging;
+using OfficeOpenXml.Utils;
+using OfficeOpenXml.Utils.Drawings;
using OfficeOpenXml.Utils.EnumUtils;
using OfficeOpenXml.Utils.FileUtils;
using OfficeOpenXml.Utils.XML;
@@ -25,6 +28,7 @@ Date Author Change
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Security.Cryptography;
using System.Text;
using System.Xml;
@@ -731,8 +735,8 @@ internal void GetFromBounds(out int fromRow, out int fromRowOff, out int fromCol
{
if (CellAnchor == eEditAs.Absolute)
{
- GetToRowFromPixels(Position.Y, out fromRow, out fromRowOff);
- GetToColumnFromPixels(Position.X, out fromCol, out fromColOff);
+ GetToRowFromPixels(Position.Y / (double)EMU_PER_PIXEL, out fromRow, out fromRowOff);
+ GetToColumnFromPixels(Position.X / (double)EMU_PER_PIXEL, out fromCol, out fromColOff);
}
else
{
@@ -747,7 +751,7 @@ internal void GetToBounds(out int toRow, out int toRowOff, out int toCol, out in
if (CellAnchor == eEditAs.Absolute)
{
GetToRowFromPixels((Position.Y + Size.Height) / EMU_PER_PIXEL, out toRow, out toRowOff);
- GetToColumnFromPixels(Position.X + Size.Width / EMU_PER_PIXEL, out toCol, out toColOff);
+ GetToColumnFromPixels((Position.X + Size.Width) / EMU_PER_PIXEL, out toCol, out toColOff);
}
else
{
@@ -855,15 +859,14 @@ internal double GetPixelWidth()
if (CellAnchor == eEditAs.TwoCell)
{
ExcelWorksheet ws = _drawings.Worksheet;
- double mdw = ws.Workbook.MaxFontWidth;
pix = -From.ColumnOff / (double)EMU_PER_PIXEL;
for (int col = From.Column + 1; col <= To.Column; col++)
{
- pix += MathHelper.TruncateDouble(((256 * ws.GetColumnWidth(col) + MathHelper.TruncateDouble(128 / mdw)) / 256) * mdw);
+ pix += PixelHelper.GetColumnWidth(ws, col);
}
- var w = MathHelper.TruncateDouble(((256 * ws.GetColumnWidth(To.Column + 1) + MathHelper.TruncateDouble(128 / mdw)) / 256) * mdw);
+ var w = PixelHelper.GetColumnWidth(ws, To.Column + 1);
pix += Math.Min(w, Convert.ToDouble(To.ColumnOff) / EMU_PER_PIXEL);
}
else
@@ -898,9 +901,9 @@ internal double GetPixelHeight()
pix = -(From.RowOff / (double)EMU_PER_PIXEL);
for (int row = From.Row + 1; row <= To.Row; row++)
{
- pix += ws.GetRowHeight(row) / 0.75;
+ pix += PixelHelper.GetRowHeight(ws, row);
}
- var h = ws.GetRowHeight(To.Row + 1) / 0.75;
+ var h = PixelHelper.GetRowHeight(ws, To.Row + 1);
pix += Math.Min(h, Convert.ToDouble(To.RowOff) / EMU_PER_PIXEL);
}
else
@@ -939,12 +942,12 @@ internal void CalcRowFromPixelTop(double pixels, out int row, out int rowOff)
ExcelWorksheet ws = _drawings.Worksheet;
double mdw = ws.Workbook.MaxFontWidth;
double prevPix = 0;
- double pix = ws.GetRowHeight(1) / 0.75;
+ double pix = PixelHelper.GetRowHeight(ws, 1);
int r = 2;
while (pix < pixels)
{
prevPix = pix;
- pix += (int)(ws.GetRowHeight(r++) / 0.75);
+ pix += (int)PixelHelper.GetRowHeight(ws, r++);
}
if (pix == pixels)
@@ -987,15 +990,14 @@ internal void CalcColFromPixelLeft(double pixels, out int column, out int column
{
ExcelWorksheet ws = _drawings.Worksheet;
- double mdw = ws.Workbook.MaxFontWidth;
double prevPix = 0;
- double pix = (int)MathHelper.TruncateDouble(((256 * ws.GetColumnWidth(1) + MathHelper.TruncateDouble(128 / mdw)) / 256) * mdw);
+ double pix = (int)PixelHelper.GetColumnWidth(ws, 1);
int col = 2;
while (pix < pixels)
{
prevPix = pix;
- pix += (int)MathHelper.TruncateDouble(((256 * ws.GetColumnWidth(col++) + MathHelper.TruncateDouble(128 / mdw)) / 256) * mdw);
+ pix += (int)PixelHelper.GetColumnWidth(ws, col++);
}
if (pix == pixels)
{
@@ -1033,20 +1035,40 @@ internal void SetPixelHeight(double pixels)
internal void GetToRowFromPixels(double pixels, out int toRow, out int rowOff, int fromRow = -1, int fromRowOff = -1)
{
+ if (From == null && this is not ExcelControl)
+ {
+ // Absolute anchor path
+ double remaining = pixels;
+ int currentRow = 1;
+
+ while (true)
+ {
+ double rowPix = PixelHelper.GetRowHeight(_drawings.Worksheet, currentRow);
+ if (remaining < rowPix)
+ break;
+
+ remaining -= rowPix;
+ currentRow++;
+ }
+
+ toRow = currentRow - 1;
+ rowOff = (int)(remaining);
+ return;
+ }
if (fromRow < 0)
{
fromRow = From.Row;
fromRowOff = From.RowOff;
}
ExcelWorksheet ws = _drawings.Worksheet;
- var pixOff = pixels - ((ws.GetRowHeight(fromRow + 1) / 0.75) - (fromRowOff / (double)EMU_PER_PIXEL));
+ var pixOff = pixels - (PixelHelper.GetRowHeight(ws, fromRow + 1) - (fromRowOff / (double)EMU_PER_PIXEL));
double prevPixOff = pixels;
int row = fromRow + 1;
while (pixOff >= 0)
{
prevPixOff = pixOff;
- pixOff -= (ws.GetRowHeight(++row) / 0.75);
+ pixOff -= PixelHelper.GetRowHeight(ws, ++row);
}
toRow = row - 1;
if (fromRow == toRow)
@@ -1086,19 +1108,35 @@ internal void SetPixelWidth(double pixels)
internal void GetToColumnFromPixels(double pixels, out int col, out int colOff, int fromColumn = -1, int fromColumnOff = -1)
{
ExcelWorksheet ws = _drawings.Worksheet;
- double mdw = ws.Workbook.MaxFontWidth;
- if (fromColumn < 0)
+ if (From == null && this is not ExcelControl)
+ {
+ // Absolute anchor path
+ double remaining = pixels;
+ int currentCol = 1;
+ double colPix = PixelHelper.GetColumnWidth(ws, currentCol);
+ while (remaining >= colPix)
+ {
+ remaining -= colPix;
+ currentCol++;
+ colPix = PixelHelper.GetColumnWidth(ws, currentCol);
+ }
+
+ col = currentCol-1;
+ colOff = (int)(remaining);
+ return;
+ }
+ if (From != null && fromColumn < 0)
{
fromColumn = From.Column;
fromColumnOff = From.ColumnOff;
}
- double pixOff = pixels - (MathHelper.TruncateDouble(((256 * ws.GetColumnWidth(fromColumn + 1) + MathHelper.TruncateDouble(128 / mdw)) / 256) * mdw) - fromColumnOff / EMU_PER_PIXEL);
+ double pixOff = pixels - (PixelHelper.GetColumnWidth(ws, fromColumn + 1) - fromColumnOff / EMU_PER_PIXEL);
double offset = (double)fromColumnOff / EMU_PER_PIXEL + pixels;
col = fromColumn + 2;
while (pixOff >= 0)
{
offset = pixOff;
- pixOff -= MathHelper.TruncateDouble(((256 * ws.GetColumnWidth(col++) + MathHelper.TruncateDouble(128 / mdw)) / 256) * mdw);
+ pixOff -= PixelHelper.GetColumnWidth(ws, col++);
}
colOff = (int)offset;
}
diff --git a/src/EPPlus/Utils/Drawing/PixelHelper.cs b/src/EPPlus/Utils/Drawing/PixelHelper.cs
new file mode 100644
index 000000000..3057de3c8
--- /dev/null
+++ b/src/EPPlus/Utils/Drawing/PixelHelper.cs
@@ -0,0 +1,47 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************
+ Date Author Change
+ *************************************************************************************************
+ 05/08/2026 EPPlus Software AB Initial release
+ *************************************************************************************************/
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+
+namespace OfficeOpenXml.Utils.Drawings
+{
+ ///
+ /// Helper methods for converting between worksheet coordinates and pixels.
+ ///
+ internal static class PixelHelper
+ {
+ ///
+ /// Returns the width of a column in pixels, using the same formula
+ /// Excel uses internally.
+ ///
+ /// The worksheet.
+ /// The 1-based column index.
+ /// The column width in pixels.
+ internal static double GetColumnWidth(ExcelWorksheet ws, int column)
+ {
+ double mdw = ws.Workbook.MaxFontWidth;
+ return MathHelper.TruncateDouble(
+ ((256 * ws.GetColumnWidth(column) + MathHelper.TruncateDouble(128 / mdw)) / 256) * mdw);
+ }
+
+ ///
+ /// Returns the height of a row in pixels.
+ ///
+ /// The worksheet.
+ /// The 1-based row index.
+ /// The row height in pixels.
+ internal static double GetRowHeight(ExcelWorksheet ws, int row)
+ {
+ return ws.GetRowHeight(row) / 0.75;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlusTest/Drawing/CopyDrawingTests.cs b/src/EPPlusTest/Drawing/CopyDrawingTests.cs
index 171142bef..1ab47a2d5 100644
--- a/src/EPPlusTest/Drawing/CopyDrawingTests.cs
+++ b/src/EPPlusTest/Drawing/CopyDrawingTests.cs
@@ -799,5 +799,157 @@ public void s814CopySameImageTwiceToEmptyNamedRanges()
SaveAndCleanup(targetPackage);
}
}
+
+
+ [TestMethod]
+ public void CopyDrawingWithAbsolutePosition ()
+ {
+ using var p = OpenTemplatePackage("Test-file.xlsx");
+ var sourceSheet = p.Workbook.Worksheets[1];
+ using var destPackage = new ExcelPackage();
+ var destSheet = destPackage.Workbook.Worksheets.Add("Dest");
+ sourceSheet.Cells[1, 1, sourceSheet.Dimension.Rows, sourceSheet.Dimension.Columns].Copy(destSheet.Cells[1, 1], ExcelRangeCopyOptionFlags.ExcludeFormulas);
+ //Assert.AreEqual(1, destSheet.Drawings.Count);
+ SaveAndCleanup(destPackage);
+ }
+
+ [TestMethod]
+ public void GetFromAndToBounds_AbsoluteAnchor_FromCornerAndToRow_ResolveCorrectly()
+ {
+ // Reproduces the customer ticket where calling GetFromBounds /
+ // GetToBounds on an absolute-anchored drawing throws NRE because
+ // From is null after the workbook has been read from disk.
+ //
+ // This test verifies the From corner and the To row resolve to the
+ // correct cell coordinates. The To column is verified separately
+ // in GetFromAndToBounds_AbsoluteAnchor_ToColumn_ResolvesCorrectly.
+ //
+ // The drawing is positioned at (6 px, 136 px) with size (1375 px,
+ // 20 px). Column widths are explicit; row heights use the workbook
+ // default (15 pt = 20 px per row). MaxFontWidth is the default 7.
+ const string fileName = "AbsoluteAnchorBounds.xlsx";
+
+ // Build and save the workbook. Saving and reloading is required to
+ // produce the From == null state that triggers the customer's NRE.
+ using (var p = OpenPackage(fileName, delete: true))
+ {
+ var ws = p.Workbook.Worksheets.Add("AbsoluteAnchorTest");
+
+ ws.Column(1).Width = 10.6640625;
+ ws.Column(2).Width = 5.5546875;
+ ws.Column(3).Width = 5;
+ ws.Column(4).Width = 7;
+ ws.Column(5).Width = 8.109375;
+ ws.Column(6).Width = 9;
+ ws.Column(7).Width = 28;
+ ws.Column(8).Width = 8.88671875;
+ ws.Column(9).Width = 8.88671875;
+ ws.Column(10).Width = 8.33203125;
+ ws.Column(11).Width = 8.33203125;
+ ws.Column(12).Width = 27;
+ ws.Column(13).Width = 7.5546875;
+ ws.Column(14).Width = 8.33203125;
+ ws.Column(15).Width = 19.5546875;
+ ws.Column(16).Width = 9;
+ ws.Column(17).Width = 7.6640625;
+ ws.Column(18).Width = 6.109375;
+ // Column 19 keeps the default width.
+
+ var pic = ws.Drawings.AddPicture("AbsBar", GetResourceFile("EPPlus.png"));
+ pic.ChangeCellAnchor(eEditAs.Absolute, PixelTop: 136, PixelLeft: 6,
+ width: 1375, height: 20);
+ p.Save();
+ }
+
+ using (var p = OpenPackage(fileName))
+ {
+ var ws = p.Workbook.Worksheets["AbsoluteAnchorTest"];
+ var pic = ws.Drawings[0];
+
+ Assert.AreEqual(eEditAs.Absolute, pic.CellAnchor);
+ Assert.IsNull(pic.From, "Reloaded absolute-anchored drawings should have null From.");
+ Assert.AreEqual(7, p.Workbook.MaxFontWidth, "Test assumes default MaxFontWidth = 7.");
+
+ pic.GetFromBounds(out int fromRow, out int fromRowOff,
+ out int fromCol, out int fromColOff);
+ pic.GetToBounds(out int toRow, out int toRowOff,
+ out int toCol, out int toColOff);
+
+ // From corner at pixel (6, 136).
+ // Column: pixel 6 falls inside column 1 (0-indexed: 0), 6 px in.
+ // Row: pixel 136 with row height 20 px; six full rows consume
+ // 120 px, leaving 16 px offset in row 7.
+ Assert.AreEqual(0, fromCol, "From column should be A (0-indexed).");
+ Assert.AreEqual(6, fromColOff, "From column offset should be 6 px.");
+ Assert.AreEqual(6, fromRow, "From row should be 7 (0-indexed).");
+ Assert.AreEqual(16, fromRowOff, "From row offset should be 16 px.");
+
+ // To corner at pixel (1381, 156).
+ // Row: pixel 156 falls inside row 8 (0-indexed: 7), 16 px in.
+ Assert.AreEqual(7, toRow, "To row should be 8 (0-indexed).");
+ Assert.AreEqual(16, toRowOff, "To row offset should be 16 px.");
+ }
+ }
+
+ [TestMethod]
+ public void GetFromAndToBounds_AbsoluteAnchor_ToColumn_ResolvesCorrectly()
+ {
+ // Companion test to GetFromAndToBounds_AbsoluteAnchor_FromCornerAndToRow_ResolveCorrectly.
+ //
+ // Verifies that the To column resolves correctly for an absolute-
+ // anchored drawing. This test currently FAILS due to a known bug
+ // in the absolute-anchor branch of GetToColumnFromPixels: the loop
+ // measures column width using the unset 'fromColumn' parameter
+ // (-1) instead of the iterating 'currentCol', so every iteration
+ // uses the workbook default column width and the result is wrong
+ // whenever the worksheet has columns of varying widths.
+ //
+ // Expected values are derived from walking the actual column
+ // widths set up in the workbook below: pixel 1381 lands in
+ // column 19 (0-indexed: 18) with a 30 px offset.
+ //
+ // Once the bug is fixed this test should pass.
+ const string fileName = "AbsoluteAnchorBounds.xlsx";
+
+ using (var p = OpenPackage(fileName, delete: true))
+ {
+ var ws = p.Workbook.Worksheets.Add("AbsoluteAnchorTest");
+
+ ws.Column(1).Width = 10.6640625;
+ ws.Column(2).Width = 5.5546875;
+ ws.Column(3).Width = 5;
+ ws.Column(4).Width = 7;
+ ws.Column(5).Width = 8.109375;
+ ws.Column(6).Width = 9;
+ ws.Column(7).Width = 28;
+ ws.Column(8).Width = 8.88671875;
+ ws.Column(9).Width = 8.88671875;
+ ws.Column(10).Width = 8.33203125;
+ ws.Column(11).Width = 8.33203125;
+ ws.Column(12).Width = 27;
+ ws.Column(13).Width = 7.5546875;
+ ws.Column(14).Width = 8.33203125;
+ ws.Column(15).Width = 19.5546875;
+ ws.Column(16).Width = 9;
+ ws.Column(17).Width = 7.6640625;
+ ws.Column(18).Width = 6.109375;
+
+ var pic = ws.Drawings.AddPicture("AbsBar", GetResourceFile("EPPlus.png"));
+ pic.ChangeCellAnchor(eEditAs.Absolute, PixelTop: 136, PixelLeft: 6,
+ width: 1375, height: 20);
+ p.Save();
+ }
+
+ using (var p = OpenPackage(fileName))
+ {
+ var ws = p.Workbook.Worksheets["AbsoluteAnchorTest"];
+ var pic = ws.Drawings[0];
+
+ pic.GetToBounds(out _, out _, out int toCol, out int toColOff);
+
+ Assert.AreEqual(18, toCol, "To column should be S (0-indexed).");
+ Assert.AreEqual(30, toColOff, "To column offset should be ~30 px.");
+ }
+ }
}
}