using QWERTYkez.ExcelProcessor.Editors; using System.Runtime.InteropServices; namespace QWERTYkez.ExcelProcessor; /// /// Предоставляет потокобезопасный процессор для чтения и записи документов Excel (xlsx / xlsm) формата. /// Наследует от и добавляет операции изменения документа. /// internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter { // Работа с общей таблицей строк private SharedStringTablePart? _sharedStringPart; private SharedStringTable? _sharedStringTable; internal static Dictionary? _calibrationTable; // cw -> width_pts internal bool TryGetCalibrateCoeff(double targetPoints, out double result) { result = 0; try { // Используем Interop для точной калибровки _calibrationTable ??= CalibrateWidthCoeffUsingInterop(); } catch { } _calibrationTable ??= []; if (_calibrationTable.Count < 2) return false; // Получаем отсортированные по ключу пары var keys = _calibrationTable.Keys.OrderBy(k => k).ToList(); int lastIndex = keys.Count - 1; if (targetPoints <= _calibrationTable[keys[0]]) { result = keys[0]; return true; } if (targetPoints >= _calibrationTable[keys[lastIndex]]) { result = keys[lastIndex]; return true; } // Бинарный поиск интервала int left = 0, right = keys.Count - 1; while (right - left > 1) { int mid = (left + right) / 2; if (_calibrationTable[keys[mid]] < targetPoints) left = mid; else right = mid; } int cwLow = keys[left]; int cwHigh = keys[right]; double widthLow = _calibrationTable[cwLow]; double widthHigh = _calibrationTable[cwHigh]; // Линейная интерполяция result = cwLow + (targetPoints - widthLow) * (cwHigh - cwLow) / (widthHigh - widthLow); return true; } private Dictionary CalibrateWidthCoeffUsingInterop() { object? excelApp = null; object? workbooks = null; object? workbook = null; object? sheets = null; object? worksheet = null; object? columns = null; object? columnA = null; try { Type? excelType = Type.GetTypeFromProgID("Excel.Application") ?? throw new InvalidOperationException("Excel не установлен"); excelApp = Activator.CreateInstance(excelType); dynamic dynamicApp = excelApp; dynamicApp.DisplayAlerts = false; dynamicApp.ScreenUpdating = false; workbooks = dynamicApp.Workbooks; workbook = ((dynamic)workbooks).Add(); // Избегаем скрытых ссылок: сначала получаем коллекцию Sheets, потом элемент sheets = ((dynamic)workbook).Sheets; worksheet = ((dynamic)sheets)[1]; columns = ((dynamic)worksheet).Columns; columnA = ((dynamic)columns)[1]; var table = new Dictionary(); dynamic dynamicColumnA = columnA; for (int cw = 1; cw <= 100; cw++) { dynamicColumnA.ColumnWidth = cw; double widthPt = (double)dynamicColumnA.Width; double widthCm = (double)dynamicApp.PointsToCentimeters(widthPt); table.Add(cw, widthCm); } return table; } finally { // Освобождаем строго в обратном порядке создания if (columnA != null) Marshal.ReleaseComObject(columnA); if (columns != null) Marshal.ReleaseComObject(columns); if (worksheet != null) Marshal.ReleaseComObject(worksheet); if (sheets != null) Marshal.ReleaseComObject(sheets); if (workbook != null) { try { ((dynamic)workbook).Close(false); } catch { } Marshal.ReleaseComObject(workbook); } if (workbooks != null) Marshal.ReleaseComObject(workbooks); if (excelApp != null) { try { ((dynamic)excelApp).Quit(); } catch { } Marshal.ReleaseComObject(excelApp); } // Принудительный запуск сборщика мусора для очистки dynamic-оберток GC.Collect(); GC.WaitForPendingFinalizers(); } } private void EnsureSharedStringTable() { if (_sharedStringPart != null) return; _sharedStringPart = _doc.WorkbookPart?.GetPartsOfType().FirstOrDefault(); if (_sharedStringPart == null) { _sharedStringPart = _doc.WorkbookPart?.AddNewPart(); _sharedStringPart!.SharedStringTable = new SharedStringTable(); } _sharedStringTable = _sharedStringPart.SharedStringTable; } internal string GetSharedString(uint index) { EnsureSharedStringTable(); if (_sharedStringTable?.Count?.Value is not { } val || index >= val) return string.Empty; var si = _sharedStringTable.ElementAt((int)index); // Обычный текст var text = si.GetFirstChild()?.Text; if (text != null) return text; // Rich text var sb = new StringBuilder(); foreach (var run in si.Elements()) { sb.Append(run.GetFirstChild()?.Text); } return sb.ToString(); } internal int GetOrAddSharedString(string value) { EnsureSharedStringTable(); if (_sharedStringTable == null) return -1; // Поиск существующей строки int idx = 0; foreach (var si in _sharedStringTable.Elements()) { var text = si.GetFirstChild()?.Text; if (text == value) return idx; idx++; } // Добавление новой var newSi = new SharedStringItem(); newSi.Append(new Text(value)); _sharedStringTable.Append(newSi); _sharedStringTable.Count = (uint)_sharedStringTable.Elements().Count(); return idx; } // Определение заблокирована ли ячейка internal bool IsCellLocked(uint styleIndex) { var cellFormat = GetCellFormatAt(styleIndex); if (cellFormat == null) return false; // По умолчанию ячейка заблокирована, если Protection не задан или Locked = true return cellFormat.Protection == null || cellFormat.Protection.Locked == null || cellFormat.Protection.Locked.Value; } // Кэши числовых форматов private readonly Dictionary _numberFormatCache = []; private readonly Dictionary _numberFormatIdToPattern = []; // Кэши для компонентов стилей (чтобы не создавать дубликаты) private readonly Dictionary _fontCache = []; private readonly Dictionary _fillCache = []; private readonly Dictionary _borderCache = []; private readonly Dictionary _alignmentCache = []; // Кэш составных стилей (CellFormat) private readonly Dictionary<(int fontId, int fillId, int borderId, int alignId, int numFmtId), int> _cellFormatCache = []; // Конструктор, фабричные методы – без изменений (опущены) #region Управление числовыми форматами (расширение IBook) public IReadOnlyList GetNumberFormats() { // Возвращаем все пользовательские форматы (Id >= 164), встроенные не включаем var result = new List(); var stylesheet = _doc.WorkbookPart?.WorkbookStylesPart?.Stylesheet; if (stylesheet?.NumberingFormats != null) { foreach (var nf in stylesheet.NumberingFormats.Elements()) { if (nf.NumberFormatId?.Value >= 164 && nf.FormatCode?.Value is string code) { var pattern = new NumberFormatPattern(code); pattern.Attach((ushort)nf.NumberFormatId.Value); result.Add(pattern); } } } return result; } public NumberFormatPattern CreateNumberFormat(string formatCode) { if (string.IsNullOrEmpty(formatCode)) throw new ArgumentException("Format code cannot be empty", nameof(formatCode)); lock (_syncLock) { return CreateNumberFormatInternal(formatCode); } } // Внутренний метод получения NumberFormatPattern по индексу стиля ячейки internal NumberFormatPattern? GetNumberFormat(uint cellStyleIndex) { var cellFormat = GetCellFormatAt(cellStyleIndex); if (cellFormat?.NumberFormatId?.Value is uint numFmtId && cellFormat.ApplyNumberFormat?.Value == true) { if (_numberFormatIdToPattern.TryGetValue(numFmtId, out var pattern)) return pattern; // Пытаемся найти в стилях книги var stylesheet = _doc.WorkbookPart?.WorkbookStylesPart?.Stylesheet; if (stylesheet?.NumberingFormats != null) { foreach (var nf in stylesheet.NumberingFormats.Elements()) { if (nf.NumberFormatId?.Value == numFmtId && nf.FormatCode?.Value is string code) { pattern = new NumberFormatPattern(code); pattern.Attach((ushort)numFmtId); _numberFormatIdToPattern[numFmtId] = pattern; return pattern; } } } // Встроенный формат if (numFmtId < 164) { // Для встроенных форматов код формата определяем по специальной таблице (не хардкодим!). // Вместо этого возвращаем формат только с Id, без FormatCode. pattern = new NumberFormatPattern(string.Empty); pattern.Attach((ushort)numFmtId); _numberFormatIdToPattern[numFmtId] = pattern; return pattern; } } return null; } #endregion #region Управление составными стилями (CellFormat) // Получить индекс CellFormat для комбинации компонентов internal int GetOrCreateCellFormatId( NumberFormatPattern? numberFormat = null, CellFont? font = null, CellFill? fill = null, CellBorder? border = null, CellAlign? align = null) { lock (_syncLock) { // Получаем или создаём числовой формат ID int numFmtId = -1; if (numberFormat != null) { if (numberFormat.Id.HasValue && numberFormat.Id.Value < 164) numFmtId = numberFormat.Id.Value; else numFmtId = (int)GetOrCreateNumberFormatId(numberFormat); } // Получаем или создаём Font, Fill, Border, Alignment int fontId = font.HasValue ? GetOrCreateFontId(font.Value) : -1; int fillId = fill.HasValue ? GetOrCreateFillId(fill.Value) : -1; int borderId = border.HasValue ? GetOrCreateBorderId(border.Value) : -1; int alignId = align.HasValue ? GetOrCreateAlignmentId(align.Value) : -1; var key = (fontId, fillId, borderId, alignId, numFmtId); if (_cellFormatCache.TryGetValue(key, out int existingIndex)) return existingIndex; var stylesheet = EnsureStylesheet(); var cellFormats = stylesheet.CellFormats ?? CreateCellFormats(stylesheet); var cellFormat = new CellFormat(); if (fontId >= 0) { cellFormat.FontId = (uint)fontId; cellFormat.ApplyFont = true; } if (fillId >= 0) { cellFormat.FillId = (uint)fillId; cellFormat.ApplyFill = true; } if (borderId >= 0) { cellFormat.BorderId = (uint)borderId; cellFormat.ApplyBorder = true; } if (alignId >= 0) { cellFormat.Alignment = GetAlignmentFromCache(alignId); cellFormat.ApplyAlignment = true; } if (numFmtId >= 0) { cellFormat.NumberFormatId = (uint)numFmtId; cellFormat.ApplyNumberFormat = true; } cellFormats.Append(cellFormat); int newIndex = (int)(cellFormats.Count?.Value ?? 0); cellFormats.Count = (uint)(newIndex + 1); _cellFormatCache[key] = newIndex; return newIndex; } } #endregion #region Вспомогательные методы для работы со стилями private Stylesheet EnsureStylesheet() { var workbookPart = _doc.WorkbookPart ?? throw new InvalidOperationException("No WorkbookPart"); var stylesPart = workbookPart.WorkbookStylesPart; if (stylesPart == null) { stylesPart = workbookPart.AddNewPart(); stylesPart.Stylesheet = new Stylesheet(); } var stylesheet = stylesPart.Stylesheet!; if (stylesheet.Fonts == null) { stylesheet.Fonts = new Fonts(); stylesheet.Fonts.Append(new Font()); // минимум один шрифт stylesheet.Fonts.Count = 1; } if (stylesheet.Fills == null) { stylesheet.Fills = new Fills(); stylesheet.Fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } }); stylesheet.Fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Gray125 } }); stylesheet.Fills.Count = 2; } if (stylesheet.Borders == null) { stylesheet.Borders = new Borders(); stylesheet.Borders.Append(new Border()); // пустая граница stylesheet.Borders.Count = 1; } if (stylesheet.CellFormats == null) { stylesheet.CellFormats = new CellFormats(); stylesheet.CellFormats.Append(new CellFormat()); // стандартный стиль stylesheet.CellFormats.Count = 1; } return stylesheet; } private uint GetOrCreateNumberFormatId(NumberFormatPattern pattern) { if (pattern.Id.HasValue) return (uint)pattern.Id.Value; // Создаём через внутренний метод (без дополнительного lock) var created = CreateNumberFormatInternal(pattern.Format); return (uint)created.Id!.Value; } private int GetOrCreateFontId(CellFont font) { if (_fontCache.TryGetValue(font, out int id)) return id; var stylesheet = EnsureStylesheet(); var fonts = stylesheet.Fonts!; var excelFont = font.ToFont() ?? new Font(); fonts.Append(excelFont); uint newId = fonts.Count!.Value; fonts.Count = newId + 1u; return _fontCache[font] = (int)newId; } private int GetOrCreateFillId(CellFill fill) { if (_fillCache.TryGetValue(fill, out int id)) return id; var stylesheet = EnsureStylesheet(); var fills = stylesheet.Fills!; var excelFill = fill.ToFill() ?? new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } }; fills.Append(excelFill); uint newId = fills.Count!.Value; fills.Count = newId + 1; return _fillCache[fill] = (int)newId; } private int GetOrCreateBorderId(CellBorder border) { if (_borderCache.TryGetValue(border, out int id)) return id; var stylesheet = EnsureStylesheet(); var borders = stylesheet.Borders!; var excelBorder = border.ToBorder() ?? new Border(); borders.Append(excelBorder); int newId = (int)borders.Count!.Value; borders.Count = (uint)(newId + 1); _borderCache[border] = newId; return newId; } private int GetOrCreateAlignmentId(CellAlign align) { if (_alignmentCache.TryGetValue(align, out int id)) return id; // Выравнивание не хранится отдельно, а встраивается в CellFormat. // Поэтому мы не создаём отдельный элемент, а возвращаем уникальный ID для кэша стилей. // Но для целостности кэша сохраняем ID. id = _alignmentCache.Count; _alignmentCache[align] = id; return id; } private Alignment GetAlignmentFromCache(int alignId) { foreach (var pair in _alignmentCache) if (pair.Value == alignId) return pair.Key.ToAlignment() ?? new Alignment(); return new Alignment(); } private CellFormat? GetCellFormatAt(uint index) { var stylesheet = EnsureStylesheet(); if (stylesheet.CellFormats == null || index >= stylesheet.CellFormats.Count!.Value) return null; return (CellFormat)stylesheet.CellFormats.ElementAt((int)index); } // Вспомогательные создания коллекций private static NumberingFormats CreateNumberingFormats(Stylesheet stylesheet) { var nfs = new NumberingFormats(); stylesheet.NumberingFormats = nfs; return nfs; } private static CellFormats CreateCellFormats(Stylesheet stylesheet) { var cfs = new CellFormats(); stylesheet.CellFormats = cfs; return cfs; } #endregion private bool _isModified = false; internal ExcelWriter() { } #region Factory Methods internal static ExcelWriter? CreateFromData(ReadOnlyMemory data, string destinationPath) { if (data.IsEmpty || string.IsNullOrEmpty(destinationPath)) return null; var ms = new MemoryStream(); try { // Копируем данные в MemoryStream ms.Write(data.ToArray(), 0, data.Length); ms.Position = 0; var doc = SpreadsheetDocument.Open(ms, true, new OpenSettings { AutoSave = false }); if (doc is not null) { return new ExcelWriter { _ms = ms, _doc = doc, FilePath = destinationPath, _originalSourcePath = null // нет исходного файла }; } doc?.Dispose(); } catch { } ms?.Dispose(); return null; } internal static new ExcelWriter? CreateFromData(ReadOnlyMemory data) { if (data.IsEmpty) return null; var ms = new MemoryStream(); try { ms.Write(data.ToArray(), 0, data.Length); ms.Position = 0; var doc = SpreadsheetDocument.Open(ms, true, new OpenSettings { AutoSave = false }); if (doc is not null) { return new ExcelWriter { _ms = ms, _doc = doc, FilePath = null // из памяти – нет файла }; } doc?.Dispose(); } catch { } ms?.Dispose(); return null; } internal static ExcelWriter? CreateInternal(FileInfo sourceFile, string? destinationPath = null!) { if (sourceFile is null || !sourceFile.Exists) return null; var ms = new MemoryStream(); try { using (var file = new FileStream(sourceFile.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) { file.CopyTo(ms); } ms.Position = 0; var doc = SpreadsheetDocument.Open(ms, isEditable: true, new OpenSettings { AutoSave = false }); if (doc is not null) { return new ExcelWriter { _ms = ms, _doc = doc, _originalSourcePath = sourceFile.FullName, FilePath = destinationPath }; } doc?.Dispose(); } catch { } ms?.Dispose(); return null; } /// /// Создаёт новый пустой документ Excel с одним листом. /// /// Путь, по которому будет сохранён документ (необязательный). /// Экземпляр для редактирования нового документа. internal static ExcelWriter CreateNew(string? destinationPath = null) { var ms = new MemoryStream(); try { var doc = SpreadsheetDocument.Create(ms, SpreadsheetDocumentType.Workbook); var workbookPart = doc.AddWorkbookPart(); workbookPart.Workbook = new Workbook(); var worksheetPart = workbookPart.AddNewPart(); worksheetPart.Worksheet = new Worksheet(new SheetData()); var sheets = workbookPart.Workbook.AppendChild(new Sheets()); //sheets.AppendChild(new Sheet() //{ // Id = workbookPart.GetIdOfPart(worksheetPart), // SheetId = 1, // Name = "Лист1" //}); return new ExcelWriter { _ms = ms, _doc = doc, FilePath = destinationPath, _originalSourcePath = null }; } catch { ms.Dispose(); throw; } } #endregion #region Properties /// /// Получает значение, указывающее, будет ли документ сохранен с перезаписью исходного файла. /// public bool WillOverwriteSource => FilePath == _originalSourcePath; #endregion #region Replace // string void IExcelWriter.Replace(string oldValue, string newValue, StringComparison comparisonType) { ThrowIfDisposed(); if (string.IsNullOrEmpty(oldValue)) throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue)); if (string.IsNullOrEmpty(newValue)) return; lock (_syncLock) { _doc.Replace(oldValue, newValue, comparisonType); _isModified = true; } } void IExcelWriter.Replace(IDictionary replacements, StringComparison comparisonType) => ((IExcelWriter)this).Replace((IEnumerable>)replacements, comparisonType); void IExcelWriter.Replace(IEnumerable> replacements, StringComparison comparisonType) { ThrowIfDisposed(); if (replacements is null) return; lock (_syncLock) { _doc.Replace(replacements, comparisonType); _isModified = true; } } // double void IExcelWriter.Replace(string oldValue, double newValue, string? format, StringComparison comparisonType) { ThrowIfDisposed(); if (string.IsNullOrEmpty(oldValue)) throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue)); lock (_syncLock) { _doc.Replace(oldValue, newValue, format); _isModified = true; } } void IExcelWriter.Replace(IDictionary replacements, string? format, StringComparison comparisonType) => ((IExcelWriter)this).Replace((IEnumerable>)replacements, format, comparisonType); void IExcelWriter.Replace(IEnumerable> replacements, string? format, StringComparison comparisonType) { ThrowIfDisposed(); if (replacements == null) return; lock (_syncLock) { _doc.Replace(replacements, format, comparisonType); _isModified = true; } } // float void IExcelWriter.Replace(string oldValue, float newValue, string? format, StringComparison comparisonType) { ThrowIfDisposed(); if (string.IsNullOrEmpty(oldValue)) throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue)); lock (_syncLock) { _doc.Replace(oldValue, newValue, format); _isModified = true; } } void IExcelWriter.Replace(IDictionary replacements, string? format, StringComparison comparisonType) => ((IExcelWriter)this).Replace((IEnumerable>)replacements, format, comparisonType); void IExcelWriter.Replace(IEnumerable> replacements, string? format, StringComparison comparisonType) { ThrowIfDisposed(); if (replacements == null) return; lock (_syncLock) { _doc.Replace(replacements, format, comparisonType); _isModified = true; } } // int void IExcelWriter.Replace(string oldValue, int newValue, StringComparison comparisonType) { ThrowIfDisposed(); if (string.IsNullOrEmpty(oldValue)) throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue)); lock (_syncLock) { _doc.Replace(oldValue, newValue); _isModified = true; } } void IExcelWriter.Replace(IDictionary replacements, StringComparison comparisonType) => ((IExcelWriter)this).Replace((IEnumerable>)replacements, comparisonType); void IExcelWriter.Replace(IEnumerable> replacements, StringComparison comparisonType) { ThrowIfDisposed(); if (replacements == null) return; lock (_syncLock) { _doc.Replace(replacements, comparisonType); _isModified = true; } } // long void IExcelWriter.Replace(string oldValue, long newValue, StringComparison comparisonType) { ThrowIfDisposed(); if (string.IsNullOrEmpty(oldValue)) throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue)); lock (_syncLock) { _doc.Replace(oldValue, newValue); _isModified = true; } } void IExcelWriter.Replace(IDictionary replacements, StringComparison comparisonType) => ((IExcelWriter)this).Replace((IEnumerable>)replacements, comparisonType); void IExcelWriter.Replace(IEnumerable> replacements, StringComparison comparisonType) { ThrowIfDisposed(); if (replacements == null) return; lock (_syncLock) { _doc.Replace(replacements, comparisonType); _isModified = true; } } #endregion #region Save Operations /// /// Сохраняет документ в файл, указанный при создании процессора. /// public void Save() { ThrowIfDisposed(); if (string.IsNullOrEmpty(FilePath)) throw new InvalidOperationException("Cannot save - no file path specified"); SaveTo(FilePath!); } /// /// Сохраняет документ в указанный файл. /// public void SaveTo(string path) { ThrowIfDisposed(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("Path cannot be null or empty", nameof(path)); lock (_syncLock) { try { if (_ms is not null) { EnsureFullCalculationOnLoad(); _doc.Save(); _ms.Position = 0; using var fileStream = new FileStream( path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 65536); _ms.CopyTo(fileStream); _isModified = false; } } catch (Exception ex) { throw new IOException($"Failed to save document to '{path}'", ex); } } } internal byte[] GetDocumentBytes() { lock (_syncLock) { EnsureFullCalculationOnLoad(); _doc.Save(); _ms.Position = 0; return _ms.ToArray(); } } public void SaveTo(Stream outputStream) { lock (_syncLock) { EnsureFullCalculationOnLoad(); _doc.Save(); _ms.Position = 0; _ms.CopyTo(outputStream); } } /// /// Пытается сохранить документ в указанный файл. /// public bool TrySaveTo(string path, out Exception? error) { error = null; if (!IsValid || string.IsNullOrEmpty(path)) { error = new InvalidOperationException("Processor is not valid or path is empty"); return false; } try { SaveTo(path); return true; } catch (Exception ex) { error = ex; #if DEBUG Debug.WriteLine($"[DEBUG] Failed to save to '{path}': {ex.Message}"); #endif return false; } } /// /// Асинхронно сохраняет документ в указанный файл. /// public async Task SaveToAsync(string path, CancellationToken cancellationToken = default) { ThrowIfDisposed(); if (string.IsNullOrEmpty(path)) throw new ArgumentException("Path cannot be null or empty", nameof(path)); if (_ms is null) throw new InvalidOperationException("Memory stream is not available"); byte[] buffer; lock (_syncLock) { EnsureFullCalculationOnLoad(); _doc.Save(); _ms.Position = 0; buffer = _ms.ToArray(); } using var fileStream = new FileStream( path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 81920, useAsync: true); await fileStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); } /// Создаёт read-only процессор из текущего документа. internal ExcelReader ToReader() { lock (_syncLock) { EnsureFullCalculationOnLoad(); // важно для формул _doc.Save(); _ms.Position = 0; var data = _ms.ToArray(); return ExcelReader.CreateFromData(data) ?? throw new InvalidOperationException("Failed to create reader from the current document"); } } #endregion #region Dispose Pattern protected override void Dispose(bool disposing) { if (_disposed) return; lock (_syncLock) { if (disposing) { if (_isModified && !string.IsNullOrEmpty(FilePath)) { try { EnsureFullCalculationOnLoad(); _doc.Save(); Directory.CreateDirectory(Path.GetDirectoryName(FilePath)); _ms.Position = 0; using var fileStream = new FileStream( FilePath!, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 81920); _ms.CopyTo(fileStream); } catch (Exception ex) { #if DEBUG Debug.WriteLine($"[DEBUG] Auto-save failed during Dispose: {ex.Message}"); #endif } } } base.Dispose(disposing); } } #endregion private void EnsureFullCalculationOnLoad() { if (_doc?.WorkbookPart?.Workbook == null) return; var workbook = _doc.WorkbookPart.Workbook; var calcProps = workbook.CalculationProperties; if (calcProps == null) { calcProps = new CalculationProperties(); workbook.CalculationProperties = calcProps; } calcProps.ForceFullCalculation = true; calcProps.FullCalculationOnLoad = true; } #region IBook Implementation /// public IReadOnlyList GetSheets() { ThrowIfDisposed(); lock (_syncLock) { var sheets = new List(); var workbookPart = _doc.WorkbookPart; if (workbookPart?.Workbook?.Sheets == null) return sheets; foreach (Sheet sheetElement in workbookPart.Workbook.Sheets.Elements()) { var sheet = new ExcelSheet(this, sheetElement, sheetElement.SheetId?.Value ?? 0); sheets.Add(sheet); } return sheets; } } /// public ISheet? Sheet(string name) { ThrowIfDisposed(); if (string.IsNullOrEmpty(name)) return null; lock (_syncLock) { var workbookPart = _doc.WorkbookPart; if (workbookPart?.Workbook?.Sheets == null) return null; foreach (Sheet sheetElement in workbookPart.Workbook.Sheets.Elements()) { if (sheetElement.Name?.Value == name) return new ExcelSheet(this, sheetElement, sheetElement.SheetId?.Value ?? 0); } return null; } } /// public bool TryGetSheet(string name, out ISheet sheet) { sheet = Sheet(name)!; return sheet != null; } /// public bool TryAddSheet(string name, Action? edit) { ThrowIfDisposed(); if (string.IsNullOrEmpty(name)) return false; lock (_syncLock) { var workbookPart = _doc.WorkbookPart; if (workbookPart?.Workbook?.Sheets == null) return false; // Проверка уникальности имени foreach (Sheet s in workbookPart.Workbook.Sheets.Elements()) { if (s.Name?.Value == name) return false; } // Создание нового листа var worksheetPart = workbookPart.AddNewPart(); worksheetPart.Worksheet = new Worksheet(new SheetData()); // Вычисление нового SheetId uint maxSheetId = 0; foreach (Sheet s in workbookPart.Workbook.Sheets.Elements()) { if (s.SheetId?.Value > maxSheetId) maxSheetId = s.SheetId.Value; } uint newSheetId = maxSheetId + 1; var sheetElement = new Sheet { Id = workbookPart.GetIdOfPart(worksheetPart), SheetId = newSheetId, Name = name }; workbookPart.Workbook.Sheets.Append(sheetElement); _isModified = true; var newSheet = new ExcelSheet(this, sheetElement, newSheetId); edit?.Invoke(newSheet); return true; } } /// public bool TryRemoveSheet(string name) { var sheet = Sheet(name); return sheet != null && TryRemoveSheet(sheet); } /// public bool TryRemoveSheet(ISheet sheet) { ThrowIfDisposed(); if (sheet is not ExcelSheet xSheet || xSheet.Book != this) return false; lock (_syncLock) { var workbookPart = _doc.WorkbookPart; if (workbookPart?.Workbook?.Sheets == null) return false; var sheetElement = xSheet.SheetElement; if (sheetElement?.Parent != workbookPart.Workbook.Sheets) return false; // Удаление связанной части string partId = sheetElement.Id?.Value!; if (!string.IsNullOrEmpty(partId)) { var part = workbookPart.GetPartById(partId); if (part != null) workbookPart.DeletePart(part); } sheetElement.Remove(); _isModified = true; return true; } } #endregion #region IExcelWriter /// public void Edit(Action edit) { ThrowIfDisposed(); if (edit is null) throw new ArgumentNullException(nameof(edit)); lock (_syncLock) { edit(this); _isModified = true; } } #endregion // Добавить в класс ExcelWriter internal CellAlign GetCellAlign(uint styleIndex) { var cellFormat = GetCellFormatAt(styleIndex); return CellAlign.FromAlignment(cellFormat?.Alignment); } internal CellBorder GetCellBorder(uint styleIndex) { var cellFormat = GetCellFormatAt(styleIndex); if (cellFormat?.BorderId?.Value is not uint borderId) return default; var borderElement = GetBorderById(borderId); return CellBorder.FromBorder(borderElement); } internal CellFill GetCellFill(uint styleIndex) { var cellFormat = GetCellFormatAt(styleIndex); if (cellFormat?.FillId?.Value is not uint fillId) return default; var fill = GetFillById(fillId); return CellFill.FromFill(fill); } internal CellFont GetCellFont(uint styleIndex) { var cellFormat = GetCellFormatAt(styleIndex); if (cellFormat?.FontId?.Value is not uint fontId) return default; var font = GetFontById(fontId); return CellFont.FromFont(font); } // Внутренний метод, предполагает, что _syncLock уже захвачен вызывающим private NumberFormatPattern CreateNumberFormatInternal(string formatCode) { // Проверяем кэш по коду if (_numberFormatCache.TryGetValue(formatCode, out var existing)) return existing; // Получаем или создаём NumberingFormat в стилях var stylesheet = EnsureStylesheet(); var numberingFormats = stylesheet.NumberingFormats ?? CreateNumberingFormats(stylesheet); uint newId = 164; foreach (var nf in numberingFormats.Elements()) { if (nf.FormatCode?.Value == formatCode) { var pattern = new NumberFormatPattern(formatCode); pattern.Attach((ushort)nf.NumberFormatId!.Value); _numberFormatCache[formatCode] = pattern; return pattern; } if (nf.NumberFormatId?.Value >= newId) newId = nf.NumberFormatId.Value + 1; } var newNf = new NumberingFormat { NumberFormatId = newId, FormatCode = formatCode }; numberingFormats.Append(newNf); numberingFormats.Count = (uint)numberingFormats.Elements().Count(); var result = new NumberFormatPattern(formatCode); result.Attach((ushort)newId); _numberFormatCache[formatCode] = result; return result; } // Вспомогательные методы для извлечения элементов из стилей (нужно закешировать или обращаться напрямую) private Border? GetBorderById(uint borderId) { if (_cachedBorders == null) { var borders = EnsureStylesheet().Borders; _cachedBorders = borders?.Elements().ToList() ?? []; } return borderId < _cachedBorders.Count ? _cachedBorders[(int)borderId] : null; } private List? _cachedBorders; private Fill? GetFillById(uint borderId) { if (_cachedFills == null) { var fills = EnsureStylesheet().Fills; _cachedFills = fills?.Elements().ToList() ?? []; } return borderId < _cachedFills.Count ? _cachedFills[(int)borderId] : null; } private List? _cachedFills; private Font? GetFontById(uint borderId) { if (_cachedFonts == null) { var borders = EnsureStylesheet().Borders; _cachedFonts = borders?.Elements().ToList() ?? []; } return borderId < _cachedFonts.Count ? _cachedFonts[(int)borderId] : null; } private List? _cachedFonts; }