Files
DCIT/DCIT.Core/Services/OfficeDocConversionService.cs
2026-07-13 10:04:36 +08:00

518 lines
18 KiB
C#
Raw Permalink 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.
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
{
}
}
}
}