From 26b51fa93e19c3398bca085639ce52608c50c5c9 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Wed, 11 Mar 2026 00:01:18 +0100 Subject: [PATCH 1/6] Adds right-to-left layout exporting capability --- src/MiniExcel/OpenXml/Constants/WorksheetXml.cs | 4 ++-- .../OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs | 4 ++-- src/MiniExcel/OpenXml/OpenXmlConfiguration.cs | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs index 50cdcbc7..5e6df8f8 100644 --- a/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs @@ -16,8 +16,8 @@ internal static class WorksheetXml internal const string StartSheetViews = ""; internal const string EndSheetViews = ""; - internal static string StartSheetView( int tabSelected=0, int workbookViewId=0 ) - => $""; + internal static string StartSheetView(int tabSelected = 0, int workbookViewId = 0, bool rightToLeft = false) + => $""; internal const string EndSheetView = ""; internal const string StartSheetData = ""; diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs index 639c50e2..da5b2ef1 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs @@ -80,14 +80,14 @@ private ExcellSheetInfo GetSheetInfos(string sheetName) private string GetSheetViews() { // exit early if no style to write - if (_configuration.FreezeRowCount <= 0 && _configuration.FreezeColumnCount <= 0) + if (_configuration.FreezeRowCount <= 0 && _configuration.FreezeColumnCount <= 0 && !_configuration.RightToLeft) return string.Empty; var sb = new StringBuilder(); // start sheetViews sb.Append(WorksheetXml.StartSheetViews); - sb.Append(WorksheetXml.StartSheetView()); + sb.Append(WorksheetXml.StartSheetView(rightToLeft: _configuration.RightToLeft)); // Write panes sb.Append(GetPanes()); diff --git a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs index 898fff07..190683db 100644 --- a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs @@ -9,6 +9,7 @@ public class OpenXmlConfiguration : Configuration public bool FillMergedCells { get; set; } public TableStyles TableStyles { get; set; } = TableStyles.Default; public bool AutoFilter { get; set; } = true; + public bool RightToLeft { get; set; } = false; public int FreezeRowCount { get; set; } = 1; public int FreezeColumnCount { get; set; } = 0; public bool EnableConvertByteArray { get; set; } = true; From b39fdd2655319f0b747a9c8694771ba5798ea864 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Wed, 11 Mar 2026 21:20:03 +0100 Subject: [PATCH 2/6] Correcting edge case bug in the parsing of template escape parameters --- .../ExcelOpenXmlTemplate.Impl.cs | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs index dc6da1d7..5d86359e 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs @@ -106,6 +106,8 @@ public string ToXmlString(string prefix) => $"<{prefix}mergeCell ref=\"{ColumnHelper.GetAlphabetColumnName(X1)}{Y1}:{ColumnHelper.GetAlphabetColumnName(X2)}{Y2}\"/>"; } + internal enum SpecialCellType { None, Group, Endgroup, Merge, Header } + internal class MergeCellIndex { public int RowStart { get; set; } @@ -378,7 +380,31 @@ private void WriteSheetXml(Stream outputFileStream, XmlDocument doc, XmlNode she var rowInfo = _xRowInfos[rowNo]; var row = rowInfo.Row; - if (row.InnerText.Contains("@group")) + SpecialCellType specialCellType = SpecialCellType.None; + foreach (XmlNode c in row.GetElementsByTagName("c")) + { + switch (c.InnerText) + { + case "@group": + specialCellType = SpecialCellType.Group; + break; + case "@endgroup": + specialCellType = SpecialCellType.Endgroup; + break; + case "@merge": + case "@endmerge": + specialCellType = SpecialCellType.Merge; + break; + case var s when s.StartsWith("@header"): + specialCellType = SpecialCellType.Header; + break; + } + + if (specialCellType != SpecialCellType.None) + break; + } + + if (specialCellType == SpecialCellType.Group) { groupingStarted = true; hasEverGroupStarted = true; @@ -387,7 +413,7 @@ private void WriteSheetXml(Stream outputFileStream, XmlDocument doc, XmlNode she prevHeader = ""; continue; } - else if (row.InnerText.Contains("@endgroup")) + else if (specialCellType == SpecialCellType.Endgroup) { if (cellIEnumerableValuesIndex >= cellIEnumerableValues.Count - 1) { @@ -404,23 +430,19 @@ private void WriteSheetXml(Stream outputFileStream, XmlDocument doc, XmlNode she isFirstRound = false; continue; } - else if (row.InnerText.Contains("@header")) + else if (specialCellType == SpecialCellType.Header) { isHeaderRow = true; } - else if (mergeCells) + else if (specialCellType == SpecialCellType.Merge) { - if (row.InnerText.Contains("@merge") || row.InnerText.Contains("@endmerge")) - { - mergeRowCount++; - continue; - } + mergeRowCount++; + continue; } if (groupingStarted && !isCellIEnumerableValuesSet) { - cellIEnumerableValues = rowInfo.CellIlListValues - ?? rowInfo.CellIEnumerableValues.Cast().ToList(); + cellIEnumerableValues = rowInfo.CellIlListValues ?? rowInfo.CellIEnumerableValues.Cast().ToList(); isCellIEnumerableValuesSet = true; } From 4ffdc687eb8f4a08ad17293c4cbb5e513dbd63b6 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Wed, 11 Mar 2026 21:53:44 +0100 Subject: [PATCH 3/6] Refactored cell width writing logic Corrected cell width calculations to default to a cell width of 8.43 characters (64px) and also take into account the 5px padding to add to the xml value which was previously mising and resulted in a discrepancy with the value displayed in the UI. --- .../Attributes/ExcelColumnAttribute.cs | 2 +- .../OpenXml/ExcelOpenXmlSheetWriter.Async.cs | 4 +- .../OpenXml/ExcelOpenXmlSheetWriter.cs | 4 +- src/MiniExcel/OpenXml/ExcelWidthCollection.cs | 80 ++++++++++--------- src/MiniExcel/OpenXml/OpenXmlConfiguration.cs | 2 +- .../MiniExcelAutoAdjustWidthTests.cs | 47 +++++------ 6 files changed, 71 insertions(+), 68 deletions(-) diff --git a/src/MiniExcel/Attributes/ExcelColumnAttribute.cs b/src/MiniExcel/Attributes/ExcelColumnAttribute.cs index 36211b8f..953061ce 100644 --- a/src/MiniExcel/Attributes/ExcelColumnAttribute.cs +++ b/src/MiniExcel/Attributes/ExcelColumnAttribute.cs @@ -13,7 +13,7 @@ public class ExcelColumnAttribute : Attribute public string Name { get; set; } public string[] Aliases { get; set; } - public double Width { get; set; } = 9.28515625; + public double Width { get; set; } = 8.42857143; public string Format { get; set; } public bool Ignore { get; set; } public ColumnType Type { get; set; } = ColumnType.Value; diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs index b86c0173..e25d5e75 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs @@ -229,11 +229,11 @@ private async Task WriteValuesAsync(MiniExcelAsyncStreamWriter writer, obje if (_configuration.EnableAutoWidth) { columnWidthsPlaceholderPosition = await WriteColumnWidthPlaceholdersAsync(writer, props); - widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props); + widths = ExcelWidthCollection.FromProps(props, _configuration.MinWidth, _configuration.MaxWidth); } else { - await WriteColumnsWidthsAsync(writer, ExcelColumnWidth.FromProps(props), cancellationToken); + await WriteColumnsWidthsAsync(writer, ExcelWidthCollection.FromProps(props), cancellationToken); } //header diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 7f069f55..9d669c2e 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -221,11 +221,11 @@ private int WriteValues(MiniExcelStreamWriter writer, object values) if (_configuration.EnableAutoWidth) { columnWidthsPlaceholderPosition = WriteColumnWidthPlaceholders(writer, maxColumnIndex); - widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props); + widths = ExcelWidthCollection.FromProps(props, _configuration.MinWidth, _configuration.MaxWidth); } else { - WriteColumnsWidths(writer, ExcelColumnWidth.FromProps(props)); + WriteColumnsWidths(writer, ExcelWidthCollection.FromProps(props)); } //header diff --git a/src/MiniExcel/OpenXml/ExcelWidthCollection.cs b/src/MiniExcel/OpenXml/ExcelWidthCollection.cs index 494c1ade..ad2ce929 100644 --- a/src/MiniExcel/OpenXml/ExcelWidthCollection.cs +++ b/src/MiniExcel/OpenXml/ExcelWidthCollection.cs @@ -1,5 +1,6 @@ using MiniExcelLibs.Utils; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -7,65 +8,66 @@ namespace MiniExcelLibs.OpenXml { public sealed class ExcelColumnWidth { + // Aptos is the default font for Office 2023 and onwards, over which the width of cells are calculated at the size of 11pt. + // Priorly it was Calibri, which had very similar parameters, so no visual differences should be noticed. + private const double DefaultCellPadding = 5; + private const double Aptos11MaxDigitWidth = 7; + public const double Aptos11Padding = DefaultCellPadding / Aptos11MaxDigitWidth; + public int Index { get; set; } public double Width { get; set; } + + public static double GetWidthFromTextLength(double characters) + => Math.Round(characters + Aptos11Padding, 8); + } + + public sealed class ExcelWidthCollection : IReadOnlyCollection + { + private readonly Dictionary _columnWidths; + private readonly double _maxWidth; + + public IReadOnlyCollection Columns => _columnWidths.Values.ToList(); - internal static IEnumerable FromProps(ICollection props, double? minWidth = null) + private ExcelWidthCollection(ICollection columnWidths, double maxWidth) + { + _maxWidth = ExcelColumnWidth.GetWidthFromTextLength(maxWidth); + _columnWidths = columnWidths.ToDictionary(x => x.Index); + } + + internal static ExcelWidthCollection FromProps(ICollection mappings, double? minWidth = null, double maxWidth = 200) { var i = 1; - foreach (var p in props) + var columnWidths = new List(); + + foreach (var map in mappings) { - if (p?.ExcelColumnWidth != null || minWidth != null) + if (map?.ExcelColumnWidth != null || minWidth != null) { - var colIndex = p?.ExcelColumnIndex + 1; - yield return new ExcelColumnWidth - { - Index = colIndex ?? i, - Width = p?.ExcelColumnWidth ?? minWidth.Value - }; + var colIndex = map?.ExcelColumnIndex + 1 ?? i; + var width = map?.ExcelColumnWidth ?? minWidth.Value; + + columnWidths.Add(new ExcelColumnWidth { Index = colIndex, Width = width + ExcelColumnWidth.Aptos11Padding }); } i++; } - } - } - - public sealed class ExcelWidthCollection - { - private readonly Dictionary _columnWidths; - private readonly double _maxWidth; - - public IEnumerable Columns => _columnWidths.Values; - internal ExcelWidthCollection(double minWidth, double maxWidth, ICollection props) - { - _maxWidth = maxWidth; - _columnWidths = ExcelColumnWidth.FromProps(props, minWidth).ToDictionary(x => x.Index); + return new ExcelWidthCollection(columnWidths, maxWidth); } - public void AdjustWidth(int columnIndex, string columnValue) + internal void AdjustWidth(int columnIndex, string columnValue) { - if (!string.IsNullOrEmpty(columnValue) - && _columnWidths.TryGetValue(columnIndex, out var currentWidth)) + if (!string.IsNullOrEmpty(columnValue) && _columnWidths.TryGetValue(columnIndex, out var currentWidth)) { - var adjustedWidth = Math.Max(currentWidth.Width, GetApproximateTextWidth(columnValue.Length)); + var desiredWidth = ExcelColumnWidth.GetWidthFromTextLength(columnValue.Length); + var adjustedWidth = Math.Max(currentWidth.Width, desiredWidth); currentWidth.Width = Math.Min(_maxWidth, adjustedWidth); } } - /// - /// Get the approximate width of the given text for Calibri 11pt - /// - /// - /// Rounds the result to 2 decimal places. - /// - public static double GetApproximateTextWidth(int textLength) - { - const double characterWidthFactor = 1.2; // Estimated factor for Calibri, 11pt - const double padding = 2; // Add some padding for extra spacing + public IEnumerator GetEnumerator() => _columnWidths.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - var excelColumnWidth = (textLength * characterWidthFactor) + padding; - return Math.Round(excelColumnWidth, 2); - } + public int Count => _columnWidths.Count; } } diff --git a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs index 190683db..18355809 100644 --- a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs @@ -35,7 +35,7 @@ public class OpenXmlConfiguration : Configuration /// public bool EnableAutoWidth { get; set; } - public double MinWidth { get; set; } = 9.28515625; + public double MinWidth { get; set; } = 8.42857143; public double MaxWidth { get; set; } = 200; } diff --git a/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs b/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs index e101fa00..49083387 100644 --- a/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs +++ b/tests/MiniExcelTests/MiniExcelAutoAdjustWidthTests.cs @@ -4,6 +4,7 @@ using MiniExcelLibs.Tests.Utils; using System.Data; using System.Data.SQLite; +using System.Diagnostics; using Xunit; namespace MiniExcelLibs.Tests; @@ -150,42 +151,43 @@ private static void AssertExpectedWidth(string path, OpenXmlConfiguration config var columns = worksheetPart.Worksheet.GetFirstChild(); Assert.False(columns == null, "No column width information was written."); + foreach (var column in columns.Elements()) { - var expectedWidth = column.Min.Value switch + var expectedWidth = column.Min?.Value switch { - 1 => ExcelWidthCollection.GetApproximateTextWidth(AutoAdjustTestParameters.column1MaxStringLength), - 2 => ExcelWidthCollection.GetApproximateTextWidth(AutoAdjustTestParameters.column2MaxStringLength), + 1 => AutoAdjustTestParameters.Column1MaxLen, + 2 => AutoAdjustTestParameters.Column2MaxLen, 3 => configuration.MinWidth, 4 => configuration.MaxWidth, - _ => throw new Exception("Unexpected column"), + _ => throw new UnreachableException() }; - Assert.Equal(expectedWidth, column.Width?.Value); + Assert.Equal(ExcelColumnWidth.GetWidthFromTextLength(expectedWidth), Math.Round(column.Width!.Value, 8)); } } private static class AutoAdjustTestParameters { - public const int column1MaxStringLength = 32; - public const int column2MaxStringLength = 16; - public const int column3MaxStringLength = 2; - public const int column4MaxStringLength = 100; - public const int minStringLength = 8; - public const int maxStringLength = 50; + internal const int Column1MaxLen = 32; + internal const int Column2MaxLen = 16; + private const int Column3MaxLen = 2; + private const int Column4MaxLen = 100; public static List GetTestData() => [ - new string[] - { - new('1', column1MaxStringLength), new('2', column2MaxStringLength / 2), - new('3', column3MaxStringLength / 2), new('4', column1MaxStringLength) - }, - new string[] - { - new('1', column1MaxStringLength / 2), new('2', column2MaxStringLength), - new('3', column3MaxStringLength), new('4', column4MaxStringLength) - } + [ + new('1', Column1MaxLen), + new('2', Column2MaxLen / 2), + new('3', Column3MaxLen / 2), + new('4', Column4MaxLen) + ], + [ + new('1', Column1MaxLen / 2), + new('2', Column2MaxLen), + new('3', Column3MaxLen), + new('4', Column4MaxLen) + ] ]; public static List> GetDictionaryTestData() => GetTestData() @@ -198,8 +200,7 @@ public static List> GetDictionaryTestData() => GetTes { EnableAutoWidth = true, FastMode = true, - MinWidth = ExcelWidthCollection.GetApproximateTextWidth(minStringLength), - MaxWidth = ExcelWidthCollection.GetApproximateTextWidth(maxStringLength) + MaxWidth = 50 }; } } \ No newline at end of file From 8eb548a8816b0dfb2f807a7d3a5ba93e48520671 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Thu, 12 Mar 2026 00:15:43 +0100 Subject: [PATCH 4/6] Adds hidden columns functionality The new `ExcelHiddenAttribute` and property `MiniExcelColumnAttribute.Hidden` now allow for a property to be mapped as a hidden column in Excel's UI. --- README.md | 29 ++++++++++++------- .../Attributes/ExcelColumnAttribute.cs | 1 + .../Attributes/ExcelHiddenAttribute.cs | 11 +++++++ .../OpenXml/Constants/WorksheetXml.cs | 6 ++-- .../OpenXml/ExcelOpenXmlSheetWriter.cs | 2 +- src/MiniExcel/OpenXml/ExcelWidthCollection.cs | 9 ++++-- src/MiniExcel/Utils/CustomPropertyHelper.cs | 4 +++ 7 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 src/MiniExcel/Attributes/ExcelHiddenAttribute.cs diff --git a/README.md b/README.md index 3328916f..e7b66a74 100644 --- a/README.md +++ b/README.md @@ -1073,7 +1073,18 @@ public class Dto } ``` -#### 4. Multiple column names mapping to the same property. +#### 4. Set Column Visibility(MiniExcelHiddenAttribute) +```csharp +public class Dto +{ + public string Name { get; set; } + + [MiniExcelHidden] + public int SecretPoints { get; set; } +} +``` + +#### 5. Multiple column names mapping to the same property. ```csharp public class Dto @@ -1084,9 +1095,7 @@ public class Dto } ``` - - -#### 5. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute +#### 6. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute Since 1.24.0, system supports System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute @@ -1102,9 +1111,7 @@ public class TestIssueI4TXGTDto } ``` - - -#### 6. ExcelColumnAttribute +#### 7. ExcelColumnAttribute Since V1.26.0, multiple attributes can be simplified like : ```csharp @@ -1114,12 +1121,12 @@ Since V1.26.0, multiple attributes can be simplified like : public string MyProperty { get; set; } [ExcelColumn(Name = "CreateDate", Index = 1,Format ="yyyy-MM",Width =100)] public DateTime MyProperty2 { get; set; } + [ExcelColumn(Name = "SecretColumn", Hidden = true)] + public int MyProperty3 { get; set; } } ``` - - -#### 7. DynamicColumnAttribute +#### 8. DynamicColumnAttribute Since V1.26.0, we can set the attributes of Column dynamically ```csharp @@ -1138,7 +1145,7 @@ Since V1.26.0, we can set the attributes of Column dynamically ``` ![image](https://user-images.githubusercontent.com/12729184/164510353-5aecbc4e-c3ce-41e8-b6cf-afd55eb23b68.png) -#### 8. DynamicSheetAttribute +#### 9. DynamicSheetAttribute Since V1.31.4 we can set the attributes of Sheet dynamically. We can set sheet name and state (visibility). ```csharp diff --git a/src/MiniExcel/Attributes/ExcelColumnAttribute.cs b/src/MiniExcel/Attributes/ExcelColumnAttribute.cs index 953061ce..70d0c203 100644 --- a/src/MiniExcel/Attributes/ExcelColumnAttribute.cs +++ b/src/MiniExcel/Attributes/ExcelColumnAttribute.cs @@ -15,6 +15,7 @@ public class ExcelColumnAttribute : Attribute public string[] Aliases { get; set; } public double Width { get; set; } = 8.42857143; public string Format { get; set; } + public bool Hidden { get; set; } public bool Ignore { get; set; } public ColumnType Type { get; set; } = ColumnType.Value; diff --git a/src/MiniExcel/Attributes/ExcelHiddenAttribute.cs b/src/MiniExcel/Attributes/ExcelHiddenAttribute.cs new file mode 100644 index 00000000..fcda446a --- /dev/null +++ b/src/MiniExcel/Attributes/ExcelHiddenAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiniExcelLibs.Attributes +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class ExcelHiddenAttribute : Attribute + { + public bool ExcelHidden { get; set; } + public ExcelHiddenAttribute(bool excelHidden = true) => ExcelHidden = excelHidden; + } +} diff --git a/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs index 5e6df8f8..37ece9d3 100644 --- a/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs @@ -45,10 +45,10 @@ internal static string PaneSelection( string pane, string activeCell, string sqr internal const string EndRow = ""; internal const string StartCols = ""; - internal static string Column(int colIndex, double columnWidth) - => $@""; + internal static string Column(int colIndex, double columnWidth, bool hidden = false) + => $@"