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
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|