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;
}