626 lines
20 KiB
C#
626 lines
20 KiB
C#
|
|
using QWERTYkez.WordProcessor.Builders;
|
|||
|
|
|
|||
|
|
namespace QWERTYkez.WordProcessor;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Предоставляет потокобезопасный процессор для чтения и записи документов Word (DOCX) формата.
|
|||
|
|
/// <para>Наследует от <see cref="WordReader"/> и добавляет операции изменения документа.</para>
|
|||
|
|
/// </summary>
|
|||
|
|
internal sealed class WordWriter : WordReader, IWordWriter
|
|||
|
|
{
|
|||
|
|
private bool _isModified = false;
|
|||
|
|
|
|||
|
|
internal WordWriter() { }
|
|||
|
|
|
|||
|
|
#region Factory Methods
|
|||
|
|
|
|||
|
|
internal static WordWriter? 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 = WordprocessingDocument.Open(ms, true, new OpenSettings { AutoSave = false });
|
|||
|
|
if (doc.MainDocumentPart?.Document?.Body is { } body)
|
|||
|
|
{
|
|||
|
|
return new WordWriter
|
|||
|
|
{
|
|||
|
|
_ms = ms,
|
|||
|
|
_doc = doc,
|
|||
|
|
_body = body,
|
|||
|
|
FilePath = destinationPath,
|
|||
|
|
_originalSourcePath = null // нет исходного файла
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc.Dispose();
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
|
|||
|
|
ms?.Dispose();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal static new WordWriter? 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 = WordprocessingDocument.Open(ms, true, new OpenSettings { AutoSave = false });
|
|||
|
|
if (doc.MainDocumentPart?.Document?.Body is { } body)
|
|||
|
|
{
|
|||
|
|
return new WordWriter
|
|||
|
|
{
|
|||
|
|
_ms = ms,
|
|||
|
|
_doc = doc,
|
|||
|
|
_body = body,
|
|||
|
|
FilePath = null, // нет привязки к файлу
|
|||
|
|
_originalSourcePath = null
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc.Dispose();
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
|
|||
|
|
ms.Dispose();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal static WordWriter? 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 = WordprocessingDocument.Open(ms, isEditable: true,
|
|||
|
|
new OpenSettings { AutoSave = false });
|
|||
|
|
|
|||
|
|
if (doc.MainDocumentPart?.Document?.Body is { } body)
|
|||
|
|
{
|
|||
|
|
var processor = new WordWriter
|
|||
|
|
{
|
|||
|
|
_ms = ms,
|
|||
|
|
_doc = doc,
|
|||
|
|
_body = body,
|
|||
|
|
_originalSourcePath = sourceFile.FullName,
|
|||
|
|
FilePath = destinationPath ?? sourceFile.FullName
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return processor;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc?.Dispose();
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
|
|||
|
|
ms?.Dispose();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Создаёт новый пустой документ Word.
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="destinationPath">Путь, по которому будет сохранён документ (необязательный).</param>
|
|||
|
|
/// <returns>Экземпляр <see cref="WordWriter"/> для редактирования нового документа.</returns>
|
|||
|
|
internal static WordWriter CreateNew(string? destinationPath = null)
|
|||
|
|
{
|
|||
|
|
var ms = new MemoryStream();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// Создаём документ, НЕ используем using
|
|||
|
|
var doc = WordprocessingDocument.Create(ms, WordprocessingDocumentType.Document);
|
|||
|
|
var mainPart = doc.AddMainDocumentPart();
|
|||
|
|
mainPart.Document = new Document();
|
|||
|
|
var body = new Body();
|
|||
|
|
mainPart.Document.AppendChild(body);
|
|||
|
|
|
|||
|
|
var writer = new WordWriter
|
|||
|
|
{
|
|||
|
|
_ms = ms,
|
|||
|
|
_doc = doc,
|
|||
|
|
_body = body,
|
|||
|
|
FilePath = destinationPath,
|
|||
|
|
_originalSourcePath = null
|
|||
|
|
};
|
|||
|
|
return writer;
|
|||
|
|
}
|
|||
|
|
catch
|
|||
|
|
{
|
|||
|
|
ms.Dispose();
|
|||
|
|
throw;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Properties
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Получает значение, указывающее, будет ли документ сохранен с перезаписью исходного файла.
|
|||
|
|
/// </summary>
|
|||
|
|
public bool WillOverwriteSource => FilePath == _originalSourcePath;
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Replace text
|
|||
|
|
|
|||
|
|
public void ReplaceString(string oldValue, params string[] newValues) => ReplaceString(oldValue, (IEnumerable<string>)newValues);
|
|||
|
|
public void ReplaceString(string oldValue, IEnumerable<string> newValues)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (string.IsNullOrEmpty(oldValue))
|
|||
|
|
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
|||
|
|
|
|||
|
|
if (newValues is null) return;
|
|||
|
|
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] [WordProcessor.Replace] START: '{oldValue}' -> [{string.Join(", ", newValues)}] (comparison: {StringComparison.OrdinalIgnoreCase})");
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// 1. Основной текст
|
|||
|
|
_body.Replace(oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
// 2. Таблицы в основном тексте
|
|||
|
|
TableTextProcessor.ReplaceInTables(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
// 3. Колонтитулы
|
|||
|
|
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ReplaceString(IDictionary<string, string> replacements) =>
|
|||
|
|
/* */ReplaceString((IEnumerable<KeyValuePair<string, string>>)replacements);
|
|||
|
|
public void ReplaceString(IEnumerable<KeyValuePair<string, string>> replacements)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (replacements is null) return;
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// 1. Основной текст
|
|||
|
|
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
// 2. Таблицы в основном тексте
|
|||
|
|
foreach (var kvp in replacements)
|
|||
|
|
{
|
|||
|
|
if (!string.IsNullOrEmpty(kvp.Key))
|
|||
|
|
{
|
|||
|
|
TableTextProcessor.ReplaceInTables(_body, kvp.Key, [kvp.Value ?? string.Empty], StringComparison.OrdinalIgnoreCase);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, replacements, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ReplaceString(IDictionary<string, IEnumerable<string>> replacements) =>
|
|||
|
|
/* */ReplaceString((IEnumerable<KeyValuePair<string, IEnumerable<string>>>)replacements);
|
|||
|
|
public void ReplaceString(IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (replacements is null) return;
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// 1. Основной текст
|
|||
|
|
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
// 2. Таблицы в основном тексте
|
|||
|
|
TableTextProcessor.ReplaceInTables(_body, replacements, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
// 3. Колонтитулы
|
|||
|
|
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, replacements, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Replace ReplaceItem
|
|||
|
|
public void ReplaceItem(string oldValue, params ReplaceItem[] newValues) => ReplaceItem(oldValue, (IEnumerable<ReplaceItem>)newValues);
|
|||
|
|
public void ReplaceItem(string oldValue, IEnumerable<ReplaceItem> newValues)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (string.IsNullOrEmpty(oldValue))
|
|||
|
|
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
|||
|
|
|
|||
|
|
if (newValues is null) return;
|
|||
|
|
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] [WordProcessor.Replace] START: '{oldValue}' -> [{string.Join(", ", newValues)}] (comparison: {StringComparison.OrdinalIgnoreCase})");
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// 1. Основной текст
|
|||
|
|
_body.Replace(oldValue, newValues, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
var texts = newValues.Select(val => val.Text).ToArray();
|
|||
|
|
|
|||
|
|
// 2. Таблицы в основном тексте
|
|||
|
|
TableTextProcessor.ReplaceInTables(_body, oldValue, texts, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
// 3. Колонтитулы
|
|||
|
|
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, oldValue, texts, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ReplaceItem(IDictionary<string, ReplaceItem> replacements) =>
|
|||
|
|
/* */ReplaceItem((IEnumerable<KeyValuePair<string, ReplaceItem>>)replacements);
|
|||
|
|
public void ReplaceItem(IEnumerable<KeyValuePair<string, ReplaceItem>> replacements)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (replacements is null) return;
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// 1. Основной текст
|
|||
|
|
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
|
|||
|
|
var texts = replacements.ToDictionary(val => val.Key, val => val.Value.Text);
|
|||
|
|
|
|||
|
|
// 2. Таблицы в основном тексте
|
|||
|
|
foreach (var kvp in texts)
|
|||
|
|
{
|
|||
|
|
if (!string.IsNullOrEmpty(kvp.Key))
|
|||
|
|
{
|
|||
|
|
TableTextProcessor.ReplaceInTables(_body, kvp.Key, kvp.Value, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Колонтитулы
|
|||
|
|
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, texts, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ReplaceItem(IDictionary<string, IEnumerable<ReplaceItem>> replacements) =>
|
|||
|
|
/* */ReplaceItem((IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>)replacements);
|
|||
|
|
public void ReplaceItem(IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (replacements is null) return;
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// 1. Основной текст
|
|||
|
|
_body.Replace(replacements, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
|
|||
|
|
var texts = replacements.ToDictionary(val => val.Key, val => val.Value.Select(val => val.Text));
|
|||
|
|
|
|||
|
|
// 2. Таблицы в основном тексте
|
|||
|
|
TableTextProcessor.ReplaceInTables(_body, texts, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
// 3. Колонтитулы
|
|||
|
|
HeaderFooterProcessor.ReplaceInHeadersFooters(_doc, texts, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Добавляет новый параграф с указанным текстом в конец документа.
|
|||
|
|
/// </summary>
|
|||
|
|
public void AddParagraph(string text, bool preserveFormatting = true)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (_body is null)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
var paragraph = new Paragraph();
|
|||
|
|
var run = new Run();
|
|||
|
|
|
|||
|
|
if (preserveFormatting && _body.Elements<Paragraph>().FirstOrDefault() is { } firstPara)
|
|||
|
|
{
|
|||
|
|
if (firstPara.ParagraphProperties is not null)
|
|||
|
|
{
|
|||
|
|
paragraph.ParagraphProperties = firstPara.ParagraphProperties.CloneNode(true) as ParagraphProperties;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (firstPara.Elements<Run>().FirstOrDefault()?.RunProperties is not null)
|
|||
|
|
{
|
|||
|
|
run.RunProperties = firstPara.Elements<Run>().First().RunProperties?.CloneNode(true) as RunProperties;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
string processedText = text.Replace(' ', '\u00A0');
|
|||
|
|
run.AppendChild(new Text(processedText));
|
|||
|
|
paragraph.AppendChild(run);
|
|||
|
|
_body.AppendChild(paragraph);
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region Replace to table
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Заменяет параграф, содержащий указанный текст, на таблицу, созданную с помощью TableBuilder
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="oldValue">Текст для поиска в параграфах</param>
|
|||
|
|
/// <param name="buildTable">Действие для настройки таблицы через TableBuilder</param>
|
|||
|
|
public void ReplaceToTable(string oldValue, Action<ITable> buildTable)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (string.IsNullOrEmpty(oldValue))
|
|||
|
|
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
|||
|
|
|
|||
|
|
if (buildTable is null) return;
|
|||
|
|
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceToTable] Looking for '{oldValue}' to replace with custom table");
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// Используем метод расширения для Body с TableBuilder
|
|||
|
|
ReplaceToTableExt.ReplaceParagraphsContainingTextToTable(_body, oldValue, buildTable);
|
|||
|
|
_isModified = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Replace to text
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Заменяет параграф, содержащий указанный текст, на таблицу, созданную с помощью TextBuilder
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="oldValue">Текст для поиска в параграфах</param>
|
|||
|
|
/// <param name="buildText">Действие для настройки таблицы через TextBuilder</param>
|
|||
|
|
public void ReplaceToText(string oldValue, Action<IText> buildText)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (string.IsNullOrEmpty(oldValue))
|
|||
|
|
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
|||
|
|
|
|||
|
|
if (buildText is null) return;
|
|||
|
|
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceToText] Looking for '{oldValue}' to replace with custom text");
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
// Используем метод расширения для Body с TextBuilder
|
|||
|
|
ReplaceToTextExt.ReplaceParagraphsContainingTextToText(_body, oldValue, buildText);
|
|||
|
|
_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)
|
|||
|
|
{
|
|||
|
|
_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)
|
|||
|
|
{
|
|||
|
|
_doc.Save();
|
|||
|
|
_ms.Position = 0;
|
|||
|
|
return _ms.ToArray();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void SaveTo(Stream outputStream)
|
|||
|
|
{
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
_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)
|
|||
|
|
{
|
|||
|
|
_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 WordReader ToReader()
|
|||
|
|
{
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
_doc.Save();
|
|||
|
|
_ms.Position = 0;
|
|||
|
|
var data = _ms.ToArray();
|
|||
|
|
return WordReader.CreateFromData(data) ?? throw new InvalidOperationException("Failed to create reader");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Dispose Pattern
|
|||
|
|
|
|||
|
|
protected override void Dispose(bool disposing)
|
|||
|
|
{
|
|||
|
|
if (_disposed)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
if (disposing)
|
|||
|
|
{
|
|||
|
|
if (_isModified && !string.IsNullOrEmpty(FilePath))
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
_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
|
|||
|
|
}
|