diff --git a/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs index c02f83ed..1316e990 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs @@ -12,7 +12,7 @@ public class MiniExcelColumnAttribute : Attribute public bool Ignore { get; set; } internal int FormatId { get; private set; } = -1; - public double Width { get; set; } = 9.28515625; + public double Width { get; set; } = 8.42857143; public ColumnType Type { get; set; } = ColumnType.Value; public int Index diff --git a/src/MiniExcel.OpenXml/Models/DrawingDto.cs b/src/MiniExcel.OpenXml/Models/DrawingDto.cs deleted file mode 100644 index 43ddbcf4..00000000 --- a/src/MiniExcel.OpenXml/Models/DrawingDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MiniExcelLib.OpenXml.Models; - -internal class DrawingDto -{ - internal string ID { get; set; } = $"R{Guid.NewGuid():N}"; -} \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs new file mode 100644 index 00000000..d6786d14 --- /dev/null +++ b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs @@ -0,0 +1,68 @@ +namespace MiniExcelLib.OpenXml.Models; + +public sealed class ExcelColumnWidth(int index, double width) +{ + // 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. + // todo: consider making other fonts available + private const double DefaultCellPadding = 5; + private const double Aptos11MaxDigitWidth = 7; + public const double Aptos11Padding = DefaultCellPadding / Aptos11MaxDigitWidth; + + public int Index { get; } = index; + public double Width { get; set; } = width; + + public static double GetWidthFromTextLength(double characters) + => Math.Round(characters + Aptos11Padding, 8); +} + + +public sealed class ExcelColumnWidthCollection : IReadOnlyCollection +{ + private readonly Dictionary _columnWidths; + private readonly double _maxWidth; + + public IReadOnlyCollection Columns => _columnWidths.Values; + + private ExcelColumnWidthCollection(ICollection columnWidths, double maxWidth) + { + _maxWidth = ExcelColumnWidth.GetWidthFromTextLength(maxWidth); + _columnWidths = columnWidths.ToDictionary(x => x.Index); + } + + internal static ExcelColumnWidthCollection GetFromMappings(ICollection mappings, double? minWidth = null, double maxWidth = 200) + { + var i = 1; + List columnWidths = []; + + foreach (var map in mappings) + { + if (map?.ExcelColumnWidth is not null || minWidth is not null) + { + var colIndex = map?.ExcelColumnIndex + 1 ?? i; + var width = map?.ExcelColumnWidth ?? minWidth!.Value; + + columnWidths.Add(new ExcelColumnWidth(colIndex, width + ExcelColumnWidth.Aptos11Padding)); + } + + i++; + } + + return new ExcelColumnWidthCollection(columnWidths, maxWidth); + } + + internal void AdjustWidth(int columnIndex, string columnValue) + { + if (!string.IsNullOrEmpty(columnValue) && _columnWidths.TryGetValue(columnIndex, out var currentWidth)) + { + var desiredWidth = ExcelColumnWidth.GetWidthFromTextLength(columnValue.Length); + var adjustedWidth = Math.Max(currentWidth.Width, desiredWidth); + currentWidth.Width = Math.Min(_maxWidth, adjustedWidth); + } + } + + public IEnumerator GetEnumerator() => _columnWidths.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _columnWidths.Count; +} diff --git a/src/MiniExcel.OpenXml/Models/ExcelWidthCollection.cs b/src/MiniExcel.OpenXml/Models/ExcelWidthCollection.cs deleted file mode 100644 index e1ac0d38..00000000 --- a/src/MiniExcel.OpenXml/Models/ExcelWidthCollection.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace MiniExcelLib.OpenXml.Models; - -public sealed class ExcelColumnWidth -{ - public int Index { get; set; } - public double Width { get; set; } - - internal static IEnumerable FromProps(ICollection props, double? minWidth = null) - { - var i = 1; - foreach (var p in props) - { - if (p?.ExcelColumnWidth is not null || minWidth is not null) - { - var colIndex = p?.ExcelColumnIndex + 1; - yield return new ExcelColumnWidth - { - Index = colIndex ?? i, - Width = p?.ExcelColumnWidth ?? minWidth.Value - }; - } - - 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); - } - - public void AdjustWidth(int columnIndex, string columnValue) - { - if (!string.IsNullOrEmpty(columnValue) && _columnWidths.TryGetValue(columnIndex, out var currentWidth)) - { - var adjustedWidth = Math.Max(currentWidth.Width, GetApproximateTextWidth(columnValue.Length)); - 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 - - var excelColumnWidth = textLength * characterWidthFactor + padding; - return Math.Round(excelColumnWidth, 2); - } -} \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs index 9056f6f7..9b3ceeaa 100644 --- a/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs @@ -37,7 +37,7 @@ public class OpenXmlConfiguration : MiniExcelBaseConfiguration /// Calculate column widths automatically from each column value. /// 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/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index dc4875d3..5f0fbf80 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -281,16 +281,17 @@ private async Task WriteValuesAsync(EnhancedStreamWriter writer, object val await writer.WriteAsync(GetSheetViews(), cancellationToken).ConfigureAwait(false); //cols:width - ExcelWidthCollection? widths = null; + ExcelColumnWidthCollection? widths = null; long columnWidthsPlaceholderPosition = 0; if (_configuration.EnableAutoWidth) { columnWidthsPlaceholderPosition = await WriteColumnWidthPlaceholdersAsync(writer, maxColumnIndex, cancellationToken).ConfigureAwait(false); - widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props); + widths = ExcelColumnWidthCollection.GetFromMappings(props, _configuration.MinWidth, _configuration.MaxWidth); } else { - await WriteColumnsWidthsAsync(writer, ExcelColumnWidth.FromProps(props), cancellationToken).ConfigureAwait(false); + var colWidths = ExcelColumnWidthCollection.GetFromMappings(props); + await WriteColumnsWidthsAsync(writer, colWidths.Columns, cancellationToken).ConfigureAwait(false); } //header @@ -451,7 +452,7 @@ private async Task WriteCellAsync(EnhancedStreamWriter writer, string cellRefere } [CreateSyncVersion] - private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int cellIndex, object? value, MiniExcelColumnInfo columnInfo, ExcelWidthCollection? widthCollection, CancellationToken cancellationToken = default) + private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int cellIndex, object? value, MiniExcelColumnInfo columnInfo, ExcelColumnWidthCollection? widthCollection, CancellationToken cancellationToken = default) { if (columnInfo?.CustomFormatter is not null) { @@ -482,7 +483,7 @@ private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int var columnType = columnInfo.ExcelColumnType; /*Prefix and suffix blank space will lost after SaveAs #294*/ - var preserveSpace = cellValue is [' ', ..] or [.., ' ']; + var preserveSpace = cellValue.StartsWith(" ") || cellValue.EndsWith(" "); await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, GetCellXfId(styleIndex), cellValue, preserveSpace: preserveSpace, columnType: columnType), cancellationToken).ConfigureAwait(false); widthCollection?.AdjustWidth(cellIndex, cellValue); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelAutoAdjustWidthTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelAutoAdjustWidthTests.cs index 5b6ba009..7c201925 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelAutoAdjustWidthTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelAutoAdjustWidthTests.cs @@ -52,7 +52,8 @@ public void AutoAdjustWidthEnumerable() var path = file.ToString(); var configuration = AutoAdjustTestParameters.GetConfiguration(); - _excelExporter.Export(path, AutoAdjustTestParameters.GetDictionaryTestData(), configuration: configuration); + var data = AutoAdjustTestParameters.GetDictionaryTestData(); + _excelExporter.Export(path, data, configuration: configuration); AssertExpectedWidth(path, configuration); } @@ -70,7 +71,7 @@ public async Task AutoAdjustWidthDataReader_Async() await using var command = new SQLiteCommand(Db.GenerateDummyQuery(AutoAdjustTestParameters.GetDictionaryTestData()), connection); connection.Open(); await using var reader = await command.ExecuteReaderAsync(); - await _excelExporter.ExportAsync(path, reader, configuration: configuration); + await _excelExporter.ExportAsync(path, reader, configuration: configuration, overwriteFile: true); } AssertExpectedWidth(path, configuration); @@ -149,43 +150,41 @@ private static void AssertExpectedWidth(string path, OpenXmlConfiguration config var columns = worksheetPart?.Worksheet.GetFirstChild(); Assert.False(columns is null, "No column width information was written."); + foreach (var column in columns.Elements()) { var expectedWidth = column.Min?.Value switch { - 1 => ExcelWidthCollection.GetApproximateTextWidth(AutoAdjustTestParameters.Column1MaxStringLength), - 2 => ExcelWidthCollection.GetApproximateTextWidth(AutoAdjustTestParameters.Column2MaxStringLength), + 1 => AutoAdjustTestParameters.Column1MaLen, + 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; - - public static List GetTestData() => + internal const int Column1MaLen = 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', Column1MaLen), + new('2', Column2MaxLen / 2), + new('3', Column3MaxLen / 2), + new('4', Column4MaxLen) + ], + [ + new('1', Column1MaLen / 2), + new('2', Column2MaxLen), + new('3', Column3MaxLen), + new('4', Column4MaxLen) + ] ]; public static List> GetDictionaryTestData() => GetTestData() .Select(row => row @@ -197,8 +196,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 +}