diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs index 733eddb0..e80b77a7 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs @@ -6,7 +6,7 @@ using DocumentFormat.OpenXml.Spreadsheet; using MiniExcelLib.Benchmarks.Utils; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.FluentMapping; using MiniExcelLib.OpenXml.FluentMapping.Api; using NPOI.XSSF.UserModel; diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs index 83218068..4d06bd45 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs @@ -5,7 +5,7 @@ using DocumentFormat.OpenXml.Spreadsheet; using ExcelDataReader; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.FluentMapping; using MiniExcelLib.OpenXml.FluentMapping.Api; using NPOI.XSSF.UserModel; diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs index 20cc7d92..8a37a075 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs @@ -2,7 +2,7 @@ using ClosedXML.Report; using MiniExcelLib.Benchmarks.Utils; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.FluentMapping; using MiniExcelLib.OpenXml.FluentMapping.Api; diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs index 1852a375..ea90613b 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using MiniExcelLib.Benchmarks.Utils; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; namespace MiniExcelLib.Benchmarks.BenchmarkSections; diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs index 85408fc1..cebcd1e4 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs @@ -7,9 +7,9 @@ public interface IMiniExcelWriteAdapter IEnumerable GetRows(List mappings, CancellationToken cancellationToken = default); } -public readonly struct CellWriteInfo(object? value, int cellIndex, MiniExcelColumnMapping? mapping) +public readonly struct CellWriteInfo(object? value, int index, MiniExcelColumnMapping? mapping) { public object? Value { get; } = value; - public int CellIndex { get; } = cellIndex; + public int Index { get; } = index; public MiniExcelColumnMapping? Mapping { get; } = mapping; } diff --git a/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs b/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs index 1eb002a6..03ce8479 100644 --- a/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs +++ b/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs @@ -1,9 +1,6 @@ namespace MiniExcelLib.Core.Helpers; -public sealed partial class MiniExcelStreamWriter(Stream stream, Encoding encoding, int bufferSize) : IDisposable -#if NET8_0_OR_GREATER - , IAsyncDisposable -#endif +public sealed partial class MiniExcelStreamWriter(Stream stream, Encoding encoding, int bufferSize) : IDisposable, IAsyncDisposable { // if leaveOpen is set to false, the StreamWriter closes the underlying stream synchronously in a finally block. // Since we want to avoid all synchronous operations when dealing with streams we leave it open here, as it will disposed from the caller anyways @@ -57,14 +54,20 @@ public void Dispose() } } -#if NET8_0_OR_GREATER public async ValueTask DisposeAsync() { if (!_disposed) { - await _streamWriter.DisposeAsync().ConfigureAwait(false); + await CastAndDispose(_streamWriter).ConfigureAwait(false); _disposed = true; } + + static async ValueTask CastAndDispose(IDisposable? resource) + { + if (resource is IAsyncDisposable asyncDisposable) + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + else + resource?.Dispose(); + } } -#endif } diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs index 8f8378b1..5042d6d1 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs @@ -1,11 +1,27 @@ -namespace MiniExcelLib.OpenXml.Api; +// ReSharper disable once CheckNamespace +namespace MiniExcelLib.OpenXml; public sealed partial class OpenXmlExporter { internal OpenXmlExporter() { } + /// + /// Inserts a new worksheet into an existing OpenXml document. + /// + /// The path to the OpenXml document to modify. + /// The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a or a . + /// The name to assign to the new worksheet. + /// If true, includes the header row in the new sheet; otherwise, only data rows are written. + /// If true, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists. + /// Optional configuration settings for the insert operation. + /// Optional progress reporter to track insertion progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// The number of rows written to the new sheet. + /// + /// FastMode is automatically enabled for this process and disabling it will result in . + /// [CreateSyncVersion] - public async Task InsertSheetAsync(string path, object value, string? sheetName = "Sheet1", + public async Task InsertSheetAsync(string path, object value, string sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { @@ -27,8 +43,24 @@ public async Task InsertSheetAsync(string path, object value, string? sheet return await InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Inserts a new worksheet into an existing OpenXml document. + /// + /// The stream containing the OpenXml document to modify. + /// The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a IEnumeable<IDictionary<string, object>>, a or a . + /// The name to assign to the new worksheet. + /// If true, includes the header row in the new sheet; otherwise, only data rows are written. + /// If true, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists. + /// Optional configuration settings for the insert operation. + /// Optional progress reporter to track insertion progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// The number of rows written to the new sheet. + /// + /// The stream position is reset to the end before writing. + /// FastMode is automatically enabled for this process and disabling it will result in . + /// [CreateSyncVersion] - public async Task InsertSheetAsync(Stream stream, object value, string? sheetName = "Sheet1", + public async Task InsertSheetAsync(Stream stream, object value, string sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { @@ -39,12 +71,24 @@ public async Task InsertSheetAsync(Stream stream, object value, string? she .CreateAsync(stream, value, sheetName, printHeader, configuration, cancellationToken) .ConfigureAwait(false); - return await writer.InsertAsync(overwriteSheet, cancellationToken: cancellationToken).ConfigureAwait(false); + return await writer.InsertAsync(overwriteSheet, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Exports data to a file as an OpenXml document. + /// + /// The path to write the OpenXml document to. + /// The data object to export. This can be an enumerable collection of a reference type, a IEnumeable<IDictionary<string, object>>, a or a . + /// If true, includes the header row in the output; otherwise, only data rows are written. + /// The name to assign to the worksheet. + /// If true, overwrites the file at the specified path, otherwise a will be raised if the file already exists. + /// Optional configuration settings for the export operation. + /// Optional progress reporter to track export progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// An array of integers representing the number of rows written for each exported sheet. [CreateSyncVersion] public async Task ExportAsync(string path, object value, bool printHeader = true, - string? sheetName = "Sheet1", bool overwriteFile = false, OpenXmlConfiguration? configuration = null, + string sheetName = "Sheet1", bool overwriteFile = false, OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { if (Path.GetExtension(path).Equals(".xlsm", StringComparison.InvariantCultureIgnoreCase)) @@ -59,8 +103,22 @@ public async Task ExportAsync(string path, object value, bool printHeader return await ExportAsync(stream, value, printHeader, sheetName, configuration, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Exports data to a stream as an OpenXml document. + /// + /// The stream to write the OpenXml document. + /// The data object to export. This can be an enumerable collection of a reference type, a IEnumeable<IDictionary<string, object>>, a or a . + /// If true, includes the header row in the output; otherwise, only data rows are written. + /// The name to assign to the worksheet. + /// Optional configuration settings for the export operation. + /// Optional progress reporter to track export progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// An array of integers representing the number of rows written for each exported sheet. + /// + /// The stream position is not reset before writing. + /// [CreateSyncVersion] - public async Task ExportAsync(Stream stream, object value, bool printHeader = true, string? sheetName = "Sheet1", + public async Task ExportAsync(Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { var writer = await OpenXmlWriter diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index 24e78a8b..738db2f0 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -1,8 +1,7 @@ -using MiniExcelLib.OpenXml; -using OpenXmlReader = MiniExcelLib.OpenXml.OpenXmlReader; +using MiniExcelLib.Core; // ReSharper disable once CheckNamespace -namespace MiniExcelLib.Core; +namespace MiniExcelLib.OpenXml; public sealed partial class OpenXmlImporter { diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs index 462e0e2b..dd6d486f 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs @@ -1,10 +1,9 @@ -using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.Picture; using MiniExcelLib.OpenXml.Templates; using OpenXmlTemplate = MiniExcelLib.OpenXml.Templates.OpenXmlTemplate; // ReSharper disable once CheckNamespace -namespace MiniExcelLib.Core; +namespace MiniExcelLib.OpenXml; public sealed partial class OpenXmlTemplater { diff --git a/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs b/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs index b5717dc6..782f71ba 100644 --- a/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs +++ b/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs @@ -1,6 +1,7 @@ using MiniExcelLib.Core; -namespace MiniExcelLib.OpenXml.Api; +// ReSharper disable once CheckNamespace +namespace MiniExcelLib.OpenXml; public static class ProviderExtensions { diff --git a/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs b/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs index 1ea810f6..4de4779f 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs @@ -6,11 +6,13 @@ internal static class ExcelFileNames internal const string SharedStrings = "xl/sharedStrings.xml"; internal const string ContentTypes = "[Content_Types].xml"; + internal const string Person = "xl/persons/person.xml"; internal const string Styles = "xl/styles.xml"; internal const string Workbook = "xl/workbook.xml"; internal const string WorkbookRels = "xl/_rels/workbook.xml.rels"; + internal const string Worksheet = "xl/worksheets/sheet"; internal static string SheetRels(int sheetId) => $"xl/worksheets/_rels/sheet{sheetId}.xml.rels"; internal static string Drawing(int sheetIndex) => $"xl/drawings/drawing{sheetIndex + 1}.xml"; internal static string DrawingRels(int sheetIndex) => $"xl/drawings/_rels/drawing{sheetIndex + 1}.xml.rels"; -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 868d512b..13ba39dd 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -11,6 +11,12 @@ static ExcelXml() DefaultDrawing = XmlHelper.MinifyXml(DefaultDrawing); } + internal const string InlineStringDataType = "inlineStr"; + internal const string CalculatedStringDataType = "str"; + internal const string SharedStringDataType = "s"; + internal const string NumericDataType = "n"; + internal const string BooleanDataType = "b"; + internal const string EmptySheetXml = """"""; internal static readonly string DefaultRels = @@ -68,7 +74,25 @@ static ExcelXml() """; - internal const string DefaultSharedString = ""; + internal static string SharedStrings(Dictionary sharedStrings) + { + var sb = new StringBuilder(); + sb.Append(""""); + + foreach(var (text, _) in sharedStrings.OrderBy(x => x.Value)) + { + sb.Append("{XmlHelper.EncodeXml(text)}"); + } + + sb.Append(""); + return sb.ToString(); + } internal const string StartTypes = """"""; internal static string ContentType(string contentType, string partName) => $""; @@ -122,5 +146,4 @@ internal static string DrawingXml(FileDto file, int fileIndex) internal static string Sheet(SheetDto sheetDto, int sheetId) => $""""""; - -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index dd69f623..fdfbee8a 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -52,10 +52,22 @@ internal static string Column(int colIndex, double columnWidth, bool hidden = fa internal const string EndCols = ""; internal static string EmptyCell(string cellReference, string styleIndex) => $""; - - //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) internal static string Cell(string cellReference, string? cellType, string styleIndex, string? cellValue, bool preserveSpace = false, ColumnType columnType = ColumnType.Value) - => $"{cellValue}"; + { + return cellType switch + { + _ when columnType == ColumnType.Formula + => $"""{XmlHelper.EncodeXml(cellValue)}""", + + ExcelXml.InlineStringDataType + => $"""{XmlHelper.EncodeXml(cellValue)}""", + + ExcelXml.SharedStringDataType + => $"""{cellValue}""", + + _ => $"""{cellValue}""" + }; + } internal static string Autofilter(string dimensionRef) => $""; internal static string Drawing(int sheetIndex) => $""; diff --git a/src/MiniExcel.OpenXml/GlobalUsings.cs b/src/MiniExcel.OpenXml/GlobalUsings.cs index 05f2a69b..c8bc554b 100644 --- a/src/MiniExcel.OpenXml/GlobalUsings.cs +++ b/src/MiniExcel.OpenXml/GlobalUsings.cs @@ -15,5 +15,4 @@ global using MiniExcelLib.OpenXml.Helpers; global using MiniExcelLib.OpenXml.Models; global using MiniExcelLib.OpenXml.Utils; -global using MiniExcelLib.OpenXml.Zip; global using Zomp.SyncMethodGenerator; diff --git a/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs b/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs index 656e1c70..42634a7a 100644 --- a/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs +++ b/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs @@ -3,6 +3,7 @@ internal static class ThrowHelper { private static readonly byte[] ZipArchiveHeader = [0x50, 0x4B]; + private static readonly char[] InvalidSheetNameCharacters = ['\\', '/', '?', '*', '[', ']']; private const int ExcelMaxSheetNameLength = 31; internal static void ThrowIfInvalidOpenXml(Stream stream) @@ -23,9 +24,12 @@ internal static void ThrowIfInvalidOpenXml(Stream stream) internal static void ThrowIfInvalidSheetName(string? sheetName) { if (string.IsNullOrEmpty(sheetName)) - throw new ArgumentException("Sheet names cannot be empty or null"); + throw new ArgumentException("Sheet names cannot be empty"); if (sheetName.Length > ExcelMaxSheetNameLength) throw new ArgumentException("Sheet names must be less than 31 characters"); + + if (sheetName.Intersect(InvalidSheetNameCharacters).Any()) + throw new ArgumentException($"Sheet names cannot contain any of the following characters: {string.Join(", ", InvalidSheetNameCharacters)}"); } } diff --git a/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs index d6b652c0..cbb1f838 100644 --- a/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs +++ b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs @@ -53,7 +53,7 @@ internal static ExcelColumnWidthCollection GetFromMappings(ICollection + /// Prioritizes memory usage over file size during export by writing text directly to cells. + /// Ideal when exporting big datasets. + /// + Inline, + + /// + /// Prioritizes file size over memory usage during export by storing unique strings in the sharedStrings table. + /// Ideal for standard files with repetitive text. + /// + Shared +} diff --git a/src/MiniExcel.OpenXml/OpenXmlReader.cs b/src/MiniExcel.OpenXml/OpenXmlReader.cs index c623398d..683d251d 100644 --- a/src/MiniExcel.OpenXml/OpenXmlReader.cs +++ b/src/MiniExcel.OpenXml/OpenXmlReader.cs @@ -344,8 +344,7 @@ private ZipArchiveEntry GetSheetEntry(string? sheetName) { // if sheets count > 1 need to read xl/_rels/workbook.xml.rels var sheets = Archive.EntryCollection - .Where(w => w.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || - w.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)) + .Where(w => w.FullName.TrimStart('/').StartsWith(ExcelFileNames.Worksheet, StringComparison.OrdinalIgnoreCase)) .ToArray(); ZipArchiveEntry sheetEntry; @@ -358,24 +357,18 @@ private ZipArchiveEntry GetSheetEntry(string? sheetName) if (_config.DynamicSheets is null) throw new InvalidOperationException("Please check that parameters sheetName/Index are correct"); - var sheetConfig = _config.DynamicSheets.FirstOrDefault(ds => ds.Key == sheetName); - if (sheetConfig is not null) + if (_config.DynamicSheets.FirstOrDefault(ds => ds.Key == sheetName) is { } sheetConfig) { sheetRecord = _sheetRecords.SingleOrDefault(s => s.Name == sheetConfig.Name); } } - sheetEntry = sheets.Single(w => w.FullName == $"xl/{sheetRecord.Path}" || - w.FullName == $"/xl/{sheetRecord.Path}" || - w.FullName == sheetRecord.Path || - $"/{w.FullName}" == sheetRecord.Path); + sheetEntry = sheets.Single(w => w.FullName.TrimStart('/') == $"xl/{sheetRecord?.Path}" || w.FullName == sheetRecord?.Path?.TrimStart('/')); } else if (sheets.Length > 1) { SetWorkbookRels(Archive.EntryCollection); var s = _sheetRecords[0]; - sheetEntry = sheets.Single(w => w.FullName == $"xl/{s.Path}" || - w.FullName == $"/xl/{s.Path}" || - w.FullName.TrimStart('/') == s.Path?.TrimStart('/')); + sheetEntry = sheets.Single(w => w.FullName.TrimStart('/') == $"xl/{s.Path}" || w.FullName.TrimStart('/') == s.Path?.TrimStart('/')); } else { @@ -421,8 +414,7 @@ private async Task SetSharedStringsAsync(CancellationToken cancellationToken = d if (SharedStrings is { Count: > 0 }) return; - var sharedStringsEntry = Archive.GetEntry("xl/sharedStrings.xml"); - if (sharedStringsEntry is null) + if (Archive.GetEntry(ExcelFileNames.SharedStrings) is not { } sharedStringsEntry) return; var idx = 0; @@ -466,7 +458,7 @@ private static async IAsyncEnumerable ReadWorkbookAsync(ReadOnlyCol #endif ); - var entry = entries.Single(w => w.FullName == "xl/workbook.xml"); + var entry = entries.Single(w => w.FullName == ExcelFileNames.Workbook); #if NET8_0_OR_GREATER var stream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableStream = stream.ConfigureAwait(false); @@ -562,7 +554,7 @@ await reader.SkipAsync() .CreateListAsync(cancellationToken) .ConfigureAwait(false); - var entry = entries.Single(w => w.FullName == "xl/_rels/workbook.xml.rels"); + var entry = entries.Single(w => w.FullName == ExcelFileNames.WorkbookRels); #if NET8_0_OR_GREATER var stream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); @@ -755,8 +747,7 @@ internal async Task> GetDimensionsAsync(CancellationToken canc var ranges = new List(); var sheets = Archive.EntryCollection.Where(e => - e.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || - e.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)); + e.FullName.TrimStart('/').StartsWith(ExcelFileNames.Worksheet, StringComparison.OrdinalIgnoreCase)); foreach (var sheet in sheets) { @@ -1131,7 +1122,7 @@ internal async Task ReadCommentsAsync(string? sheetName, Cance throw new InvalidDataException($"There is no sheet named {sheetName}"); List people = []; - if (Archive.GetEntry("xl/persons/person.xml") is { } persons) + if (Archive.GetEntry(ExcelFileNames.Person) is { } persons) { #if NET8_0_OR_GREATER var personStream = await persons.OpenAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index 9e44f8af..bdb882b9 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -1,5 +1,6 @@ using MiniExcelLib.OpenXml.Constants; using System.ComponentModel; +using MiniExcelLib.Core.Attributes; using static MiniExcelLib.Core.Helpers.ImageHelper; namespace MiniExcelLib.OpenXml; @@ -9,9 +10,14 @@ internal partial class OpenXmlWriter private const string DefaultCellStyleIndex = "0"; private const string HeaderCellStyleIndex = "1"; private const string RegularCellStyleIndex = "2"; + private const string DateCellStyleIndex = "3"; + private const string FillCellStyleIndex = "4"; + private const string TimeCellStyleIndex = "5"; + private static readonly DateTime ExcelZeroDate = new(1899, 12, 31); - private readonly Dictionary _zipDictionary = []; + private readonly Dictionary _zipContentsMap = []; + private readonly Dictionary _sharedStrings = []; private IEnumerable<(SheetDto Sheet, object? Data)> GetSheets() { @@ -43,8 +49,8 @@ internal partial class OpenXmlWriter } sheetId++; - var defaultSheetInfo = GetSheetInfos(_defaultSheetName); - yield return (defaultSheetInfo.ToDto(sheetId), _value); + var sheetInfo = GetSheetInfos(_sheetName); + yield return (sheetInfo.ToDto(sheetId), _value); } private ExcelSheetInfo GetSheetInfos(string sheetName) @@ -157,37 +163,40 @@ private string GetPanes() return sb.ToString(); } - private (string StyleIndex, string? DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull) + private (string StyleIndex, string DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnMapping, bool valueIsNull) { if (valueIsNull) - return (RegularCellStyleIndex, "str", string.Empty); + return (RegularCellStyleIndex, GetStringType(), string.Empty); if (value is string str) - return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(str)); + { + var styleIndex = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : RegularCellStyleIndex; + return (styleIndex, GetStringType(), str); + } - var type = GetValueType(value, columnInfo); + var type = GetValueType(value, columnMapping); - if (columnInfo is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) + if (columnMapping is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { - var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture); - return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(formattedStr)); + var formattedStr = formattableValue.ToString(columnMapping.ExcelFormat, _configuration.Culture); + return (RegularCellStyleIndex, GetStringType(), formattedStr); } if (type == typeof(DateTime)) - return GetDateTimeValue((DateTime)value, columnInfo); + return GetDateTimeValue((DateTime)value, columnMapping); if (type == typeof(DateTimeOffset)) - return GetDateTimeValue(((DateTimeOffset)value).DateTime, columnInfo); + return GetDateTimeValue(((DateTimeOffset)value).DateTime, columnMapping); if (type == typeof(TimeSpan)) - return GetTimeSpanValue((TimeSpan)value, columnInfo); + return GetTimeSpanValue((TimeSpan)value, columnMapping); #if NET8_0_OR_GREATER if (type == typeof(DateOnly)) - return GetDateTimeValue(((DateOnly)value).ToDateTime(default), columnInfo); + return GetDateTimeValue(((DateOnly)value).ToDateTime(default), columnMapping); if (type == typeof(TimeOnly)) - return GetTimeSpanValue(((TimeOnly)value).ToTimeSpan(), columnInfo); + return GetTimeSpanValue(((TimeOnly)value).ToTimeSpan(), columnMapping); #endif if (type.IsEnum) @@ -202,34 +211,44 @@ private string GetPanes() } description ??= value.ToString(); - return (RegularCellStyleIndex, "str", description); + return (RegularCellStyleIndex, GetStringType(), description); } if (TypeHelper.IsNumericType(type)) { var cellValue = GetNumericValue(value, type); - if (columnInfo?.ExcelFormat is null) + if (columnMapping?.ExcelFormat is null) { - var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? "n" : "str"; + var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? ExcelXml.NumericDataType : GetStringType(); return (RegularCellStyleIndex, dataType, cellValue); } - return (columnInfo.ExcelFormatId.ToString(), null, cellValue); + return (columnMapping.ExcelFormatId.ToString(), ExcelXml.NumericDataType, cellValue); } if (type == typeof(bool)) - return (RegularCellStyleIndex, "b", (bool)value ? "1" : "0"); + return (RegularCellStyleIndex, ExcelXml.BooleanDataType, (bool)value ? "1" : "0"); if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) { - if (!_configuration.EnableWriteFilePath) - return ("4", "str", ""); + if (!_configuration.EnableWriteFilePath) + return (FillCellStyleIndex, ExcelXml.CalculatedStringDataType, ""); var base64 = GetFileValue(rowIndex, cellIndex, value); - return ("4", "str", XmlHelper.EncodeXml(base64)); + return (FillCellStyleIndex, ExcelXml.InlineStringDataType, base64); } - return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(value.ToString())); + return (RegularCellStyleIndex, GetStringType(), value.ToString()); + + string GetStringType() + { + if (columnMapping?.ExcelColumnType == ColumnType.Formula) + return ExcelXml.CalculatedStringDataType; + + return _configuration.StringStorageMode == StringStorageMode.Shared + ? ExcelXml.SharedStringDataType + : ExcelXml.InlineStringDataType; + } } private static Type GetValueType(object value, MiniExcelColumnMapping? columnInfo) @@ -310,20 +329,20 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) } //todo:reconsider cultureinfo - private (string, string?, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping? columnMapping) + private (string, string, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping? columnMapping) { string? cellValue; if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture)) { cellValue = value.ToString(_configuration.Culture); - return (RegularCellStyleIndex, (string?)"str", cellValue); + return (RegularCellStyleIndex, ExcelXml.CalculatedStringDataType, cellValue); } var oaDate = CorrectDateTimeValue(value); cellValue = oaDate.ToString(CultureInfo.InvariantCulture); - var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "3"; + var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : DateCellStyleIndex; - return (format, null, cellValue); + return (format, ExcelXml.NumericDataType, cellValue); } private static double CorrectDateTimeValue(DateTime value) @@ -342,15 +361,15 @@ private static double CorrectDateTimeValue(DateTime value) return oaDate; } - private (string, string?, string) GetTimeSpanValue(TimeSpan value, MiniExcelColumnMapping? columnMapping) + private (string, string, string) GetTimeSpanValue(TimeSpan value, MiniExcelColumnMapping? columnMapping) { if (value.TotalDays >= 1) return GetDateTimeValue(ExcelZeroDate + value, columnMapping); - var cellValue = (value.TotalSeconds / 86400).ToString(CultureInfo.InvariantCulture); - var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "5"; + var cellValue = value.TotalDays.ToString(CultureInfo.InvariantCulture); + var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : TimeCellStyleIndex; - return (format, null, cellValue); + return (format, ExcelXml.NumericDataType, cellValue); } private static string GetDimensionRef(int maxRowIndex, int maxColumnIndex) @@ -416,9 +435,9 @@ private void GenerateWorkBookXmls( private string GetContentTypesXml() { var sb = new StringBuilder(ExcelXml.StartTypes); - foreach (var p in _zipDictionary) + foreach (var p in _zipContentsMap) { - sb.Append(ExcelXml.ContentType(p.Value.ContentType, p.Key)); + sb.Append(ExcelXml.ContentType(p.Value, p.Key)); } sb.Append(ExcelXml.EndTypes); diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 02219182..898d0eef 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -18,13 +18,13 @@ internal partial class OpenXmlWriter : IMiniExcelWriter private readonly List _files = []; private readonly SheetStyleBuildContext _sheetStyleBuildContext; - private readonly string? _defaultSheetName; + private readonly string _sheetName; private readonly bool _printHeader; private readonly object? _value; private int _currentSheetIndex = 0; - private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string? sheetName, OpenXmlConfiguration configuration, bool printHeader) + private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string sheetName, OpenXmlConfiguration configuration, bool printHeader) { _stream = stream; @@ -33,13 +33,13 @@ private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string? _value = value; _printHeader = printHeader; - _defaultSheetName = sheetName; + _sheetName = sheetName; - _sheetStyleBuildContext = new SheetStyleBuildContext(_zipDictionary, _archive, Utf8WithBom); + _sheetStyleBuildContext = new SheetStyleBuildContext(_zipContentsMap, _archive, Utf8WithBom); } [CreateSyncVersion] - internal static async ValueTask CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default) + internal static async ValueTask CreateAsync(Stream stream, object? value, string sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default) { ThrowHelper.ThrowIfInvalidSheetName(sheetName); @@ -64,10 +64,9 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati using var disposableArchive = _archive; #endif await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken).ConfigureAwait(false); - await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken).ConfigureAwait(false); await using var sbc = _sheetStyleBuildContext.ConfigureAwait(false); - var styleBuilder = await GetSheetStyleBuilderAsync(_sheetStyleBuildContext, cancellationToken).ConfigureAwait(false); + var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); var sheets = GetSheets(); var rowsWritten = new List(); @@ -82,7 +81,13 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati } await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); - await GenerateEndXmlAsync(cancellationToken).ConfigureAwait(false); + + await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); + await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); + await GenerateDrawinRelXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateDrawingXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateWorkbookXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); return rowsWritten.ToArray(); } @@ -93,81 +98,95 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? if (!_configuration.FastMode) throw new InvalidOperationException("Insert requires fast mode to be enabled"); - try - { #if NET10_0_OR_GREATER - await using var disposableArchive = _archive.ConfigureAwait(false); + await using var disposableArchive = _archive.ConfigureAwait(false); #else - using var disposableArchive = _archive; + using var disposableArchive = _archive; #endif + await using var sbc = _sheetStyleBuildContext.ConfigureAwait(false); - using var reader = await OpenXmlReader.CreateAsync(_stream, _configuration, cancellationToken: cancellationToken).ConfigureAwait(false); - var rels = await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false) ?? []; + using var reader = await OpenXmlReader.CreateAsync(_stream, _configuration, cancellationToken: cancellationToken).ConfigureAwait(false); + var rels = await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false) ?? []; - _sheets.AddRange(rels - .OrderBy(sheet => sheet.Id) - .Select(sheet => new SheetDto - { - Name = sheet.Name, - SheetIdx = (int)sheet.Id, - State = sheet.State - }) - ); + _sheets.AddRange(rels + .OrderBy(sheet => sheet.Id) + .Select(sheet => new SheetDto + { + Name = sheet.Name, + SheetIdx = (int)sheet.Id, + State = sheet.State + }) + ); - var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName); - if (existSheetDto is not null && !overwriteSheet) - throw new Exception($"Sheet \"{_defaultSheetName}\" already exist"); + var existingSheetDto = _sheets.SingleOrDefault(s => s.Name == _sheetName); + if (existingSheetDto is not null && !overwriteSheet) + throw new Exception($"Sheet \"{_sheetName}\" already exists"); - // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. - var styleBuilder = await GetSheetStyleBuilderAsync(_sheetStyleBuildContext, cancellationToken).ConfigureAwait(false); + // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. + var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); - int rowsWritten; - if (existSheetDto is null) - { - _currentSheetIndex = (int)rels.Max(m => m.Id) + 1; - var insertSheetInfo = GetSheetInfos(_defaultSheetName); - var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); - _sheets.Add(insertSheetDto); - rowsWritten = await CreateSheetXmlAsync(_value, insertSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); - } - else + var sharedStringsEntry = _archive.GetEntry(ExcelFileNames.SharedStrings); + if (sharedStringsEntry is not null) + { +#if NET8_0_OR_GREATER + var sharedStringsStream = await sharedStringsEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await using var disposableStream = sharedStringsStream.ConfigureAwait(false); +#else + using var sharedStringsStream = await sharedStringsEntry.OpenAsync(cancellationToken).ConfigureAwait(false); +#endif + + var index = 0; + await foreach (var sharedString in XmlReaderHelper.GetSharedStringsAsync(sharedStringsStream, cancellationToken).ConfigureAwait(false)) { - _currentSheetIndex = existSheetDto.SheetIdx; - _archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete(); - rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); + _sharedStrings.Add(sharedString, index++); } + } + + int rowsWritten; + if (existingSheetDto is null) + { + _currentSheetIndex = (int)rels.Max(m => m.Id) + 1; + var insertSheetInfo = GetSheetInfos(_sheetName); + var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); + _sheets.Add(insertSheetDto); + rowsWritten = await CreateSheetXmlAsync(_value, insertSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); + } + else + { + _currentSheetIndex = existingSheetDto.SheetIdx; + _archive.Entries.Single(s => s.FullName == existingSheetDto.Path).Delete(); + rowsWritten = await CreateSheetXmlAsync(_value, existingSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); + } - await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); - await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); + await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); + await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); - await GenerateDrawinRelXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); + sharedStringsEntry?.Delete(); + await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); - await GenerateDrawingXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); + await GenerateDrawinRelXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); - GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); - foreach (var (key, value) in sheetsRelsXml) - { - var sheetRelsXmlPath = ExcelFileNames.SheetRels(key); - _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); - await CreateZipEntryAsync(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", value), cancellationToken).ConfigureAwait(false); - } + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); + await GenerateDrawingXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete(); - await CreateZipEntryAsync(ExcelFileNames.Workbook, ExcelContentTypes.Workbook, ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken).ConfigureAwait(false); + GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); + foreach (var (key, value) in sheetsRelsXml) + { + var sheetRelsXmlPath = ExcelFileNames.SheetRels(key); + _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); + await CreateZipEntryAsync(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", value), cancellationToken).ConfigureAwait(false); + } - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.WorkbookRels)?.Delete(); - await CreateZipEntryAsync(ExcelFileNames.WorkbookRels, null, ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete(); + await CreateZipEntryAsync(ExcelFileNames.Workbook, ExcelContentTypes.Workbook, ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken).ConfigureAwait(false); - await InsertContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.WorkbookRels)?.Delete(); + await CreateZipEntryAsync(ExcelFileNames.WorkbookRels, null, ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken).ConfigureAwait(false); - return rowsWritten; - } - finally - { - await _sheetStyleBuildContext.DisposeAsync().ConfigureAwait(false); - } + await InsertContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); + + return rowsWritten; } [CreateSyncVersion] @@ -179,13 +198,13 @@ private async Task CreateSheetXmlAsync(object? values, string sheetPath, IP #if NET8_0_OR_GREATER var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableZipStream = zipStream.ConfigureAwait(false); - - var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); - await using var disposableWriter = writer.ConfigureAwait(false); #else using var zipStream = entry.Open(); - using var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); #endif + + var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); + await using var disposableWriter = writer.ConfigureAwait(false); + if (values is null) { await WriteEmptySheetAsync(writer).ConfigureAwait(false); @@ -195,7 +214,7 @@ private async Task CreateSheetXmlAsync(object? values, string sheetPath, IP rowsWritten = await WriteValuesAsync(writer, values, cancellationToken, progress).ConfigureAwait(false); } - _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, ExcelContentTypes.Worksheet)); + _zipContentsMap.Add(sheetPath, ExcelContentTypes.Worksheet); return rowsWritten; } @@ -306,10 +325,10 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va cancellationToken.ThrowIfCancellationRequested(); await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); - foreach (var cellValue in row) + foreach (var cell in row) { cancellationToken.ThrowIfCancellationRequested(); - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Mapping, widths, cancellationToken).ConfigureAwait(false); + await WriteCellAsync(writer, currentRowIndex, cell.Index, cell.Value, cell.Mapping, widths, cancellationToken).ConfigureAwait(false); progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); @@ -323,9 +342,9 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va cancellationToken.ThrowIfCancellationRequested(); await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); - foreach (var cellValue in row) + foreach (var cell in row) { - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Mapping, widths, cancellationToken).ConfigureAwait(false); + await WriteCellAsync(writer, currentRowIndex, cell.Index, cell.Value, cell.Mapping, widths, cancellationToken).ConfigureAwait(false); progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); @@ -434,7 +453,7 @@ private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List GetSheetStyleBuilderAsync(SheetStyleBuildContext context, CancellationToken cancellationToken = default) + private async Task GetSheetStyleBuilderAsync(CancellationToken cancellationToken = default) { SheetStyleBuilderBase builder = _configuration.TableStyles switch { - TableStyles.None => new MinimalSheetStyleBuilder(context), - TableStyles.Default => new DefaultSheetStyleBuilder(context, _configuration.StyleOptions), + TableStyles.None => new MinimalSheetStyleBuilder(_sheetStyleBuildContext), + TableStyles.Default => new DefaultSheetStyleBuilder(_sheetStyleBuildContext, _configuration.StyleOptions), _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) }; var newInfos = builder.GetGeneratedElementInfos(); - await context.CreateAsync(newInfos, cancellationToken).ConfigureAwait(false); + await _sheetStyleBuildContext.CreateAsync(newInfos, cancellationToken).ConfigureAwait(false); return builder; } @@ -558,11 +582,7 @@ await CreateZipEntryAsync( private async Task GenerateWorkbookXmlAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - - GenerateWorkBookXmls( - out StringBuilder workbookXml, - out StringBuilder workbookRelsXml, - out Dictionary sheetsRelsXml); + GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); foreach (var (key, value) in sheetsRelsXml) { @@ -586,6 +606,12 @@ await CreateZipEntryAsync( cancellationToken).ConfigureAwait(false); } + [CreateSyncVersion] + private async Task GenerateSharedStringsAsync(CancellationToken cancellationToken) + { + await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.SharedStrings(_sharedStrings), cancellationToken).ConfigureAwait(false); + } + [CreateSyncVersion] private async Task GenerateContentTypesXmlAsync(CancellationToken cancellationToken) { @@ -622,14 +648,14 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke partNames.Add(partName); } - foreach (var p in _zipDictionary) + foreach (var (entry, contentType) in _zipContentsMap) { cancellationToken.ThrowIfCancellationRequested(); - var partName = $"/{p.Key}"; - if (!partNames.Contains(partName)) + var entryPath = $"/{entry}"; + if (!partNames.Contains(entryPath)) { - var newElement = new XElement(ns + "Override", new XAttribute("ContentType", p.Value.ContentType), new XAttribute("PartName", partName)); + var newElement = new XElement(ns + "Override", new XAttribute("ContentType", contentType), new XAttribute("PartName", entryPath)); typesElement.Add(newElement); } } @@ -652,17 +678,16 @@ private async Task CreateZipEntryAsync(string path, string? contentType, string #if NET8_0_OR_GREATER var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableZipStream = zipStream.ConfigureAwait(false); - - var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); - await using var disposableWriter = writer.ConfigureAwait(false); #else using var zipStream = entry.Open(); - using var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); #endif + + var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); + await using var disposableWriter = writer.ConfigureAwait(false); await writer.WriteAsync(content, cancellationToken).ConfigureAwait(false); - if (contentType is not (null or "")) - _zipDictionary.Add(path, new ZipPackageInfo(entry, contentType)); + if (!string.IsNullOrEmpty(contentType)) + _zipContentsMap.Add(path, contentType); } [CreateSyncVersion] @@ -741,11 +766,9 @@ async Task LoadWorkbook() if (sheets.Find(s => s.Attribute("name")?.Value.Equals(sheetName, StringComparison.OrdinalIgnoreCase) is true) is not { } sheet) throw new InvalidDataException($"Sheet {sheetName} not found"); - if (!string.IsNullOrEmpty(newSheetName)) + if (newSheetName is not null) { - if (newSheetName.Length > 31) - throw new ArgumentException($"The name \"{newSheetName}\" is too long, the maximum allowed length is 31 characters."); - + ThrowHelper.ThrowIfInvalidSheetName(newSheetName); sheet.SetAttributeValue("name", newSheetName); } diff --git a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs index 45fbeab5..54b1f2ad 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs @@ -275,9 +275,9 @@ protected override async Task GenerateCellStyleXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "0").ConfigureAwait(false); @@ -317,8 +317,8 @@ protected override async Task GenerateCellStyleXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); @@ -414,8 +414,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); @@ -451,8 +451,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "14").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); @@ -484,8 +484,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false); @@ -503,8 +503,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "21").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); @@ -540,8 +540,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + i + _context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); diff --git a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs index 7228be1d..4f657422 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs @@ -110,16 +110,13 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteEndElementAsync().ConfigureAwait(false); const int numFmtIndex = 166; - var index = 0; - for (var i = 0; i < _context.CustomFormatCount; i++) + for (var i = 1; i <= _context.CustomFormatCount; i++) { - index++; - /* * zipDictionary, ZipArchive archive, Encoding encoding) : IDisposable, IAsyncDisposable +internal sealed partial class SheetStyleBuildContext(Dictionary contentTypes, ZipArchive archive, Encoding encoding) : IDisposable, IAsyncDisposable { private const string EmptyStylesXml = """ @@ -10,7 +10,7 @@ internal sealed partial class SheetStyleBuildContext(Dictionary """; - private readonly Dictionary _zipDictionary = zipDictionary; + private readonly Dictionary _contentTypes = contentTypes; private readonly ZipArchive _archive = archive; private readonly Encoding _encoding = encoding; @@ -176,7 +176,7 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella if (_oldStyleXmlZipEntry is null) { - _zipDictionary.Add(ExcelFileNames.Styles, new ZipPackageInfo(_newStyleXmlZipEntry!, ExcelContentTypes.Styles)); + _contentTypes.Add(ExcelFileNames.Styles, ExcelContentTypes.Styles); } else { @@ -197,7 +197,7 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella await tempStream.CopyToAsync(newStream, 4096, cancellationToken).ConfigureAwait(false); } - _zipDictionary[ExcelFileNames.Styles] = new ZipPackageInfo(finalStyleXmlZipEntry, ExcelContentTypes.Styles); + _contentTypes[ExcelFileNames.Styles] = ExcelContentTypes.Styles; _newStyleXmlZipEntry?.Delete(); _newStyleXmlZipEntry = null; } diff --git a/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs b/src/MiniExcel.OpenXml/Utils/OpenXmlZip.cs similarity index 95% rename from src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs rename to src/MiniExcel.OpenXml/Utils/OpenXmlZip.cs index 1c13805a..28387d13 100644 --- a/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs +++ b/src/MiniExcel.OpenXml/Utils/OpenXmlZip.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace MiniExcelLib.OpenXml.Zip; +namespace MiniExcelLib.OpenXml.Utils; /// Copied & modified from ExcelDataReader ZipWorker @MIT License internal sealed partial class OpenXmlZip : IDisposable, IAsyncDisposable diff --git a/src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs b/src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs deleted file mode 100644 index f8ca652a..00000000 --- a/src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MiniExcelLib.OpenXml.Zip; - -internal class ZipPackageInfo(ZipArchiveEntry zipArchiveEntry, string contentType) -{ - public ZipArchiveEntry ZipArchiveEntry { get; set; } = zipArchiveEntry; - public string ContentType { get; set; } = contentType; -} \ No newline at end of file diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index af744c9f..51c016bd 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -2,16 +2,13 @@ using MiniExcelLib; using MiniExcelLib.Core; using MiniExcelLib.Csv; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.Models; using MiniExcelLib.OpenXml.Picture; -using MiniExcelLibs.OpenXml; using Zomp.SyncMethodGenerator; using NewMiniExcel = MiniExcelLib.Core.MiniExcel; -using OpenXmlExporter = MiniExcelLib.OpenXml.Api.OpenXmlExporter; -using OpenXmlImporter = MiniExcelLib.Core.OpenXmlImporter; -using OpenXmlTemplater = MiniExcelLib.Core.OpenXmlTemplater; +using NewOpenXmlConfiguration = MiniExcelLib.OpenXml.OpenXmlConfiguration; // ReSharper disable once CheckNamespace namespace MiniExcelLibs; @@ -39,7 +36,7 @@ public static MiniExcelDataReader GetReader(string path, bool useHeaderRow = fal var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.GetDataReader(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), + ExcelType.XLSX => ExcelImporter.GetDataReader(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration), ExcelType.CSV => CsvImporter.GetDataReader(path, useHeaderRow), _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; @@ -50,7 +47,7 @@ public static MiniExcelDataReader GetReader(this Stream stream, bool useHeaderRo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.GetDataReader(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), + ExcelType.XLSX => ExcelImporter.GetDataReader(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration), ExcelType.CSV => CsvImporter.GetDataReader(stream, useHeaderRow), _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; @@ -64,7 +61,7 @@ public static async Task InsertAsync(string path, object value, string shee var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(path, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(path, value, sheetName, printHeader, overwriteSheet, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.AppendAsync(path, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -78,7 +75,7 @@ public static async Task InsertAsync(this Stream stream, object value, stri var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.AppendAsync(stream, value, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -92,7 +89,7 @@ public static async Task SaveAsAsync(string path, object value, bool prin var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -106,7 +103,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -118,7 +115,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(path, sheetName, startCell, hasHeader, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(path, sheetName, startCell, hasHeader, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(path, hasHeader, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -130,7 +127,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(stream, sheetName, startCell, hasHeader, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(stream, sheetName, startCell, hasHeader, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, hasHeader, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -142,7 +139,7 @@ public static IAsyncEnumerable QueryAsync(string path, bool useHeaderRo var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -154,7 +151,7 @@ public static IAsyncEnumerable QueryAsync(this Stream stream, bool useH var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -175,7 +172,7 @@ public static IAsyncEnumerable QueryRangeAsync(string path, bool useHea var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startCell, endCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startCell, endCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -187,7 +184,7 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -199,7 +196,7 @@ public static IAsyncEnumerable QueryRangeAsync(string path, bool useHea var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -211,7 +208,7 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -221,27 +218,27 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(string path, string templatePath, object value, IConfiguration? configuration = null, CancellationToken cancellationToken = default) - => await ExcelTemplater.FillTemplateAsync(path, templatePath, value, true, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(path, templatePath, value, true, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(string path, byte[] templateBytes, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(path, templateBytes, value, true, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(path, templateBytes, value, true, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(this Stream stream, string templatePath, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(stream, templatePath, value, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(stream, templatePath, value, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(this Stream stream, byte[] templateBytes, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(stream, templateBytes, value, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(stream, templateBytes, value, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(string path, Stream templateStream, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(path, templateStream, value, true, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(path, templateStream, value, true, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(this Stream stream, Stream templateStream, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(stream, templateStream, value, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(stream, templateStream, value, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); #region MergeCells @@ -251,7 +248,7 @@ public static async Task MergeSameCellsAsync(string mergedFilePath, string path, if (excelType != ExcelType.XLSX) throw new NotSupportedException("MergeSameCells is only supported for Xlsx files"); - await ExcelTemplater.MergeSameCellsAsync(mergedFilePath, path, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + await ExcelTemplater.MergeSameCellsAsync(mergedFilePath, path, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] @@ -260,7 +257,7 @@ public static async Task MergeSameCellsAsync(this Stream stream, string path, Ex if (excelType != ExcelType.XLSX) throw new NotSupportedException("MergeSameCells is only supported for Xlsx files"); - await ExcelTemplater.MergeSameCellsAsync(stream, path, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + await ExcelTemplater.MergeSameCellsAsync(stream, path, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] @@ -269,7 +266,7 @@ public static async Task MergeSameCellsAsync(this Stream stream, byte[] filePath if (excelType != ExcelType.XLSX) throw new NotSupportedException("MergeSameCells is only supported for Xlsx files"); - await ExcelTemplater.MergeSameCellsAsync(stream, filePath, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + await ExcelTemplater.MergeSameCellsAsync(stream, filePath, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); } #endregion @@ -281,7 +278,7 @@ public static async Task QueryAsDataTableAsync(string path, bool useH var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.QueryAsDataTableAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -294,26 +291,26 @@ public static async Task QueryAsDataTableAsync(this Stream stream, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.QueryAsDataTableAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; } [CreateSyncVersion] - public static async Task> GetSheetNamesAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetNamesAsync(string path, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetNamesAsync(path, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] - public static async Task> GetSheetNamesAsync(this Stream stream, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetNamesAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetNamesAsync(stream, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] - public static async Task> GetSheetInformationsAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetInformationsAsync(string path, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetInformationsAsync(path, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] - public static async Task> GetSheetInformationsAsync(this Stream stream, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetInformationsAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetInformationsAsync(stream, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] @@ -322,7 +319,7 @@ public static async Task> GetColumnsAsync(string path, bool var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.GetColumnNamesAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; @@ -334,7 +331,7 @@ public static async Task> GetColumnsAsync(this Stream stream var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.GetColumnNamesAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; diff --git a/src/MiniExcel/MiniExcelConverter.cs b/src/MiniExcel/MiniExcelConverter.cs index 564e6e96..ebec5f89 100644 --- a/src/MiniExcel/MiniExcelConverter.cs +++ b/src/MiniExcel/MiniExcelConverter.cs @@ -1,7 +1,7 @@ using MiniExcelLib.Core; using MiniExcelLib.Core.Helpers; using MiniExcelLib.Csv; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using Zomp.SyncMethodGenerator; namespace MiniExcelLib; diff --git a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs index a8176b55..5487c9e5 100644 --- a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs @@ -1,6 +1,4 @@ -using MiniExcelLib.OpenXml.Api; - -namespace MiniExcelLib.Csv.Tests; +namespace MiniExcelLib.Csv.Tests; public class AsyncIssueTests { diff --git a/tests/MiniExcel.Csv.Tests/IssueTests.cs b/tests/MiniExcel.Csv.Tests/IssueTests.cs index 6eb398b6..47f8adcc 100644 --- a/tests/MiniExcel.Csv.Tests/IssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/IssueTests.cs @@ -1,4 +1,4 @@ -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; namespace MiniExcelLib.Csv.Tests; diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj index a31d2f89..ec76bc97 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj @@ -50,7 +50,6 @@ - diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs index e3aede7f..5370e2a9 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs @@ -1534,4 +1534,28 @@ class Issue951 public object this[string test] => new(); } + + [Fact] + public async Task TestIssue627() + { + var data = new[] { new { LongNumber = "1550432695793487872" } }; + + var config = new OpenXmlConfiguration + { + DynamicColumns = + [ + new DynamicExcelColumn("LongNumber") { Format = "@" } + ] + }; + + await using var ms = new MemoryStream(); + await _excelExporter.ExportAsync(ms, data, configuration: config); + ms.Seek(0, SeekOrigin.Begin); + + using var package = new ExcelPackage(ms); + var cell = package.Workbook.Worksheets[0].Cells["A2"]; + + Assert.Equal("1550432695793487872", cell.GetValue()); + Assert.Equal("@", cell.Style.Numberformat.Format); + } } \ No newline at end of file diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 739722fb..59ffd65a 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -203,7 +203,7 @@ public void TestIssue405() _excelExporter.Export(path.ToString(), value); var xml = SheetHelper.GetZipFileContent(path.ToString(), "xl/sharedStrings.xml"); - Assert.StartsWith("(() => _excelExporter.ExportAsync(ms1, Array.Empty(), sheetName: "Sheet?")); + + await using var ms2 = new MemoryStream(); + await Assert.ThrowsAsync(() => _excelExporter.InsertSheetAsync(ms2, Array.Empty(), sheetName: "Sheet[]")); + + await using var ms3 = new MemoryStream(); + using var package = new ExcelPackage(ms3); + package.Workbook.Worksheets.Add("Sheet1"); + await package.SaveAsync(); + + ms1.Seek(0, SeekOrigin.Begin); + await Assert.ThrowsAsync(() => _excelExporter.AlterSheetAsync(ms3, "Sheet1", "Sheet*")); + } } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs index 44bbcccc..fedb55ef 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs @@ -1713,4 +1713,22 @@ public void ExportAndQueryFieldsWithoutAttributeTest() Assert.Contains("Mapped", rows[0].Keys); Assert.DoesNotContain("NotMappedField", rows[0].Keys); } + + [Fact] + public async Task InvalidSheetNameCharactersShouldThrow() + { + await using var ms1 = new MemoryStream(); + Assert.Throws(() => _excelExporter.Export(ms1, Array.Empty(), sheetName: "Sheet?")); + + await using var ms2 = new MemoryStream(); + Assert.Throws(() => _excelExporter.InsertSheet(ms2, Array.Empty(), sheetName: "Sheet[]")); + + await using var ms3 = new MemoryStream(); + using var package = new ExcelPackage(ms3); + package.Workbook.Worksheets.Add("Sheet1"); + package.Save(); + + ms1.Seek(0, SeekOrigin.Begin); + Assert.Throws(() => _excelExporter.AlterSheet(ms3, "Sheet1", "Sheet*")); + } }