diff --git a/src/EPPlus/Core/AutofitHelper.cs b/src/EPPlus/Core/AutofitHelper.cs index a121b3480..968f1abae 100644 --- a/src/EPPlus/Core/AutofitHelper.cs +++ b/src/EPPlus/Core/AutofitHelper.cs @@ -23,6 +23,8 @@ namespace OfficeOpenXml.Core { internal class AutofitHelper { + // Approximate width in pixels (at 96 DPI) of the autofilter dropdown arrow rendered by Excel. + private const double AutoFilterArrowWidthPixels = 15d; private ExcelRangeBase _range; ITextMeasurer _genericMeasurer = new GenericFontMetricsTextMeasurer(); MeasurementFont _nonExistingFont = new MeasurementFont() { FontFamily = FontSize.NonExistingFont }; @@ -126,9 +128,19 @@ internal void AutofitColumn(double MinimumWidth, double MaximumWidth) { if (af.Collide(fromRow, col, toRow, col) != eAddressCollition.No) { - var cell = worksheet.Cells[af.Address]; + var cell = worksheet.Cells[af._fromRow, col]; var cellStyleId = styles.CellXfs[cell.StyleID]; currentMaxWidth = GetTextLength(cell, textLengthCache, styles, cellStyleId, normalSize, MaximumWidth, currentMaxWidth); + // Reserve room for the autofilter dropdown arrow. The arrow is a fixed-size UI + // element (~15px at 96 DPI), so it is added as a constant converted to column + // width units (normalSize = width of the normal font's reference char in pixels). + // It is intentionally not affected by AutofitScaleFactor, since the arrow's + // pixel size does not shrink when the user requests tighter text margins. + currentMaxWidth += AutoFilterArrowWidthPixels / normalSize; + if (currentMaxWidth >= MaximumWidth) + { + currentMaxWidth = MaximumWidth; + } } } foreach (var cell in worksheet.Cells[fromRow, col, toRow, col]) diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index c6258b639..c767615cc 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -160,6 +160,58 @@ public void i1314() package.Dispose(); } } + + [TestMethod] + public void AutofitAutofilterTest() + { + using var package = OpenPackage("AutofitAutofilterTest.xlsx", true); + + // Two sheets with identical data: one with an autofilter, one without. + // After autofit, the filtered columns should be wider than the unfiltered + // ones by the reserved width of the dropdown arrow. + var wsFilter = package.Workbook.Worksheets.Add("WithFilter"); + var wsNoFilter = package.Workbook.Worksheets.Add("NoFilter"); + + foreach (var ws in new[] { wsFilter, wsNoFilter }) + { + // Headers are the widest text in each column - the data below is deliberately + // shorter so the column width is driven by the header (+ the dropdown arrow + // on the filtered sheet). + ws.Cells["A1"].Value = "Department"; + ws.Cells["B1"].Value = "Annual Budget"; + ws.Cells["C1"].Value = "Region Name"; + + ws.Cells["A2"].Value = "Sales"; + ws.Cells["B2"].Value = 1200; + ws.Cells["C2"].Value = "North"; + + ws.Cells["A3"].Value = "IT"; + ws.Cells["B3"].Value = 980; + ws.Cells["C3"].Value = "West"; + + ws.Cells["A4"].Value = "HR"; + ws.Cells["B4"].Value = 540; + ws.Cells["C4"].Value = "East"; + } + + // Only one sheet gets the autofilter. + wsFilter.Cells["A1:C4"].AutoFilter = true; + + wsFilter.Cells["A1:C4"].AutoFitColumns(); + wsNoFilter.Cells["A1:C4"].AutoFitColumns(); + + for (int col = 1; col <= 3; col++) + { + var filterWidth = wsFilter.Column(col).Width; + var noFilterWidth = wsNoFilter.Column(col).Width; + System.Diagnostics.Debug.WriteLine($"Column {col}: filter={filterWidth}, noFilter={noFilterWidth}"); + Assert.IsTrue(filterWidth > noFilterWidth, + $"Column {col}: filtered width ({filterWidth}) should be greater than unfiltered width ({noFilterWidth})."); + } + + SaveAndCleanup(package); + } + [TestMethod] public void i1317() { diff --git a/src/EPPlusTest/WorkSheetTests.cs b/src/EPPlusTest/WorkSheetTests.cs index ed53a0be5..f9fe27deb 100644 --- a/src/EPPlusTest/WorkSheetTests.cs +++ b/src/EPPlusTest/WorkSheetTests.cs @@ -2180,6 +2180,68 @@ public void AutoFitColumnTest() Assert.AreEqual(125d, ws.Columns[1].Width, 5d); SaveAndCleanup(p); } + + [TestMethod] + public void AutofitAutofilterTest() + { + using var package = OpenTemplatePackage("AutoFitAutofilter.xlsx"); + var ws = package.Workbook.Worksheets.Add("Sheet1"); + + // Headers are the widest text in each column - the data below is deliberately + // shorter so the column width is driven by the header + the autofilter dropdown arrow. + ws.Cells["A1"].Value = "Department"; + ws.Cells["B1"].Value = "Annual Budget"; + ws.Cells["C1"].Value = "Region Name"; + + // Data rows - all shorter than the headers above them. + ws.Cells["A2"].Value = "Sales"; + ws.Cells["B2"].Value = 1200; + ws.Cells["C2"].Value = "North"; + + ws.Cells["A3"].Value = "IT"; + ws.Cells["B3"].Value = 980; + ws.Cells["C3"].Value = "West"; + + ws.Cells["A4"].Value = "HR"; + ws.Cells["B4"].Value = 540; + ws.Cells["C4"].Value = "East"; + + // Apply autofilter across the header row + data. + ws.Cells["A1:C4"].AutoFilter = true; + + // Autofit the columns. + ws.Cells["A1:C4"].AutoFitColumns(); + + // Inspect what EPPlus actually produced for each column. + System.Diagnostics.Debug.WriteLine($"Column A (Department): {ws.Column(1).Width}"); + System.Diagnostics.Debug.WriteLine($"Column B (Annual Budget): {ws.Column(2).Width}"); + System.Diagnostics.Debug.WriteLine($"Column C (Region Name): {ws.Column(3).Width}"); + + // Save the workbook + SaveAndCleanup(package); + } + + [TestMethod] + public void AutoFitColumnsWithAutoFilter() + { + var ws = _pck.Workbook.Worksheets.Add("AutofitAutoFilter"); + ws.Cells["A1"].Value = "hour"; + ws.Cells["B1"].Value = "minute"; + ws.Cells["A2"].Value = 12; + ws.Cells["B2"].Value = 30; + + ws.Cells["A1:B2"].AutoFilter = true; + + ws.Cells["A1:B2"].AutoFitColumns(); + + // Without the fix, the AutoFilter header row range (A1:B1) is measured as a whole. + // Under the hood, worksheet.Cells["A1:B1"].TextForWidth evaluated to "System.Object[,]" (16 chars), + // which forced a minimum width of ~16.07 points. + // With the fix, the specific cell for each column in the AutoFilter is measured, + // resulting in a narrow width matching "hour" / "minute". + Assert.IsTrue(ws.Column(1).Width < 12d, $"Column 1 width should be small but was {ws.Column(1).Width}"); + Assert.IsTrue(ws.Column(2).Width < 12d, $"Column 2 width should be small but was {ws.Column(2).Width}"); + } [TestMethod] public void CopyOverwrite() {