using XmlReaderHelper = MiniExcelLib.OpenXml.Utils.XmlReaderHelper; namespace MiniExcelLib.OpenXml.Styles; internal partial class OpenXmlStyles { private static readonly string[] Ns = [Schemas.SpreadsheetmlXmlMain, Schemas.SpreadsheetmlXmlStrictNs]; private readonly Dictionary _cellXfs = new(); private readonly Dictionary _cellStyleXfs = new(); private readonly Dictionary _customFormats = new(); private readonly List _fontColors = []; private OpenXmlStyles() { } [CreateSyncVersion] internal static async Task CreateAsync(OpenXmlZip zip, CancellationToken cancellationToken = default) { if (zip.GetEntry(ExcelFileNames.Styles) is not { } entry) throw new InvalidDataException("The OpenXml styles.xml file could not be found, the document might be malformed."); var entryStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableEntryStream = entryStream.ConfigureAwait(false); using var reader = XmlReader.Create(entryStream, XmlReaderHelper.GetXmlReaderSettings()); var openXmlStyles = new OpenXmlStyles(); if (!reader.IsStartElement("styleSheet", Ns) || !await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) { return openXmlStyles; } while (!reader.EOF) { if (reader.IsStartElement("cellXfs", Ns)) { if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; var index = 0; while (!reader.EOF) { if (reader.IsStartElement("xf", Ns)) { int.TryParse(reader.GetAttribute("xfId"), out var xfId); int.TryParse(reader.GetAttribute("numFmtId"), out var numFmtId); int.TryParse(reader.GetAttribute("fontId"), out var fontId); openXmlStyles._cellXfs.Add(index, new StyleRecord { XfId = xfId, NumFmtId = numFmtId, FontId = fontId }); await reader.SkipAsync().ConfigureAwait(false); index++; } else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) break; } } else if (reader.IsStartElement("cellStyleXfs", Ns)) { if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; var index = 0; while (!reader.EOF) { if (reader.IsStartElement("xf", Ns)) { int.TryParse(reader.GetAttribute("xfId"), out var xfId); int.TryParse(reader.GetAttribute("numFmtId"), out var numFmtId); openXmlStyles._cellStyleXfs.Add(index, new StyleRecord() { XfId = xfId, NumFmtId = numFmtId }); await reader.SkipAsync().ConfigureAwait(false); index++; } else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } else if (reader.IsStartElement("fonts", Ns)) { if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; while (!reader.EOF) { if (reader.IsStartElement("font", Ns)) { openXmlStyles._fontColors.Add(await ReadFontColorAsync(reader, cancellationToken).ConfigureAwait(false)); } else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } else if (reader.IsStartElement("numFmts", Ns)) { if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; while (!reader.EOF) { if (reader.IsStartElement("numFmt", Ns)) { _ = int.TryParse(reader.GetAttribute("numFmtId"), out var numFmtId); var formatCode = reader.GetAttribute("formatCode"); //TODO: determine the type according to the format var type = typeof(string); if (DateTimeHelper.IsDateTimeFormat(formatCode)) { type = typeof(DateTime?); } openXmlStyles._customFormats.TryAdd(numFmtId, new NumberFormatString(formatCode, type)); await reader.SkipAsync().ConfigureAwait(false); } else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } return openXmlStyles; } [CreateSyncVersion] private static async Task ReadFontColorAsync(XmlReader reader, CancellationToken cancellationToken) { ExcelColor? color = null; if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) return color; while (!reader.EOF) { if (reader.IsStartElement("color", Ns)) { color = new ExcelColor( auto: reader.GetAttribute("auto") is "1" or "true", rgb: reader.GetAttribute("rgb"), theme: int.TryParse(reader.GetAttribute("theme"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var theme) ? theme : null, indexed: int.TryParse(reader.GetAttribute("indexed"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var indexed) ? indexed : null, tint: double.TryParse(reader.GetAttribute("tint"), NumberStyles.Float, CultureInfo.InvariantCulture, out var tint) ? tint : 0); await reader.SkipAsync().ConfigureAwait(false); } else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } return color; } internal ExcelColor? GetFontColor(int index) { if (!_cellXfs.TryGetValue(index, out var styleRecord)) return null; var fontId = styleRecord.FontId; return fontId >= 0 && fontId < _fontColors.Count ? _fontColors[fontId] : null; } internal NumberFormatString? GetStyleFormat(int index) { if (!_cellXfs.TryGetValue(index, out var styleRecord)) return null; if (Formats.TryGetValue(styleRecord.NumFmtId, out var numberFormat)) return numberFormat; return _customFormats.GetValueOrDefault(styleRecord.NumFmtId); } public object? ConvertValueByStyleFormat(int index, object? value) { if (value is not double number) return value; var sf = GetStyleFormat(index); if (sf?.Type is null) return value; if (sf.Type == typeof(DateTime?)) return DateTimeHelper.IsValidOADateTime(number) ? DateTime.FromOADate(number) : value; if (sf.Type == typeof(TimeSpan?)) return TimeSpan.FromDays(number); return value; } private static Dictionary Formats { get; } = new() { { 0, new NumberFormatString("General", typeof(string)) }, { 1, new NumberFormatString("0", typeof(decimal?)) }, { 2, new NumberFormatString("0.00", typeof(decimal?)) }, { 3, new NumberFormatString("#,##0", typeof(decimal?)) }, { 4, new NumberFormatString("#,##0.00", typeof(decimal?)) }, { 5, new NumberFormatString("\"$\"#,##0_);(\"$\"#,##0)", typeof(decimal?)) }, { 6, new NumberFormatString("\"$\"#,##0_);[Red](\"$\"#,##0)", typeof(decimal?)) }, { 7, new NumberFormatString("\"$\"#,##0.00_);(\"$\"#,##0.00)", typeof(decimal?)) }, { 8, new NumberFormatString("\"$\"#,##0.00_);[Red](\"$\"#,##0.00)", typeof(string)) }, { 9, new NumberFormatString("0%", typeof(decimal?)) }, { 10, new NumberFormatString("0.00%", typeof(string)) }, { 11, new NumberFormatString("0.00E+00", typeof(string)) }, { 12, new NumberFormatString("# ?/?", typeof(string)) }, { 13, new NumberFormatString("# ??/??", typeof(string)) }, { 14, new NumberFormatString("d/m/yyyy", typeof(DateTime?)) }, { 15, new NumberFormatString("d-mmm-yy", typeof(DateTime?)) }, { 16, new NumberFormatString("d-mmm", typeof(DateTime?)) }, { 17, new NumberFormatString("mmm-yy", typeof(DateTime?)) }, { 18, new NumberFormatString("h:mm AM/PM", typeof(TimeSpan?)) }, { 19, new NumberFormatString("h:mm:ss AM/PM", typeof(TimeSpan?)) }, { 20, new NumberFormatString("h:mm", typeof(TimeSpan?)) }, { 21, new NumberFormatString("h:mm:ss", typeof(TimeSpan?)) }, { 22, new NumberFormatString("m/d/yy h:mm", typeof(DateTime?)) }, // 23..36 international/unused { 37, new NumberFormatString("#,##0_);(#,##0)", typeof(string)) }, { 38, new NumberFormatString("#,##0_);[Red](#,##0)", typeof(string)) }, { 39, new NumberFormatString("#,##0.00_);(#,##0.00)", typeof(string)) }, { 40, new NumberFormatString("#,##0.00_);[Red](#,##0.00)", typeof(string)) }, { 41, new NumberFormatString("_(\"$\"* #,##0_);_(\"$\"* (#,##0);_(\"$\"* \"-\"_);_(@_)", typeof(string)) }, { 42, new NumberFormatString("_(* #,##0_);_(* (#,##0);_(* \"-\"_);_(@_)", typeof(string)) }, { 43, new NumberFormatString("_(\"$\"* #,##0.00_);_(\"$\"* (#,##0.00);_(\"$\"* \"-\"??_);_(@_)", typeof(string)) }, { 44, new NumberFormatString("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)", typeof(string)) }, { 45, new NumberFormatString("mm:ss", typeof(TimeSpan?)) }, { 46, new NumberFormatString("[h]:mm:ss", typeof(TimeSpan?)) }, { 47, new NumberFormatString("mm:ss.0", typeof(TimeSpan?)) }, { 48, new NumberFormatString("##0.0E+0", typeof(string)) }, { 49, new NumberFormatString("@", typeof(string)) }, // issue 222 { 58, new NumberFormatString("m/d",typeof(DateTime?)) }, // custom format start with 176 }; } internal class NumberFormatString(string? formatCode, Type type, bool needConvertToString = false) { public string? FormatCode { get; } = formatCode; public Type Type { get; set; } = type; public bool NeedConvertToString { get; } = needConvertToString; } internal class StyleRecord { public int XfId { get; set; } public int NumFmtId { get; set; } public int FontId { get; set; } }