From fdfe883904b1e14e6208fed019e56d27b0b3ec31 Mon Sep 17 00:00:00 2001 From: duszekmestre Date: Sun, 19 May 2024 08:29:15 +0200 Subject: [PATCH] CSV enumeration and code reusage (#600) * 1900 year DateTime correction * Rewrite CSV enumeration * Maximize code reusage * Reuse OADate correction * Update to latest version of CodeQL * Update CodeQL to v3 --------- Co-authored-by: Lukasz Arciszewski --- .editorconfig | 16 + .github/workflows/codeql-analysis.yml | 6 +- src/MiniExcel/Csv/CsvWriter.cs | 178 +++++----- .../OpenXml/ExcelOpenXmlSheetWriter.Async.cs | 328 ++++++++---------- .../ExcelOpenXmlSheetWriter.DefualtOpenXml.cs | 121 ++++++- .../OpenXml/ExcelOpenXmlSheetWriter.cs | 322 +++++++---------- src/MiniExcel/Utils/CustomPropertyHelper.cs | 65 +++- 7 files changed, 556 insertions(+), 480 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bc3e7b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +[*.{cs,vb}] + +# IDE0009: Member access should be qualified. +dotnet_style_qualification_for_event = false + +# IDE0009: Member access should be qualified. +dotnet_style_qualification_for_field = false + +# IDE0009: Member access should be qualified. +dotnet_style_qualification_for_property = false + +# IDE0009: Member access should be qualified. +dotnet_style_qualification_for_method = false + +# IDE0065: Misplaced using directive +csharp_using_directive_placement = outside_namespace \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 66d611e..83a1801 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/src/MiniExcel/Csv/CsvWriter.cs b/src/MiniExcel/Csv/CsvWriter.cs index 48f0896..ee50da7 100644 --- a/src/MiniExcel/Csv/CsvWriter.cs +++ b/src/MiniExcel/Csv/CsvWriter.cs @@ -42,100 +42,94 @@ namespace MiniExcelLibs.Csv } var type = _value.GetType(); - Type genericType = null; - if (_value is IDataReader) + if (_value is IDataReader dataReader) { - GenerateSheetByIDataReader(_value, seperator, newLine, _writer); + GenerateSheetByIDataReader(dataReader, seperator, newLine, _writer); } - else if (_value is IEnumerable) + else if (_value is IEnumerable enumerable) { - var values = _value as IEnumerable; - List keys = new List(); - List props = null; - string mode = null; - - // check mode - { - foreach (var item in values) //TODO: need to optimize - { - if (item != null && mode == null) - { - if (item is IDictionary) - { - var item2 = item as IDictionary; - mode = "IDictionary"; - foreach (var key in item2.Keys) - keys.Add(key); - } - else if (item is IDictionary) - { - var item2 = item as IDictionary; - mode = "IDictionary"; - foreach (var key in item2.Keys) - keys.Add(key); - } - else - { - mode = "Properties"; - genericType = item.GetType(); - props = CustomPropertyHelper.GetSaveAsProperties(genericType, _configuration); - } - - break; - } - } - } - - //if(mode == null) - // throw new NotImplementedException($"Type {type?.Name} & genericType {genericType?.Name} not Implemented. please issue for me."); - - if (keys.Count == 0 && props == null) - { - _writer.Write(newLine); - this._writer.Flush(); - return; - } - - if (this._printHeader) - { - if (props != null) - { - _writer.Write(string.Join(seperator, props.Select(s => CsvHelpers.ConvertToCsvValue(s?.ExcelColumnName, _configuration.AlwaysQuote, _configuration.Seperator)))); - _writer.Write(newLine); - } - else if (keys.Count > 0) - { - _writer.Write(string.Join(seperator, keys.Select(s => CsvHelpers.ConvertToCsvValue(s.ToString(), _configuration.AlwaysQuote, _configuration.Seperator)))); - _writer.Write(newLine); - } - else - { - throw new InvalidOperationException("Please issue for me."); - } - } - - if (mode == "IDictionary") //Dapper Row - GenerateSheetByDapperRow(_writer, _value as IEnumerable, keys.Cast().ToList(), seperator, newLine); - else if (mode == "IDictionary") //IDictionary - GenerateSheetByIDictionary(_writer, _value as IEnumerable, keys, seperator, newLine); - else if (mode == "Properties") - GenerateSheetByProperties(_writer, _value as IEnumerable, props, seperator, newLine); - else - throw new NotImplementedException($"Type {type?.Name} & genericType {genericType?.Name} not Implemented. please issue for me."); + GenerateSheetByIEnumerable(enumerable, seperator, newLine, _writer); } - else if (_value is DataTable) + else if (_value is DataTable dataTable) { - GenerateSheetByDataTable(_writer, _value as DataTable, seperator, newLine); + GenerateSheetByDataTable(_writer, dataTable, seperator, newLine); } else { - throw new NotImplementedException($"Type {type?.Name} & genericType {genericType?.Name} not Implemented. please issue for me."); + throw new NotImplementedException($"Type {type?.Name} not Implemented. please issue for me."); } + this._writer.Flush(); } } + private void GenerateSheetByIEnumerable(IEnumerable values, string seperator, string newLine, StreamWriter writer) + { + Type genericType = null; + List props = null; + string mode = null; + + var enumerator = values.GetEnumerator(); + var empty = !enumerator.MoveNext(); + if (empty) + { + // only when empty IEnumerable need to check this issue #133 https://github.com/shps951023/MiniExcel/issues/133 + genericType = TypeHelper.GetGenericIEnumerables(values).FirstOrDefault(); + if (genericType == null || genericType == typeof(object) // sometime generic type will be object, e.g: https://user-images.githubusercontent.com/12729184/132812859-52984314-44d1-4ee8-9487-2d1da159f1f0.png + || typeof(IDictionary).IsAssignableFrom(genericType) + || typeof(IDictionary).IsAssignableFrom(genericType) + || typeof(KeyValuePair).IsAssignableFrom(genericType)) + { + _writer.Write(newLine); + this._writer.Flush(); + return; + } + + mode = "Properties"; + props = CustomPropertyHelper.GetSaveAsProperties(genericType, _configuration); + } + else + { + var firstItem = enumerator.Current; + if (firstItem is IDictionary genericDic) + { + mode = "IDictionary"; + props = CustomPropertyHelper.GetDictionaryColumnInfo(genericDic, null, _configuration); + } + else if (firstItem is IDictionary dic) + { + mode = "IDictionary"; + props = CustomPropertyHelper.GetDictionaryColumnInfo(null, dic, _configuration); + mode = "IDictionary"; + } + else + { + mode = "Properties"; + genericType = firstItem.GetType(); + props = CustomPropertyHelper.GetSaveAsProperties(genericType, _configuration); + } + } + + if (this._printHeader) + { + _writer.Write(string.Join(seperator, props.Select(s => CsvHelpers.ConvertToCsvValue(s?.ExcelColumnName, _configuration.AlwaysQuote, _configuration.Seperator)))); + _writer.Write(newLine); + } + + if (!empty) + { + if (mode == "IDictionary") //Dapper Row + GenerateSheetByDapperRow(_writer, enumerator, props.Select(x => x.Key.ToString()).ToList(), seperator, newLine); + else if (mode == "IDictionary") //IDictionary + GenerateSheetByIDictionary(_writer, enumerator, props.Select(x => x.Key).ToList(), seperator, newLine); + else if (mode == "Properties") + GenerateSheetByProperties(_writer, enumerator, props, seperator, newLine); + else + throw new NotImplementedException($"Mode for genericType {genericType?.Name} not Implemented. please issue for me."); + } + } + public void Insert() { SaveAs(); @@ -202,34 +196,37 @@ namespace MiniExcelLibs.Csv } } - private void GenerateSheetByProperties(StreamWriter writer, IEnumerable value, List props, string seperator, string newLine) + private void GenerateSheetByProperties(StreamWriter writer, IEnumerator value, List props, string seperator, string newLine) { - foreach (var v in value) + do { + var v = value.Current; var values = props.Select(s => CsvHelpers.ConvertToCsvValue(ToCsvString(s?.Property.GetValue(v), s), _configuration.AlwaysQuote, _configuration.Seperator)); writer.Write(string.Join(seperator, values)); writer.Write(newLine); - } + } while (value.MoveNext()); } - private void GenerateSheetByIDictionary(StreamWriter writer, IEnumerable value, List keys, string seperator, string newLine) + private void GenerateSheetByIDictionary(StreamWriter writer, IEnumerator value, List keys, string seperator, string newLine) { - foreach (IDictionary v in value) + do { + var v = (IDictionary)value.Current; var values = keys.Select(key => CsvHelpers.ConvertToCsvValue(ToCsvString(v[key], null), _configuration.AlwaysQuote, _configuration.Seperator)); writer.Write(string.Join(seperator, values)); writer.Write(newLine); - } + } while (value.MoveNext()); } - private void GenerateSheetByDapperRow(StreamWriter writer, IEnumerable value, List keys, string seperator, string newLine) + private void GenerateSheetByDapperRow(StreamWriter writer, IEnumerator value, List keys, string seperator, string newLine) { - foreach (IDictionary v in value) + do { + var v = (IDictionary)value.Current; var values = keys.Select(key => CsvHelpers.ConvertToCsvValue(ToCsvString(v[key], null), _configuration.AlwaysQuote, _configuration.Seperator)); writer.Write(string.Join(seperator, values)); writer.Write(newLine); - } + } while (value.MoveNext()); } public string ToCsvString(object value, ExcelColumnInfo p) @@ -283,5 +280,4 @@ namespace MiniExcelLibs.Csv GC.SuppressFinalize(this); } } - -} +} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs index 85f5357..e79aa96 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs @@ -21,43 +21,43 @@ namespace MiniExcelLibs.OpenXml switch (_value) { case IDictionary sheets: + { + var sheetId = 0; + _sheets.RemoveAt(0);//TODO:remove + foreach (var sheet in 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 + sheetId++; + var sheetInfos = GetSheetInfos(sheet.Key); + var sheetDto = sheetInfos.ToDto(sheetId); + _sheets.Add(sheetDto); //TODO:remove - currentSheetIndex = sheetId; + currentSheetIndex = sheetId; - await CreateSheetXmlAsync(sheet.Value, sheetDto.Path, cancellationToken); - } - - break; + 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) { - 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 + sheetId++; + var sheetInfos = GetSheetInfos(dt.TableName); + var sheetDto = sheetInfos.ToDto(sheetId); + _sheets.Add(sheetDto); //TODO:remove - currentSheetIndex = sheetId; + currentSheetIndex = sheetId; - await CreateSheetXmlAsync(dt, sheetDto.Path, cancellationToken); - } - - break; + await CreateSheetXmlAsync(dt, sheetDto.Path, cancellationToken); } + break; + } + default: //Single sheet export. currentSheetIndex++; @@ -72,25 +72,8 @@ namespace MiniExcelLibs.OpenXml internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationToken) { - await CreateZipEntryAsync("_rels/.rels", "application/vnd.openxmlformats-package.relationships+xml", ExcelOpenXmlSheetWriter._defaultRels, cancellationToken); - await CreateZipEntryAsync("xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", ExcelOpenXmlSheetWriter._defaultSharedString, cancellationToken); - } - - private async Task CreateZipEntryAsync(string path, string contentType, string content, CancellationToken cancellationToken) - { - ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); - using (var zipStream = entry.Open()) - using (MiniExcelAsyncStreamWriter writer = new MiniExcelAsyncStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize, cancellationToken)) - await writer.WriteAsync(content); - if (!string.IsNullOrEmpty(contentType)) - _zipDictionary.Add(path, new ZipPackageInfo(entry, contentType)); - } - - private async Task CreateZipEntryAsync(string path, byte[] content, CancellationToken cancellationToken) - { - ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); - using (var zipStream = entry.Open()) - await zipStream.WriteAsync(content, 0, content.Length, 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); } private async Task CreateSheetXmlAsync(object value, string sheetPath, CancellationToken cancellationToken) @@ -244,13 +227,13 @@ namespace MiniExcelLibs.OpenXml if (firstItem is IDictionary genericDic) { mode = "IDictionary"; - props = GetDictionaryColumnInfo(genericDic, null); + props = CustomPropertyHelper.GetDictionaryColumnInfo(genericDic, null, _configuration); maxColumnIndex = props.Count; } else if (firstItem is IDictionary dic) { mode = "IDictionary"; - props = GetDictionaryColumnInfo(null, dic); + props = CustomPropertyHelper.GetDictionaryColumnInfo(null, dic, _configuration); //maxColumnIndex = dic.Keys.Count; maxColumnIndex = props.Count; // why not using keys, because ignore attribute ![image](https://user-images.githubusercontent.com/12729184/163686902-286abb70-877b-4e84-bd3b-001ad339a84a.png) } @@ -416,7 +399,7 @@ namespace MiniExcelLibs.OpenXml } var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); - WriteCAsync(writer, r, columnName: p.ExcelColumnName); + await WriteCAsync(writer, r, columnName: p.ExcelColumnName); xIndex++; } @@ -450,10 +433,14 @@ namespace MiniExcelLibs.OpenXml 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 - //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) + { + //to check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) await writer.WriteAsync($"{v}"); + } } private async Task GenerateSheetByColumnInfoAsync(MiniExcelAsyncStreamWriter writer, IEnumerator value, List props, int xIndex = 1, int yIndex = 1) @@ -474,6 +461,7 @@ namespace MiniExcelLibs.OpenXml cellIndex++; continue; } + object cellValue = null; if (isDic) { @@ -489,11 +477,12 @@ namespace MiniExcelLibs.OpenXml { cellValue = p.Property.GetValue(v); } - await WriteCellAsync(writer, yIndex, cellIndex, cellValue, p); + await WriteCellAsync(writer, yIndex, cellIndex, cellValue, p); cellIndex++; } + await writer.WriteAsync($""); yIndex++; } while (value.MoveNext()); @@ -503,132 +492,121 @@ namespace MiniExcelLibs.OpenXml private async Task GenerateEndXmlAsync(CancellationToken cancellationToken) { - //Files + await AddFilesToZipAsync(cancellationToken); + + await GenerateStylesXmlAsync(cancellationToken); + + await GenerateDrawinRelXmlAsync(cancellationToken); + + await GenerateDrawingXmlAsync(cancellationToken); + + await GenerateWorkbookXmlAsync(cancellationToken); + + await GenerateContentTypesXmlAsync(cancellationToken); + } + + private async Task AddFilesToZipAsync(CancellationToken cancellationToken) + { + foreach (var item in _files) { - foreach (var item in _files) - { - await this.CreateZipEntryAsync(item.Path, item.Byte, cancellationToken); - } - } - - // styles.xml - { - var styleXml = string.Empty; - - if (_configuration.TableStyles == TableStyles.None) - { - styleXml = _noneStylesXml; - } - else if (_configuration.TableStyles == TableStyles.Default) - { - styleXml = _defaultStylesXml; - } - - await CreateZipEntryAsync(@"xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", styleXml, cancellationToken); - } - - // drawing rel - { - for (int j = 0; j < _sheets.Count; j++) - { - var drawing = new StringBuilder(); - foreach (var i in _files.Where(w => w.IsImage && w.SheetId == j + 1)) - { - drawing.AppendLine($@""); - } - await CreateZipEntryAsync($"xl/drawings/_rels/drawing{j + 1}.xml.rels", "", - _defaultDrawingXmlRels.Replace("{{format}}", drawing.ToString()), cancellationToken); - } - - } - // drawing - { - for (int j = 0; j < _sheets.Count; j++) - { - var drawing = new StringBuilder(); - foreach (var i in _files.Where(w => w.IsImage && w.SheetId == j + 1)) - { - drawing.Append($@" - - {i.CellIndex - 1/* why -1 : https://user-images.githubusercontent.com/12729184/150460189-f08ed939-44d4-44e1-be6e-9c533ece6be8.png*/} - 0 - {i.RowIndex - 1} - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - "); - } - await CreateZipEntryAsync($"xl/drawings/drawing{j + 1}.xml", "application/vnd.openxmlformats-officedocument.drawing+xml", - _defaultDrawing.Replace("{{format}}", drawing.ToString()), cancellationToken); - } - } - - // workbook.xml 、 workbookRelsXml - { - var workbookXml = new StringBuilder(); - var workbookRelsXml = new StringBuilder(); - - 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 = $@""; - await CreateZipEntryAsync($"xl/worksheets/_rels/sheet{s.SheetIdx}.xml.rels", "", - _defaultSheetRelXml.Replace("{{format}}", sheetRelsXml), cancellationToken); - } - await CreateZipEntryAsync(@"xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", - _defaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken); - await CreateZipEntryAsync(@"xl/_rels/workbook.xml.rels", "", - _defaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken); - } - - //[Content_Types].xml - { - var sb = new StringBuilder(@""); - foreach (var p in _zipDictionary) - sb.Append($""); - sb.Append(""); - ZipArchiveEntry entry = _archive.CreateEntry("[Content_Types].xml", CompressionLevel.Fastest); - using (var zipStream = entry.Open()) - using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) - writer.Write(sb.ToString()); + await this.CreateZipEntryAsync(item.Path, item.Byte, cancellationToken); } } + + /// + /// styles.xml + /// + private async Task GenerateStylesXmlAsync(CancellationToken cancellationToken) + { + var styleXml = GetStylesXml(); + + await CreateZipEntryAsync( + @"xl/styles.xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", + styleXml, + cancellationToken); + } + + 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); + } + } + + private async Task GenerateDrawingXmlAsync(CancellationToken cancellationToken) + { + for (int sheetIndex = 0; sheetIndex < _sheets.Count; sheetIndex++) + { + var drawing = GetDrawingXml(sheetIndex); + await CreateZipEntryAsync( + $"xl/drawings/drawing{sheetIndex + 1}.xml", + "application/vnd.openxmlformats-officedocument.drawing+xml", + _defaultDrawing.Replace("{{format}}", drawing), + cancellationToken); + } + } + + /// + /// workbook.xml 、 workbookRelsXml + /// + private async Task GenerateWorkbookXmlAsync(CancellationToken cancellationToken) + { + GenerateWorkBookXmls( + out StringBuilder workbookXml, + out StringBuilder workbookRelsXml, + out Dictionary sheetsRelsXml); + + foreach (var sheetRelsXml in sheetsRelsXml) + { + await CreateZipEntryAsync( + $"xl/worksheets/_rels/sheet{sheetRelsXml.Key}.xml.rels", + null, + _defaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value), + cancellationToken); + } + + await CreateZipEntryAsync( + @"xl/workbook.xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", + _defaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), + cancellationToken); + + await CreateZipEntryAsync( + @"xl/_rels/workbook.xml.rels", + null, + _defaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), + cancellationToken); + } + + /// + /// [Content_Types].xml + /// + private async Task GenerateContentTypesXmlAsync(CancellationToken cancellationToken) + { + var contentTypes = GetContentTypesXml(); + + await CreateZipEntryAsync(@"[Content_Types].xml", null, contentTypes, cancellationToken); + } + + private async Task CreateZipEntryAsync(string path, string contentType, string content, CancellationToken cancellationToken) + { + ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); + using (var zipStream = entry.Open()) + using (MiniExcelAsyncStreamWriter writer = new MiniExcelAsyncStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize, cancellationToken)) + await writer.WriteAsync(content); + if (!string.IsNullOrEmpty(contentType)) + _zipDictionary.Add(path, new ZipPackageInfo(entry, contentType)); + } + + private async Task CreateZipEntryAsync(string path, byte[] content, CancellationToken cancellationToken) + { + ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); + using (var zipStream = entry.Open()) + await zipStream.WriteAsync(content, 0, content.Length, cancellationToken); + } } -} +} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefualtOpenXml.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefualtOpenXml.cs index 1dc0d39..efc3398 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefualtOpenXml.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefualtOpenXml.cs @@ -1,6 +1,7 @@ using MiniExcelLibs.Zip; using System.Collections.Generic; -using System.IO.Compression; +using System.Linq; +using System.Text; namespace MiniExcelLibs.OpenXml { @@ -46,8 +47,8 @@ namespace MiniExcelLibs.OpenXml - - + + "; @@ -182,27 +183,113 @@ namespace MiniExcelLibs.OpenXml private static readonly string _defaultSharedString = ""; private static string MinifyXml(string xml) => xml.Replace("\r", "").Replace("\n", "").Replace("\t", ""); - internal void GenerateDefaultOpenXml() + private string GetStylesXml() { - CreateZipEntry("_rels/.rels", "application/vnd.openxmlformats-package.relationships+xml", ExcelOpenXmlSheetWriter._defaultRels); - CreateZipEntry("xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", ExcelOpenXmlSheetWriter._defaultSharedString); + var styleXml = string.Empty; + + if (_configuration.TableStyles == TableStyles.None) + { + styleXml = _noneStylesXml; + } + else if (_configuration.TableStyles == TableStyles.Default) + { + styleXml = _defaultStylesXml; + } + + return styleXml; } - private void CreateZipEntry(string path, string contentType, string content) + private string GetDrawingRelationshipXml(int sheetIndex) { - 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)); + var drawing = new StringBuilder(); + foreach (var i in _files.Where(w => w.IsImage && w.SheetId == sheetIndex + 1)) + { + drawing.AppendLine($@""); + } + + return drawing.ToString(); } - private void CreateZipEntry(string path, byte[] content) + private string GetDrawingXml(int sheetIndex) { - ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); - using (var zipStream = entry.Open()) - zipStream.Write(content, 0, content.Length); + var drawing = new StringBuilder(); + foreach (var file in _files.Where(w => w.IsImage && w.SheetId == sheetIndex + 1)) + { + drawing.Append($@" + + {file.CellIndex - 1/* why -1 : https://user-images.githubusercontent.com/12729184/150460189-f08ed939-44d4-44e1-be6e-9c533ece6be8.png*/} + 0 + {file.RowIndex - 1} + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + "); + } + + 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(); } } } diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index feac59a..5418193 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -9,6 +9,7 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Text; +using System.Threading; using static MiniExcelLibs.Utils.ImageHelper; namespace MiniExcelLibs.OpenXml @@ -120,6 +121,12 @@ namespace MiniExcelLibs.OpenXml _archive.Dispose(); } + internal void GenerateDefaultOpenXml() + { + CreateZipEntry("_rels/.rels", "application/vnd.openxmlformats-package.relationships+xml", _defaultRels); + CreateZipEntry("xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", _defaultSharedString); + } + private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable values) { var maxColumnIndex = 0; @@ -168,13 +175,13 @@ namespace MiniExcelLibs.OpenXml if (firstItem is IDictionary genericDic) { mode = "IDictionary"; - props = GetDictionaryColumnInfo(genericDic, null); + props = CustomPropertyHelper.GetDictionaryColumnInfo(genericDic, null, _configuration); maxColumnIndex = props.Count; } else if (firstItem is IDictionary dic) { mode = "IDictionary"; - props = GetDictionaryColumnInfo(null, dic); + props = CustomPropertyHelper.GetDictionaryColumnInfo(null, dic, _configuration); //maxColumnIndex = dic.Keys.Count; maxColumnIndex = props.Count; // why not using keys, because ignore attribute ![image](https://user-images.githubusercontent.com/12729184/163686902-286abb70-877b-4e84-bd3b-001ad339a84a.png) } @@ -303,55 +310,6 @@ namespace MiniExcelLibs.OpenXml _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml")); } - private List GetDictionaryColumnInfo(IDictionary dicString, IDictionary dic) - { - List props; - var _props = new List(); - if (dicString != null) - foreach (var key in dicString.Keys) - SetDictionaryColumnInfo(_props, key); - else if (dic != null) - foreach (var key in dic.Keys) - SetDictionaryColumnInfo(_props, key); - else - throw new NotSupportedException("SetDictionaryColumnInfo Error"); - props = CustomPropertyHelper.SortCustomProps(_props); - return props; - } - - private void SetDictionaryColumnInfo(List _props, object key) - { - var p = new ExcelColumnInfo(); - p.ExcelColumnName = key?.ToString(); - p.Key = key; - // TODO:Dictionary value type is not fiexed - //var _t = - //var gt = Nullable.GetUnderlyingType(p.PropertyType); - var isIgnore = false; - if (_configuration.DynamicColumns != null && _configuration.DynamicColumns.Length > 0) - { - var dynamicColumn = _configuration.DynamicColumns.SingleOrDefault(_ => _.Key == key.ToString()); - if (dynamicColumn != null) - { - p.Nullable = true; - //p.ExcludeNullableType = item2[key]?.GetType(); - if (dynamicColumn.Format != null) - p.ExcelFormat = dynamicColumn.Format; - if (dynamicColumn.Aliases != null) - p.ExcelColumnAliases = dynamicColumn.Aliases; - if (dynamicColumn.IndexName != null) - p.ExcelIndexName = dynamicColumn.IndexName; - p.ExcelColumnIndex = dynamicColumn.Index; - if (dynamicColumn.Name != null) - p.ExcelColumnName = dynamicColumn.Name; - isIgnore = dynamicColumn.Ignore; - p.ExcelColumnWidth = dynamicColumn.Width; - } - } - if (!isIgnore) - _props.Add(p); - } - private void SetGenericTypePropertiesMode(Type genericType, ref string mode, out int maxColumnIndex, out List props) { mode = "Properties"; @@ -563,16 +521,7 @@ namespace MiniExcelLibs.OpenXml } else if (columnInfo == null || columnInfo.ExcelFormat == null) { - var oaDate = ((DateTime)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; - } + var oaDate = CorrectDateTimeValue((DateTime)value); dataType = null; styleIndex = "3"; @@ -596,16 +545,7 @@ namespace MiniExcelLibs.OpenXml else if (columnInfo == null || columnInfo.ExcelFormat == null) { var day = (DateOnly)value; - var oaDate = day.ToDateTime(TimeOnly.MinValue).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; - } + var oaDate = CorrectDateTimeValue(day.ToDateTime(TimeOnly.MinValue)); dataType = null; styleIndex = "3"; @@ -629,6 +569,22 @@ namespace MiniExcelLibs.OpenXml 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"); @@ -834,131 +790,98 @@ namespace MiniExcelLibs.OpenXml private void GenerateEndXml() { - //Files + AddFilesToZip(); + + GenerateStylesXml(); + + GenerateDrawinRelXml(); + + GenerateDrawingXml(); + + GenerateWorkbookXml(); + + GenerateContentTypesXml(); + } + + private void AddFilesToZip() + { + foreach (var item in _files) { - foreach (var item in _files) - { - this.CreateZipEntry(item.Path, item.Byte); - } + this.CreateZipEntry(item.Path, item.Byte); + } + } + + /// + /// styles.xml + /// + private void GenerateStylesXml() + { + var styleXml = GetStylesXml(); + CreateZipEntry(@"xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", styleXml); + } + + private void GenerateDrawinRelXml() + { + for (int sheetIndex = 0; sheetIndex < _sheets.Count; sheetIndex++) + { + var drawing = GetDrawingRelationshipXml(sheetIndex); + CreateZipEntry( + $"xl/drawings/_rels/drawing{sheetIndex + 1}.xml.rels", + null, + _defaultDrawingXmlRels.Replace("{{format}}", drawing)); + } + } + + private void GenerateDrawingXml() + { + for (int sheetIndex = 0; sheetIndex < _sheets.Count; sheetIndex++) + { + var drawing = GetDrawingXml(sheetIndex); + + CreateZipEntry( + $"xl/drawings/drawing{sheetIndex + 1}.xml", + "application/vnd.openxmlformats-officedocument.drawing+xml", + _defaultDrawing.Replace("{{format}}", drawing)); + } + } + + /// + /// workbook.xml 、 workbookRelsXml + /// + private void GenerateWorkbookXml() + { + GenerateWorkBookXmls( + out StringBuilder workbookXml, + out StringBuilder workbookRelsXml, + out Dictionary sheetsRelsXml); + + foreach (var sheetRelsXml in sheetsRelsXml) + { + CreateZipEntry( + $"xl/worksheets/_rels/sheet{sheetRelsXml.Key}.xml.rels", + null, + _defaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value)); } - // styles.xml - { - var styleXml = string.Empty; + CreateZipEntry( + @"xl/workbook.xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", + _defaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString())); - if (_configuration.TableStyles == TableStyles.None) - { - styleXml = _noneStylesXml; - } - else if (_configuration.TableStyles == TableStyles.Default) - { - styleXml = _defaultStylesXml; - } - CreateZipEntry(@"xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", styleXml); - } + CreateZipEntry( + @"xl/_rels/workbook.xml.rels", + null, + _defaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString())); + } - // drawing rel - { - for (int j = 0; j < _sheets.Count; j++) - { - var drawing = new StringBuilder(); - foreach (var i in _files.Where(w => w.IsImage && w.SheetId == j + 1)) - { - drawing.AppendLine($@""); - } - CreateZipEntry($"xl/drawings/_rels/drawing{j + 1}.xml.rels", "", - _defaultDrawingXmlRels.Replace("{{format}}", drawing.ToString())); - } + /// + /// [Content_Types].xml + /// + private void GenerateContentTypesXml() + { + var contentTypes = GetContentTypesXml(); - } - // drawing - { - for (int j = 0; j < _sheets.Count; j++) - { - var drawing = new StringBuilder(); - foreach (var i in _files.Where(w => w.IsImage && w.SheetId == j + 1)) - { - drawing.Append($@" - - {i.CellIndex - 1/* why -1 : https://user-images.githubusercontent.com/12729184/150460189-f08ed939-44d4-44e1-be6e-9c533ece6be8.png*/} - 0 - {i.RowIndex - 1} - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - "); - } - CreateZipEntry($"xl/drawings/drawing{j + 1}.xml", "application/vnd.openxmlformats-officedocument.drawing+xml", - _defaultDrawing.Replace("{{format}}", drawing.ToString())); - } - } - - // workbook.xml 、 workbookRelsXml - { - var workbookXml = new StringBuilder(); - var workbookRelsXml = new StringBuilder(); - - 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 = $@""; - CreateZipEntry($"xl/worksheets/_rels/sheet{s.SheetIdx}.xml.rels", "", - _defaultSheetRelXml.Replace("{{format}}", sheetRelsXml)); - } - CreateZipEntry(@"xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", - _defaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString())); - CreateZipEntry(@"xl/_rels/workbook.xml.rels", "", - _defaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString())); - } - - //[Content_Types].xml - { - var sb = new StringBuilder(@""); - foreach (var p in _zipDictionary) - sb.Append($""); - sb.Append(""); - ZipArchiveEntry entry = _archive.CreateEntry("[Content_Types].xml", CompressionLevel.Fastest); - using (var zipStream = entry.Open()) - using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) - writer.Write(sb.ToString()); - } + CreateZipEntry(@"[Content_Types].xml", null, contentTypes); } private string GetDimensionRef(int maxRowIndex, int maxColumnIndex) @@ -975,9 +898,26 @@ namespace MiniExcelLibs.OpenXml return dimensionRef; } + 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/Utils/CustomPropertyHelper.cs b/src/MiniExcel/Utils/CustomPropertyHelper.cs index 2f21b88..eb1813d 100644 --- a/src/MiniExcel/Utils/CustomPropertyHelper.cs +++ b/src/MiniExcel/Utils/CustomPropertyHelper.cs @@ -3,6 +3,7 @@ using MiniExcelLibs.Attributes; using MiniExcelLibs.OpenXml; using System; + using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -168,7 +169,6 @@ return source.ToString(); } - private static IEnumerable ConvertToExcelCustomPropertyInfo(PropertyInfo[] props, Configuration configuration) { // solve : https://github.com/shps951023/MiniExcel/issues/138 @@ -214,7 +214,6 @@ return ConvertToExcelCustomPropertyInfo(type.GetProperties(bindingFlags), configuration); } - internal static ExcellSheetInfo GetExcellSheetInfo(Type type, Configuration configuration) { // default options @@ -247,5 +246,65 @@ return sheetInfo; } + + internal static List GetDictionaryColumnInfo(IDictionary dicString, IDictionary dic, Configuration configuration) + { + List props; + var _props = new List(); + if (dicString != null) + { + foreach (var key in dicString.Keys) + { + SetDictionaryColumnInfo(_props, key, configuration); + } + } + else if (dic != null) + { + foreach (var key in dic.Keys) + { + SetDictionaryColumnInfo(_props, key, configuration); + } + } + else + { + throw new NotSupportedException("SetDictionaryColumnInfo Error"); + } + + props = SortCustomProps(_props); + return props; + } + + internal static void SetDictionaryColumnInfo(List _props, object key, Configuration configuration) + { + var p = new ExcelColumnInfo(); + p.ExcelColumnName = key?.ToString(); + p.Key = key; + // TODO:Dictionary value type is not fiexed + //var _t = + //var gt = Nullable.GetUnderlyingType(p.PropertyType); + var isIgnore = false; + if (configuration.DynamicColumns != null && configuration.DynamicColumns.Length > 0) + { + var dynamicColumn = configuration.DynamicColumns.SingleOrDefault(_ => _.Key == key.ToString()); + if (dynamicColumn != null) + { + p.Nullable = true; + //p.ExcludeNullableType = item2[key]?.GetType(); + if (dynamicColumn.Format != null) + p.ExcelFormat = dynamicColumn.Format; + if (dynamicColumn.Aliases != null) + p.ExcelColumnAliases = dynamicColumn.Aliases; + if (dynamicColumn.IndexName != null) + p.ExcelIndexName = dynamicColumn.IndexName; + p.ExcelColumnIndex = dynamicColumn.Index; + if (dynamicColumn.Name != null) + p.ExcelColumnName = dynamicColumn.Name; + isIgnore = dynamicColumn.Ignore; + p.ExcelColumnWidth = dynamicColumn.Width; + } + } + if (!isIgnore) + _props.Add(p); + } } -} +} \ No newline at end of file