feat: add DOCX workbench and Word automation
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using DocumentAutomation.Shared;
|
||||
using DocxWorkbench.Core;
|
||||
using Word = Microsoft.Office.Interop.Word;
|
||||
|
||||
namespace DocxWorkbench.WordInterop;
|
||||
|
||||
public sealed class WordDocumentService : IWordDocumentService
|
||||
{
|
||||
public Task<WordDocumentResult> BuildDocumentAsync(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken cancellationToken)
|
||||
=> Task.Run(() => Build(request, progress, cancellationToken), cancellationToken);
|
||||
|
||||
public Task<WordPreviewResult> ExportPdfAsync(string docxPath, string outputPdfPath, CancellationToken cancellationToken)
|
||||
=> Task.Run(() => ExportPdf(docxPath, outputPdfPath, cancellationToken), cancellationToken);
|
||||
|
||||
private static WordDocumentResult Build(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken token)
|
||||
{
|
||||
DocumentInputValidation.Validate(request); var started = Stopwatch.StartNew();
|
||||
var temp = Path.Combine(Path.GetTempPath(), "DocumentAutomation", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(temp);
|
||||
var working = Path.Combine(temp, "working.docx"); File.Copy(request.TemplatePath, working, true);
|
||||
Word.Application? app = null; Word.Document? document = null; string insertion = "none"; int? pid = null;
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested(); progress?.Report("Запуск Word COM"); app = new Word.Application { Visible = false, DisplayAlerts = Word.WdAlertLevel.wdAlertsNone }; pid = GetProcessId(app);
|
||||
document = app.Documents.Open(working, ReadOnly: false, AddToRecentFiles: false, Visible: false);
|
||||
progress?.Report("Поиск точки вставки DOC_BODY"); var range = FindInsertionRange(document, out insertion);
|
||||
if (range is null) throw new InvalidOperationException("В шаблоне не найдена точка вставки DOC_BODY.");
|
||||
token.ThrowIfCancellationRequested(); range.InsertFile(request.BodyPath, ConfirmConversions: false, Link: false, Attachment: false);
|
||||
progress?.Report("Обновление полей и пагинация"); document.Repaginate(); document.Fields.Update();
|
||||
for (var i = 1; i <= document.Sections.Count; i++) { var section = document.Sections[i]; foreach (Word.HeaderFooter hf in section.Headers) hf.Range.Fields.Update(); foreach (Word.HeaderFooter hf in section.Footers) hf.Range.Fields.Update(); }
|
||||
document.SaveAs2(request.OutputDocxPath, Word.WdSaveFormat.wdFormatXMLDocument);
|
||||
var logPath = Path.ChangeExtension(request.OutputDocxPath, ".generation.json"); File.WriteAllText(logPath, JsonSerializer.Serialize(new { operationId = Guid.NewGuid(), template = request.TemplatePath, body = request.BodyPath, output = request.OutputDocxPath, insertionPoint = insertion, wordPid = pid, durationMs = started.ElapsedMilliseconds, completedAtUtc = DateTime.UtcNow }, new JsonSerializerOptions { WriteIndented = true }));
|
||||
return new WordDocumentResult { OutputDocxPath = request.OutputDocxPath, LogPath = logPath, Duration = started.Elapsed, InsertionPoint = insertion };
|
||||
}
|
||||
finally { if (document is not null) { try { document.Close(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(document); } if (app is not null) { try { app.Quit(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(app); } try { Directory.Delete(temp, true); } catch { } }
|
||||
}
|
||||
|
||||
private static WordPreviewResult ExportPdf(string docxPath, string outputPdfPath, CancellationToken token)
|
||||
{
|
||||
if (!File.Exists(docxPath)) throw new FileNotFoundException("DOCX не найден.", docxPath); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPdfPath))!); var sw = Stopwatch.StartNew(); Word.Application? app = null; Word.Document? doc = null;
|
||||
try { token.ThrowIfCancellationRequested(); app = new Word.Application { Visible = false, DisplayAlerts = Word.WdAlertLevel.wdAlertsNone }; doc = app.Documents.Open(docxPath, ReadOnly: true, AddToRecentFiles: false, Visible: false); doc.ExportAsFixedFormat(outputPdfPath, Word.WdExportFormat.wdExportFormatPDF); return new WordPreviewResult { PdfPath = outputPdfPath, Duration = sw.Elapsed }; }
|
||||
finally { if (doc is not null) { try { doc.Close(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(doc); } if (app is not null) { try { app.Quit(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(app); } }
|
||||
}
|
||||
|
||||
private static Word.Range? FindInsertionRange(Word.Document document, out string point)
|
||||
{
|
||||
foreach (Word.ContentControl control in document.ContentControls) if (string.Equals(control.Tag, "DOC_BODY", StringComparison.OrdinalIgnoreCase) || string.Equals(control.Title, "DOC_BODY", StringComparison.OrdinalIgnoreCase)) { point = "ContentControl:DOC_BODY"; return control.Range; }
|
||||
if (document.Bookmarks.Exists("DOC_BODY")) { point = "Bookmark:DOC_BODY"; return document.Bookmarks["DOC_BODY"].Range; }
|
||||
var search = document.Content; search.Find.ClearFormatting(); search.Find.Text = "{{DOC_BODY}}"; if (search.Find.Execute()) { point = "Marker:{{DOC_BODY}}"; search.Text = ""; return search; } point = "none"; return null;
|
||||
}
|
||||
|
||||
private static int? GetProcessId(Word.Application app) { try { return Process.GetProcessesByName("WINWORD").OrderByDescending(p => p.StartTime).FirstOrDefault()?.Id; } catch { return null; } }
|
||||
}
|
||||
Reference in New Issue
Block a user