using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace DCIT.Core.Services { public class OfficeDocConversionService : IDocConversionService, IDocumentFieldUpdateService { private const int WdFormatDocument97 = 0; private const int WdFormatDocumentDefault = 16; private static readonly string[] WpsProgIds = { "KWPS.Application", "kwps.Application", "WPS.Application", "wps.Application", }; private readonly string _applicationBaseDirectory; public OfficeDocConversionService(string applicationBaseDirectory = null) { _applicationBaseDirectory = string.IsNullOrWhiteSpace(applicationBaseDirectory) ? AppContext.BaseDirectory : applicationBaseDirectory; } public void Convert(string sourcePath, string outputPath) { if (string.IsNullOrWhiteSpace(sourcePath)) { throw new ArgumentNullException("sourcePath"); } if (string.IsNullOrWhiteSpace(outputPath)) { throw new ArgumentNullException("outputPath"); } var sourceExtension = NormalizeExtension(sourcePath); var outputExtension = NormalizeExtension(outputPath); if (!IsSupportedConversion(sourceExtension, outputExtension)) { throw new NotSupportedException("仅支持 .doc 与 .docx 之间互相转换。"); } var outputDirectory = Path.GetDirectoryName(outputPath); if (!string.IsNullOrWhiteSpace(outputDirectory)) { Directory.CreateDirectory(outputDirectory); } if (File.Exists(outputPath)) { File.Delete(outputPath); } var errors = new List(); foreach (var progId in GetOfficeAutomationProgIds()) { try { ConvertWithOfficeAutomationBackend(progId, sourcePath, outputPath, outputExtension); return; } catch (Exception ex) { errors.Add(progId + ": " + ex.Message); } } var docToPath = ResolveDocToPath(); if (!string.IsNullOrWhiteSpace(docToPath)) { try { ConvertWithDocToBackend(docToPath, sourcePath, outputPath, outputExtension); return; } catch (Exception ex) { errors.Add("DocTo: " + ex.Message); } } throw new InvalidOperationException( "未找到可用的 .doc 转换链路。请确认已安装可自动化的 Microsoft Word 或 WPS;如自动化不可用,可提供 docto.exe 作为兜底转换工具。" + Environment.NewLine + string.Join(Environment.NewLine, errors.Where(item => !string.IsNullOrWhiteSpace(item)))); } public void UpdateDynamicContent(string documentPath) { if (string.IsNullOrWhiteSpace(documentPath)) { throw new ArgumentNullException("documentPath"); } if (!File.Exists(documentPath)) { throw new FileNotFoundException("要更新动态内容的文档不存在。", documentPath); } var extension = NormalizeExtension(documentPath); if (!string.Equals(extension, ".docx", StringComparison.OrdinalIgnoreCase) && !string.Equals(extension, ".doc", StringComparison.OrdinalIgnoreCase)) { throw new NotSupportedException("动态内容更新仅支持 .doc 或 .docx 文档。"); } var errors = new List(); foreach (var progId in GetOfficeAutomationProgIds()) { try { UpdateDynamicContentWithOfficeAutomationBackend(progId, documentPath); return; } catch (Exception ex) { errors.Add(progId + ": " + ex.Message); } } throw new InvalidOperationException( "未找到可用的目录/域刷新链路。请确保已安装可自动化的 Microsoft Word 或 WPS。" + Environment.NewLine + string.Join(Environment.NewLine, errors.Where(item => !string.IsNullOrWhiteSpace(item)))); } protected virtual IEnumerable GetOfficeAutomationProgIds() { yield return "Word.Application"; foreach (var progId in WpsProgIds) { yield return progId; } } protected virtual string ResolveDocToPath() { var candidates = new List(); var envPath = Environment.GetEnvironmentVariable("DCIT_DOCTO_PATH"); if (!string.IsNullOrWhiteSpace(envPath)) { candidates.Add(envPath); } candidates.Add(Path.Combine(_applicationBaseDirectory, "docto.exe")); candidates.Add(Path.Combine(_applicationBaseDirectory, "tools", "DocTo", "docto.exe")); foreach (var candidate in candidates) { if (!string.IsNullOrWhiteSpace(candidate) && File.Exists(candidate)) { return candidate; } } var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; foreach (var segment in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) { try { var candidate = Path.Combine(segment.Trim(), "docto.exe"); if (File.Exists(candidate)) { return candidate; } } catch { } } return null; } protected virtual void ConvertWithDocToBackend(string docToPath, string sourcePath, string outputPath, string outputExtension) { var format = string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase) ? "wdFormatDocumentDefault" : "wdFormatDocument97"; var startInfo = new ProcessStartInfo { FileName = docToPath, Arguments = string.Format("-F \"{0}\" -O \"{1}\" -T {2}", sourcePath, outputPath, format), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = Path.GetDirectoryName(docToPath) ?? _applicationBaseDirectory, }; using (var process = Process.Start(startInfo)) { if (process == null) { throw new InvalidOperationException("无法启动 docto.exe。"); } var standardOutput = process.StandardOutput.ReadToEnd(); var standardError = process.StandardError.ReadToEnd(); process.WaitForExit(120000); if (process.ExitCode != 0 || !File.Exists(outputPath)) { throw new InvalidOperationException( "docto.exe 转换失败。" + Environment.NewLine + (string.IsNullOrWhiteSpace(standardOutput) ? string.Empty : standardOutput + Environment.NewLine) + standardError); } } } protected virtual void ConvertWithOfficeAutomationBackend(string progId, string sourcePath, string outputPath, string outputExtension) { ConvertWithWordAutomation(progId, sourcePath, outputPath, outputExtension); } protected virtual void UpdateDynamicContentWithOfficeAutomationBackend(string progId, string documentPath) { UpdateDynamicContentWithWordAutomation(progId, documentPath); } private static bool IsSupportedConversion(string sourceExtension, string outputExtension) { if (string.Equals(sourceExtension, outputExtension, StringComparison.OrdinalIgnoreCase)) { return false; } return (string.Equals(sourceExtension, ".doc", StringComparison.OrdinalIgnoreCase) || string.Equals(sourceExtension, ".docx", StringComparison.OrdinalIgnoreCase)) && (string.Equals(outputExtension, ".doc", StringComparison.OrdinalIgnoreCase) || string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase)); } private static string NormalizeExtension(string path) { return Path.GetExtension(path) ?? string.Empty; } private static void ConvertWithWordAutomation(string progId, string sourcePath, string outputPath, string outputExtension) { var appType = Type.GetTypeFromProgID(progId, false); if (appType == null) { throw new InvalidOperationException("未注册 COM ProgID: " + progId); } object application = null; object documents = null; object document = null; try { application = Activator.CreateInstance(appType); if (application == null) { throw new InvalidOperationException("无法创建 COM 实例: " + progId); } TrySetProperty(application, "Visible", false); TrySetProperty(application, "DisplayAlerts", 0); documents = GetProperty(application, "Documents"); if (documents == null) { throw new InvalidOperationException("无法获取 Documents 集合。"); } document = InvokeMethod(documents, "Open", sourcePath, false, true); if (document == null) { throw new InvalidOperationException("无法打开文档。"); } var fileFormat = string.Equals(outputExtension, ".docx", StringComparison.OrdinalIgnoreCase) ? WdFormatDocumentDefault : WdFormatDocument97; try { InvokeMethod(document, "SaveAs2", outputPath, fileFormat); } catch (MissingMethodException) { InvokeMethod(document, "SaveAs", outputPath, fileFormat); } if (!File.Exists(outputPath)) { throw new InvalidOperationException("Office 自动化未生成目标文件。"); } } finally { SafeCloseDocument(document); SafeQuitApplication(application); ReleaseComObject(document); ReleaseComObject(documents); ReleaseComObject(application); } } private static void UpdateDynamicContentWithWordAutomation(string progId, string documentPath) { var appType = Type.GetTypeFromProgID(progId, false); if (appType == null) { throw new InvalidOperationException("未注册 COM ProgID: " + progId); } object application = null; object documents = null; object document = null; try { application = Activator.CreateInstance(appType); if (application == null) { throw new InvalidOperationException("无法创建 COM 实例: " + progId); } TrySetProperty(application, "Visible", false); TrySetProperty(application, "DisplayAlerts", 0); documents = GetProperty(application, "Documents"); if (documents == null) { throw new InvalidOperationException("无法获取 Documents 集合。"); } document = InvokeMethod(documents, "Open", documentPath, false, false); if (document == null) { throw new InvalidOperationException("无法打开文档。"); } TryInvokeMethod(document, "Activate"); TryInvokeMethod(document, "Repaginate"); TryUpdateCollectionItems(document, "TablesOfContents", "Update", "UpdatePageNumbers"); TryUpdateCollectionItems(document, "TablesOfFigures", "Update"); TryUpdateCollectionItems(document, "TablesOfAuthorities", "Update"); TryUpdateCollection(document, "Fields", "Update"); TryUpdateCollection(document, "Indexes", "Update"); TryInvokeMethod(document, "Repaginate"); InvokeMethod(document, "Save"); } finally { SafeCloseDocument(document); SafeQuitApplication(application); ReleaseComObject(document); ReleaseComObject(documents); ReleaseComObject(application); } } private static object GetProperty(object instance, string propertyName) { return instance.GetType().InvokeMember( propertyName, System.Reflection.BindingFlags.GetProperty, null, instance, null); } private static void TrySetProperty(object instance, string propertyName, object value) { try { instance.GetType().InvokeMember( propertyName, System.Reflection.BindingFlags.SetProperty, null, instance, new[] { value }); } catch { } } private static object InvokeMethod(object instance, string methodName, params object[] arguments) { return instance.GetType().InvokeMember( methodName, System.Reflection.BindingFlags.InvokeMethod, null, instance, arguments); } private static bool TryInvokeMethod(object instance, string methodName, params object[] arguments) { try { InvokeMethod(instance, methodName, arguments); return true; } catch { return false; } } private static void TryUpdateCollection(object instance, string propertyName, string updateMethodName) { object collection = null; try { collection = GetProperty(instance, propertyName); if (collection == null) { return; } TryInvokeMethod(collection, updateMethodName); } finally { ReleaseComObject(collection); } } private static void TryUpdateCollectionItems(object instance, string propertyName, params string[] methodNames) { object collection = null; try { collection = GetProperty(instance, propertyName); if (collection == null) { return; } var countObject = GetProperty(collection, "Count"); var count = System.Convert.ToInt32(countObject); for (var index = 1; index <= count; index++) { object item = null; try { item = InvokeMethod(collection, "Item", index); foreach (var methodName in methodNames.Where(name => !string.IsNullOrWhiteSpace(name))) { TryInvokeMethod(item, methodName); } } finally { ReleaseComObject(item); } } } catch { } finally { ReleaseComObject(collection); } } private static void SafeCloseDocument(object document) { if (document == null) { return; } try { InvokeMethod(document, "Close", false); } catch { } } private static void SafeQuitApplication(object application) { if (application == null) { return; } try { InvokeMethod(application, "Quit", false); } catch { } } private static void ReleaseComObject(object instance) { try { if (instance != null && Marshal.IsComObject(instance)) { Marshal.FinalReleaseComObject(instance); } } catch { } } } }