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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/EPPlus.Interfaces/Drawing/Text/ITextMeasurer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,55 @@ public interface ITextMeasurer
TextMeasurement MeasureText(string text, MeasurementFont font);
/// <summary>
/// If the text measurer should measure wrap text cells.
/// Only CR, LF or CRLF should be considered.
/// Line breaks are considered on explicit newlines (CR, LF, CRLF) as well as soft wrap boundaries (spaces, tabs, and hyphens).
/// </summary>
bool MeasureWrappedTextCells { get; set; }
}

/// <summary>
/// Extension interface for advanced text measurement including cell-specific wrapping and last line padding.
/// </summary>
public interface IWrapTextMeasurer : ITextMeasurer
{
/// <summary>
/// Measures the supplied text with cell-specific wrapping and last-line padding
/// </summary>
/// <param name="text">The text to measure</param>
/// <param name="font">Font of the text to measure</param>
/// <param name="wrapText">Whether word wrap is enabled for this cell</param>
/// <param name="lastLinePadding">Padding in pixels to add only to the last wrapped line (e.g. for autofilter arrows)</param>
/// <returns>A <see cref="TextMeasurement"/></returns>
TextMeasurement MeasureText(string text, MeasurementFont font, bool wrapText, float lastLinePadding);
}

/// <summary>
/// High-compatibility extension methods for <see cref="ITextMeasurer"/>
/// </summary>
public static class TextMeasurerExtensions
{
/// <summary>
/// Measures the supplied text with cell-specific wrapping and last-line padding, using the advanced interface if supported, or falling back to interface-level properties.
/// </summary>
public static TextMeasurement MeasureText(this ITextMeasurer measurer, string text, MeasurementFont font, bool wrapText, float lastLinePadding)
{
var wrapMeasurer = measurer as IWrapTextMeasurer;
if (wrapMeasurer != null)
{
return wrapMeasurer.MeasureText(text, font, wrapText, lastLinePadding);
}

var prev = measurer.MeasureWrappedTextCells;
try
{
measurer.MeasureWrappedTextCells = wrapText;
var measurement = measurer.MeasureText(text, font);
measurement.Width += lastLinePadding;
return measurement;
}
finally
{
measurer.MeasureWrappedTextCells = prev;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace OfficeOpenXml.SystemDrawing.Text
public class SystemDrawingTextMeasurer : ITextMeasurer, IDisposable
{
/// <summary>
/// If the text measurer should measure wrap text cells.
/// Only CR, LF or CRLF should be considered.
/// If the text measurer should measure wrap text cells during AutoFit calculations.
/// Line breaks are considered on explicit newlines (CR, LF, CRLF) as well as soft wrap boundaries (spaces, tabs, and hyphens).
/// </summary>
public bool MeasureWrappedTextCells
{
Expand Down
85 changes: 65 additions & 20 deletions src/EPPlus/Core/AutofitHelper.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*************************************************************************************************
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
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
Expand All @@ -24,7 +24,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;
// Set to 22d (17 pixels physical dropdown button width + 5 pixels extra padding to prevent text from sticking).
private const double AutoFilterArrowWidthPixels = 22d;
private ExcelRangeBase _range;
ITextMeasurer _genericMeasurer = new GenericFontMetricsTextMeasurer();
MeasurementFont _nonExistingFont = new MeasurementFont() { FontFamily = FontSize.NonExistingFont };
Expand Down Expand Up @@ -111,7 +112,7 @@ internal void AutofitColumn(double MinimumWidth, double MaximumWidth)
afAddr[afAddr.Count - 1]._ws = _range.WorkSheetName;
}
}

for (int col = fromCol; col <= toCol; col++)
{
if (worksheet.Column(col).Hidden) //Issue 15338
Expand All @@ -130,13 +131,16 @@ internal void AutofitColumn(double MinimumWidth, double MaximumWidth)
{
var cell = worksheet.Cells[af._fromRow, col];
var cellStyleId = styles.CellXfs[cell.StyleID];
currentMaxWidth = GetTextLength(cell, textLengthCache, styles, cellStyleId, normalSize, MaximumWidth, currentMaxWidth);
if (cellStyleId.WrapText && _textSettings.MeasureWrappedTextCells == false) continue;

// 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;
// element (17px at 96 DPI + 5px padding). Excel renders this dropdown arrow *only* on the last
// line of a header cell. Preceding lines in a wrapped cell can overlap into the
// dropdown area. To avoid inflating the column width unnecessarily, we pass this
// 22px padding as `lastLinePadding` so that the text measurer only adds it to
// the last wrapped line of the cell.
float lastLinePadding = (float)AutoFilterArrowWidthPixels;
currentMaxWidth = GetTextLength(cell, textLengthCache, styles, cellStyleId, normalSize, MaximumWidth, currentMaxWidth, lastLinePadding);
if (currentMaxWidth >= MaximumWidth)
{
currentMaxWidth = MaximumWidth;
Expand Down Expand Up @@ -164,7 +168,19 @@ internal void AutofitColumn(double MinimumWidth, double MaximumWidth)
worksheet._package.DoAdjustDrawings = doAdjust;
}

private double GetTextLength(ExcelRangeBase cell, Dictionary<MeasurementFont, int> textLengthCache, ExcelStyles styles, Style.XmlAccess.ExcelXfs cellStyleId, float normalSize, double MaximumWidth, double currentMaxWidth)
/// <summary>
/// Calculates the text width of a cell and updates the current maximum column width
/// </summary>
/// <param name="cell">The cell to measure</param>
/// <param name="textLengthCache">Cache for measured font lengths</param>
/// <param name="styles">The Excel styles collection</param>
/// <param name="cellStyleId">The XML style definition of the cell</param>
/// <param name="normalSize">The width of the normal font's reference char in pixels</param>
/// <param name="MaximumWidth">The maximum allowed column width</param>
/// <param name="currentMaxWidth">The currently tracked maximum width of the column</param>
/// <param name="lastLinePadding">Optional padding in pixels to apply only to the last wrapped line of text (e.g. for AutoFilter dropdown arrow). Defaults to 0f because most cells do not have an adjacent dropdown.</param>
/// <returns>The updated maximum column width</returns>
private double GetTextLength(ExcelRangeBase cell, Dictionary<MeasurementFont, int> textLengthCache, ExcelStyles styles, Style.XmlAccess.ExcelXfs cellStyleId, float normalSize, double MaximumWidth, double currentMaxWidth, float lastLinePadding = 0f)
{
var fontID = cellStyleId.FontId;
MeasurementFont measurementFont;
Expand Down Expand Up @@ -193,7 +209,7 @@ private double GetTextLength(ExcelRangeBase cell, Dictionary<MeasurementFont, in
var textForWidth = cell.TextForWidth;
var text = textForWidth + (indent > 0 && !string.IsNullOrEmpty(textForWidth) ? new string('_', indent) : "");
if (text.Length > 32000) { text = text.Substring(0, 32000); } //Issue

if(cell.Style.WrapText==false)
{
text = text.Replace("\r","").Replace("\n","");
Expand All @@ -203,7 +219,7 @@ private double GetTextLength(ExcelRangeBase cell, Dictionary<MeasurementFont, in
{
return currentMaxWidth;
}
var size = MeasureString(text, fontID, measureCache);
var size = MeasureString(text, fontID, cellStyleId.WrapText, lastLinePadding, measureCache);

double width;
double rotation = cellStyleId.TextRotation;
Expand Down Expand Up @@ -236,22 +252,51 @@ private double GetTextLength(ExcelRangeBase cell, Dictionary<MeasurementFont, in
return currentMaxWidth;
}

private TextMeasurement MeasureString(string text, int fontID, Dictionary<ulong, TextMeasurement> measureCache)
/// <summary>
/// Measures a text string using the specified font, with caching support
/// </summary>
/// <param name="text">The text to measure</param>
/// <param name="fontID">ID of the font to use</param>
/// <param name="wrapText">Whether word wrapping is enabled on the cell style</param>
/// <param name="lastLinePadding">Padding in pixels to apply only to the last line's width</param>
/// <param name="measureCache">High-performance lookup cache for measured strings</param>
/// <returns>A <see cref="TextMeasurement"/> representing the width and height</returns>
private TextMeasurement MeasureString(string text, int fontID, bool wrapText, float lastLinePadding, Dictionary<ulong, TextMeasurement> measureCache)
{
ulong key = ((ulong)((uint)text.GetHashCode()) << 32) | (uint)fontID;
// Create a high-performance 64-bit cache key based on the original key calculation.
// Original baseline:
// ulong key = ((ulong)((uint)text.GetHashCode()) << 32) | (uint)fontID;
//
// We extend this by packing wrapText and lastLinePadding flags into the high bits of the lower 32-bit word.

// 1. Shift the 32-bit string hash code into the upper 32 bits (bits 32-63)
ulong hashPart = (ulong)(uint)text.GetHashCode() << 32;

// 2. Set boolean flags in the upper two bits of the lower 32-bit word (bits 30 and 31)
ulong paddingFlag = lastLinePadding > 0f ? (1UL << 31) : 0UL;
ulong wrapFlag = wrapText ? (1UL << 30) : 0UL;

// 3. Mask the font ID to the remaining 30 bits (bits 0-29).
// This supports up to 1,073,741,823 unique fonts while preventing any bit-flag overflow.
ulong fontPart = (ulong)fontID & 0x3FFFFFFF;

// 4. Combine all parts into a single 64-bit key
ulong key = hashPart | paddingFlag | wrapFlag | fontPart;

if (!measureCache.TryGetValue(key, out var measurement))
{
var measurer = _textSettings.PrimaryTextMeasurer;
var font = _fontCache[fontID];
measurement = measurer.MeasureText(text, font);

measurement = _textSettings.PrimaryTextMeasurer.MeasureText(text, font, wrapText, lastLinePadding);

if (measurement.IsEmpty && _textSettings.FallbackTextMeasurer != null && _textSettings.FallbackTextMeasurer != _textSettings.PrimaryTextMeasurer)
{
measurer = _textSettings.FallbackTextMeasurer;
measurement = measurer.MeasureText(text, font);
measurement = _textSettings.FallbackTextMeasurer.MeasureText(text, font, wrapText, lastLinePadding);
}
if (measurement.IsEmpty && _fontWidthDefault != null)
{
measurement = MeasureGeneric(text, _textSettings, font);
measurement.Width += lastLinePadding;
}
if (!measurement.IsEmpty && _textSettings.AutofitScaleFactor != 1f)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*************************************************************************************************
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
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
Expand All @@ -17,28 +17,42 @@ Date Author Change

namespace OfficeOpenXml.Core.Worksheet.Core.Worksheet.Fonts.GenericMeasurements
{
internal class GenericFontMetricsTextMeasurer : GenericFontMetricsTextMeasurerBase, ITextMeasurer
internal class GenericFontMetricsTextMeasurer : GenericFontMetricsTextMeasurerBase, IWrapTextMeasurer
{
/// <summary>
/// If the text measurer should measure wrap text cells.
/// Only CR, LF or CRLF should be considered.
/// If the text measurer should measure wrap text cells during AutoFit calculations.
/// Line breaks are considered on explicit newlines (CR, LF, CRLF) as well as soft wrap boundaries (spaces, tabs, and hyphens).
/// </summary>
public bool MeasureWrappedTextCells
{
get;
set;
public bool MeasureWrappedTextCells
{
get;
set;
}

/// <summary>
/// Measures the supplied text
/// </summary>
/// <param name="text">The text to measure</param>
/// <param name="font">Font of the text to measure</param>
/// <returns>A <see cref="TextMeasurement"/></returns>
public TextMeasurement MeasureText(string text, MeasurementFont font)
{
return MeasureText(text, font, MeasureWrappedTextCells);
}

/// <summary>
/// Measures the supplied text with cell-specific wrapping and last-line padding
/// </summary>
/// <param name="text">The text to measure</param>
/// <param name="font">Font of the text to measure</param>
/// <param name="wrapText">Whether word wrap is enabled for this cell</param>
/// <param name="lastLinePadding">Padding in pixels to add only to the last wrapped line (e.g. for autofilter arrows)</param>
/// <returns>A <see cref="TextMeasurement"/></returns>
public TextMeasurement MeasureText(string text, MeasurementFont font, bool wrapText, float lastLinePadding = 0f)
{
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, wrapText, lastLinePadding);
}

public bool ValidForEnvironment()
Expand Down
Loading
Loading