Files
DCIT/DCIT.Core/Services/OfficeDocConversionService.cs

518 lines
18 KiB
C#
Raw Normal View History

2026-07-13 10:04:36 +08:00
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<string>();
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<string>();
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<string> GetOfficeAutomationProgIds()
{
yield return "Word.Application";
foreach (var progId in WpsProgIds)
{
yield return progId;
}
}
protected virtual string ResolveDocToPath()
{
var candidates = new List<string>();
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
{
}
}
}
}