利用 Word 自訂屬性夾帶資訊
2 |
先說說應用情境:我有個外部傳入的結構化資料需要套表產生 Word 表格,使用者預先做好範本 Word 檔,調好表格大小、文字對齊、字型顏色樣式... 等等:
理想目標是傳入包含編號、分類、廠牌型號、數量等屬性的物件陣列轉成 Word 表格。
using System.Collections.Generic;
using System.IO;
namespace EmbDataToDocx
{
class Program
{
static void Main(string[] args)
{
var data = new List<EquipItem>()
{
new EquipItem()
{
Id = "NB001",
Catg = EquipCatgs.筆電,
BrandModel = "Lenovo Thinkpad T470p",
Qty = 1
},
new EquipItem()
{
Id = "MC001",
Catg = EquipCatgs.滑鼠,
BrandModel = "Logitech Anywhere2S",
Qty = 1
},
new EquipItem()
{
Id = "PT001",
Catg = EquipCatgs.印表機,
BrandModel = "FX DocuPrint M225dw",
Qty = 1
}
};
var docx = DemoDocxListGenerator.GenerateList(data);
File.WriteAllBytes("D:\\Test.docx", docx);
}
}
}
結果文件範例如下:
這個 Word 檔可當成電子化作業流程的附件,必要時輸出紙本也很美觀大方,且由於範本全由使用者自訂,可符合規章制度的特殊要求。
而在這個應用裡,我希望 Word 檔具備多用途,表格部分供肉眼閱讀,匯入系統時則可從中擷取資料加以利用。之前已介紹過使用 .NET 程式擷取 Word 表格內容的技巧,用 OpenXML SDK 解析表格不是難事。但在這個案例中,Word 表格由資料物件轉換而來,再解析 Word 表格還原回來源物件等於繞了一圈,像是資料交換先印成紙本再 OCR 一般好笑。因此,我有個點子 - 設法在 Word 檔內嵌原始資料 JSON,擷取資料時直接取出 JSON 還原即可。
研究了一下,Word 文件提共所謂的自訂屬性(Csutom Property),MSDoc 有篇詳細介紹 -Set a custom property in a word processing document (Open XML SDK),自訂屬性支援日期、整數、浮點數、文字、布林等型別,複雜資料轉成 XML 或 JSON 即可當成文字儲存,故可滿足各式資料需求。
官方文件介紹得很詳細,我如法炮製了一個 AddDataJsonToDocx 方法,為 docx 加入一個名為 DataJson 的客製屬性存入資料的 JSON 內容:
public static byte[] AddDataJsonToDocx(byte[] docx, object data)
{
using (var ms = new MemoryStream())
{
ms.Write(docx, 0, docx.Length);
using (var wd = WordprocessingDocument.Open(ms, true))
{
var custProps = wd.CustomFilePropertiesPart;
if (custProps == null)
{
custProps = wd.AddCustomFilePropertiesPart();
custProps.Properties = new DocumentFormat.OpenXml.CustomProperties.Properties();
}
custProps.Properties.Append
(new DocumentFormat.OpenXml.CustomProperties.CustomDocumentProperty()
{
VTLPWSTR = new DocumentFormat.OpenXml.VariantTypes.VTLPWSTR(
JsonConvert.SerializeObject(data)),
Name = "DataJson",
//自訂屬性的專屬FormatId
FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}",
PropertyId = custProps.Properties.Count() + 2
});
wd.Save();
return ms.ToArray();
}
}
}
呼叫程式稍加修改:
var docx = DemoDocxListGenerator.GenerateList(data);
var docxUpd = DemoDocxListGenerator.AddDataJsonToDocx(docx, data);
File.WriteAllBytes("D:\\Test.docx", docxUpd);
自訂屬性從 Word 也能查詢,方法是從「檔案」/「資訊」/「摘要資訊」開啟「進階摘要資訊」:
下方將會出現自訂屬性:
不過我的 DataJson 不是給人看的,是給程式讀的。下面展示如何用程式讀取自訂屬性,這裡用 PowerShell 示範:(PowerShell 直接內嵌 C# 程式碼的說明可參考前文)
Param ([Parameter(Mandatory=$true)][string]$docxPath)
$ErrorActionPreference = "STOP"
Add-Type -Path "$PSScriptRoot\DocumentFormat.OpenXml.dll"
Add-Type -TypeDefinition @"
using System.IO;
using System.Linq;
using System;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocumentFormat.OpenXml.CustomProperties;
public class DocxCustPropReader
{
public static string ReadDataJson(string srcPath)
{
using (var ms = new MemoryStream(File.ReadAllBytes(srcPath)))
{
using (var doc = WordprocessingDocument.Open(ms, false))
{
if (doc.CustomFilePropertiesPart == null)
return `"No custom property found.`";
var custProp = doc.CustomFilePropertiesPart.Properties
.Select(o => new CustomDocumentProperty(o.OuterXml))
.FirstOrDefault(o => o.Name == `"DataJson`");
if (custProp != null) return custProp.VTLPWSTR.Text;
return `"Custom property DataJson not found!`";
}
}
}
}
"@ -Language CSharp -ReferencedAssemblies ("$PSScriptRoot\DocumentFormat.OpenXml.dll","$PSScriptRoot\WindowsBase.dll")
[DocxCustPropReader]::ReadDataJson($docxPath)
大成功!
留下小問題:夾帶原始資料固可省去解析表格內容的困擾,但一旦表格內容被使用者更改,便有表格內容(使用者認知)與資料 JSON (程式解讀)不一致的風險。若文件被定義為唯讀,最簡單的解法是為文件加上防修改保護,這部分留到下次再聊。
Example of setting custom porperty of docx to store extra data.
Comments
# by 黃R
第一個超連結有誤,404error@@
# by Jeffrey
to 黃R,修正了,謝謝提醒。