diff --git a/src/EPPlus/Constants/Schemas.cs b/src/EPPlus/Constants/Schemas.cs index 38e7557ba..6241b3ed3 100644 --- a/src/EPPlus/Constants/Schemas.cs +++ b/src/EPPlus/Constants/Schemas.cs @@ -20,5 +20,7 @@ internal class Schemas internal const string schemaRichValueRel = "http://schemas.microsoft.com/office/spreadsheetml/2022/richvaluerel"; internal const string schemaWebImage = "http://schemas.microsoft.com/office/spreadsheetml/2020/richdatawebimage"; internal const string schemaDataMashup = "http://schemas.microsoft.com/DataMashup"; + + internal const string schemaCalcFeature = "http://schemas.microsoft.com/office/spreadsheetml/2018/calcfeatures"; } } diff --git a/src/EPPlus/Core/RangeCopyHelper.cs b/src/EPPlus/Core/RangeCopyHelper.cs index 2238c7ddb..da70afee8 100644 --- a/src/EPPlus/Core/RangeCopyHelper.cs +++ b/src/EPPlus/Core/RangeCopyHelper.cs @@ -925,23 +925,27 @@ private void CopyMergedCells(Dictionary copiedMergedCells) private void CopyFullRow() { - if (_sourceRange._fromRow == 1 && _sourceRange._toRow == ExcelPackage.MaxRows) + _sourceRange.GetAddressDimensionFullRowAndColumn(out int dimFromRow, out int dimFromCol, out int dimToRow, out int dimToCol); + if (dimFromRow == 0 && dimFromCol==0) return; + if (_sourceRange._fromRow == 1 && _sourceRange._toRow == ExcelPackage.MaxRows && dimFromCol > 0) { - for (int col = 0; col < _sourceRange.Columns; col++) + var diff = dimFromCol - _sourceRange._fromCol; + for (int col = 0; col < (dimToCol-dimFromCol + 1); col++) { - _destinationRange.Worksheet.Column(_destinationRange.Start.Column + col).OutlineLevel = _sourceRange.Worksheet.Column(_sourceRange._fromCol + col).OutlineLevel; + _destinationRange.Worksheet.Column(_destinationRange.Start.Column + col + diff).OutlineLevel = _sourceRange.Worksheet.Column(_sourceRange._fromCol + col + diff).OutlineLevel; } } - if (EnumUtil.HasFlag(_copyOptions, ExcelRangeCopyOptionFlags.IncludeFullRow)) + if (EnumUtil.HasFlag(_copyOptions, ExcelRangeCopyOptionFlags.IncludeFullRow) && dimFromRow > 0) { var sourceRowOrig = _sourceRange._fromRow; var destRowOrig = _destinationRange._fromRow; - for (int i = 0; i < _sourceRange.Rows; i++) + var diff = dimFromRow - _sourceRange._fromRow; + for (int i = 0; i < (dimToRow - dimFromRow + 1); i++) { - var sourceRow = _sourceRange.Worksheet.Row(sourceRowOrig + i); - var destRow = _destinationRange.Worksheet.Row(destRowOrig + i); + var sourceRow = _sourceRange.Worksheet.Row(sourceRowOrig + i + diff); + var destRow = _destinationRange.Worksheet.Row(destRowOrig + i + diff); destRow.Height = sourceRow.Height; } @@ -950,23 +954,27 @@ private void CopyFullRow() private void CopyFullColumn() { + _sourceRange.GetAddressDimensionFullRowAndColumn(out int dimFromRow, out int dimFromCol, out int dimToRow, out int dimToCol); + if (dimFromRow == 0 && dimFromCol == 0) return; if (_sourceRange._fromCol == 1 && _sourceRange._toCol == ExcelPackage.MaxColumns) { - for (int row = 0; row < _sourceRange.Rows; row++) + var diff = dimFromRow - _sourceRange._fromRow; + for (int row = 0; row < (dimToRow - dimFromRow + 1); row++) { - _destinationRange.Worksheet.Row(_destinationRange.Start.Row + row).OutlineLevel = _sourceRange.Worksheet.Row(_sourceRange._fromRow + row).OutlineLevel; + _destinationRange.Worksheet.Row(_destinationRange.Start.Row + row + diff).OutlineLevel = _sourceRange.Worksheet.Row(_sourceRange._fromRow + row + diff).OutlineLevel; } } - if(EnumUtil.HasFlag(_copyOptions, ExcelRangeCopyOptionFlags.IncludeFullColumn)) + if(EnumUtil.HasFlag(_copyOptions, ExcelRangeCopyOptionFlags.IncludeFullColumn) && dimFromCol > 0) { var destColOrig = _destinationRange._fromCol; var sourceColOrig = _sourceRange._fromCol; - for (int i = 0; i < _sourceRange.Columns; i++) + var diff = dimFromCol - _sourceRange._fromCol; + for (int i = 0; i < (dimToCol - dimFromCol+1); i++) { - var sourceCol = _sourceRange.Worksheet.Column(sourceColOrig + i); - var destCol = _destinationRange.Worksheet.Column(destColOrig + i); + var sourceCol = _sourceRange.Worksheet.Column(sourceColOrig + i + diff); + var destCol = _destinationRange.Worksheet.Column(destColOrig + i + diff); destCol.Width = sourceCol.Width; } diff --git a/src/EPPlus/Drawing/ExcelDrawing.cs b/src/EPPlus/Drawing/ExcelDrawing.cs index 1778afb66..933d23fd2 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; } @@ -1384,10 +1422,20 @@ public void SetPosition(int Row, int RowOffsetPixels, int Column, int ColumnOffs _height = GetPixelHeight(); } - From.Row = Row; - From.RowOff = RowOffsetPixels * EMU_PER_PIXEL; - From.Column = Column; - From.ColumnOff = ColumnOffsetPixels * EMU_PER_PIXEL; + if (CellAnchor == eEditAs.Absolute) + { + GetPixelHeightFromRow(Row, RowOffsetPixels, out int pixelHeight); + + Position.Y = (int)(pixelHeight * EMU_PER_PIXEL); + Position.X = (int)(ColumnOffsetPixels * EMU_PER_PIXEL); + } + else + { + From.Row = Row; + From.RowOff = RowOffsetPixels * EMU_PER_PIXEL; + From.Column = Column; + From.ColumnOff = ColumnOffsetPixels * EMU_PER_PIXEL; + } if (CellAnchor == eEditAs.TwoCell) { _left = GetPixelLeft(); @@ -1399,6 +1447,36 @@ public void SetPosition(int Row, int RowOffsetPixels, int Column, int ColumnOffs _doNotAdjust = false; UpdatePositionAndSizeXml(); } + private void GetPixelWidthFromRow(int toCol, int colOffsetPixels, out int pixelWidth) + { + ExcelWorksheet ws = _drawings.Worksheet; + double mdw = ws.Workbook.MaxFontWidth; + + pixelWidth = 0; + for (int col = 0; col < toCol; col++) + { + pixelWidth += ws.GetColumnWidthPixels(col, mdw); + } + pixelWidth += colOffsetPixels; + } + private void GetPixelHeightFromRow(int toRow, int rowOffsetPixels, out int pixelHeight) + { + pixelHeight = 0; + var cache = _drawings.Worksheet.RowHeightCache; + for (int row = 0; row < toRow; row++) + { + lock (cache) + { + if (!cache.ContainsKey(row)) + { + cache.Add(row, _drawings.Worksheet.GetRowHeight(row + 1)); + } + } + pixelHeight += (int)(cache[row] / 0.75); + } + pixelHeight += rowOffsetPixels; + } + /// /// Set size in Percent. /// Note that resizing columns / rows after using this function will effect the size of the drawing diff --git a/src/EPPlus/ExcelPackage.cs b/src/EPPlus/ExcelPackage.cs index 10dc020b7..6aec485c6 100644 --- a/src/EPPlus/ExcelPackage.cs +++ b/src/EPPlus/ExcelPackage.cs @@ -864,6 +864,7 @@ private XmlNamespaceManager CreateDefaultNSM() ns.AddNamespace("xda", schemaDynamicArrays); ns.AddNamespace("clbl", schemaMipLabelMetadata); ns.AddNamespace("xfpb", Schemas.schemaFeaturePropertyBag); + ns.AddNamespace("xcalcf", Schemas.schemaCalcFeature); return ns; } #region SavePart diff --git a/src/EPPlus/ExcelRangeBase.cs b/src/EPPlus/ExcelRangeBase.cs index a681380e3..b358fe103 100644 --- a/src/EPPlus/ExcelRangeBase.cs +++ b/src/EPPlus/ExcelRangeBase.cs @@ -771,31 +771,70 @@ private object GetValueArray() } return v; } - private ExcelAddressBase GetAddressDim(ExcelRangeBase addr) + internal ExcelAddressBase GetAddressDimension() { - int fromRow, fromCol, toRow, toCol; - var d = _worksheet.Dimension; - fromRow = addr._fromRow < d._fromRow ? d._fromRow : addr._fromRow; - fromCol = addr._fromCol < d._fromCol ? d._fromCol : addr._fromCol; - - toRow = addr._toRow > d._toRow ? d._toRow : addr._toRow; - toCol = addr._toCol > d._toCol ? d._toCol : addr._toCol; - - if (addr._fromRow == fromRow && addr._fromCol == fromCol && addr._toRow == toRow && addr._toCol == _toCol) + GetAddressDimensionFullRowAndColumn(out int dimFromRow, out int dimFromCol, out int dimToRow, out int dimToCol); + //If the range is only full column or full row the dimension of the worksheet, return null. + if (dimFromCol==0 || dimFromRow>dimToCol || dimFromCol > dimToCol) { - return addr; + return null; } else { - if (_fromRow > _toRow || _fromCol > _toCol) + return new ExcelAddressBase(dimFromRow, dimFromCol, dimToRow, dimToCol); + } + } + internal void GetAddressDimensionFullRowAndColumn(out int fromRow, out int fromCol, out int toRow, out int toCol) + { + var d = _worksheet.Dimension; + fromRow = toRow = fromCol = toCol = 0; + if (d == null) + { + if(_worksheet._values.ColumnCount==0) { - return null; + return; } else { - return new ExcelAddressBase(fromRow, fromCol, toRow, toCol); + int row = 0, col = _worksheet._values.ColumnCount - 1; + + fromCol = _worksheet._values._columnIndex[0].Index; + if(_worksheet._values.GetPrevCell(ref row, ref col, 0, 0, col)) + { + var lastCol = _worksheet._values.GetValue(row, col)._value as ExcelColumn; + toCol = lastCol.ColumnMax; + } + + fromRow = ExcelPackage.MaxRows; + toRow = 0; + for (int c=0;c<_worksheet._values.ColumnCount;c++) + { + var pMin = _worksheet._values._columnIndex[c]._pages[0].MinIndex; + if (pMin < fromRow) + { + fromRow = pMin; + } + var pMax = _worksheet._values._columnIndex[c]._pages[_worksheet._values._columnIndex[c].PageCount-1].MaxIndex; + if (pMax > toRow) + { + toRow = pMax; + } + } } } + else + { + fromRow = d._fromRow; + fromCol = d._fromCol; + toRow = d._toRow; + toCol = d._toCol; + } + + if(fromRow > 0) fromRow = _fromRow < fromRow ? fromRow : _fromRow; + if(fromCol > 0) fromCol = _fromCol < fromCol ? fromCol : _fromCol; + + if(toRow > 0) toRow = _toRow > toRow ? toRow : _toRow; + if(toCol > 0) toCol = _toCol > toCol ? toCol : _toCol; } private object GetSingleValue() @@ -1505,6 +1544,20 @@ public ExcelWorksheet Worksheet return _worksheet; } } + public ExcelAddressBase DimensionAdjustedAddress + { + get + { + if (_worksheet.Dimension == null) + { + return this; + } + else + { + return GetAddressDimension(); + } + } + } /// /// Address including sheet name /// diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 96d54380b..02b0c79d7 100644 --- a/src/EPPlus/ExcelWorkbook.cs +++ b/src/EPPlus/ExcelWorkbook.cs @@ -16,6 +16,7 @@ Date Author Change using System.Collections.Generic; using System.Text; using System.Globalization; +using System.Linq; using OfficeOpenXml.VBA; using OfficeOpenXml.FormulaParsing; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; @@ -26,7 +27,6 @@ Date Author Change using OfficeOpenXml.Drawing.Slicer; using OfficeOpenXml.ThreadedComments; using OfficeOpenXml.Table; -using System.Linq; using OfficeOpenXml.Table.PivotTable; using OfficeOpenXml.Drawing; using OfficeOpenXml.Constants; @@ -319,6 +319,20 @@ private void SetUris() internal int _nextDrawingId = 2; internal int _nextTableID = int.MinValue; internal int _nextPivotCacheId = 1; + + bool _workbookCreatedInEPPlus; + // xcalcf:feature entries + string[] _calcFeatureStrings = { + "microsoft.com:RD", + "microsoft.com:Single", + "microsoft.com:FV", + "microsoft.com:CNMTM", + "microsoft.com:LET_WF", + "microsoft.com:LAMBDA_WF", + "microsoft.com:ARRAYTEXT_WF" + }; + + internal int GetNewPivotCacheId() { return _nextPivotCacheId++; @@ -1153,9 +1167,10 @@ private void CreateWorkbookXml(XmlNamespaceManager namespaceManager) { _workbookXml = _package.GetXmlFromUri(WorkbookUri); ValidateWorkbookNamespace(); - } + } else { + _workbookCreatedInEPPlus = true; // create a new workbook part and add to the package Packaging.ZipPackagePart partWorkbook = _package.ZipPackage.CreatePart(WorkbookUri, @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", _package.Compression); @@ -1171,12 +1186,24 @@ private void CreateWorkbookXml(XmlNamespaceManager namespaceManager) _workbookXml.AppendChild(wbElem); + XmlElement fileVersion = _workbookXml.CreateElement("fileVersion", ExcelPackage.schemaMain); + UpdateFileVersionAttributes(fileVersion); + wbElem.AppendChild(fileVersion); + // create the bookViews and workbooks element XmlElement bookViews = _workbookXml.CreateElement("bookViews", ExcelPackage.schemaMain); wbElem.AppendChild(bookViews); XmlElement workbookView = _workbookXml.CreateElement("workbookView", ExcelPackage.schemaMain); bookViews.AppendChild(workbookView); + XmlElement calcPr = _workbookXml.CreateElement("calcPr", ExcelPackage.schemaMain); + calcPr.SetAttribute("calcId", "191029"); //Set the version of the calc engine to the latest known version. This will make sure that Excel does not downgrade the calculation engine and that new functions are supported. + wbElem.AppendChild(calcPr); + + XmlElement extLst = _workbookXml.CreateElement("extLst", ExcelPackage.schemaMain); + AddCalculationFeatures(extLst); + wbElem.AppendChild(extLst); + // save it to the package StreamWriter stream = new StreamWriter(partWorkbook.GetStream(FileMode.Create, FileAccess.Write)); _workbookXml.Save(stream); @@ -1184,6 +1211,77 @@ private void CreateWorkbookXml(XmlNamespaceManager namespaceManager) _package.ZipPackage.Flush(); } } + + private void AddCalculationFeatures(XmlElement extLst) + { + //Include the extLst with the calc features to make sure new functions are supported in Excel. + XmlElement ext = _workbookXml.CreateElement("ext", ExcelPackage.schemaMain); + ext.SetAttribute("uri", "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"); + extLst.AppendChild(ext); + + XmlElement calcFeatures = _workbookXml.CreateElement("xcalcf", "calcFeatures", Schemas.schemaCalcFeature); + + foreach (string name in _calcFeatureStrings) + { + XmlElement feature = _workbookXml.CreateElement("xcalcf", "feature", Schemas.schemaCalcFeature); + feature.SetAttribute("name", name); + calcFeatures.AppendChild(feature); + } + + ext.AppendChild(calcFeatures); + } + + private static void UpdateFileVersionAttributes(XmlElement fileVersion) + { + fileVersion.SetAttribute("appName", "xl"); //We write "xl" here to ensure compatibility with Excel. + fileVersion.SetAttribute("lastEdited", "7"); //Set the last edited version to the latest known version. This will make sure that Excel does not downgrade the file and that new features are supported. + fileVersion.SetAttribute("lowestEdited", "7"); //Set the lowest edited version to the latest known version. This will make sure that Excel does not downgrade the file and that new features are supported. + } + /// + /// To support functions introduced in newer versions of Excel, we need to make sure that the file version and calculation engine version are set to the latest known version and that the calculation features are included in the file. + /// This method ensures that this is the case. + /// It is called when creating a new workbook and when loading a template. + /// + internal void EnsureCalculationFeatures() + { + var fileVersion= (XmlElement)GetNode("d:fileVersion"); + if(fileVersion == null) + { + fileVersion=CreateNode("d:fileVersion") as XmlElement; + } + UpdateFileVersionAttributes(fileVersion); + + var calcPr = (XmlElement)CreateNode("d:calcPr"); + calcPr.SetAttribute("calcId", "191029"); //Set the version of the calc engine to the latest known version. This will make sure that Excel does not downgrade the calculation engine and that new functions are supported. + + var extLst = (XmlElement)GetNode("d:extLst"); + if(extLst == null) + { + extLst = CreateNode("d:extLst") as XmlElement; + AddCalculationFeatures(extLst); + } + else + { + var calcFeatures = (XmlElement)GetNode("d:extLst/d:ext[@uri='{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}']/xcalcf:calcFeatures"); + if(calcFeatures==null) + { + var extNode = (XmlElement)CreateNode("d:extLst/d:ext", false, true); + extNode.SetAttribute("uri", "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"); + calcFeatures = _workbookXml.CreateElement("xcalcf", "calcFeatures", Schemas.schemaCalcFeature); + extNode.AppendChild(calcFeatures); + } + + foreach (string name in _calcFeatureStrings) + { + if (calcFeatures.SelectSingleNode($"xcalcf:feature[@name=\"{name}\"]", NameSpaceManager) == null) + { + XmlElement feature = _workbookXml.CreateElement("xcalcf", "feature", Schemas.schemaCalcFeature); + feature.SetAttribute("name", name); + calcFeatures.AppendChild(feature); + } + } + } + } #endregion #region StylesXml private XmlDocument _stylesXml; @@ -1466,7 +1564,9 @@ internal void Save() // Workbook Save DeleteCalcChain(); SetXmlNodeBool("d:calcPr/@fullPrecision", FullPrecision, false); - + + if(_workbookCreatedInEPPlus == false) EnsureCalculationFeatures(); //Ensure that the calculation features are included in the file to make sure that new functions are supported. + if (_vba == null && !_package.ZipPackage.PartExists(new Uri(ExcelVbaProject.PartUri, UriKind.Relative))) { if (Part.ContentType != ContentTypes.contentTypeWorkbookDefault && diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs index d8ccdb85e..0fe297050 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs @@ -56,7 +56,7 @@ internal static RpnOptimizedDependencyChain Execute(ExcelWorkbook wb, ExcelCalcu } } ExecuteChain(depChain, wb.Names, options, true); - + return depChain; } internal static RpnOptimizedDependencyChain Execute(ExcelWorksheet ws, ExcelCalculationOption options) diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/Text/ArrayToText.cs b/src/EPPlus/FormulaParsing/Excel/Functions/Text/ArrayToText.cs index 242f4dd59..d2b5d214b 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/Text/ArrayToText.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/Text/ArrayToText.cs @@ -98,7 +98,7 @@ public override CompileResult Execute(IList arguments, Parsing } return CreateResult(resultStr, DataType.String); } - + public override string NamespacePrefix => "_xlfn."; private static string GetStringVal(object val, int format) { string strVal = string.Empty; 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."); + } + } } } diff --git a/src/EPPlusTest/Issues/DrawingIssues.cs b/src/EPPlusTest/Issues/DrawingIssues.cs index 00146aad9..8d52b8b5b 100644 --- a/src/EPPlusTest/Issues/DrawingIssues.cs +++ b/src/EPPlusTest/Issues/DrawingIssues.cs @@ -196,6 +196,26 @@ public void i2303() SaveAndCleanup(package); } } + [TestMethod] + public void s1045() + { + using var p = OpenTemplatePackage("s1045.xlsx"); + var sourceSheet = p.Workbook.Worksheets[1]; + + using var destPackage = OpenPackage("s1045-copy.xlsx", true); + var destSheet = destPackage.Workbook.Worksheets.Add("Dest"); + + + + // This line will throw System.NullReferenceException in EPPlus + + // at OfficeOpenXml.Drawing.ExcelDrawing.GetToRowFromPixels -> GetFromBounds -> GetAddress -> CopyDrawings -> Copy + + sourceSheet.Cells.Copy(destSheet.Cells[1, 1], ExcelRangeCopyOptionFlags.ExcludeFormulas); + + Console.WriteLine("Done (no crash)."); + SaveAndCleanup(destPackage); + } } }