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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
4 changes: 2 additions & 2 deletions src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ public interface IMiniExcelWriteAdapter
IEnumerable<CellWriteInfo[]> GetRows(List<MiniExcelColumnMapping> 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;
}
17 changes: 10 additions & 7 deletions src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
70 changes: 64 additions & 6 deletions src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
namespace MiniExcelLib.OpenXml.Api;
// ReSharper disable once CheckNamespace
namespace MiniExcelLib.OpenXml;

public sealed partial class OpenXmlExporter
{
internal OpenXmlExporter() { }

/// <summary>
/// Inserts a new worksheet into an existing OpenXml document.
/// </summary>
/// <param name="path">The path to the OpenXml document to modify.</param>
/// <param name="value">The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a <see cref="DataTable" /> or a <see cref="IDataReader"/>.</param>
/// <param name="sheetName">The name to assign to the new worksheet.</param>
/// <param name="printHeader">If <c>true</c>, includes the header row in the new sheet; otherwise, only data rows are written.</param>
/// <param name="overwriteSheet">If <c>true</c>, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists.</param>
/// <param name="configuration">Optional configuration settings for the insert operation.</param>
/// <param name="progress">Optional progress reporter to track insertion progress. The report value represents the number of cells written.</param>
/// <param name="cancellationToken">A cancellation token to monitor for cancellation requests.</param>
/// <returns>The number of rows written to the new sheet.</returns>
/// <remarks>
/// FastMode is automatically enabled for this process and disabling it will result in <see cref="InvalidOperationException"/>.
/// </remarks>
[CreateSyncVersion]
public async Task<int> InsertSheetAsync(string path, object value, string? sheetName = "Sheet1",
public async Task<int> InsertSheetAsync(string path, object value, string sheetName = "Sheet1",
bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null,
IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
Expand All @@ -27,8 +43,24 @@ public async Task<int> InsertSheetAsync(string path, object value, string? sheet
return await InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Inserts a new worksheet into an existing OpenXml document.
/// </summary>
/// <param name="stream">The stream containing the OpenXml document to modify.</param>
/// <param name="value">The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a <c>IEnumeable&lt;IDictionary&lt;string, object&gt;&gt;</c>, a <see cref="DataTable" /> or a <see cref="IDataReader"/>.</param>
/// <param name="sheetName">The name to assign to the new worksheet.</param>
/// <param name="printHeader">If <c>true</c>, includes the header row in the new sheet; otherwise, only data rows are written.</param>
/// <param name="overwriteSheet">If <c>true</c>, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists.</param>
/// <param name="configuration">Optional configuration settings for the insert operation.</param>
/// <param name="progress">Optional progress reporter to track insertion progress. The report value represents the number of cells written.</param>
/// <param name="cancellationToken">A cancellation token to monitor for cancellation requests.</param>
/// <returns>The number of rows written to the new sheet.</returns>
/// <remarks>
/// The stream position is reset to the end before writing.
/// FastMode is automatically enabled for this process and disabling it will result in <see cref="InvalidOperationException"/>.
/// </remarks>
[CreateSyncVersion]
public async Task<int> InsertSheetAsync(Stream stream, object value, string? sheetName = "Sheet1",
public async Task<int> InsertSheetAsync(Stream stream, object value, string sheetName = "Sheet1",
bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null,
IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
Expand All @@ -39,12 +71,24 @@ public async Task<int> 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);
}

/// <summary>
/// Exports data to a file as an OpenXml document.
/// </summary>
/// <param name="path">The path to write the OpenXml document to.</param>
/// <param name="value">The data object to export. This can be an enumerable collection of a reference type, a <c>IEnumeable&lt;IDictionary&lt;string, object&gt;&gt;</c>, a <see cref="DataTable" /> or a <see cref="IDataReader"/>.</param>
/// <param name="printHeader">If <c>true</c>, includes the header row in the output; otherwise, only data rows are written.</param>
/// <param name="sheetName">The name to assign to the worksheet.</param>
/// <param name="overwriteFile">If <c>true</c>, overwrites the file at the specified path, otherwise a <see cref="IOException"/> will be raised if the file already exists.</param>
/// <param name="configuration">Optional configuration settings for the export operation.</param>
/// <param name="progress">Optional progress reporter to track export progress. The report value represents the number of cells written.</param>
/// <param name="cancellationToken">A cancellation token to monitor for cancellation requests.</param>
/// <returns>An array of integers representing the number of rows written for each exported sheet.</returns>
[CreateSyncVersion]
public async Task<int[]> 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<int>? progress = null, CancellationToken cancellationToken = default)
{
if (Path.GetExtension(path).Equals(".xlsm", StringComparison.InvariantCultureIgnoreCase))
Expand All @@ -59,8 +103,22 @@ public async Task<int[]> ExportAsync(string path, object value, bool printHeader
return await ExportAsync(stream, value, printHeader, sheetName, configuration, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Exports data to a stream as an OpenXml document.
/// </summary>
/// <param name="stream">The stream to write the OpenXml document.</param>
/// <param name="value">The data object to export. This can be an enumerable collection of a reference type, a <c>IEnumeable&lt;IDictionary&lt;string, object&gt;&gt;</c>, a <see cref="DataTable" /> or a <see cref="IDataReader"/>.</param>
/// <param name="printHeader">If <c>true</c>, includes the header row in the output; otherwise, only data rows are written.</param>
/// <param name="sheetName">The name to assign to the worksheet.</param>
/// <param name="configuration">Optional configuration settings for the export operation.</param>
/// <param name="progress">Optional progress reporter to track export progress. The report value represents the number of cells written.</param>
/// <param name="cancellationToken">A cancellation token to monitor for cancellation requests.</param>
/// <returns>An array of integers representing the number of rows written for each exported sheet.</returns>
/// <remarks>
/// The stream position is not reset before writing.
/// </remarks>
[CreateSyncVersion]
public async Task<int[]> ExportAsync(Stream stream, object value, bool printHeader = true, string? sheetName = "Sheet1",
public async Task<int[]> ExportAsync(Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1",
OpenXmlConfiguration? configuration = null, IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
var writer = await OpenXmlWriter
Expand Down
5 changes: 2 additions & 3 deletions src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
3 changes: 1 addition & 2 deletions src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
3 changes: 2 additions & 1 deletion src/MiniExcel.OpenXml/Api/ProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using MiniExcelLib.Core;

namespace MiniExcelLib.OpenXml.Api;
// ReSharper disable once CheckNamespace
namespace MiniExcelLib.OpenXml;

public static class ProviderExtensions
{
Expand Down
4 changes: 3 additions & 1 deletion src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
29 changes: 26 additions & 3 deletions src/MiniExcel.OpenXml/Constants/ExcelXml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """<?xml version="1.0" encoding="utf-8"?><x:worksheet xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><x:dimension ref="A1"/><x:sheetData></x:sheetData></x:worksheet>""";

internal static readonly string DefaultRels =
Expand Down Expand Up @@ -68,7 +74,25 @@ static ExcelXml()
</Relationships>
""";

internal const string DefaultSharedString = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" count=\"0\" uniqueCount=\"0\"></sst>";
internal static string SharedStrings(Dictionary<string, int> sharedStrings)
{
var sb = new StringBuilder();
sb.Append("""<?xml version="1.0" encoding="UTF-8" standalone="yes" ?><x:sst xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" """);
sb.Append($"uniqueCount=\"{sharedStrings.Count}\">");

foreach(var (text, _) in sharedStrings.OrderBy(x => x.Value))
{
sb.Append("<x:si><x:t");

if (text.StartsWith(" ") || text.EndsWith(" "))
sb.Append(" xml:space=\"preserve\"");

sb.Append($">{XmlHelper.EncodeXml(text)}</x:t></x:si>");
}

sb.Append("</x:sst>");
return sb.ToString();
}

internal const string StartTypes = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings" Extension="bin"/><Default ContentType="application/xml" Extension="xml"/><Default ContentType="image/jpeg" Extension="jpg"/><Default ContentType="image/png" Extension="png"/><Default ContentType="image/gif" Extension="gif"/><Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>""";
internal static string ContentType(string contentType, string partName) => $"<Override ContentType=\"{contentType}\" PartName=\"/{partName}\" />";
Expand Down Expand Up @@ -122,5 +146,4 @@ internal static string DrawingXml(FileDto file, int fileIndex)

internal static string Sheet(SheetDto sheetDto, int sheetId)
=> $"""<x:sheet name="{XmlHelper.EncodeXml(sheetDto.Name)}" sheetId="{sheetId}"{(string.IsNullOrWhiteSpace(sheetDto.State) ? string.Empty : $" state=\"{sheetDto.State}\"")} r:id="{sheetDto.ID}" />""";

}
}
18 changes: 15 additions & 3 deletions src/MiniExcel.OpenXml/Constants/WorksheetXml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,22 @@ internal static string Column(int colIndex, double columnWidth, bool hidden = fa
internal const string EndCols = "</x:cols>";

internal static string EmptyCell(string cellReference, string styleIndex) => $"<x:c r=\"{cellReference}\" s=\"{styleIndex}\"></x:c>";

//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)
=> $"<x:c r=\"{cellReference}\"{(cellType is null ? string.Empty : $" t=\"{cellType}\"")} s=\"{styleIndex}\"{(preserveSpace ? " xml:space=\"preserve\"" : string.Empty)}><x:{(columnType == ColumnType.Formula ? "f" : "v")}>{cellValue}</x:{(columnType == ColumnType.Formula ? "f" : "v")}></x:c>";
{
return cellType switch
{
_ when columnType == ColumnType.Formula
=> $"""<x:c r="{cellReference}" t="{ExcelXml.CalculatedStringDataType}" s="{styleIndex}"{(preserveSpace ? " xml:space=\"preserve\"" : "")}><x:f>{XmlHelper.EncodeXml(cellValue)}</x:f></x:c>""",

ExcelXml.InlineStringDataType
=> $"""<x:c r="{cellReference}" t="{ExcelXml.InlineStringDataType}" s="{styleIndex}"><x:is><x:t{(preserveSpace ? " xml:space=\"preserve\"" : "")}>{XmlHelper.EncodeXml(cellValue)}</x:t></x:is></x:c>""",

ExcelXml.SharedStringDataType
=> $"""<x:c r="{cellReference}" t="{ExcelXml.SharedStringDataType}" s="{styleIndex}"><x:v>{cellValue}</x:v></x:c>""",

_ => $"""<x:c r="{cellReference}" t="{cellType}" s="{styleIndex}"><x:v>{cellValue}</x:v></x:c>"""
};
}

internal static string Autofilter(string dimensionRef) => $"<x:autoFilter ref=\"{dimensionRef}\" />";
internal static string Drawing(int sheetIndex) => $"<x:drawing r:id=\"drawing{sheetIndex}\" />";
Expand Down
1 change: 0 additions & 1 deletion src/MiniExcel.OpenXml/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 5 additions & 1 deletion src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)}");
}
}
2 changes: 1 addition & 1 deletion src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ internal static ExcelColumnWidthCollection GetFromMappings(ICollection<MiniExcel
return new ExcelColumnWidthCollection(columnWidths, maxWidth);
}

internal void AdjustWidth(int columnIndex, string columnValue)
internal void AdjustWidth(int columnIndex, string? columnValue)
{
if (!string.IsNullOrEmpty(columnValue) && _columnWidths.TryGetValue(columnIndex, out var currentWidth))
{
Expand Down
Loading
Loading