using Elwig.Models.Dtos; using System; using System.Collections.Generic; using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Threading.Tasks; namespace Elwig.Helpers.Export { public class OdsFile : IDisposable { protected readonly string FileName; protected readonly ZipArchive ZipArchive; protected StreamWriter? Content; private readonly List _tables; public OdsFile(string filename) { FileName = filename; File.Delete(filename); ZipArchive = ZipFile.Open(FileName, ZipArchiveMode.Create); _tables = []; } public void Dispose() { AddTrailer().GetAwaiter().GetResult(); ZipArchive?.Dispose(); GC.SuppressFinalize(this); } private async Task AddHeader() { var mimetype = ZipArchive.CreateEntry("mimetype", CompressionLevel.NoCompression); using (var writer = new StreamWriter(mimetype.Open(), Utils.UTF8)) { await writer.WriteAsync("application/vnd.oasis.opendocument.spreadsheet"); } var manifest = ZipArchive.CreateEntry("META-INF/manifest.xml"); using (var writer = new StreamWriter(manifest.Open(), Utils.UTF8)) { await writer.WriteAsync(""" """); } var styles = ZipArchive.CreateEntry("styles.xml"); using (var writer = new StreamWriter(styles.Open(), Utils.UTF8)) { await writer.WriteAsync(""" """); } var meta = ZipArchive.CreateEntry("meta.xml"); using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) { var now = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); await writer.WriteAsync($""" Elwig {App.Version} Elwig Elwig {now} {now} """); } var content = ZipArchive.CreateEntry("content.xml"); Content = new StreamWriter(content.Open(), Utils.UTF8); await Content.WriteAsync(""" """); for (int i = 1; i <= 100; i++) { await Content.WriteAsync($" \r\n"); } await Content.WriteAsync(""" .. :: .. :: """); } private async Task AddTrailer() { if (Content == null) await AddHeader(); if (Content == null) return; await Content.WriteAsync(""" """); Content.Close(); Content.Dispose(); Content = null; var settings = ZipArchive.CreateEntry("settings.xml"); using (var writer = new StreamWriter(settings.Open(), Utils.UTF8)) { await writer.WriteAsync(""" """); foreach (var tbl in _tables) { await writer.WriteAsync($""" 2 5 5 """); } await writer.WriteAsync(""" """); } } public async Task AddTable(DataTable table, bool mergeSubRowCells = true) { if (Content == null) await AddHeader(); if (Content == null) return; var totalSpan = table.ColumnSpans.Sum(); _tables.Add(table.Name); await Content.WriteAsync($" \r\n"); foreach (var (s, w) in table.ColumnSpans.Zip(table.ColumnWidths)) { for (int i = 0; i < s; i++) { await Content.WriteAsync(" \r\n"); } } await Content.WriteAsync( $" \r\n" + FormatCell(table.FullName, colSpan: totalSpan, style: "header") + $" \r\n" + $" \r\n" + FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader") + $" \r\n" + $" \r\n" + $" \r\n" + $" \r\n" + $" \r\n"); foreach (var (name, span, units) in table.ColumnNames.Zip(table.ColumnSpans, table.ColumnUnits)) { var hasUnits = units.Length > 0; await Content.WriteAsync(FormatCell(name, colSpan: span, rowSpan: hasUnits ? 1 : 2, style: "th")); } await Content.WriteAsync(" \r\n \r\n"); foreach (var (span, units) in table.ColumnSpans.Zip(table.ColumnUnits)) { if (units.Length == 0) { await Content.WriteAsync($" \r\n"); continue; } foreach (var u in units) { await Content.WriteAsync(FormatCell(u == null ? null : $"[{u}]", style: "th")); } } await Content.WriteAsync(" \r\n"); foreach (var row in table.GetData()) { await FormatRow(row, table.ColumnUnits, mergeSubRowCells); } await Content.WriteAsync(" \r\n"); } protected async Task FormatRow(IEnumerable row, IEnumerable colUnits, bool mergeSubRowCells) { if (Content == null) throw new InvalidOperationException(); var arrays = row.Where(c => c is Array).Cast().Select(c => c.Length).ToArray(); int rowNum = Math.Max(1, arrays.Length > 0 ? arrays.Max() : 0); for (int i = 0; i < rowNum; i++) { await Content.WriteAsync(" \r\n"); foreach (var (data, units) in row.Zip(colUnits)) { if (data is Array a) { await Content.WriteAsync(i < a.Length ? FormatCell(a.GetValue(i), units: units) : $" \r\n"); } else if (!mergeSubRowCells) { await Content.WriteAsync(FormatCell(data, units: units)); } else { await Content.WriteAsync(FormatCell(data, rowSpan: i == 0 ? rowNum : 1, isCovered: i > 0, units: units)); } } await Content.WriteAsync(" \r\n"); } } private static int GetSubCols(Type? type) { if (type != null && type.IsValueType == true && type.Name.StartsWith("ValueTuple")) return type.GetFields().Length; return 1; } protected static string FormatCell(object? data, int rowSpan = 1, int colSpan = 1, string? style = "default", bool isCovered = false, string?[]? units = null) { if (data?.GetType().IsValueType == true && data.GetType().Name.StartsWith("ValueTuple")) return string.Join("", data.GetType().GetFields().Zip(units ?? []) .Select(p => FormatCell(p.First.GetValue(data), rowSpan, colSpan, style, isCovered, [p.Second])) ); var add = (style != null ? $" table:style-name=\"{style}\"" : "") + (rowSpan > 1 || colSpan > 1 ? $" table:number-rows-spanned=\"{rowSpan}\" table:number-columns-spanned=\"{colSpan}\"" : ""); string ct = isCovered ? "table:covered-table-cell" : "table:table-cell"; var isPercent = units != null && units.Length > 0 && units[0] == "%"; string c; if (data == null) { c = $"<{ct}{add}/>"; } else if (data is bool b) { add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"boolean\"" : p)); c = $"<{ct} office:value-type=\"boolean\" calcext:value-type=\"boolean\" office:boolean-value=\"{(b ? "true" : "false")}\"{add}>{(b ? "Ja" : "Nein")}"; } else if (data is DateOnly date) { add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"date\"" : p)); c = $"<{ct} office:value-type=\"date\" calcext:value-type=\"date\" office:date-value=\"{date:yyyy-MM-dd}\"{add}>{date:dd.MM.yyyy}"; } else if (data is TimeOnly time) { add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"time\"" : p)); c = $"<{ct} office:value-type=\"time\" calcext:value-type=\"time\" office:time-value=\"{time:\\P\\THH\\Hmm\\Mss\\S}\"{add}>{time:HH:mm:ss}"; } else if (data is DateTime dt) { add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"datetime\"" : p)); c = $"<{ct} office:value-type=\"date\" calcext:value-type=\"date\" office:date-value=\"{dt:yyyy-MM-dd\\THH:mm:ss}\"{add}>{dt:dd.MM.yyyy HH:mm:ss}"; } else if (data is decimal || data is float || data is double || data is byte || data is char || data is short || data is ushort || data is int || data is uint || data is long || data is ulong) { double v = double.Parse(data?.ToString() ?? "0"); // use default culture for ToString and Parse()! if (units != null && units.Length > 0) { int n = -1; switch (units[0]) { case "%": n = 1; data = $"{v:N1}"; break; case "€": n = 2; data = $"{v:N2}"; break; case "°KMW": n = 1; data = $"{v:N1}"; break; case "°Oe": n = 0; data = $"{v:N0}"; break; case "m²": n = 0; data = $"{v:N0}"; break; } if (n >= 0) add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"N{n}\"" : p)); } c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString(CultureInfo.InvariantCulture)}\"{add}>{data}"; } else { c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}>{data}"; } return $" {c}\r\n" + (colSpan > 1 ? $" \r\n" : ""); } } }