first commit
This commit is contained in:
518
DCIT.Core/Services/OfficeDocConversionService.cs
Normal file
518
DCIT.Core/Services/OfficeDocConversionService.cs
Normal file
@@ -0,0 +1,518 @@
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user