Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 57 additions & 19 deletions src/EPPlus/Drawing/ExcelDrawing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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
{
Expand All @@ -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
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down
47 changes: 47 additions & 0 deletions src/EPPlus/Utils/Drawing/PixelHelper.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Helper methods for converting between worksheet coordinates and pixels.
/// </summary>
internal static class PixelHelper
{
/// <summary>
/// Returns the width of a column in pixels, using the same formula
/// Excel uses internally.
/// </summary>
/// <param name="ws">The worksheet.</param>
/// <param name="column">The 1-based column index.</param>
/// <returns>The column width in pixels.</returns>
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);
}

/// <summary>
/// Returns the height of a row in pixels.
/// </summary>
/// <param name="ws">The worksheet.</param>
/// <param name="row">The 1-based row index.</param>
/// <returns>The row height in pixels.</returns>
internal static double GetRowHeight(ExcelWorksheet ws, int row)
{
return ws.GetRowHeight(row) / 0.75;
}
}
}
152 changes: 152 additions & 0 deletions src/EPPlusTest/Drawing/CopyDrawingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}
}
}
Loading