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