43 lines
4.5 KiB
C#
43 lines
4.5 KiB
C#
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.Cryptography;
|
|
using DocumentAutomation.Shared;
|
|
using DocxWorkbench.Core;
|
|
|
|
namespace DocxWorkbench.WordInterop;
|
|
|
|
public sealed class WordDocumentNormalizer : IWordDocumentNormalizer, IDisposable
|
|
{
|
|
private readonly StaWordQueue _queue;
|
|
public WordDocumentNormalizer() : this(new StaWordQueue(), true) { }
|
|
internal WordDocumentNormalizer(StaWordQueue queue) : this(queue, false) { }
|
|
private readonly bool _ownsQueue;
|
|
private WordDocumentNormalizer(StaWordQueue queue, bool ownsQueue) { _queue = queue; _ownsQueue = ownsQueue; }
|
|
|
|
public Task<DocumentNormalizationResult> NormalizeAsync(DocumentNormalizationRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken)
|
|
=> _queue.Enqueue(() => NormalizeOnSta(request, progress, cancellationToken), cancellationToken);
|
|
|
|
private static DocumentNormalizationResult NormalizeOnSta(DocumentNormalizationRequest request, IProgress<WordOperationProgress>? progress, CancellationToken token)
|
|
{
|
|
if (!File.Exists(request.SourcePath)) throw new FileNotFoundException("Document source was not found.", request.SourcePath);
|
|
var extension = Path.GetExtension(request.SourcePath);
|
|
if (!extension.Equals(".doc", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".docx", StringComparison.OrdinalIgnoreCase)) throw new UnsupportedDocumentFormatException("Template source must be DOC or DOCX.");
|
|
var started = Stopwatch.StartNew(); var sourceHash = Hash(request.SourcePath); var work = Path.GetFullPath(request.WorkingDirectory); Directory.CreateDirectory(work); var normalized = Path.Combine(work, "normalized.docx"); token.ThrowIfCancellationRequested();
|
|
if (extension.Equals(".docx", StringComparison.OrdinalIgnoreCase)) { File.Copy(request.SourcePath, normalized, true); return new DocumentNormalizationResult { SourcePath = request.SourcePath, NormalizedPath = normalized, SourceSha256 = sourceHash, NormalizedSha256 = Hash(normalized), WasConverted = false, Duration = started.Elapsed }; }
|
|
dynamic? application = null; dynamic? documents = null; dynamic? document = null; var proxies = new List<object?>();
|
|
try
|
|
{
|
|
progress?.Report(new WordOperationProgress { Stage = "NormalizeDoc", Message = "Opening binary DOC read-only" }); var wordType = Type.GetTypeFromProgID("Word.Application", false) ?? throw new WordNotInstalledException("Microsoft Word is not installed or is not registered in COM."); application = Own(Activator.CreateInstance(wordType), proxies) ?? throw new WordAutomationException("Could not create Word.Application."); application.Visible = false; application.DisplayAlerts = 0; if (request.DisableMacros) { try { application.AutomationSecurity = 3; } catch { } } documents = Own(application.Documents, proxies); document = Own(documents.Open(request.SourcePath, false, true, false), proxies) ?? throw new WordAutomationException("Word could not open the DOC source read-only."); token.ThrowIfCancellationRequested(); progress?.Report(new WordOperationProgress { Stage = "NormalizeDoc", Message = "Saving temporary DOCX" }); document.SaveAs2(normalized, 12); int? pageCount = null; try { document.Repaginate(); pageCount = (int)document.ComputeStatistics(2); } catch { } document.Close(0); document = null; return new DocumentNormalizationResult { SourcePath = request.SourcePath, NormalizedPath = normalized, SourceSha256 = sourceHash, NormalizedSha256 = Hash(normalized), WasConverted = true, Duration = started.Elapsed, PageCount = pageCount };
|
|
}
|
|
catch (WordNotInstalledException) { throw; }
|
|
catch (WordAutomationException) { throw; }
|
|
catch (Exception ex) { throw new WordAutomationException("DOC normalization failed.", ex); }
|
|
finally { try { if (document is not null) document.Close(0); } catch { } try { if (application is not null) application.Quit(0); } catch { } ReleaseAll(proxies); }
|
|
}
|
|
|
|
private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));
|
|
private static T? Own<T>(T? value, List<object?> proxies) where T : class { if (value is not null) proxies.Add(value); return value; }
|
|
private static void ReleaseAll(IEnumerable<object?> proxies) { foreach (var value in proxies.Reverse()) try { if (value is not null && Marshal.IsComObject(value)) Marshal.FinalReleaseComObject(value); } catch { } }
|
|
public void Dispose() { if (_ownsQueue) _queue.Dispose(); }
|
|
}
|