diff --git a/src/EPPlus.Interfaces/Drawing/Text/ITextMeasurer.cs b/src/EPPlus.Interfaces/Drawing/Text/ITextMeasurer.cs
index f148a3062b..7b17e4adbb 100644
--- a/src/EPPlus.Interfaces/Drawing/Text/ITextMeasurer.cs
+++ b/src/EPPlus.Interfaces/Drawing/Text/ITextMeasurer.cs
@@ -11,6 +11,8 @@ Date Author Change
1/4/2021 EPPlus Software AB EPPlus Interfaces 1.0
*************************************************************************************************/
+using System;
+
namespace OfficeOpenXml.Interfaces.Drawing.Text
{
///
@@ -31,9 +33,15 @@ public interface ITextMeasurer
///
TextMeasurement MeasureText(string text, MeasurementFont font);
///
- /// If the text measurer should measure wrap text cells.
- /// Only CR, LF or CRLF should be considered.
+ /// If the text measurer should measure wrapped text cells.
///
+ [Obsolete("Use WrappedTextAutofitMode instead. This property will be removed in a future major version.")]
bool MeasureWrappedTextCells { get; set; }
+
+ ///
+ /// Determines how cells with WrapText enabled are measured when calculating
+ /// column width in AutoFitColumns. Default is .
+ ///
+ eWrappedTextAutofitMode WrappedTextAutofitMode { get; set; }
}
}
diff --git a/src/EPPlus.Interfaces/Drawing/Text/eWrappedTextAutofitMode.cs b/src/EPPlus.Interfaces/Drawing/Text/eWrappedTextAutofitMode.cs
new file mode 100644
index 0000000000..812d4b2541
--- /dev/null
+++ b/src/EPPlus.Interfaces/Drawing/Text/eWrappedTextAutofitMode.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
+ *************************************************************************************************
+ 1/4/2021 EPPlus Software AB Added wrapped text autofit mode
+ *************************************************************************************************/
+namespace OfficeOpenXml.Interfaces.Drawing.Text
+{
+ ///
+ /// Determines how cells with WrapText enabled are measured when calculating
+ /// column width in AutoFitColumns.
+ ///
+ public enum eWrappedTextAutofitMode
+ {
+ ///
+ /// Cells with WrapText enabled are ignored when calculating column width.
+ /// This is the default and matches the behaviour of earlier versions.
+ ///
+ Skip = 0,
+ ///
+ /// The entire cell text is measured as a single line, ignoring wrapping.
+ ///
+ FullText = 1,
+ ///
+ /// The text is split on explicit line breaks (CR, LF, CRLF) and the width
+ /// of the widest resulting line determines the cell's contribution to the
+ /// column width.
+ ///
+ SplitNewLine = 2,
+ ///
+ /// The text is split on whitespace (space, tab, and line breaks) and on
+ /// hyphen characters (U+002D hyphen-minus and U+2010 hyphen). The width of
+ /// the widest resulting segment determines the cell's contribution to the
+ /// column width. Hyphens are visible and their width is included in the
+ /// segment they terminate; whitespace is not. Note: this yields the minimum
+ /// column width at which no single word overflows, and is not a simulation
+ /// of Excel's line wrapping.
+ ///
+ SplitWord = 3
+ }
+}
diff --git a/src/EPPlus.System.Drawing/Drawing/Text/SystemDrawingTextMeasurer.cs b/src/EPPlus.System.Drawing/Drawing/Text/SystemDrawingTextMeasurer.cs
index 303887c0a3..9e2031ace9 100644
--- a/src/EPPlus.System.Drawing/Drawing/Text/SystemDrawingTextMeasurer.cs
+++ b/src/EPPlus.System.Drawing/Drawing/Text/SystemDrawingTextMeasurer.cs
@@ -12,11 +12,19 @@ public class SystemDrawingTextMeasurer : ITextMeasurer, IDisposable
/// If the text measurer should measure wrap text cells.
/// Only CR, LF or CRLF should be considered.
///
+#pragma warning disable 618
public bool MeasureWrappedTextCells
{
get;
set;
}
+#pragma warning restore 618
+ public eWrappedTextAutofitMode WrappedTextAutofitMode
+ {
+ get;
+ set;
+ }
+
public SystemDrawingTextMeasurer()
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT &&
diff --git a/src/EPPlus/Core/AutofitHelper.cs b/src/EPPlus/Core/AutofitHelper.cs
index 968f1abae7..9c97dae441 100644
--- a/src/EPPlus/Core/AutofitHelper.cs
+++ b/src/EPPlus/Core/AutofitHelper.cs
@@ -147,7 +147,7 @@ internal void AutofitColumn(double MinimumWidth, double MaximumWidth)
{
var cellStyleId = styles.CellXfs[cell.StyleID];
if (cell.Merge == true) continue;
- if (cellStyleId.WrapText && _textSettings.MeasureWrappedTextCells == false) continue;
+ if (cellStyleId.WrapText && _textSettings.WrappedTextAutofitMode == eWrappedTextAutofitMode.Skip) continue;
currentMaxWidth = GetTextLength(cell, textLengthCache, styles, cellStyleId, normalSize, MaximumWidth, currentMaxWidth);
if (currentMaxWidth >= MaximumWidth)
{
diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs
index 990dc86d1f..f72b29d464 100644
--- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs
+++ b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurer.cs
@@ -23,11 +23,24 @@ internal class GenericFontMetricsTextMeasurer : GenericFontMetricsTextMeasurerBa
/// If the text measurer should measure wrap text cells.
/// Only CR, LF or CRLF should be considered.
///
+#pragma warning disable 618
public bool MeasureWrappedTextCells
-{
+ {
get;
set;
}
+#pragma warning restore 618
+ ///
+ ///
+ ///
+ ///
+ public eWrappedTextAutofitMode WrappedTextAutofitMode
+ {
+ get;
+ set;
+ }
+
+
///
/// Measures the supplied text
///
@@ -38,7 +51,7 @@ public TextMeasurement MeasureText(string text, MeasurementFont font)
{
var fontKey = GetKey(font.FontFamily, font.Style);
if (!IsValidFont(fontKey)) return TextMeasurement.Empty;
- return MeasureTextInternal(text, fontKey, font.Style, font.Size, MeasureWrappedTextCells);
+ return MeasureTextInternal(text, fontKey, font.Style, font.Size, WrappedTextAutofitMode);
}
public bool ValidForEnvironment()
diff --git a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs
index 5ceab9145e..ca0c34a0cb 100644
--- a/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs
+++ b/src/EPPlus/Core/Worksheet/Fonts/GenericFontMetrics/GenericFontMetricsTextMeasurerBase.cs
@@ -47,27 +47,53 @@ internal protected bool IsValidFont(uint fontKey)
return _fonts.ContainsKey(fontKey);
}
- internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey, MeasurementFontStyles style, float size, bool wrapText = false)
+ internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey, MeasurementFontStyles style, float size, eWrappedTextAutofitMode mode = eWrappedTextAutofitMode.Skip)
{
var sFont = _fonts[fontKey];
+
+ // Width of the current segment (a "segment" is a line in SplitNewLine mode,
+ // or a word in SplitWord mode). In FullText/Skip the whole text is one segment.
var width = 0f;
- var maxWidth = 0f;
var widthEA = 0f;
+
+ // Width of the widest segment seen so far.
+ var maxWidth = 0f;
+ var maxWidthEA = 0f;
+
for (var x = 0; x < text.Length; x++)
{
var fnt = sFont;
var c = text[x];
- if(wrapText && (c=='\n' || c=='\r'))
+
+ if (IsSegmentBoundary(c, mode))
{
- if(x>0 && c=='\r' && text[x-1]=='\n')
+ // A CRLF pair is a single line break, not two.
+ if (x > 0 && c == '\r' && text[x - 1] == '\n')
{
- continue; //CRLF should be handled as one new line.
+ continue;
}
- if(width>maxWidth)
+
+ // A visible boundary character (hyphen) remains at the end of the
+ // segment it terminates, so its own width is added before the break.
+ if (IsVisibleBoundary(c) && sFont.CharMetrics.ContainsKey(c))
+ {
+ width += fnt.ClassWidths[sFont.CharMetrics[c]];
+ }
+
+ // Close the current segment: keep it if it is the widest so far.
+ if ((width + widthEA) > (maxWidth + maxWidthEA))
{
maxWidth = width;
- width = 0;
+ maxWidthEA = widthEA;
}
+
+ // Start a new, empty segment.
+ width = 0f;
+ widthEA = 0f;
+
+ // The boundary character itself is not part of the next segment.
+ // (Visible boundaries were already counted into the closed segment above.)
+ continue;
}
//If east Asian char use default regardless of actual font.
@@ -83,16 +109,23 @@ internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey
if (Char.IsDigit(c)) fw *= FontScaleFactors.DigitsScalingFactor;
width += fw;
}
- else if (char.IsControl(c)==false)
+ else if (char.IsControl(c) == false)
{
width += sFont.ClassWidths[fnt.DefaultWidthClass];
}
}
}
- if(maxWidth > width)
+
+ // Close the final segment.
+ if ((width + widthEA) > (maxWidth + maxWidthEA))
{
- width = maxWidth;
+ maxWidth = width;
+ maxWidthEA = widthEA;
}
+
+ width = maxWidth;
+ widthEA = maxWidthEA;
+
width *= size;
widthEA *= size;
var sf = _fontScaleFactors.GetScaleFactor(fontKey, width);
@@ -102,6 +135,35 @@ internal protected TextMeasurement MeasureTextInternal(string text, uint fontKey
return new TextMeasurement(width, height);
}
+ ///
+ /// Returns true if the character ends the current measurement segment for the given mode.
+ ///
+ private static bool IsSegmentBoundary(char c, eWrappedTextAutofitMode mode)
+ {
+ switch (mode)
+ {
+ case eWrappedTextAutofitMode.SplitNewLine:
+ return c == '\n' || c == '\r';
+ case eWrappedTextAutofitMode.SplitWord:
+ return c == '\n' || c == '\r' || c == ' ' || c == '\t'
+ || c == '\u002D' // hyphen-minus
+ || c == '\u2010'; // hyphen
+ default:
+ // FullText and Skip: the whole string is a single segment.
+ return false;
+ }
+ }
+
+ ///
+ /// Returns true if the boundary character is visible and therefore contributes
+ /// its own width to the segment it terminates (hyphens). Whitespace and line
+ /// breaks are invisible and contribute no width.
+ ///
+ private static bool IsVisibleBoundary(char c)
+ {
+ return c == '\u002D' || c == '\u2010';
+ }
+
static Dictionary AlphabetChars = new Dictionary
{
{'a', 0x06 },
diff --git a/src/EPPlus/ExcelTextSettings.cs b/src/EPPlus/ExcelTextSettings.cs
index 52fa9a222d..afcb8a516b 100644
--- a/src/EPPlus/ExcelTextSettings.cs
+++ b/src/EPPlus/ExcelTextSettings.cs
@@ -41,7 +41,7 @@ public ITextMeasurer PrimaryTextMeasurer
set
{
_primaryTextMeasurer = value;
- _primaryTextMeasurer.MeasureWrappedTextCells = _measureWrappedTextCells;
+ _primaryTextMeasurer.WrappedTextAutofitMode = WrappedTextAutofitMode;
}
}
///
@@ -56,7 +56,10 @@ public ITextMeasurer FallbackTextMeasurer
set
{
_fallbackTextMeasurer = value;
- _fallbackTextMeasurer.MeasureWrappedTextCells = _measureWrappedTextCells;
+ if(value != null)
+ {
+ _fallbackTextMeasurer.WrappedTextAutofitMode = WrappedTextAutofitMode;
+ }
}
}
///
@@ -87,22 +90,25 @@ public ITextMeasurer GenericTextMeasurer
/// Measures a text with default settings when there is no other option left...
///
internal DefaultTextMeasurer DefaultTextMeasurer { get; set; }
+
+ private eWrappedTextAutofitMode _wrappedTextAutofitMode = eWrappedTextAutofitMode.Skip;
+
///
- /// Should return true if the text measurer should measure wrap text cells. Only CR, LF or CRLF should be considered
+ /// Determines how cells with enabled are measured
+ /// when calculating column width in AutoFitColumns. The default is ,
+ /// which ignores wrapped cells during autofit.
///
- /// True if the measurer can be .
- bool _measureWrappedTextCells=false;
- internal bool MeasureWrappedTextCells
- {
+ public eWrappedTextAutofitMode WrappedTextAutofitMode
+ {
get
{
- return _measureWrappedTextCells;
+ return _wrappedTextAutofitMode;
}
set
{
- _measureWrappedTextCells = value;
- PrimaryTextMeasurer.MeasureWrappedTextCells = value;
- if (FallbackTextMeasurer != null) FallbackTextMeasurer.MeasureWrappedTextCells = value;
+ _wrappedTextAutofitMode = value;
+ PrimaryTextMeasurer.WrappedTextAutofitMode = value;
+ if(FallbackTextMeasurer != null) FallbackTextMeasurer.WrappedTextAutofitMode = value;
}
}
}
diff --git a/src/EPPlusTest/AutofitTests.cs b/src/EPPlusTest/AutofitTests.cs
new file mode 100644
index 0000000000..dca8b2654f
--- /dev/null
+++ b/src/EPPlusTest/AutofitTests.cs
@@ -0,0 +1,368 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlusTest
+{
+ [TestClass]
+ public class AutofitTests : TestBase
+ {
+ static ExcelPackage _pck;
+ [ClassInitialize]
+ public static void Init(TestContext context)
+ {
+ InitBase();
+ _pck = OpenPackage("Worksheet.xlsx", true);
+ }
+ [ClassCleanup]
+ public static void Cleanup()
+ {
+ var dirName = _pck.File.DirectoryName;
+ var fileName = _pck.File.FullName;
+
+ SaveAndCleanup(_pck);
+ if (File.Exists(fileName))
+ {
+ File.Copy(fileName, dirName + "\\WorksheetRead.xlsx", true);
+ }
+ }
+
+ [TestMethod]
+ public void AutoFitColumns()
+ {
+ var ws = _pck.Workbook.Worksheets.Add("Autofit");
+ ws.Cells["A1:H1"].Value = "Auto fit column that is veeery long...";
+ ws.Cells["A1:H1"].Style.Font.Name = "Arial";
+ ws.Cells["B1"].Style.TextRotation = 30;
+ ws.Cells["C1"].Style.TextRotation = 45;
+ ws.Cells["D1"].Style.TextRotation = 75;
+ ws.Cells["E1"].Style.TextRotation = 90;
+ ws.Cells["F1"].Style.TextRotation = 120;
+ ws.Cells["G1"].Style.TextRotation = 135;
+ ws.Cells["H1"].Style.TextRotation = 180;
+ ws.Cells["A1:H1"].AutoFitColumns(0);
+
+ ws.Column(40).AutoFit();
+ }
+ [TestMethod]
+ public void AutoFitColumn()
+ {
+ var ws = _pck.Workbook.Worksheets.Add("Autofit2");
+ ws.Cells["A1:A10"].Value = "Auto fit column that is veeery long...";
+ ws.Cells["A1:A10"].Style.Font.Name = "Arial";
+ ws.Columns[1].AutoFit();
+ }
+
+ [TestMethod]
+ public void AutoFitColumnTest()
+ {
+ var p = OpenTemplatePackage("AutoFitWorkbook.xlsx");
+ var ws = p.Workbook.Worksheets[0];
+ var start = DateTime.Now;
+ ws.Columns[1].AutoFit();
+ var end = DateTime.Now;
+ TimeSpan span = end - start;
+ 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 Autofit_Skip()
+ {
+ // Skip: a WrapText cell must not contribute to the column width at all.
+ // Column A holds a long wrapped cell; column B holds nothing. Under Skip the
+ // wrapped cell is ignored, so both columns end up at the same (default) width.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.Skip;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Line 1\nLine 2 is a bit longer\nLine 3";
+ ws.Cells["A1"].Style.WrapText = true;
+ // Act
+ ws.Cells["A1:B1"].AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "Skip should ignore the wrapped cell, so column A matches the empty column B");
+ }
+
+ [TestMethod]
+ public void Autofit_FullText()
+ {
+ // FullText: the entire cell text is measured as a single line.
+ // The reference cell B holds the same text; because a WrapText cell keeps its
+ // newlines (measured as zero width), B must use identical newline placement so
+ // both cells measure the exact same visible characters.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.FullText;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Line 1\nLine 2 is a bit longer\nLine 3";
+ ws.Cells["B1"].Value = "Line 1\nLine 2 is a bit longer\nLine 3";
+ ws.Cells["A1:B1"].Style.WrapText = true;
+ // Act
+ ws.Cells.AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "FullText should measure the entire string, matching the identical reference cell");
+ }
+
+ [TestMethod]
+ public void Autofit_SplitNewLine()
+ {
+ // SplitNewLine: the widest newline-separated line drives the width.
+ // Reference cell B holds that line ("Line 2 is a bit longer") on its own.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitNewLine;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Line 1\nLine 2 is a bit longer\nLine 3";
+ ws.Cells["B1"].Value = "Line 2 is a bit longer";
+ ws.Cells["A1:B1"].Style.WrapText = true;
+ // Act
+ ws.Cells.AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "Column A (widest line) should match column B (that line in full)");
+ }
+
+ [TestMethod]
+ public void Autofit_SplitWord()
+ {
+ // SplitWord: the widest whitespace/hyphen-separated segment drives the width.
+ // Reference cell B holds that word ("aVeryLongWord") on its own.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitWord;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "short longer aVeryLongWord medium";
+ ws.Cells["B1"].Value = "aVeryLongWord";
+ ws.Cells["A1:B1"].Style.WrapText = true;
+ // Act
+ ws.Cells.AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "Column A (widest word) should match column B (that word in full)");
+ }
+
+ [TestMethod]
+ public void Autofit_SplitWord_HyphenIsVisibleAndBreaksTheWord()
+ {
+ // A hyphen is a visible break boundary: it terminates the preceding segment
+ // AND its own width is counted into that segment. So the widest segment of
+ // "aVeryLongWord-x" is "aVeryLongWord-" (word + trailing hyphen), not the bare word.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitWord;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "aVeryLongWord-x";
+ ws.Cells["B1"].Value = "aVeryLongWord-"; // includes the trailing hyphen
+ ws.Cells["C1"].Value = "aVeryLongWord"; // bare word, no hyphen
+ ws.Cells["A1:C1"].Style.WrapText = true;
+ // Act
+ ws.Cells.AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "Segment should include the trailing hyphen (visible boundary)");
+ Assert.IsTrue(ws.Columns[1].Width > ws.Columns[3].Width,
+ "Segment with hyphen should be wider than the bare word without it");
+ }
+
+ [TestMethod]
+ public void Autofit_SplitNewLine_CrlfCountsAsSingleBreak()
+ {
+ // A CRLF pair must be treated as one line break, not two. If it were counted
+ // as two breaks it would create an empty phantom segment between the lines,
+ // but that has zero width and would not change the result. What this guards
+ // is that the CR is not measured as a separate one-character segment and that
+ // the widest line is identified correctly across a CRLF boundary.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitNewLine;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Short\r\nLine 2 is a bit longer\r\nShort";
+ ws.Cells["B1"].Value = "Line 2 is a bit longer";
+ ws.Cells["A1:B1"].Style.WrapText = true;
+ // Act
+ ws.Cells.AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "CRLF should be treated as a single line break; widest line should match column B");
+ }
+
+ [TestMethod]
+ public void Autofit_SplitNewLine_WidestSegmentFirstIsStillChosen()
+ {
+ // The widest segment appears FIRST here. This guards against a reset bug where
+ // the running width of an earlier (wider) segment fails to carry over into the
+ // max comparison once a later, narrower segment is measured.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitNewLine;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Line 1 is clearly the longest\nShort\nAlso short";
+ ws.Cells["B1"].Value = "Line 1 is clearly the longest";
+ ws.Cells["A1:B1"].Style.WrapText = true;
+ // Act
+ ws.Cells.AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "The widest line is the first one and must still drive the column width");
+ }
+
+ [TestMethod]
+ public void Autofit_SplitNewLine_EastAsianWidthResetsPerLine()
+ {
+ // Regression test for the East Asian width bug: previously the EA width (widthEA)
+ // accumulated across ALL lines and was never reset at a line break, so a multi-line
+ // CJK cell was measured as the SUM of every line's EA width instead of the widest
+ // single line. Here the three lines are 2, 5 and 3 hiragana characters; the correct
+ // result is the width of the 5-character line. With the bug it would be roughly the
+ // width of all 10 characters combined.
+ using var package = new ExcelPackage();
+ package.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitNewLine;
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "\u3042\u3042\n\u3042\u3042\u3042\u3042\u3042\n\u3042\u3042\u3042"; // , ,
+ ws.Cells["B1"].Value = "\u3042\u3042\u3042\u3042\u3042"; // (widest line)
+ ws.Cells["A1:B1"].Style.WrapText = true;
+ // Act
+ ws.Cells.AutoFitColumns();
+ // Assert
+ Assert.AreEqual(ws.Columns[2].Width, ws.Columns[1].Width, 0.0001d,
+ "Multi-line CJK column should match its widest line, not the sum of all lines");
+ }
+
+ [TestMethod]
+ public void AutoFitTestWithDifferenLengths()
+ {
+ using (var p = OpenPackage("SimpleAutofitTests.xlsx", true))
+ {
+ //p.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitWord;
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Little";
+ ws.Cells["A2"].Value = "MEDIUM";
+ ws.Cells["A3"].Value = "Largeeeeeesssst";
+ ws.Cells["A4"].Value = "Larg-ish";
+
+ ws.Cells["B1"].Value = "I should not be autofit";
+
+ var bWidth = ws.Columns[2].Width;
+
+ ws.Cells["A1:A4"].AutoFitColumns();
+
+ var bWidthAfter = ws.Columns[2].Width;
+
+ //Untouched cols should remain untouched
+ Assert.AreEqual(bWidth, bWidthAfter);
+
+ ws.Cells["A5"].Value = "Very large but outside the range of what should be fitted";
+
+ var widthBeforeA = ws.Columns[1].Width;
+
+ ws.Cells["A1:A4"].AutoFitColumns();
+
+ var widthAfterA = ws.Columns[1].Width;
+
+
+ //Untouched cells within same column Probaly should not change the column
+ //technically different from excel but also different syntax
+ Assert.AreEqual(widthBeforeA, widthAfterA);
+
+
+ ws.Cells.AutoFitColumns();
+
+ //Doing all cells should however
+ Assert.AreNotEqual(widthAfterA, ws.Columns[1].Width);
+ Assert.IsTrue(ws.Columns[1].Width > widthAfterA);
+
+ SaveAndCleanup(p);
+ }
+ }
+
+ [TestMethod]
+ public void AutofitOneCellCompoundingConfigs()
+ {
+ using (var p = OpenPackage("AutofitCompoundOneCell.xlsx", true))
+ {
+ //p.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitWord;
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "aaaaaaaaaaaaaaaaa";
+
+ ws.Cells["A1"].AutoFitColumns();
+
+ ws.Cells["A1"].Value = "aaaaaaaaaaaaaaaaaaaaaaaa";
+ ws.Cells["A1"].Style.WrapText = true;
+
+ ws.Cells["A1"].AutoFitColumns();
+
+ var colWidth = ws.Column(1).Width;;
+
+ SaveAndCleanup(p);
+
+ //Does not appear to match output file
+ //Might still be correct bc OS margins etc.
+ Assert.AreEqual(8.43d, ws.Column(1).Width);
+ }
+ }
+ }
+}
diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/ImageFunctionTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/ImageFunctionTests.cs
index eff68ac678..a4d6608a54 100644
--- a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/ImageFunctionTests.cs
+++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/ImageFunctionTests.cs
@@ -134,5 +134,19 @@ public void ImageTest_ShouldCacheUrls1()
Assert.AreEqual(1, httpsService.NumberOfCalls);
}
+
+ [TestMethod]
+ public void ImageTest_ShouldReturnNameErrorWhenServiceIsNull()
+ {
+ using var package = new ExcelPackage();
+ var sheet = package.Workbook.Worksheets.Add("Sheet1");
+ sheet.Cells["A1"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\", \"Alt text\", 1)";
+
+ package.Settings.ImageFunctionService = null;
+
+ sheet.Calculate();
+
+ Assert.AreEqual(ExcelErrorValue.Create(eErrorType.Name), sheet.Cells["A1"].Value);
+ }
}
}
diff --git a/src/EPPlusTest/Issues/DefinedNameIssues.cs b/src/EPPlusTest/Issues/DefinedNameIssues.cs
index 2ac08c50fd..15f7b8e221 100644
--- a/src/EPPlusTest/Issues/DefinedNameIssues.cs
+++ b/src/EPPlusTest/Issues/DefinedNameIssues.cs
@@ -253,3 +253,4 @@ static void RunTest(string name, Func<(ExcelPackage pkg, ExcelWorksheet ws1, Exc
}
}
}
+
diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs
index c767615ccf..c777640838 100644
--- a/src/EPPlusTest/Issues/WorksheetIssues.cs
+++ b/src/EPPlusTest/Issues/WorksheetIssues.cs
@@ -7,6 +7,7 @@
using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical;
using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.Interfaces.Drawing.Text;
using OfficeOpenXml.RichData;
using OfficeOpenXml.SystemDrawing.Image;
using OfficeOpenXml.SystemDrawing.Text;
@@ -880,16 +881,16 @@ private static void AddMeasureSheet(ExcelPackage p, ExcelWorksheet ws)
ws.Cells["B2"].Value = multiLineText;
- p.Settings.TextSettings.MeasureWrappedTextCells = true;
- // AutoFitColumns - calculates width as if there were no line breaks.
+ p.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.SplitNewLine;
+ // AutoFitColumns - measures the widest newline-separated line.
ws.Cells["A1:B2"].AutoFitColumns();
- p.Settings.TextSettings.MeasureWrappedTextCells = false;
+ p.Settings.TextSettings.WrappedTextAutofitMode = eWrappedTextAutofitMode.Skip;
ws.Cells["C1"].Value = multiLineText;
ws.Cells["C1"].Style.WrapText = true;
ws.Cells["D2"].Value = multiLineText;
- // AutoFitColumns - calculates width as if there were no line breaks.
+ // AutoFitColumns - wrapped cells are ignored.
ws.Cells["C1:D2"].AutoFitColumns();
}
diff --git a/src/EPPlusTest/WorkSheetTests.cs b/src/EPPlusTest/WorkSheetTests.cs
index f9fe27debb..c091bbc84d 100644
--- a/src/EPPlusTest/WorkSheetTests.cs
+++ b/src/EPPlusTest/WorkSheetTests.cs
@@ -2143,105 +2143,7 @@ public void BuildInStyles()
ws.Cells["a1:c3"].StyleName = "Normal";
// n.CustomBuildin = true;
}
- [TestMethod]
- public void AutoFitColumns()
- {
- var ws = _pck.Workbook.Worksheets.Add("Autofit");
- ws.Cells["A1:H1"].Value = "Auto fit column that is veeery long...";
- ws.Cells["A1:H1"].Style.Font.Name = "Arial";
- ws.Cells["B1"].Style.TextRotation = 30;
- ws.Cells["C1"].Style.TextRotation = 45;
- ws.Cells["D1"].Style.TextRotation = 75;
- ws.Cells["E1"].Style.TextRotation = 90;
- ws.Cells["F1"].Style.TextRotation = 120;
- ws.Cells["G1"].Style.TextRotation = 135;
- ws.Cells["H1"].Style.TextRotation = 180;
- ws.Cells["A1:H1"].AutoFitColumns(0);
-
- ws.Column(40).AutoFit();
- }
- [TestMethod]
- public void AutoFitColumn()
- {
- var ws = _pck.Workbook.Worksheets.Add("Autofit2");
- ws.Cells["A1:A10"].Value = "Auto fit column that is veeery long...";
- ws.Cells["A1:A10"].Style.Font.Name = "Arial";
- ws.Columns[1].AutoFit();
- }
- [TestMethod]
- public void AutoFitColumnTest()
- {
- var p = OpenTemplatePackage("AutoFitWorkbook.xlsx");
- var ws = p.Workbook.Worksheets[0];
- var start = DateTime.Now;
- ws.Columns[1].AutoFit();
- var end = DateTime.Now;
- TimeSpan span = end - start;
- 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()
{