Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.ExcelProcessor/ExcelWriter.cs
2026-06-05 15:58:03 +07:00

1293 lines
42 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using QWERTYkez.ExcelProcessor.Editors;
using System.Runtime.InteropServices;
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Предоставляет потокобезопасный процессор для чтения и записи документов Excel (xlsx / xlsm) формата.
/// <para>Наследует от <see cref="ExcelReader"/> и добавляет операции изменения документа.</para>
/// </summary>
internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
{
// Работа с общей таблицей строк
private SharedStringTablePart? _sharedStringPart;
private SharedStringTable? _sharedStringTable;
internal static Dictionary<int, double>? _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<int, double> 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<int, double>();
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<SharedStringTablePart>().FirstOrDefault();
if (_sharedStringPart == null)
{
_sharedStringPart = _doc.WorkbookPart?.AddNewPart<SharedStringTablePart>();
_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>()?.Text;
if (text != null) return text;
// Rich text
var sb = new StringBuilder();
foreach (var run in si.Elements<Run>())
{
sb.Append(run.GetFirstChild<Text>()?.Text);
}
return sb.ToString();
}
internal int GetOrAddSharedString(string value)
{
EnsureSharedStringTable();
if (_sharedStringTable == null) return -1;
// Поиск существующей строки
int idx = 0;
foreach (var si in _sharedStringTable.Elements<SharedStringItem>())
{
var text = si.GetFirstChild<Text>()?.Text;
if (text == value) return idx;
idx++;
}
// Добавление новой
var newSi = new SharedStringItem();
newSi.Append(new Text(value));
_sharedStringTable.Append(newSi);
_sharedStringTable.Count = (uint)_sharedStringTable.Elements<SharedStringItem>().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<string, NumberFormatPattern> _numberFormatCache = [];
private readonly Dictionary<uint, NumberFormatPattern> _numberFormatIdToPattern = [];
// Кэши для компонентов стилей (чтобы не создавать дубликаты)
private readonly Dictionary<CellFont, int> _fontCache = [];
private readonly Dictionary<CellFill, int> _fillCache = [];
private readonly Dictionary<CellBorder, int> _borderCache = [];
private readonly Dictionary<CellAlign, int> _alignmentCache = [];
// Кэш составных стилей (CellFormat)
private readonly Dictionary<(int fontId, int fillId, int borderId, int alignId, int numFmtId), int> _cellFormatCache = [];
// Конструктор, фабричные методы без изменений (опущены)
#region Управление числовыми форматами (расширение IBook)
public IReadOnlyList<NumberFormatPattern> GetNumberFormats()
{
// Возвращаем все пользовательские форматы (Id >= 164), встроенные не включаем
var result = new List<NumberFormatPattern>();
var stylesheet = _doc.WorkbookPart?.WorkbookStylesPart?.Stylesheet;
if (stylesheet?.NumberingFormats != null)
{
foreach (var nf in stylesheet.NumberingFormats.Elements<NumberingFormat>())
{
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<NumberingFormat>())
{
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<WorkbookStylesPart>();
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<byte> 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<byte> 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;
}
/// <summary>
/// Создаёт новый пустой документ Excel с одним листом.
/// </summary>
/// <param name="destinationPath">Путь, по которому будет сохранён документ (необязательный).</param>
/// <returns>Экземпляр <see cref="ExcelWriter"/> для редактирования нового документа.</returns>
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>();
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
/// <summary>
/// Получает значение, указывающее, будет ли документ сохранен с перезаписью исходного файла.
/// </summary>
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<string, string> replacements, StringComparison comparisonType) =>
((IExcelWriter)this).Replace((IEnumerable<KeyValuePair<string, string>>)replacements, comparisonType);
void IExcelWriter.Replace(IEnumerable<KeyValuePair<string, string>> 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<string, double> replacements, string? format, StringComparison comparisonType) =>
((IExcelWriter)this).Replace((IEnumerable<KeyValuePair<string, double>>)replacements, format, comparisonType);
void IExcelWriter.Replace(IEnumerable<KeyValuePair<string, double>> 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<string, float> replacements, string? format, StringComparison comparisonType) =>
((IExcelWriter)this).Replace((IEnumerable<KeyValuePair<string, float>>)replacements, format, comparisonType);
void IExcelWriter.Replace(IEnumerable<KeyValuePair<string, float>> 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<string, int> replacements, StringComparison comparisonType) =>
((IExcelWriter)this).Replace((IEnumerable<KeyValuePair<string, int>>)replacements, comparisonType);
void IExcelWriter.Replace(IEnumerable<KeyValuePair<string, int>> 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<string, long> replacements, StringComparison comparisonType) =>
((IExcelWriter)this).Replace((IEnumerable<KeyValuePair<string, long>>)replacements, comparisonType);
void IExcelWriter.Replace(IEnumerable<KeyValuePair<string, long>> replacements, StringComparison comparisonType)
{
ThrowIfDisposed();
if (replacements == null) return;
lock (_syncLock)
{
_doc.Replace(replacements, comparisonType);
_isModified = true;
}
}
#endregion
#region Save Operations
/// <summary>
/// Сохраняет документ в файл, указанный при создании процессора.
/// </summary>
public void Save()
{
ThrowIfDisposed();
if (string.IsNullOrEmpty(FilePath))
throw new InvalidOperationException("Cannot save - no file path specified");
SaveTo(FilePath!);
}
/// <summary>
/// Сохраняет документ в указанный файл.
/// </summary>
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);
}
}
/// <summary>
/// Пытается сохранить документ в указанный файл.
/// </summary>
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;
}
}
/// <summary>
/// Асинхронно сохраняет документ в указанный файл.
/// </summary>
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);
}
/// <summary> Создаёт read-only процессор из текущего документа. </summary>
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
/// <inheritdoc />
public IReadOnlyList<ISheet> GetSheets()
{
ThrowIfDisposed();
lock (_syncLock)
{
var sheets = new List<ISheet>();
var workbookPart = _doc.WorkbookPart;
if (workbookPart?.Workbook?.Sheets == null)
return sheets;
foreach (Sheet sheetElement in workbookPart.Workbook.Sheets.Elements<Sheet>())
{
var sheet = new ExcelSheet(this, sheetElement, sheetElement.SheetId?.Value ?? 0);
sheets.Add(sheet);
}
return sheets;
}
}
/// <inheritdoc />
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<Sheet>())
{
if (sheetElement.Name?.Value == name)
return new ExcelSheet(this, sheetElement, sheetElement.SheetId?.Value ?? 0);
}
return null;
}
}
/// <inheritdoc />
public bool TryGetSheet(string name, out ISheet sheet)
{
sheet = Sheet(name)!;
return sheet != null;
}
/// <inheritdoc />
public bool TryAddSheet(string name, Action<ISheet>? 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<Sheet>())
{
if (s.Name?.Value == name)
return false;
}
// Создание нового листа
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet(new SheetData());
// Вычисление нового SheetId
uint maxSheetId = 0;
foreach (Sheet s in workbookPart.Workbook.Sheets.Elements<Sheet>())
{
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;
}
}
/// <inheritdoc />
public bool TryRemoveSheet(string name)
{
var sheet = Sheet(name);
return sheet != null && TryRemoveSheet(sheet);
}
/// <inheritdoc />
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
/// <inheritdoc />
public void Edit(Action<IBook> 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<NumberingFormat>())
{
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<NumberingFormat>().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<Border>().ToList() ?? [];
}
return borderId < _cachedBorders.Count ? _cachedBorders[(int)borderId] : null;
}
private List<Border>? _cachedBorders;
private Fill? GetFillById(uint borderId)
{
if (_cachedFills == null)
{
var fills = EnsureStylesheet().Fills;
_cachedFills = fills?.Elements<Fill>().ToList() ?? [];
}
return borderId < _cachedFills.Count ? _cachedFills[(int)borderId] : null;
}
private List<Fill>? _cachedFills;
private Font? GetFontById(uint borderId)
{
if (_cachedFonts == null)
{
var borders = EnsureStylesheet().Borders;
_cachedFonts = borders?.Elements<Font>().ToList() ?? [];
}
return borderId < _cachedFonts.Count ? _cachedFonts[(int)borderId] : null;
}
private List<Font>? _cachedFonts;
}