diff --git a/src/EPPlus/Constants/Schemas.cs b/src/EPPlus/Constants/Schemas.cs index 38e7557ba0..6241b3ed30 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 2238c7ddb8..da70afee85 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 1778afb660..07f0ea368d 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) + while (pix < pixels && r <= ExcelPackage.MaxRows) { 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) + while (pix < pixels && col <= ExcelPackage.MaxColumns) { 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 && currentRow <= ExcelPackage.MaxRows) + { + 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) + while (pixOff >= 0 && row < ExcelPackage.MaxRows) { 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 && currentCol < ExcelPackage.MaxColumns) + { + 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 10dc020b79..6aec485c67 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 a681380e37..1723b3e9d6 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() @@ -1506,6 +1545,25 @@ public ExcelWorksheet Worksheet } } /// + /// Gets the range address adjusted within the worksheet dimension address. + /// If the worksheet dimension is null or the range is outside of the dimension, null will be returned. + /// If the range is partly outside the worksheet dimension it will be adjusted to fit inside the dimension. + /// + public ExcelAddressBase DimensionAdjustedAddress + { + get + { + if (_worksheet.Dimension == null) + { + return null; + } + else + { + return GetAddressDimension(); + } + } + } + /// /// Address including sheet name /// public new string FullAddress diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 96d54380b3..02b0c79d79 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 d8ccdb85ea..0fe2970505 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 242f4dd593..d2b5d214b9 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 0000000000..3057de3c87 --- /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/Core/Worksheet/WorksheetCoreTests.cs b/src/EPPlusTest/Core/Worksheet/WorksheetCoreTests.cs index eccffefcfc..242e356d38 100644 --- a/src/EPPlusTest/Core/Worksheet/WorksheetCoreTests.cs +++ b/src/EPPlusTest/Core/Worksheet/WorksheetCoreTests.cs @@ -224,6 +224,43 @@ public void ValidateDimensionValueTest() Assert.AreEqual("B6:C7", ws.DimensionByValue.Address); } } + [TestMethod] + public void ValidateAdjustedDimensionTest() + { + using (var p = new ExcelPackage()) + { + var ws = p.Workbook.Worksheets.Add("Sheet1"); + ws.Cells["A4:H10"].Style.Numberformat.Format = "0"; + ws.Cells["B6:C7"].Value = 1; + + var range = ws.Cells["A1:K100"]; + Assert.AreEqual("A4:H10", range.DimensionAdjustedAddress.Address); + } + } + [TestMethod] + public void ValidateAdjustedDimensionOutsideDimension() + { + using (var p = new ExcelPackage()) + { + var ws = p.Workbook.Worksheets.Add("Sheet1"); + + ws.Cells["B6"].Value = 1; + var range = ws.Cells["D6"]; + Assert.IsNull(range.DimensionAdjustedAddress); //If range is outside Dimension, null should be returned. + } + } + [TestMethod] + public void ValidateAdjustedDimensionEmptyTest() + { + using (var p = new ExcelPackage()) + { + var ws = p.Workbook.Worksheets.Add("Sheet1"); + + var range = ws.Cells["A1:K100"]; + Assert.IsNull(range.DimensionAdjustedAddress); //If Dimension is null, null should be returned. + } + } + [TestMethod] public void ValidateDimensionValue2Test() { diff --git a/src/EPPlusTest/Drawing/CopyDrawingTests.cs b/src/EPPlusTest/Drawing/CopyDrawingTests.cs index 171142befd..1ab47a2d51 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 00146aad97..8d52b8b5bb 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); + } } }