diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 83a18010..cc178d01 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index dd202500..68fa7dec 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v1 with: diff --git a/src/MiniExcel/OpenXml/Constants/ExcelContentTypes.cs b/src/MiniExcel/OpenXml/Constants/ExcelContentTypes.cs new file mode 100644 index 00000000..52ea48e4 --- /dev/null +++ b/src/MiniExcel/OpenXml/Constants/ExcelContentTypes.cs @@ -0,0 +1,12 @@ +namespace MiniExcelLibs.OpenXml.Constants +{ + internal static class ExcelContentTypes + { + internal const string Relationships = "application/vnd.openxmlformats-package.relationships+xml"; + internal const string SharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"; + internal const string Worksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"; + internal const string Styles = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"; + internal const string Drawing = "application/vnd.openxmlformats-officedocument.drawing+xml"; + internal const string Workbook = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"; + } +} diff --git a/src/MiniExcel/OpenXml/Constants/ExcelFileNames.cs b/src/MiniExcel/OpenXml/Constants/ExcelFileNames.cs new file mode 100644 index 00000000..aac4e023 --- /dev/null +++ b/src/MiniExcel/OpenXml/Constants/ExcelFileNames.cs @@ -0,0 +1,19 @@ +namespace MiniExcelLibs.OpenXml.Constants +{ + internal static class ExcelFileNames + { + internal const string Rels = "_rels/.rels"; + internal const string SharedStrings = "xl/sharedStrings.xml"; + + internal const string ContentTypes = "[Content_Types].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 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"; + } +} diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefualtOpenXml.cs b/src/MiniExcel/OpenXml/Constants/ExcelXml.cs similarity index 61% rename from src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefualtOpenXml.cs rename to src/MiniExcel/OpenXml/Constants/ExcelXml.cs index efc3398b..a4efba29 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefualtOpenXml.cs +++ b/src/MiniExcel/OpenXml/Constants/ExcelXml.cs @@ -1,37 +1,36 @@ -using MiniExcelLibs.Zip; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using MiniExcelLibs.OpenXml.Models; -namespace MiniExcelLibs.OpenXml +namespace MiniExcelLibs.OpenXml.Constants { - internal partial class ExcelOpenXmlSheetWriter : IExcelWriter + internal static class ExcelXml { - private readonly Dictionary _zipDictionary = new Dictionary(); - - static ExcelOpenXmlSheetWriter() + static ExcelXml() { - _defaultRels = MinifyXml(_defaultRels); - _defaultWorkbookXml = MinifyXml(_defaultWorkbookXml); - _defaultStylesXml = MinifyXml(_defaultStylesXml); - _defaultWorkbookXmlRels = MinifyXml(_defaultWorkbookXmlRels); - _defaultSheetRelXml = MinifyXml(_defaultSheetRelXml); - _defaultDrawing = MinifyXml(_defaultDrawing); + DefaultRels = MinifyXml(DefaultRels); + DefaultWorkbookXml = MinifyXml(DefaultWorkbookXml); + DefaultStylesXml = MinifyXml(DefaultStylesXml); + DefaultWorkbookXmlRels = MinifyXml(DefaultWorkbookXmlRels); + DefaultSheetRelXml = MinifyXml(DefaultSheetRelXml); + DefaultDrawing = MinifyXml(DefaultDrawing); } - private static readonly string _defaultRels = @" + private static string MinifyXml(string xml) => xml.Replace("\r", "").Replace("\n", "").Replace("\t", ""); + + internal static readonly string EmptySheetXml = $@""; + + internal static readonly string DefaultRels = @" "; - private static readonly string _defaultWorkbookXmlRels = @" + internal static readonly string DefaultWorkbookXmlRels = @" {{sheets}} "; - private static readonly string _noneStylesXml = @" + internal static readonly string NoneStylesXml = @" @@ -53,7 +52,7 @@ static ExcelOpenXmlSheetWriter() "; - private static readonly string _defaultStylesXml = @" + internal static readonly string DefaultStylesXml = @" @@ -157,7 +156,7 @@ static ExcelOpenXmlSheetWriter() "; - private static readonly string _defaultWorkbookXml = @" + internal static readonly string DefaultWorkbookXml = @" @@ -165,57 +164,38 @@ static ExcelOpenXmlSheetWriter() "; - private static readonly string _defaultSheetRelXml = @" + internal static readonly string DefaultSheetRelXml = @" {{format}} "; - private static readonly string _defaultDrawing = @" + internal static readonly string DefaultDrawing = @" {{format}} "; - private static readonly string _defaultDrawingXmlRels = @" + internal static readonly string DefaultDrawingXmlRels = @" {{format}} "; - private static readonly string _defaultSharedString = ""; - private static string MinifyXml(string xml) => xml.Replace("\r", "").Replace("\n", "").Replace("\t", ""); - - private string GetStylesXml() - { - var styleXml = string.Empty; + internal static readonly string DefaultSharedString = ""; - if (_configuration.TableStyles == TableStyles.None) - { - styleXml = _noneStylesXml; - } - else if (_configuration.TableStyles == TableStyles.Default) - { - styleXml = _defaultStylesXml; - } + internal static readonly string StartTypes = @""; + internal static string ContentType(string contentType, string partName) => $""; + internal static readonly string EndTypes = ""; - return styleXml; - } + internal static string WorksheetRelationship(SheetDto sheetDto) + => $@""; - private string GetDrawingRelationshipXml(int sheetIndex) - { - var drawing = new StringBuilder(); - foreach (var i in _files.Where(w => w.IsImage && w.SheetId == sheetIndex + 1)) - { - drawing.AppendLine($@""); - } + internal static string ImageRelationship(FileDto image) + => $@""; - return drawing.ToString(); - } + internal static string DrawingRelationship(int sheetId) + => $@""; - private string GetDrawingXml(int sheetIndex) - { - var drawing = new StringBuilder(); - foreach (var file in _files.Where(w => w.IsImage && w.SheetId == sheetIndex + 1)) - { - drawing.Append($@" + internal static string DrawingXml(FileDto file, int fileIndex) + => $@" {file.CellIndex - 1/* why -1 : https://user-images.githubusercontent.com/12729184/150460189-f08ed939-44d4-44e1-be6e-9c533ece6be8.png*/} 0 @@ -225,7 +205,7 @@ private string GetDrawingXml(int sheetIndex) - + @@ -247,49 +227,9 @@ private string GetDrawingXml(int sheetIndex) - "); - } - - return drawing.ToString(); - } - - private void GenerateWorkBookXmls( - out StringBuilder workbookXml, - out StringBuilder workbookRelsXml, - out Dictionary sheetsRelsXml) - { - workbookXml = new StringBuilder(); - workbookRelsXml = new StringBuilder(); - sheetsRelsXml = new Dictionary(); - var sheetId = 0; - foreach (var s in _sheets) - { - sheetId++; - if (string.IsNullOrEmpty(s.State)) - { - workbookXml.AppendLine($@""); - } - else - { - workbookXml.AppendLine($@""); - } + "; - workbookRelsXml.AppendLine($@""); - - //TODO: support multiple drawing - //TODO: ../drawings/drawing1.xml or /xl/drawings/drawing1.xml - var sheetRelsXml = $@""; - sheetsRelsXml.Add(s.SheetIdx, sheetRelsXml); - } - } - - private string GetContentTypesXml() - { - var sb = new StringBuilder(@""); - foreach (var p in _zipDictionary) - sb.Append($""); - sb.Append(""); - return sb.ToString(); - } + internal static string Sheet(SheetDto sheetDto, int sheetId) + => $@""; } } diff --git a/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs new file mode 100644 index 00000000..5f1f7202 --- /dev/null +++ b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs @@ -0,0 +1,41 @@ +using System.Globalization; + +namespace MiniExcelLibs.OpenXml.Constants +{ + internal class WorksheetXml + { + internal const string StartWorksheet = @""; + internal const string StartWorksheetWithRelationship = @""; + internal const string EndWorksheet = ""; + + internal const string StartDimension = @" $"{StartDimension}{dimensionRef}\"/>"; + + internal const string StartSheetData = ""; + internal const string EndSheetData = ""; + + internal static string StartRow(int rowIndex) + => $""; + internal const string EndRow = ""; + + internal const string StartCols = ""; + internal static string Column(int? colIndex, double? columnWidth) + => $@""; + 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) + => $"{cellValue}"; + + internal static string Autofilter(string dimensionRef) + => $""; + + internal static string Drawing(int sheetIndex) + => $""; + + } +} diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs index 1c78edba..100b415d 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs @@ -1,5 +1,4 @@ -using MiniExcelLibs.Exceptions; -using MiniExcelLibs.Utils; +using MiniExcelLibs.Utils; using MiniExcelLibs.Zip; using System; using System.Collections.Generic; diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs index e79aa96e..bf1b8946 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs @@ -1,4 +1,5 @@ -using MiniExcelLibs.Utils; +using MiniExcelLibs.OpenXml.Constants; +using MiniExcelLibs.Utils; using MiniExcelLibs.Zip; using System; using System.Collections; @@ -18,52 +19,13 @@ internal partial class ExcelOpenXmlSheetWriter : IExcelWriter { await GenerateDefaultOpenXmlAsync(cancellationToken); - switch (_value) - { - case IDictionary sheets: - { - var sheetId = 0; - _sheets.RemoveAt(0);//TODO:remove - foreach (var sheet in sheets) - { - sheetId++; - var sheetInfos = GetSheetInfos(sheet.Key); - var sheetDto = sheetInfos.ToDto(sheetId); - _sheets.Add(sheetDto); //TODO:remove - - currentSheetIndex = sheetId; - - await CreateSheetXmlAsync(sheet.Value, sheetDto.Path, cancellationToken); - } - - break; - } - - case DataSet sheets: - { - var sheetId = 0; - _sheets.RemoveAt(0);//TODO:remove - foreach (DataTable dt in sheets.Tables) - { - sheetId++; - var sheetInfos = GetSheetInfos(dt.TableName); - var sheetDto = sheetInfos.ToDto(sheetId); - _sheets.Add(sheetDto); //TODO:remove - - currentSheetIndex = sheetId; - - await CreateSheetXmlAsync(dt, sheetDto.Path, cancellationToken); - } + var sheets = GetSheets(); - break; - } - - default: - //Single sheet export. - currentSheetIndex++; - - await CreateSheetXmlAsync(_value, _sheets[0].Path, cancellationToken); - break; + foreach (var sheet in sheets) + { + _sheets.Add(sheet.Item1); //TODO:remove + currentSheetIndex = sheet.Item1.SheetIdx; + await CreateSheetXmlAsync(sheet.Item2, sheet.Item1.Path, cancellationToken); } await GenerateEndXmlAsync(cancellationToken); @@ -72,8 +34,8 @@ internal partial class ExcelOpenXmlSheetWriter : IExcelWriter internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationToken) { - await CreateZipEntryAsync("_rels/.rels", "application/vnd.openxmlformats-package.relationships+xml", _defaultRels, cancellationToken); - await CreateZipEntryAsync("xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", _defaultSharedString, cancellationToken); + await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken); + await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken); } private async Task CreateSheetXmlAsync(object value, string sheetPath, CancellationToken cancellationToken) @@ -106,28 +68,26 @@ private async Task CreateSheetXmlAsync(object value, string sheetPath, Cancellat } } End: //for re-using code - _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml")); + _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, ExcelContentTypes.Worksheet)); } private async Task WriteEmptySheetAsync(MiniExcelAsyncStreamWriter writer) { - await writer.WriteAsync($@""); + await writer.WriteAsync(ExcelXml.EmptySheetXml); } private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter writer, IDataReader reader) { long dimensionWritePosition = 0; - await writer.WriteAsync($@""); - var xIndex = 1; + await writer.WriteAsync(WorksheetXml.StartWorksheet); var yIndex = 1; - var maxColumnIndex = 0; - var maxRowIndex = 0; + int maxColumnIndex; + int maxRowIndex; { - if (_configuration.FastMode) { - dimensionWritePosition = await writer.WriteAndFlushAsync($@""); // end of code will be replaced + dimensionWritePosition = await writer.WriteAndFlushAsync(WorksheetXml.StartDimension); + await writer.WriteAsync(WorksheetXml.DimensionPlaceholder); // end of code will be replaced } var props = new List(); @@ -141,7 +101,7 @@ private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter wr await WriteColumnsWidthsAsync(writer, props); - await writer.WriteAsync(""); + await writer.WriteAsync(WorksheetXml.StartSheetData); int fieldCount = reader.FieldCount; if (_printHeader) { @@ -151,15 +111,15 @@ private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter wr while (reader.Read()) { - await writer.WriteAsync($""); - xIndex = 1; + await writer.WriteAsync(WorksheetXml.StartRow(yIndex)); + var xIndex = 1; for (int i = 0; i < fieldCount; i++) { var cellValue = reader.GetValue(i); await WriteCellAsync(writer, yIndex, xIndex, cellValue, props[i]); xIndex++; } - await writer.WriteAsync($""); + await writer.WriteAsync(WorksheetXml.EndRow); yIndex++; } @@ -167,10 +127,14 @@ private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter wr maxRowIndex = yIndex - 1; } - await writer.WriteAsync(""); + await writer.WriteAsync(WorksheetXml.EndSheetData); + if (_configuration.AutoFilter) - await writer.WriteAsync($""); - await writer.WriteAndFlushAsync(""); + { + await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex))); + } + + await writer.WriteAndFlushAsync(WorksheetXml.EndWorksheet); if (_configuration.FastMode) { @@ -243,7 +207,7 @@ private async Task GenerateSheetByEnumerableAsync(MiniExcelAsyncStreamWriter wri } } - await writer.WriteAsync($@""); + await writer.WriteAsync(WorksheetXml.StartWorksheetWithRelationship); long dimensionWritePosition = 0; @@ -251,20 +215,20 @@ private async Task GenerateSheetByEnumerableAsync(MiniExcelAsyncStreamWriter wri if (_configuration.FastMode && rowCount == null) { // Write a placeholder for the table dimensions and save thee position for later - dimensionWritePosition = await writer.WriteAndFlushAsync(""); + dimensionWritePosition = await writer.WriteAndFlushAsync(WorksheetXml.StartDimension); + await writer.WriteAsync(WorksheetXml.DimensionPlaceholder); } else { maxRowIndex = rowCount.Value + (_printHeader && rowCount > 0 ? 1 : 0); - await writer.WriteAsync($@""); + await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex))); } //cols:width await WriteColumnsWidthsAsync(writer, props); //header - await writer.WriteAsync($@""); + await writer.WriteAsync(WorksheetXml.StartSheetData); var yIndex = 1; var xIndex = 1; if (_printHeader) @@ -276,9 +240,9 @@ private async Task GenerateSheetByEnumerableAsync(MiniExcelAsyncStreamWriter wri if (!empty) { // body - switch (mode) //Dapper Row + switch (mode) { - case "IDictionary": + case "IDictionary": //Dapper Row maxRowIndex = await GenerateSheetByColumnInfoAsync>(writer, enumerator, props, xIndex, yIndex); break; case "IDictionary": @@ -292,103 +256,104 @@ private async Task GenerateSheetByEnumerableAsync(MiniExcelAsyncStreamWriter wri } } - await writer.WriteAsync(""); + await writer.WriteAsync(WorksheetXml.EndSheetData); if (_configuration.AutoFilter) - await writer.WriteAsync($""); + { + await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex))); + } + + await writer.WriteAsync(WorksheetXml.Drawing(currentSheetIndex)); + await writer.WriteAsync(WorksheetXml.EndWorksheet); // The dimension has already been written if row count is defined if (_configuration.FastMode && rowCount == null) { - // Flush and save position so that we can get back again. - var pos = await writer.FlushAsync(); - // Seek back and write the dimensions of the table writer.SetPosition(dimensionWritePosition); await writer.WriteAndFlushAsync($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}"""); - writer.SetPosition(pos); } - - await writer.WriteAsync(""); } private async Task GenerateSheetByDataTableAsync(MiniExcelAsyncStreamWriter writer, DataTable value) { var xy = ExcelOpenXmlUtils.ConvertCellToXY("A1"); - //GOTO Top Write: - await writer.WriteAsync($@""); + await writer.WriteAsync(WorksheetXml.StartWorksheet); + var yIndex = xy.Item2; + + // dimension + var maxRowIndex = value.Rows.Count + (_printHeader && value.Rows.Count > 0 ? 1 : 0); + var maxColumnIndex = value.Columns.Count; + await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex))); + + var props = new List(); + for (var i = 0; i < value.Columns.Count; i++) { - var yIndex = xy.Item2; + var columnName = value.Columns[i].Caption ?? value.Columns[i].ColumnName; + var prop = GetColumnInfosFromDynamicConfiguration(columnName); + props.Add(prop); + } - // dimension - var maxRowIndex = value.Rows.Count + (_printHeader && value.Rows.Count > 0 ? 1 : 0); - var maxColumnIndex = value.Columns.Count; - await writer.WriteAsync($@""); + await WriteColumnsWidthsAsync(writer, props); - var props = new List(); - for (var i = 0; i < value.Columns.Count; i++) + await writer.WriteAsync(WorksheetXml.StartSheetData); + if (_printHeader) + { + await writer.WriteAsync(WorksheetXml.StartRow(yIndex)); + var xIndex = xy.Item1; + foreach (var p in props) { - var columnName = value.Columns[i].Caption ?? value.Columns[i].ColumnName; - var prop = GetColumnInfosFromDynamicConfiguration(columnName); - props.Add(prop); + var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); + await WriteCellAsync(writer, r, columnName: p.ExcelColumnName); + xIndex++; } - await WriteColumnsWidthsAsync(writer, props); - - await writer.WriteAsync(""); - if (_printHeader) - { - await writer.WriteAsync($""); - var xIndex = xy.Item1; - foreach (var p in props) - { - var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); - await WriteCAsync(writer, r, columnName: p.ExcelColumnName); - xIndex++; - } + await writer.WriteAsync(WorksheetXml.EndRow); + yIndex++; + } - await writer.WriteAsync($""); - yIndex++; - } + for (int i = 0; i < value.Rows.Count; i++) + { + await writer.WriteAsync(WorksheetXml.StartRow(yIndex)); + var xIndex = xy.Item1; - for (int i = 0; i < value.Rows.Count; i++) + for (int j = 0; j < value.Columns.Count; j++) { - await writer.WriteAsync($""); - var xIndex = xy.Item1; - - for (int j = 0; j < value.Columns.Count; j++) - { - var cellValue = value.Rows[i][j]; - await WriteCellAsync(writer, yIndex, xIndex, cellValue, props[j]); - xIndex++; - } - await writer.WriteAsync($""); - yIndex++; + var cellValue = value.Rows[i][j]; + await WriteCellAsync(writer, yIndex, xIndex, cellValue, props[j]); + xIndex++; } + await writer.WriteAsync(WorksheetXml.EndRow); + yIndex++; } - await writer.WriteAsync(""); + + await writer.WriteAsync(WorksheetXml.EndSheetData); + await writer.WriteAsync(WorksheetXml.EndWorksheet); } private static async Task WriteColumnsWidthsAsync(MiniExcelAsyncStreamWriter writer, IEnumerable props) { var ecwProps = props.Where(x => x?.ExcelColumnWidth != null).ToList(); if (ecwProps.Count <= 0) + { return; - await writer.WriteAsync($@""); + } + + await writer.WriteAsync(WorksheetXml.StartCols); + foreach (var p in ecwProps) { - await writer.WriteAsync( - $@""); + await writer.WriteAsync(WorksheetXml.Column(p.ExcelColumnIndex, p.ExcelColumnWidth)); } - await writer.WriteAsync($@""); + await writer.WriteAsync(WorksheetXml.EndCols); } private static async Task PrintHeaderAsync(MiniExcelAsyncStreamWriter writer, List props) { var xIndex = 1; var yIndex = 1; - await writer.WriteAsync($""); + await writer.WriteAsync(WorksheetXml.StartRow(yIndex)); foreach (var p in props) { @@ -399,48 +364,11 @@ private static async Task PrintHeaderAsync(MiniExcelAsyncStreamWriter writer, Li } var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); - await WriteCAsync(writer, r, columnName: p.ExcelColumnName); + await WriteCellAsync(writer, r, columnName: p.ExcelColumnName); xIndex++; } - await writer.WriteAsync(""); - } - - private static async Task WriteCAsync(MiniExcelAsyncStreamWriter writer, string r, string columnName) - { - await writer.WriteAsync($""); - await writer.WriteAsync($"{ExcelOpenXmlUtils.EncodeXML(columnName)}"); //issue I45TF5 - await writer.WriteAsync($""); - await writer.WriteAsync($""); - } - - private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, int rowIndex, int cellIndex, object value, ExcelColumnInfo p) - { - var columname = ExcelOpenXmlUtils.ConvertXyToCell(cellIndex, rowIndex); - var s = "2"; - var valueIsNull = value is null || value is DBNull; - - if (_configuration.EnableWriteNullValueCell && valueIsNull) - { - await writer.WriteAsync($""); - return; - } - - var tuple = GetCellValue(rowIndex, cellIndex, value, p, valueIsNull); - - s = tuple.Item1; - var t = tuple.Item2; - var v = tuple.Item3; - - if (v != null && (v.StartsWith(" ", StringComparison.Ordinal) || v.EndsWith(" ", StringComparison.Ordinal))) /*Prefix and suffix blank space will lost after SaveAs #294*/ - { - await writer.WriteAsync($"{v}"); - } - else - { - //to check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) - await writer.WriteAsync($"{v}"); - } + await writer.WriteAsync(WorksheetXml.EndRow); } private async Task GenerateSheetByColumnInfoAsync(MiniExcelAsyncStreamWriter writer, IEnumerator value, List props, int xIndex = 1, int yIndex = 1) @@ -452,7 +380,7 @@ private async Task GenerateSheetByColumnInfoAsync(MiniExcelAsyncStreamWr // The enumerator has already moved to the first item T v = (T)value.Current; - await writer.WriteAsync($""); + await writer.WriteAsync(WorksheetXml.StartRow(yIndex)); var cellIndex = xIndex; foreach (var p in props) { @@ -483,13 +411,40 @@ private async Task GenerateSheetByColumnInfoAsync(MiniExcelAsyncStreamWr cellIndex++; } - await writer.WriteAsync($""); + await writer.WriteAsync(WorksheetXml.EndRow); yIndex++; } while (value.MoveNext()); return yIndex - 1; } + private static async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, string cellReference, string columnName) + { + await writer.WriteAsync(WorksheetXml.Cell(cellReference, "str", "1", ExcelOpenXmlUtils.EncodeXML(columnName))); + } + + private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, int rowIndex, int cellIndex, object value, ExcelColumnInfo p) + { + var columnReference = ExcelOpenXmlUtils.ConvertXyToCell(cellIndex, rowIndex); + var valueIsNull = value is null || value is DBNull; + + if (_configuration.EnableWriteNullValueCell && valueIsNull) + { + await writer.WriteAsync(WorksheetXml.EmptyCell(columnReference, "2")); + return; + } + + var tuple = GetCellValue(rowIndex, cellIndex, value, p, valueIsNull); + + var styleIndex = tuple.Item1; + var dataType = tuple.Item2; + var cellValue = tuple.Item3; + + /*Prefix and suffix blank space will lost after SaveAs #294*/ + var preserveSpace = cellValue != null && (cellValue.StartsWith(" ", StringComparison.Ordinal) || cellValue.EndsWith(" ", StringComparison.Ordinal)); + await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, styleIndex, cellValue, preserveSpace: preserveSpace)); + } + private async Task GenerateEndXmlAsync(CancellationToken cancellationToken) { await AddFilesToZipAsync(cancellationToken); @@ -521,8 +476,8 @@ private async Task GenerateStylesXmlAsync(CancellationToken cancellationToken) var styleXml = GetStylesXml(); await CreateZipEntryAsync( - @"xl/styles.xml", - "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", + ExcelFileNames.Styles, + ExcelContentTypes.Styles, styleXml, cancellationToken); } @@ -532,8 +487,11 @@ private async Task GenerateDrawinRelXmlAsync(CancellationToken cancellationToken for (int sheetIndex = 0; sheetIndex < _sheets.Count; sheetIndex++) { var drawing = GetDrawingRelationshipXml(sheetIndex); - await CreateZipEntryAsync($"xl/drawings/_rels/drawing{sheetIndex + 1}.xml.rels", "", - _defaultDrawingXmlRels.Replace("{{format}}", drawing), cancellationToken); + await CreateZipEntryAsync( + ExcelFileNames.DrawingRels(sheetIndex), + string.Empty, + ExcelXml.DefaultDrawingXmlRels.Replace("{{format}}", drawing), + cancellationToken); } } @@ -543,9 +501,9 @@ private async Task GenerateDrawingXmlAsync(CancellationToken cancellationToken) { var drawing = GetDrawingXml(sheetIndex); await CreateZipEntryAsync( - $"xl/drawings/drawing{sheetIndex + 1}.xml", - "application/vnd.openxmlformats-officedocument.drawing+xml", - _defaultDrawing.Replace("{{format}}", drawing), + ExcelFileNames.Drawing(sheetIndex), + ExcelContentTypes.Drawing, + ExcelXml.DefaultDrawing.Replace("{{format}}", drawing), cancellationToken); } } @@ -563,22 +521,22 @@ private async Task GenerateWorkbookXmlAsync(CancellationToken cancellationToken) foreach (var sheetRelsXml in sheetsRelsXml) { await CreateZipEntryAsync( - $"xl/worksheets/_rels/sheet{sheetRelsXml.Key}.xml.rels", + ExcelFileNames.SheetRels(sheetRelsXml.Key), null, - _defaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value), + ExcelXml.DefaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value), cancellationToken); } await CreateZipEntryAsync( - @"xl/workbook.xml", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", - _defaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), + ExcelFileNames.Workbook, + ExcelContentTypes.Workbook, + ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken); await CreateZipEntryAsync( - @"xl/_rels/workbook.xml.rels", + ExcelFileNames.WorkbookRels, null, - _defaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), + ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken); } @@ -589,7 +547,7 @@ private async Task GenerateContentTypesXmlAsync(CancellationToken cancellationTo { var contentTypes = GetContentTypesXml(); - await CreateZipEntryAsync(@"[Content_Types].xml", null, contentTypes, cancellationToken); + await CreateZipEntryAsync(ExcelFileNames.ContentTypes, null, contentTypes, cancellationToken); } private async Task CreateZipEntryAsync(string path, string contentType, string content, CancellationToken cancellationToken) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs new file mode 100644 index 00000000..16ead3ae --- /dev/null +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs @@ -0,0 +1,452 @@ +using MiniExcelLibs.OpenXml.Constants; +using MiniExcelLibs.OpenXml.Models; +using MiniExcelLibs.Utils; +using MiniExcelLibs.Zip; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text; +using static MiniExcelLibs.Utils.ImageHelper; + +namespace MiniExcelLibs.OpenXml +{ + internal partial class ExcelOpenXmlSheetWriter : IExcelWriter + { + public void Insert() + { + throw new NotImplementedException(); + } + + private readonly Dictionary _zipDictionary = new Dictionary(); + + private IEnumerable> GetSheets() + { + var sheetId = 0; + if (_value is IDictionary dictionary) + { + foreach (var sheet in dictionary) + { + sheetId++; + var sheetInfos = GetSheetInfos(sheet.Key); + yield return Tuple.Create(sheetInfos.ToDto(sheetId), sheet.Value); + } + + yield break; + } + + if (_value is DataSet dataSet) + { + foreach (DataTable dt in dataSet.Tables) + { + sheetId++; + var sheetInfos = GetSheetInfos(dt.TableName); + yield return Tuple.Create(sheetInfos.ToDto(sheetId), dt); + } + + yield break; + } + + sheetId++; + var defaultSheetInfo = GetSheetInfos(_defaultSheetName); + yield return Tuple.Create(defaultSheetInfo.ToDto(sheetId), _value); + } + + private ExcellSheetInfo GetSheetInfos(string sheetName) + { + var info = new ExcellSheetInfo + { + ExcelSheetName = sheetName, + Key = sheetName, + ExcelSheetState = SheetState.Visible + }; + + if (_configuration.DynamicSheets == null || _configuration.DynamicSheets.Length <= 0) + { + return info; + } + + var dynamicSheet = _configuration.DynamicSheets.SingleOrDefault(_ => _.Key == sheetName); + if (dynamicSheet == null) + { + return info; + } + + info.ExcelSheetState = dynamicSheet.State; + if (dynamicSheet.Name != null) + { + info.ExcelSheetName = dynamicSheet.Name; + } + + return info; + } + + private ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName) + { + var prop = new ExcelColumnInfo + { + ExcelColumnName = columnName, + Key = columnName + }; + + if (_configuration.DynamicColumns == null || _configuration.DynamicColumns.Length <= 0) + return prop; + + var dynamicColumn = _configuration.DynamicColumns.SingleOrDefault(_ => _.Key == columnName); + if (dynamicColumn == null || dynamicColumn.Ignore) + { + return prop; + } + + prop.Nullable = true; + prop.ExcelIgnore = dynamicColumn.Ignore; + prop.ExcelColumnIndex = dynamicColumn.Index; + prop.ExcelColumnWidth = dynamicColumn.Width; + //prop.ExcludeNullableType = item2[key]?.GetType(); + + if (dynamicColumn.Format != null) + { + prop.ExcelFormat = dynamicColumn.Format; + } + + if (dynamicColumn.Aliases != null) + { + prop.ExcelColumnAliases = dynamicColumn.Aliases; + } + + if (dynamicColumn.IndexName != null) + { + prop.ExcelIndexName = dynamicColumn.IndexName; + } + + if (dynamicColumn.Name != null) + { + prop.ExcelColumnName = dynamicColumn.Name; + } + + return prop; + } + + private void SetGenericTypePropertiesMode(Type genericType, ref string mode, out int maxColumnIndex, out List props) + { + mode = "Properties"; + if (genericType.IsValueType) + { + throw new NotImplementedException($"MiniExcel not support only {genericType.Name} value generic type"); + } + + if (genericType == typeof(string) || genericType == typeof(DateTime) || genericType == typeof(Guid)) + { + throw new NotImplementedException($"MiniExcel not support only {genericType.Name} generic type"); + } + + props = CustomPropertyHelper.GetSaveAsProperties(genericType, _configuration); + + maxColumnIndex = props.Count; + } + + private Tuple GetCellValue(int rowIndex, int cellIndex, object value, ExcelColumnInfo columnInfo, bool valueIsNull) + { + if (valueIsNull) + { + return Tuple.Create("2", "str", string.Empty); + } + + if (value is string str) + { + return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(str)); + } + + if (columnInfo?.ExcelFormat != null && value is IFormattable formattableValue) + { + var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture); + return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(formattedStr)); + } + + var type = GetValueType(value, columnInfo); + + if (type.IsEnum) + { + var description = CustomPropertyHelper.DescriptionAttr(type, value); + return Tuple.Create("2", "str", description ?? value.ToString()); + } + + if (TypeHelper.IsNumericType(type)) + { + var dataType = _configuration.Culture == CultureInfo.InvariantCulture ? "n" : "str"; + string cellValue; + + cellValue = GetNumericValue(value, type); + + return Tuple.Create("2", dataType, cellValue); + } + + if (type == typeof(bool)) + { + return Tuple.Create("2", "b", (bool)value ? "1" : "0"); + } + + if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) + { + var base64 = GetFileValue(rowIndex, cellIndex, value); + return Tuple.Create("4", "str", ExcelOpenXmlUtils.EncodeXML(base64)); + } + + if (type == typeof(DateTime)) + { + return GetDateTimeValue(value, columnInfo); + } + +#if NET6_0_OR_GREATER + if (type == typeof(DateOnly)) + { + if (_configuration.Culture != CultureInfo.InvariantCulture) + { + var cellValue = ((DateOnly)value).ToString(_configuration.Culture); + return Tuple.Create("2", "str", cellValue); + } + + if (columnInfo == null || columnInfo.ExcelFormat == null) + { + var oaDate = CorrectDateTimeValue((DateTime)value); + var cellValue = oaDate.ToString(CultureInfo.InvariantCulture); + return Tuple.Create("3", null, cellValue); + } + + // TODO: now it'll lose date type information + var formattedCellValue = ((DateOnly)value).ToString(columnInfo.ExcelFormat, _configuration.Culture); + return Tuple.Create("2", "str", formattedCellValue); + } +#endif + + return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(value.ToString())); + } + + private static Type GetValueType(object value, ExcelColumnInfo columnInfo) + { + Type type; + if (columnInfo == null || columnInfo.Key != null) + { + // TODO: need to optimize + // Dictionary need to check type every time, so it's slow.. + type = value.GetType(); + type = Nullable.GetUnderlyingType(type) ?? type; + } + else + { + type = columnInfo.ExcludeNullableType; //sometime it doesn't need to re-get type like prop + } + + return type; + } + + private string GetNumericValue(object value, Type type) + { + if (type.IsAssignableFrom(typeof(decimal))) + { + return ((decimal)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(int))) + { + return ((int)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(double))) + { + return ((double)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(long))) + { + return ((long)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(uint))) + { + return ((uint)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(ushort))) + { + return ((ushort)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(ulong))) + { + return ((ulong)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(short))) + { + return ((short)value).ToString(_configuration.Culture); + } + + if (type.IsAssignableFrom(typeof(float))) + { + return ((float)value).ToString(_configuration.Culture); + } + + return (decimal.Parse(value.ToString())).ToString(_configuration.Culture); + } + + private string GetFileValue(int rowIndex, int cellIndex, object value) + { + var bytes = (byte[])value; + + // TODO: Setting configuration because it might have high cost? + var format = GetImageFormat(bytes); + //it can't insert to zip first to avoid cache image to memory + //because sheet xml is opening.. https://github.com/shps951023/MiniExcel/issues/304#issuecomment-1017031691 + //int rowIndex, int cellIndex + var file = new FileDto() + { + Byte = bytes, + RowIndex = rowIndex, + CellIndex = cellIndex, + SheetId = currentSheetIndex + }; + + if (format != ImageFormat.unknown) + { + file.Extension = format.ToString(); + file.IsImage = true; + } + else + { + file.Extension = "bin"; + } + + _files.Add(file); + + //TODO:Convert to base64 + var base64 = $"@@@fileid@@@,{file.Path}"; + return base64; + } + + private Tuple GetDateTimeValue(object value, ExcelColumnInfo columnInfo) + { + if (_configuration.Culture != CultureInfo.InvariantCulture) + { + var cellValue = ((DateTime)value).ToString(_configuration.Culture); + return Tuple.Create("2", "str", cellValue); + } + + if (columnInfo == null || columnInfo.ExcelFormat == null) + { + var oaDate = CorrectDateTimeValue((DateTime)value); + var cellValue = oaDate.ToString(CultureInfo.InvariantCulture); + return Tuple.Create("3", null, cellValue); + } + + // TODO: now it'll lose date type information + var formattedCellValue = ((DateTime)value).ToString(columnInfo.ExcelFormat, _configuration.Culture); + return Tuple.Create("2", "str", formattedCellValue); + } + + private static double CorrectDateTimeValue(DateTime value) + { + var oaDate = value.ToOADate(); + + // Excel says 1900 was a leap year :( Replicate an incorrect behavior thanks + // to Lotus 1-2-3 decision from 1983... + // https://github.com/ClosedXML/ClosedXML/blob/develop/ClosedXML/Extensions/DateTimeExtensions.cs#L45 + const int nonExistent1900Feb29SerialDate = 60; + if (oaDate <= nonExistent1900Feb29SerialDate) + { + oaDate -= 1; + } + + return oaDate; + } + + private string GetDimensionRef(int maxRowIndex, int maxColumnIndex) + { + string dimensionRef; + if (maxRowIndex == 0 && maxColumnIndex == 0) + dimensionRef = "A1"; + else if (maxColumnIndex == 1) + dimensionRef = $"A{maxRowIndex}"; + else if (maxRowIndex == 0) + dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}1"; + else + dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}{maxRowIndex}"; + return dimensionRef; + } + + private string GetStylesXml() + { + switch (_configuration.TableStyles) + { + case TableStyles.None: + return ExcelXml.NoneStylesXml; + case TableStyles.Default: + return ExcelXml.DefaultStylesXml; + default: + return string.Empty; + } + } + + private string GetDrawingRelationshipXml(int sheetIndex) + { + var drawing = new StringBuilder(); + foreach (var image in _files.Where(w => w.IsImage && w.SheetId == sheetIndex + 1)) + { + drawing.AppendLine(ExcelXml.ImageRelationship(image)); + } + + return drawing.ToString(); + } + + private string GetDrawingXml(int sheetIndex) + { + var drawing = new StringBuilder(); + + for (int fileIndex = 0; fileIndex < _files.Count; fileIndex++) + { + var file = _files[fileIndex]; + if (file.IsImage && file.SheetId == sheetIndex + 1) + { + drawing.Append(ExcelXml.DrawingXml(file, fileIndex)); + } + } + + return drawing.ToString(); + } + + private void GenerateWorkBookXmls( + out StringBuilder workbookXml, + out StringBuilder workbookRelsXml, + out Dictionary sheetsRelsXml) + { + workbookXml = new StringBuilder(); + workbookRelsXml = new StringBuilder(); + sheetsRelsXml = new Dictionary(); + var sheetId = 0; + foreach (var sheetDto in _sheets) + { + sheetId++; + workbookXml.AppendLine(ExcelXml.Sheet(sheetDto, sheetId)); + + workbookRelsXml.AppendLine(ExcelXml.WorksheetRelationship(sheetDto)); + + //TODO: support multiple drawing + //TODO: ../drawings/drawing1.xml or /xl/drawings/drawing1.xml + sheetsRelsXml.Add(sheetDto.SheetIdx, ExcelXml.DrawingRelationship(sheetId)); + } + } + + private string GetContentTypesXml() + { + var sb = new StringBuilder(ExcelXml.StartTypes); + foreach (var p in _zipDictionary) + { + sb.Append(ExcelXml.ContentType(p.Value.ContentType, p.Key)); + } + + sb.Append(ExcelXml.EndTypes); + return sb.ToString(); + } + } +} diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 54181936..df6be3df 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -1,4 +1,6 @@ -using MiniExcelLibs.Utils; +using MiniExcelLibs.OpenXml.Constants; +using MiniExcelLibs.OpenXml.Models; +using MiniExcelLibs.Utils; using MiniExcelLibs.Zip; using System; using System.Collections; @@ -9,44 +11,19 @@ using System.IO.Compression; using System.Linq; using System.Text; -using System.Threading; using static MiniExcelLibs.Utils.ImageHelper; namespace MiniExcelLibs.OpenXml { - internal class FileDto - { - public string ID { get; set; } = $"R{Guid.NewGuid():N}"; - public string Extension { get; set; } - public string Path { get { return $"xl/media/{ID}.{Extension}"; } } - public string Path2 { get { return $"/xl/media/{ID}.{Extension}"; } } - public Byte[] Byte { get; set; } - public int RowIndex { get; set; } - public int CellIndex { get; set; } - public bool IsImage { get; set; } = false; - public int SheetId { get; set; } - } - internal class SheetDto - { - public string ID { get; set; } = $"R{Guid.NewGuid():N}"; - public string Name { get; set; } - public int SheetIdx { get; set; } - public string Path { get { return $"xl/worksheets/sheet{SheetIdx}.xml"; } } - - public string State { get; set; } - } - internal class DrawingDto - { - public string ID { get; set; } = $"R{Guid.NewGuid():N}"; - } internal partial class ExcelOpenXmlSheetWriter : IExcelWriter { private readonly MiniExcelZipArchive _archive; - private readonly static UTF8Encoding _utf8WithBom = new System.Text.UTF8Encoding(true); + private readonly static UTF8Encoding _utf8WithBom = new UTF8Encoding(true); private readonly OpenXmlConfiguration _configuration; private readonly Stream _stream; private readonly bool _printHeader; private readonly object _value; + private readonly string _defaultSheetName; private readonly List _sheets = new List(); private readonly List _files = new List(); private int currentSheetIndex = 0; @@ -63,8 +40,7 @@ public ExcelOpenXmlSheetWriter(Stream stream, object value, string sheetName, IC this._archive = new MiniExcelZipArchive(_stream, ZipArchiveMode.Create, true, _utf8WithBom); this._printHeader = printHeader; this._value = value; - var defaultSheetInfo = GetSheetInfos(sheetName); - _sheets.Add(defaultSheetInfo.ToDto(1)); //TODO:remove + this._defaultSheetName = sheetName; } public ExcelOpenXmlSheetWriter() @@ -74,64 +50,136 @@ public ExcelOpenXmlSheetWriter() public void SaveAs() { GenerateDefaultOpenXml(); + + var sheets = GetSheets(); + + foreach (var sheet in sheets) { - if (_value is IDictionary) - { - var sheetId = 0; - var sheets = _value as IDictionary; - _sheets.RemoveAt(0);//TODO:remove - foreach (var sheet in sheets) - { - sheetId++; - var sheetInfos = GetSheetInfos(sheet.Key); - var sheetDto = sheetInfos.ToDto(sheetId); - _sheets.Add(sheetDto); //TODO:remove + _sheets.Add(sheet.Item1); //TODO:remove + currentSheetIndex = sheet.Item1.SheetIdx; + CreateSheetXml(sheet.Item2, sheet.Item1.Path); + } - currentSheetIndex = sheetId; + GenerateEndXml(); + _archive.Dispose(); + } - CreateSheetXml(sheet.Value, sheetDto.Path); - } - } - else if (_value is DataSet) + internal void GenerateDefaultOpenXml() + { + CreateZipEntry(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels); + CreateZipEntry(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString); + } + + private void CreateSheetXml(object value, string sheetPath) + { + ZipArchiveEntry entry = _archive.CreateEntry(sheetPath, CompressionLevel.Fastest); + using (var zipStream = entry.Open()) + using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) + { + if (value == null) { - var sheetId = 0; - var sheets = _value as DataSet; - _sheets.RemoveAt(0);//TODO:remove - foreach (DataTable dt in sheets.Tables) - { - sheetId++; - var sheetInfos = GetSheetInfos(dt.TableName); - var sheetDto = sheetInfos.ToDto(sheetId); - _sheets.Add(sheetDto); //TODO:remove + WriteEmptySheet(writer); + goto End; //for re-using code + } - currentSheetIndex = sheetId; + //DapperRow - CreateSheetXml(dt, sheetDto.Path); - } + if (value is IDataReader) + { + GenerateSheetByIDataReader(writer, value as IDataReader); + } + else if (value is IEnumerable) + { + GenerateSheetByEnumerable(writer, value as IEnumerable); + } + else if (value is DataTable) + { + GenerateSheetByDataTable(writer, value as DataTable); } else { - //Single sheet export. - currentSheetIndex++; - - CreateSheetXml(_value, _sheets[0].Path); + throw new NotImplementedException($"Type {value.GetType().FullName} is not implemented. Please open an issue."); } } - GenerateEndXml(); - _archive.Dispose(); + End: //for re-using code + _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, ExcelContentTypes.Worksheet)); } - internal void GenerateDefaultOpenXml() + private void WriteEmptySheet(MiniExcelStreamWriter writer) { - CreateZipEntry("_rels/.rels", "application/vnd.openxmlformats-package.relationships+xml", _defaultRels); - CreateZipEntry("xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", _defaultSharedString); + writer.Write(ExcelXml.EmptySheetXml); + } + + private void GenerateSheetByIDataReader(MiniExcelStreamWriter writer, IDataReader reader) + { + long dimensionWritePosition = 0; + writer.Write(WorksheetXml.StartWorksheet); + var yIndex = 1; + int maxColumnIndex; + int maxRowIndex; + { + + if (_configuration.FastMode) + { + dimensionWritePosition = writer.WriteAndFlush(WorksheetXml.StartDimension); + writer.Write(WorksheetXml.DimensionPlaceholder); // end of code will be replaced + } + + var props = new List(); + for (var i = 0; i < reader.FieldCount; i++) + { + var columnName = reader.GetName(i); + var prop = GetColumnInfosFromDynamicConfiguration(columnName); + props.Add(prop); + } + maxColumnIndex = props.Count; + + WriteColumnsWidths(writer, props); + + writer.Write(WorksheetXml.StartSheetData); + int fieldCount = reader.FieldCount; + if (_printHeader) + { + PrintHeader(writer, props); + yIndex++; + } + + while (reader.Read()) + { + writer.Write(WorksheetXml.StartRow(yIndex)); + var xIndex = 1; + for (int i = 0; i < fieldCount; i++) + { + var cellValue = reader.GetValue(i); + WriteCell(writer, yIndex, xIndex, cellValue, columnInfo: null); + xIndex++; + } + writer.Write(WorksheetXml.EndRow); + yIndex++; + } + + // Subtract 1 because cell indexing starts with 1 + maxRowIndex = yIndex - 1; + } + writer.Write(WorksheetXml.EndSheetData); + + if (_configuration.AutoFilter) + { + writer.Write(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex))); + } + + writer.WriteAndFlush(WorksheetXml.EndWorksheet); + + if (_configuration.FastMode) + { + writer.SetPosition(dimensionWritePosition); + writer.WriteAndFlush($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}"""); + } } private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable values) { - var maxColumnIndex = 0; var maxRowIndex = 0; - List props = null; string mode = null; int? rowCount = null; @@ -153,6 +201,8 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable // Move to the first item in order to inspect the value type and determine whether it is empty var empty = !enumerator.MoveNext(); + int maxColumnIndex; + List props; if (empty) { // only when empty IEnumerable need to check this issue #133 https://github.com/shps951023/MiniExcel/issues/133 @@ -191,7 +241,7 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable } } - writer.Write($@""); + writer.Write(WorksheetXml.StartWorksheetWithRelationship); long dimensionWritePosition = 0; @@ -199,20 +249,20 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable if (_configuration.FastMode && rowCount == null) { // Write a placeholder for the table dimensions and save thee position for later - dimensionWritePosition = writer.WriteAndFlush(""); + dimensionWritePosition = writer.WriteAndFlush(WorksheetXml.StartDimension); + writer.Write(WorksheetXml.DimensionPlaceholder); } else { maxRowIndex = rowCount.Value + (_printHeader && rowCount > 0 ? 1 : 0); - writer.Write($@""); + writer.Write(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex))); } //cols:width WriteColumnsWidths(writer, props); //header - writer.Write($@""); + writer.Write(WorksheetXml.StartSheetData); var yIndex = 1; var xIndex = 1; if (_printHeader) @@ -224,107 +274,133 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable if (!empty) { // body - if (mode == "IDictionary") //Dapper Row - maxRowIndex = GenerateSheetByColumnInfo>(writer, enumerator, props, xIndex, yIndex); - else if (mode == "IDictionary") //IDictionary - maxRowIndex = GenerateSheetByColumnInfo(writer, enumerator, props, xIndex, yIndex); - else if (mode == "Properties") - maxRowIndex = GenerateSheetByColumnInfo(writer, enumerator, props, xIndex, yIndex); - else - throw new NotImplementedException($"Type {values.GetType().FullName} is not implemented. Please open an issue."); + switch (mode) + { + case "IDictionary": //Dapper Row + maxRowIndex = GenerateSheetByColumnInfo>(writer, enumerator, props, xIndex, yIndex); + break; + case "IDictionary": + maxRowIndex = GenerateSheetByColumnInfo(writer, enumerator, props, xIndex, yIndex); + break; + case "Properties": + maxRowIndex = GenerateSheetByColumnInfo(writer, enumerator, props, xIndex, yIndex); + break; + default: + throw new NotImplementedException($"Type {values.GetType().FullName} is not implemented. Please open an issue."); + } } - writer.Write(""); + writer.Write(WorksheetXml.EndSheetData); if (_configuration.AutoFilter) - writer.Write($""); + { + writer.Write(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex))); + } + + writer.Write(WorksheetXml.Drawing(currentSheetIndex)); + writer.Write(WorksheetXml.EndWorksheet); // The dimension has already been written if row count is defined if (_configuration.FastMode && rowCount == null) { - // Flush and save position so that we can get back again. - var pos = writer.Flush(); - // Seek back and write the dimensions of the table writer.SetPosition(dimensionWritePosition); writer.WriteAndFlush($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}"""); - writer.SetPosition(pos); } - - writer.Write(""); } - private static void PrintHeader(MiniExcelStreamWriter writer, List props) + private void GenerateSheetByDataTable(MiniExcelStreamWriter writer, DataTable value) { - var xIndex = 1; - var yIndex = 1; - writer.Write($""); + var xy = ExcelOpenXmlUtils.ConvertCellToXY("A1"); - foreach (var p in props) - { - if (p == null) - { - xIndex++; //reason : https://github.com/shps951023/MiniExcel/issues/142 - continue; - } + //GOTO Top Write: + writer.Write(WorksheetXml.StartWorksheet); - var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); - WriteC(writer, r, columnName: p.ExcelColumnName); - xIndex++; + var yIndex = xy.Item2; + + // dimension + var maxRowIndex = value.Rows.Count + (_printHeader && value.Rows.Count > 0 ? 1 : 0); + var maxColumnIndex = value.Columns.Count; + writer.Write(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex))); + + var props = new List(); + for (var i = 0; i < value.Columns.Count; i++) + { + var columnName = value.Columns[i].Caption ?? value.Columns[i].ColumnName; + var prop = GetColumnInfosFromDynamicConfiguration(columnName); + props.Add(prop); } - writer.Write(""); - } + WriteColumnsWidths(writer, props); - private void CreateSheetXml(object value, string sheetPath) - { - ZipArchiveEntry entry = _archive.CreateEntry(sheetPath, CompressionLevel.Fastest); - using (var zipStream = entry.Open()) - using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) + writer.Write(WorksheetXml.StartSheetData); + if (_printHeader) { - if (value == null) + writer.Write(WorksheetXml.StartRow(yIndex)); + var xIndex = xy.Item1; + foreach (var p in props) { - WriteEmptySheet(writer); - goto End; //for re-using code + var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); + WriteCell(writer, r, columnName: p.ExcelColumnName); + xIndex++; } - //DapperRow + writer.Write(WorksheetXml.EndRow); + yIndex++; + } - if (value is IDataReader) - { - GenerateSheetByIDataReader(writer, value as IDataReader); - } - else if (value is IEnumerable) - { - GenerateSheetByEnumerable(writer, value as IEnumerable); - } - else if (value is DataTable) + for (int i = 0; i < value.Rows.Count; i++) + { + writer.Write(WorksheetXml.StartRow(yIndex)); + var xIndex = xy.Item1; + + for (int j = 0; j < value.Columns.Count; j++) { - GenerateSheetByDataTable(writer, value as DataTable); - } - else - { - throw new NotImplementedException($"Type {value.GetType().FullName} is not implemented. Please open an issue."); + var cellValue = value.Rows[i][j]; + WriteCell(writer, yIndex, xIndex, cellValue, columnInfo: null); + xIndex++; } + writer.Write(WorksheetXml.EndRow); + yIndex++; } - End: //for re-using code - _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml")); + + writer.Write(WorksheetXml.EndSheetData); + writer.Write(WorksheetXml.EndWorksheet); } - private void SetGenericTypePropertiesMode(Type genericType, ref string mode, out int maxColumnIndex, out List props) + private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable props) { - mode = "Properties"; - if (genericType.IsValueType) - throw new NotImplementedException($"MiniExcel not support only {genericType.Name} value generic type"); - else if (genericType == typeof(string) || genericType == typeof(DateTime) || genericType == typeof(Guid)) - throw new NotImplementedException($"MiniExcel not support only {genericType.Name} generic type"); - props = CustomPropertyHelper.GetSaveAsProperties(genericType, _configuration); - - maxColumnIndex = props.Count; + var ecwProps = props.Where(x => x?.ExcelColumnWidth != null).ToList(); + if (ecwProps.Count <= 0) + return; + writer.Write(WorksheetXml.StartCols); + foreach (var p in ecwProps) + { + writer.Write(WorksheetXml.Column(p.ExcelColumnIndex, p.ExcelColumnWidth)); + } + + writer.Write(WorksheetXml.EndCols); } - private void WriteEmptySheet(MiniExcelStreamWriter writer) + private static void PrintHeader(MiniExcelStreamWriter writer, List props) { - writer.Write($@""); + var xIndex = 1; + var yIndex = 1; + writer.Write(WorksheetXml.StartRow(yIndex)); + + foreach (var p in props) + { + if (p == null) + { + xIndex++; //reason : https://github.com/shps951023/MiniExcel/issues/142 + continue; + } + + var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); + WriteCell(writer, r, columnName: p.ExcelColumnName); + xIndex++; + } + + writer.Write(WorksheetXml.EndRow); } private int GenerateSheetByColumnInfo(MiniExcelStreamWriter writer, IEnumerator value, List props, int xIndex = 1, int yIndex = 1) @@ -336,7 +412,7 @@ private int GenerateSheetByColumnInfo(MiniExcelStreamWriter writer, IEnumerat // The enumerator has already moved to the first item T v = (T)value.Current; - writer.Write($""); + writer.Write(WorksheetXml.StartRow(yIndex)); var cellIndex = xIndex; foreach (var columnInfo in props) { @@ -365,21 +441,22 @@ private int GenerateSheetByColumnInfo(MiniExcelStreamWriter writer, IEnumerat cellIndex++; } - writer.Write($""); + + writer.Write(WorksheetXml.EndRow); yIndex++; } while (value.MoveNext()); return yIndex - 1; } - + private void WriteCell(MiniExcelStreamWriter writer, int rowIndex, int cellIndex, object value, ExcelColumnInfo columnInfo) { - var columname = ExcelOpenXmlUtils.ConvertXyToCell(cellIndex, rowIndex); + var columnReference = ExcelOpenXmlUtils.ConvertXyToCell(cellIndex, rowIndex); var valueIsNull = value is null || value is DBNull; if (_configuration.EnableWriteNullValueCell && valueIsNull) { - writer.Write($""); // s: style index + writer.Write(WorksheetXml.EmptyCell(columnReference, "2")); return; } @@ -389,404 +466,13 @@ private void WriteCell(MiniExcelStreamWriter writer, int rowIndex, int cellIndex var dataType = tuple.Item2; // https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.cellvalues?view=openxml-3.0.1 var cellValue = tuple.Item3; - if (cellValue != null && (cellValue.StartsWith(" ", StringComparison.Ordinal) || cellValue.EndsWith(" ", StringComparison.Ordinal))) /*Prefix and suffix blank space will lost after SaveAs #294*/ - { - writer.Write($"{cellValue}"); - } - else - { - //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) - writer.Write($"{cellValue}"); - } - } - - private Tuple GetCellValue(int rowIndex, int cellIndex, object value, ExcelColumnInfo columnInfo, bool valueIsNull) - { - var styleIndex = "2"; // format code: 0.00 - var cellValue = string.Empty; - var dataType = "str"; - - if (valueIsNull) - { - // use defaults - } - else if (value is string str) - { - cellValue = ExcelOpenXmlUtils.EncodeXML(str); - } - else if (columnInfo?.ExcelFormat != null && value is IFormattable formattableValue) - { - var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture); - cellValue = ExcelOpenXmlUtils.EncodeXML(formattedStr); - } - else - { - Type type; - if (columnInfo == null || columnInfo.Key != null) - { - // TODO: need to optimize - // Dictionary need to check type every time, so it's slow.. - type = value.GetType(); - type = Nullable.GetUnderlyingType(type) ?? type; - } - else - { - type = columnInfo.ExcludeNullableType; //sometime it doesn't need to re-get type like prop - } - - if (type.IsEnum) - { - dataType = "str"; - var description = CustomPropertyHelper.DescriptionAttr(type, value); //TODO: need to optimze - if (description != null) - cellValue = description; - else - cellValue = value.ToString(); - } - else if (TypeHelper.IsNumericType(type)) - { - if (_configuration.Culture != CultureInfo.InvariantCulture) - dataType = "str"; //TODO: add style format - else - dataType = "n"; - - if (type.IsAssignableFrom(typeof(decimal))) - cellValue = ((decimal)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(Int32))) - cellValue = ((Int32)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(Double))) - cellValue = ((Double)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(Int64))) - cellValue = ((Int64)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(UInt32))) - cellValue = ((UInt32)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(UInt16))) - cellValue = ((UInt16)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(UInt64))) - cellValue = ((UInt64)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(Int16))) - cellValue = ((Int16)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(Single))) - cellValue = ((Single)value).ToString(_configuration.Culture); - else if (type.IsAssignableFrom(typeof(Single))) - cellValue = ((Single)value).ToString(_configuration.Culture); - else - cellValue = (decimal.Parse(value.ToString())).ToString(_configuration.Culture); - } - else if (type == typeof(bool)) - { - dataType = "b"; - cellValue = (bool)value ? "1" : "0"; - } - else if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) - { - var bytes = (byte[])value; - if (bytes != null) - { - // TODO: Setting configuration because it might have high cost? - var format = ImageHelper.GetImageFormat(bytes); - //it can't insert to zip first to avoid cache image to memory - //because sheet xml is opening.. https://github.com/shps951023/MiniExcel/issues/304#issuecomment-1017031691 - //int rowIndex, int cellIndex - var file = new FileDto() - { - Byte = bytes, - RowIndex = rowIndex, - CellIndex = cellIndex, - SheetId = currentSheetIndex - }; - if (format != ImageFormat.unknown) - { - file.Extension = format.ToString(); - file.IsImage = true; - } - else - { - file.Extension = "bin"; - } - _files.Add(file); - - //TODO:Convert to base64 - var base64 = $"@@@fileid@@@,{file.Path}"; - cellValue = ExcelOpenXmlUtils.EncodeXML(base64); - styleIndex = "4"; - } - } - else if (type == typeof(DateTime)) - { - if (_configuration.Culture != CultureInfo.InvariantCulture) - { - dataType = "str"; - cellValue = ((DateTime)value).ToString(_configuration.Culture); - } - else if (columnInfo == null || columnInfo.ExcelFormat == null) - { - var oaDate = CorrectDateTimeValue((DateTime)value); - - dataType = null; - styleIndex = "3"; - cellValue = oaDate.ToString(CultureInfo.InvariantCulture); - } - else - { - // TODO: now it'll lose date type information - dataType = "str"; - cellValue = ((DateTime)value).ToString(columnInfo.ExcelFormat, _configuration.Culture); - } - } -#if NET6_0_OR_GREATER - else if (type == typeof(DateOnly)) - { - if (_configuration.Culture != CultureInfo.InvariantCulture) - { - dataType = "str"; - cellValue = ((DateOnly)value).ToString(_configuration.Culture); - } - else if (columnInfo == null || columnInfo.ExcelFormat == null) - { - var day = (DateOnly)value; - var oaDate = CorrectDateTimeValue(day.ToDateTime(TimeOnly.MinValue)); - - dataType = null; - styleIndex = "3"; - cellValue = oaDate.ToString(CultureInfo.InvariantCulture); - } - else - { - // TODO: now it'll lose date type information - dataType = "str"; - cellValue = ((DateOnly)value).ToString(columnInfo.ExcelFormat, _configuration.Culture); - } - } -#endif - else - { - //TODO: _configuration.Culture - cellValue = ExcelOpenXmlUtils.EncodeXML(value.ToString()); - } - } - - return Tuple.Create(styleIndex, dataType, cellValue); - } - - private static double CorrectDateTimeValue(DateTime value) - { - var oaDate = value.ToOADate(); - - // Excel says 1900 was a leap year :( Replicate an incorrect behavior thanks - // to Lotus 1-2-3 decision from 1983... - // https://github.com/ClosedXML/ClosedXML/blob/develop/ClosedXML/Extensions/DateTimeExtensions.cs#L45 - const int nonExistent1900Feb29SerialDate = 60; - if (oaDate <= nonExistent1900Feb29SerialDate) - { - oaDate -= 1; - } - - return oaDate; - } - - private void GenerateSheetByDataTable(MiniExcelStreamWriter writer, DataTable value) - { - var xy = ExcelOpenXmlUtils.ConvertCellToXY("A1"); - - //GOTO Top Write: - writer.Write($@""); - { - var yIndex = xy.Item2; - - // dimension - var maxRowIndex = value.Rows.Count + (_printHeader && value.Rows.Count > 0 ? 1 : 0); - var maxColumnIndex = value.Columns.Count; - writer.Write($@""); - - var props = new List(); - for (var i = 0; i < value.Columns.Count; i++) - { - var columnName = value.Columns[i].Caption ?? value.Columns[i].ColumnName; - var prop = GetColumnInfosFromDynamicConfiguration(columnName); - props.Add(prop); - } - - WriteColumnsWidths(writer, props); - - writer.Write(""); - if (_printHeader) - { - writer.Write($""); - var xIndex = xy.Item1; - foreach (var p in props) - { - var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); - WriteC(writer, r, columnName: p.ExcelColumnName); - xIndex++; - } - - writer.Write($""); - yIndex++; - } - - for (int i = 0; i < value.Rows.Count; i++) - { - writer.Write($""); - var xIndex = xy.Item1; - - for (int j = 0; j < value.Columns.Count; j++) - { - var cellValue = value.Rows[i][j]; - WriteCell(writer, yIndex, xIndex, cellValue, columnInfo: null); - xIndex++; - } - writer.Write($""); - yIndex++; - } - } - writer.Write(""); - } - - private void GenerateSheetByIDataReader(MiniExcelStreamWriter writer, IDataReader reader) - { - long dimensionWritePosition = 0; - writer.Write($@""); - var xIndex = 1; - var yIndex = 1; - var maxColumnIndex = 0; - var maxRowIndex = 0; - { - - if (_configuration.FastMode) - { - dimensionWritePosition = writer.WriteAndFlush($@""); // end of code will be replaced - } - - var props = new List(); - for (var i = 0; i < reader.FieldCount; i++) - { - var columnName = reader.GetName(i); - var prop = GetColumnInfosFromDynamicConfiguration(columnName); - props.Add(prop); - } - maxColumnIndex = props.Count; - - WriteColumnsWidths(writer, props); - - writer.Write(""); - int fieldCount = reader.FieldCount; - if (_printHeader) - { - PrintHeader(writer, props); - yIndex++; - } - - while (reader.Read()) - { - writer.Write($""); - xIndex = 1; - for (int i = 0; i < fieldCount; i++) - { - var cellValue = reader.GetValue(i); - WriteCell(writer, yIndex, xIndex, cellValue, columnInfo: null); - xIndex++; - } - writer.Write($""); - yIndex++; - } - - // Subtract 1 because cell indexing starts with 1 - maxRowIndex = yIndex - 1; - } - writer.Write(""); - if (_configuration.AutoFilter) - writer.Write($""); - writer.WriteAndFlush(""); - - if (_configuration.FastMode) - { - writer.SetPosition(dimensionWritePosition); - writer.WriteAndFlush($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}"""); - } + /*Prefix and suffix blank space will lost after SaveAs #294*/ + var preserveSpace = cellValue != null && (cellValue.StartsWith(" ", StringComparison.Ordinal) || cellValue.EndsWith(" ", StringComparison.Ordinal)); + writer.Write(WorksheetXml.Cell(columnReference, dataType, styleIndex, cellValue, preserveSpace: preserveSpace)); } - private ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName) - { - var prop = new ExcelColumnInfo - { - ExcelColumnName = columnName, - Key = columnName - }; - - if (_configuration.DynamicColumns == null || _configuration.DynamicColumns.Length <= 0) - return prop; - - var dynamicColumn = _configuration.DynamicColumns.SingleOrDefault(_ => _.Key == columnName); - if (dynamicColumn == null || dynamicColumn.Ignore) - { - return prop; - } - - prop.Nullable = true; - //prop.ExcludeNullableType = item2[key]?.GetType(); - if (dynamicColumn.Format != null) - prop.ExcelFormat = dynamicColumn.Format; - if (dynamicColumn.Aliases != null) - prop.ExcelColumnAliases = dynamicColumn.Aliases; - if (dynamicColumn.IndexName != null) - prop.ExcelIndexName = dynamicColumn.IndexName; - prop.ExcelColumnIndex = dynamicColumn.Index; - if (dynamicColumn.Name != null) - prop.ExcelColumnName = dynamicColumn.Name; - prop.ExcelColumnWidth = dynamicColumn.Width; - - return prop; - } - - private ExcellSheetInfo GetSheetInfos(string sheetName) - { - var info = new ExcellSheetInfo - { - ExcelSheetName = sheetName, - Key = sheetName, - ExcelSheetState = SheetState.Visible - }; - - if (_configuration.DynamicSheets == null || _configuration.DynamicSheets.Length <= 0) - return info; - - var dynamicSheet = _configuration.DynamicSheets.SingleOrDefault(_ => _.Key == sheetName); - if (dynamicSheet == null) - { - return info; - } - - if (dynamicSheet.Name != null) - info.ExcelSheetName = dynamicSheet.Name; - info.ExcelSheetState = dynamicSheet.State; - - return info; - } - - private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable props) - { - var ecwProps = props.Where(x => x?.ExcelColumnWidth != null).ToList(); - if (ecwProps.Count <= 0) - return; - writer.Write($@""); - foreach (var p in ecwProps) - { - writer.Write( - $@""); - } - - writer.Write($@""); - } - - private static void WriteC(MiniExcelStreamWriter writer, string r, string columnName) - { - writer.Write($""); - writer.Write($"{ExcelOpenXmlUtils.EncodeXML(columnName)}"); //issue I45TF5 - writer.Write($""); - writer.Write($""); - } + private static void WriteCell(MiniExcelStreamWriter writer, string cellReference, string columnName) + => writer.Write(WorksheetXml.Cell(cellReference, "str", "1", ExcelOpenXmlUtils.EncodeXML(columnName))); private void GenerateEndXml() { @@ -817,7 +503,7 @@ private void AddFilesToZip() private void GenerateStylesXml() { var styleXml = GetStylesXml(); - CreateZipEntry(@"xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", styleXml); + CreateZipEntry(ExcelFileNames.Styles, ExcelContentTypes.Styles, styleXml); } private void GenerateDrawinRelXml() @@ -826,9 +512,9 @@ private void GenerateDrawinRelXml() { var drawing = GetDrawingRelationshipXml(sheetIndex); CreateZipEntry( - $"xl/drawings/_rels/drawing{sheetIndex + 1}.xml.rels", + ExcelFileNames.DrawingRels(sheetIndex), null, - _defaultDrawingXmlRels.Replace("{{format}}", drawing)); + ExcelXml.DefaultDrawingXmlRels.Replace("{{format}}", drawing)); } } @@ -839,14 +525,14 @@ private void GenerateDrawingXml() var drawing = GetDrawingXml(sheetIndex); CreateZipEntry( - $"xl/drawings/drawing{sheetIndex + 1}.xml", - "application/vnd.openxmlformats-officedocument.drawing+xml", - _defaultDrawing.Replace("{{format}}", drawing)); + ExcelFileNames.Drawing(sheetIndex), + ExcelContentTypes.Drawing, + ExcelXml.DefaultDrawing.Replace("{{format}}", drawing)); } } /// - /// workbook.xml 、 workbookRelsXml + /// workbook.xml、workbookRelsXml /// private void GenerateWorkbookXml() { @@ -858,20 +544,20 @@ private void GenerateWorkbookXml() foreach (var sheetRelsXml in sheetsRelsXml) { CreateZipEntry( - $"xl/worksheets/_rels/sheet{sheetRelsXml.Key}.xml.rels", + ExcelFileNames.SheetRels(sheetRelsXml.Key), null, - _defaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value)); + ExcelXml.DefaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value)); } CreateZipEntry( - @"xl/workbook.xml", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", - _defaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString())); + ExcelFileNames.Workbook, + ExcelContentTypes.Workbook, + ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString())); CreateZipEntry( - @"xl/_rels/workbook.xml.rels", + ExcelFileNames.WorkbookRels, null, - _defaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString())); + ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString())); } /// @@ -881,21 +567,7 @@ private void GenerateContentTypesXml() { var contentTypes = GetContentTypesXml(); - CreateZipEntry(@"[Content_Types].xml", null, contentTypes); - } - - private string GetDimensionRef(int maxRowIndex, int maxColumnIndex) - { - string dimensionRef; - if (maxRowIndex == 0 && maxColumnIndex == 0) - dimensionRef = "A1"; - else if (maxColumnIndex == 1) - dimensionRef = $"A{maxRowIndex}"; - else if (maxRowIndex == 0) - dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}1"; - else - dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}{maxRowIndex}"; - return dimensionRef; + CreateZipEntry(ExcelFileNames.ContentTypes, null, contentTypes); } private void CreateZipEntry(string path, string contentType, string content) @@ -903,21 +575,23 @@ private void CreateZipEntry(string path, string contentType, string content) ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); using (var zipStream = entry.Open()) using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) + { writer.Write(content); + } + if (!string.IsNullOrEmpty(contentType)) + { _zipDictionary.Add(path, new ZipPackageInfo(entry, contentType)); + } } private void CreateZipEntry(string path, byte[] content) { ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); using (var zipStream = entry.Open()) + { zipStream.Write(content, 0, content.Length); - } - - public void Insert() - { - throw new NotImplementedException(); + } } } } \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs index 0e509cbd..a607fb62 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs @@ -900,7 +900,7 @@ private void UpdateDimensionAndGetRowsInfo(Dictionary inputMaps, } else { - throw new System.Collections.Generic.KeyNotFoundException($"Please check {propNames[0]} parameter, it's not exist."); + throw new KeyNotFoundException($"Please check {propNames[0]} parameter, it's not exist."); } } diff --git a/src/MiniExcel/OpenXml/IDispsable.cs b/src/MiniExcel/OpenXml/IDispsable.cs deleted file mode 100644 index 450e9671..00000000 --- a/src/MiniExcel/OpenXml/IDispsable.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MiniExcelLibs.OpenXml -{ - internal interface IDispsable - { - } -} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs b/src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs index ffc64741..ccc59a62 100644 --- a/src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs +++ b/src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs @@ -17,7 +17,7 @@ internal class MiniExcelAsyncStreamWriter : IDisposable private readonly CancellationToken _cancellationToken; private readonly StreamWriter _streamWriter; private bool disposedValue; - public MiniExcelAsyncStreamWriter(Stream stream, Encoding encoding, int bufferSize, System.Threading.CancellationToken cancellationToken) + public MiniExcelAsyncStreamWriter(Stream stream, Encoding encoding, int bufferSize, CancellationToken cancellationToken) { this._stream = stream; this._encoding = encoding; diff --git a/src/MiniExcel/OpenXml/Models/DrawingDto.cs b/src/MiniExcel/OpenXml/Models/DrawingDto.cs new file mode 100644 index 00000000..6d8df6cd --- /dev/null +++ b/src/MiniExcel/OpenXml/Models/DrawingDto.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiniExcelLibs.OpenXml.Models +{ + internal class DrawingDto + { + internal string ID { get; set; } = $"R{Guid.NewGuid():N}"; + } +} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/Models/FileDto.cs b/src/MiniExcel/OpenXml/Models/FileDto.cs new file mode 100644 index 00000000..c7de2131 --- /dev/null +++ b/src/MiniExcel/OpenXml/Models/FileDto.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniExcelLibs.OpenXml.Models +{ + internal class FileDto + { + internal string ID { get; set; } = $"R{Guid.NewGuid():N}"; + internal string Extension { get; set; } + internal string Path { get { return $"xl/media/{ID}.{Extension}"; } } + internal string Path2 { get { return $"/xl/media/{ID}.{Extension}"; } } + internal byte[] Byte { get; set; } + internal int RowIndex { get; set; } + internal int CellIndex { get; set; } + internal bool IsImage { get; set; } = false; + internal int SheetId { get; set; } + } +} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/Models/SheetDto.cs b/src/MiniExcel/OpenXml/Models/SheetDto.cs new file mode 100644 index 00000000..4ea42876 --- /dev/null +++ b/src/MiniExcel/OpenXml/Models/SheetDto.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniExcelLibs.OpenXml.Models +{ + internal class SheetDto + { + internal string ID { get; set; } = $"R{Guid.NewGuid():N}"; + internal string Name { get; set; } + internal int SheetIdx { get; set; } + internal string Path { get { return $"xl/worksheets/sheet{SheetIdx}.xml"; } } + + internal string State { get; set; } + } +} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs b/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs index ab378e05..bbcb7394 100644 --- a/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs +++ b/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs @@ -17,7 +17,7 @@ class SharedStringsDiskCache : IDictionary, IDisposable private readonly FileStream _lengthFs; private readonly FileStream _valueFs; private bool _disposedValue; - private readonly static Encoding _encoding = new System.Text.UTF8Encoding(true); + private readonly static Encoding _encoding = new UTF8Encoding(true); public int Count => checked((int)(_maxIndx + 1)); public string this[int key] { get => GetValue(key); set => Add(key, value); } private long _maxIndx = -1; diff --git a/src/MiniExcel/Utils/CustomPropertyHelper.cs b/src/MiniExcel/Utils/CustomPropertyHelper.cs index eb1813d4..7fa06114 100644 --- a/src/MiniExcel/Utils/CustomPropertyHelper.cs +++ b/src/MiniExcel/Utils/CustomPropertyHelper.cs @@ -2,6 +2,7 @@ { using MiniExcelLibs.Attributes; using MiniExcelLibs.OpenXml; + using MiniExcelLibs.OpenXml.Models; using System; using System.Collections; using System.Collections.Generic; @@ -199,7 +200,7 @@ private static IEnumerable ConvertToExcelCustomPropertyInfo(Pro ExcludeNullableType = excludeNullableType, Nullable = gt != null, ExcelColumnAliases = excelColumnName?.Aliases ?? excelColumn?.Aliases, - ExcelColumnName = excelColumnName?.ExcelColumnName ?? p.GetAttribute()?.DisplayName ?? excelColumn?.Name ?? p.Name, + ExcelColumnName = excelColumnName?.ExcelColumnName ?? p.GetAttribute()?.DisplayName ?? excelColumn?.Name ?? p.Name, ExcelColumnIndex = p.GetAttribute()?.ExcelColumnIndex ?? excelColumnIndex, ExcelIndexName = p.GetAttribute()?.ExcelXName ?? excelColumn?.IndexName, ExcelColumnWidth = p.GetAttribute()?.ExcelColumnWidth ?? excelColumn?.Width, diff --git a/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs b/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs index 5a467ec1..d6913d95 100644 --- a/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs +++ b/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs @@ -19,7 +19,7 @@ public async Task Gb2312_Encoding_Read_Test() { System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); var path = PathHelper.GetFile("csv/gb2312_Encoding_Read_Test.csv"); - var config = new MiniExcelLibs.Csv.CsvConfiguration() + var config = new Csv.CsvConfiguration() { StreamReaderFunc = (stream) => new StreamReader(stream, encoding: Encoding.GetEncoding("gb2312")) }; @@ -37,7 +37,7 @@ public async Task SeperatorTest() new Dictionary{{ "a", @"""<>+-*//}{\\n" }, { "b", 1234567890 },{ "c", true },{ "d", new DateTime(2021, 1, 1) } }, new Dictionary{{ "a", @"Hello World" }, { "b", -1234567890 },{ "c", false },{ "d", new DateTime(2021, 1, 2) } }, }; - await MiniExcel.SaveAsAsync(path, values, configuration: new MiniExcelLibs.Csv.CsvConfiguration() { Seperator = ';' }); + await MiniExcel.SaveAsAsync(path, values, configuration: new Csv.CsvConfiguration() { Seperator = ';' }); var expected = @"a;b;c;d """"""<>+-*//}{\\n"";1234567890;True;""2021-01-01 00:00:00"" ""Hello World"";-1234567890;False;""2021-01-02 00:00:00"" @@ -296,7 +296,7 @@ await MiniExcel.SaveAsAsync(path, new[] { Assert.Equal(string.Empty, rows[1].c2); } - var config = new MiniExcelLibs.Csv.CsvConfiguration() + var config = new Csv.CsvConfiguration() { ReadEmptyStringAsNull = true }; diff --git a/tests/MiniExcelTests/MiniExcelCsvTests.cs b/tests/MiniExcelTests/MiniExcelCsvTests.cs index 904d2d8c..d17959ff 100644 --- a/tests/MiniExcelTests/MiniExcelCsvTests.cs +++ b/tests/MiniExcelTests/MiniExcelCsvTests.cs @@ -20,7 +20,7 @@ public void gb2312_Encoding_Read_Test() { System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); var path = PathHelper.GetFile("csv/gb2312_Encoding_Read_Test.csv"); - var config = new MiniExcelLibs.Csv.CsvConfiguration() + var config = new Csv.CsvConfiguration() { StreamReaderFunc = (stream) => new StreamReader(stream, encoding: Encoding.GetEncoding("gb2312")) }; @@ -37,7 +37,7 @@ public void SeperatorTest() new Dictionary{{ "a", @"""<>+-*//}{\\n" }, { "b", 1234567890 },{ "c", true },{ "d", new DateTime(2021, 1, 1) } }, new Dictionary{{ "a", @"Hello World" }, { "b", -1234567890 },{ "c", false },{ "d", new DateTime(2021, 1, 2) } }, }; - MiniExcel.SaveAs(path, values, configuration: new MiniExcelLibs.Csv.CsvConfiguration() { Seperator = ';' }); + MiniExcel.SaveAs(path, values, configuration: new Csv.CsvConfiguration() { Seperator = ';' }); var expected = @"a;b;c;d """"""<>+-*//}{\\n"";1234567890;True;""2021-01-01 00:00:00"" ""Hello World"";-1234567890;False;""2021-01-02 00:00:00"" @@ -54,7 +54,7 @@ public void AlwaysQuoteTest() new Dictionary{{ "a", @"""<>+-*//}{\\n" }, { "b", 1234567890 },{ "c", true },{ "d", new DateTime(2021, 1, 1) } }, new Dictionary{{ "a", @"Hello World" }, { "b", -1234567890 },{ "c", false },{ "d", new DateTime(2021, 1, 2) } }, }; - MiniExcel.SaveAs(path, values, configuration: new MiniExcelLibs.Csv.CsvConfiguration() { AlwaysQuote = true }); + MiniExcel.SaveAs(path, values, configuration: new Csv.CsvConfiguration() { AlwaysQuote = true }); var expected = @"""a"",""b"",""c"",""d"" """"""<>+-*//}{\\n"",""1234567890"",""True"",""2021-01-01 00:00:00"" ""Hello World"",""-1234567890"",""False"",""2021-01-02 00:00:00"" @@ -70,7 +70,7 @@ public void QuoteSpecialCharacters() { new Dictionary{{ "a", @"potato,banana" }, { "b", "text\ntest" },{ "c", "text\rpotato" },{ "d", new DateTime(2021, 1, 1) } }, }; - MiniExcel.SaveAs(path, values, configuration: new MiniExcelLibs.Csv.CsvConfiguration()); + MiniExcel.SaveAs(path, values, configuration: new Csv.CsvConfiguration()); var expected = "a,b,c,d\r\n\"potato,banana\",\"text\ntest\",\"text\rpotato\",\"2021-01-01 00:00:00\"\r\n"; Assert.Equal(expected, File.ReadAllText(path)); } diff --git a/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs index bf51e63b..a2e15d50 100644 --- a/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs @@ -108,7 +108,7 @@ public async Task Issue253() System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); var value = new[] { new { col1 = "世界你好" } }; var path = PathHelper.GetTempPath(extension: "csv"); - var config = new MiniExcelLibs.Csv.CsvConfiguration() + var config = new Csv.CsvConfiguration() { StreamWriterFunc = (stream) => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) }; @@ -1448,10 +1448,10 @@ public async Task Issue149() { var chars = new char[] {'\u0000','\u0001','\u0002','\u0003','\u0004','\u0005','\u0006','\u0007','\u0008', '\u0009', // - '\u000A', // - '\u000B','\u000C', + '\u000A', // + '\u000B','\u000C', '\u000D', // - '\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016', + '\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016', '\u0017','\u0018','\u0019','\u001A','\u001B','\u001C','\u001D','\u001E','\u001F','\u007F' }.Select(s => s.ToString()).ToArray(); diff --git a/tests/MiniExcelTests/MiniExcelIssueTests.cs b/tests/MiniExcelTests/MiniExcelIssueTests.cs index 10587dd4..220d538f 100644 --- a/tests/MiniExcelTests/MiniExcelIssueTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueTests.cs @@ -14,7 +14,6 @@ using System.ComponentModel; using System.Data; using System.Data.SQLite; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -934,7 +933,7 @@ public void TestIssue316() //Datetime error { - Assert.Throws(() => + Assert.Throws(() => { var config = new OpenXmlConfiguration() { @@ -1009,7 +1008,7 @@ public void TestIssue316() //Datetime error { - Assert.Throws(() => + Assert.Throws(() => { var config = new CsvConfiguration() { @@ -1870,7 +1869,7 @@ public void Issue253() System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); var value = new[] { new { col1 = "世界你好" } }; var path = PathHelper.GetTempPath(extension: "csv"); - var config = new MiniExcelLibs.Csv.CsvConfiguration() + var config = new CsvConfiguration() { StreamWriterFunc = (stream) => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) }; diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs index bd95f28e..1aa967e2 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs @@ -97,7 +97,7 @@ public class ExcelAttributeDemo public void CustomAttributeWihoutVaildPropertiesTest() { var path = @"../../../../../samples/xlsx/TestCustomExcelColumnAttribute.xlsx"; - Assert.Throws(() => MiniExcel.Query(path).ToList()); + Assert.Throws(() => MiniExcel.Query(path).ToList()); } [Fact] diff --git a/tests/MiniExcelTests/Utils/FileHelper.cs b/tests/MiniExcelTests/Utils/FileHelper.cs index 7e5214c6..42052ea4 100644 --- a/tests/MiniExcelTests/Utils/FileHelper.cs +++ b/tests/MiniExcelTests/Utils/FileHelper.cs @@ -13,7 +13,7 @@ public static Stream OpenRead(string path) { return File.OpenRead(path); } - catch (System.IO.IOException) + catch (IOException) { var newPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx"); File.Copy(path, newPath);