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

285 lines
8.0 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.
namespace QWERTYkez.WordProcessor;
/// <summary>
/// Предоставляет потокобезопасный процессор только для чтения документов Word (DOCX) формата.
/// <para>Не поддерживает операции изменения документа.</para>
/// </summary>
internal class WordReader : IDisposable, IWordReader
{
protected MemoryStream _ms = null!;
protected WordprocessingDocument _doc = null!;
protected Body _body = null!;
protected bool _disposed;
protected readonly object _syncLock = new();
protected string? _originalSourcePath;
public Body Body => _body;
internal WordReader() { }
#region Factory Methods
internal static WordReader? CreateInternal(FileInfo sourceFile)
{
if (sourceFile is null || !sourceFile.Exists)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
#endif
return null;
}
MemoryStream? ms = null;
WordprocessingDocument? doc = null;
try
{
ms = new MemoryStream();
using (var file = new FileStream(sourceFile.FullName,
FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{
file.CopyTo(ms);
}
ms.Position = 0;
doc = WordprocessingDocument.Open(ms, isEditable: false,
new OpenSettings { AutoSave = false });
if (doc.MainDocumentPart?.Document?.Body is not { } body)
{
#if DEBUG
Debug.WriteLine("[DEBUG] Document body is null or empty");
#endif
doc.Dispose();
ms.Dispose();
return null;
}
var processor = new WordReader
{
_ms = ms,
_doc = doc,
_body = body,
_originalSourcePath = sourceFile.FullName,
FilePath = sourceFile.FullName
};
return processor;
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Error creating read-only processor: {ex.GetType().Name}: {ex.Message}");
#endif
doc?.Dispose();
ms?.Dispose();
return null;
}
}
internal static WordReader? 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, false, new OpenSettings { AutoSave = false });
if (doc.MainDocumentPart?.Document?.Body is not { } body)
{
doc.Dispose();
ms.Dispose();
return null;
}
return new WordReader
{
_ms = ms,
_doc = doc,
_body = body,
FilePath = null // из памяти нет файла
};
}
catch
{
ms?.Dispose();
return null;
}
}
#endregion
#region Writers
public bool TryWrite(string destinationPath, Action<IWordWriter> action)
{
ThrowIfDisposed();
if (string.IsNullOrEmpty(destinationPath))
throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
if (action is null)
throw new ArgumentNullException(nameof(action));
// Копируем данные из текущего потока
byte[] data;
lock (_syncLock)
{
_ms.Position = 0;
data = _ms.ToArray();
}
using var writable = WordWriter.CreateFromData(new ReadOnlyMemory<byte>(data), destinationPath);
if (writable is null)
return false;
try
{
action(writable);
return true;
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Error in TryWrite action: {ex.Message}");
#endif
return false;
}
}
public bool TryWrite(Action<IWordWriter> write, Action<IWordReader> read)
{
ThrowIfDisposed();
if (write is null) throw new ArgumentNullException(nameof(write));
if (read is null) throw new ArgumentNullException(nameof(read));
// Копируем текущие данные
byte[] data;
lock (_syncLock)
{
_ms.Position = 0;
data = _ms.ToArray();
}
WordWriter? writable = null;
WordReader? resultReader = null;
try
{
// Создаём редактируемую копию (без привязки к файлу)
writable = WordWriter.CreateFromData(data);
if (writable is null)
return false;
// Применяем изменения
write(writable);
// Сохраняем изменения в поток и создаём read-only процессор
resultReader = writable.ToReader();
if (resultReader is null)
return false;
// Работаем с изменённой копией
read(resultReader);
return true;
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Error in TryWrite(write, read): {ex.Message}");
#endif
return false;
}
finally
{
// Гарантированно освобождаем созданные процессоры
resultReader?.Dispose();
writable?.Dispose();
}
}
#endregion
#region Properties
/// <summary>
/// Получает путь к исходному файлу.
/// </summary>
public string? FilePath { get; protected set; }
/// <summary>
/// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций.
/// </summary>
public bool IsValid => !_disposed && _doc is not null && _body is not null && _ms is not null;
/// <summary>
/// Получает текущий размер документа в байтах.
/// </summary>
public long DocumentSize => _ms.Length;
#endregion
#region Read Operations
/// <summary>
/// Находит все уникальные плейсхолдеры в формате $...$ в документе.
/// <para>Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов.</para>
/// </summary>
/// <param name="comparisonType">Способ сравнения строк при поиске (по умолчанию: без учета регистра)</param>
/// <returns>Коллекция уникальных найденных плейсхолдеров</returns>
public ISet<string> FindPlaceholders()
{
ThrowIfDisposed();
lock (_syncLock)
{
return PlaceholderFinder.FindInDocument(_doc, _body);
}
}
#endregion
#region Dispose Pattern
protected void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(GetType().Name);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
lock (_syncLock)
{
if (disposing)
{
_doc.Dispose();
_ms.Dispose();
}
_doc = null!;
_body = null!;
_ms = null!;
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
~WordReader()
{
Dispose(disposing: false);
}
#endregion
}