技術文章

從 Delphi 寫出符合 ISO 29500 Strict 規範的 XLSX 檔案

HotXLS 只要在儲存前設定一個屬性 StrictOOXML,就能從 Delphi 與 C++Builder 寫出符合 ISO/IEC 29500 Strict 規範的 Open XML 活頁簿。套件中的每個部件,從 xl/workbook.xml 一路到關聯檔案與內容類型,都會改用嚴格版的 purl.oclc.org 詞彙來寫入,而不是過渡版的 schemas.openxmlformats.org 詞彙,而 Strict 規範不允許的功能會在儲存時明確拋出例外,而不是照樣寫出

多數開發者是透過採購文件遇到這項需求。若干司法轄區的公部門標案,要求提供 ISO 標準化版本的 Open XML,而不是 Office 預設寫出的過渡版本,強制要求 ISO 29500 Strict 的檔案庫,即使 Excel 能完美開啟一份普通的 .xlsx,也仍會拒絕它。過渡版命名空間的存在,是為了相容舊有的二進位行為;嚴格版命名空間才是標準本身

Strict 與 Transitional 之間到底差在哪裡?

看得見的差異在詞彙。嚴格版活頁簿部件將 http://purl.oclc.org/ooxml/spreadsheetml/main 宣告為根命名空間,關聯參照則用 http://purl.oclc.org/ooxml/officeDocument/relationships,套件內任何地方都不得殘留任何過渡版命名空間。關聯類型也隨之改變,因此根關聯部件命名的是 .../ooxml/officeDocument/relationships/officeDocument,而非熟悉的 openxmlformats 對應版本,延伸屬性類型則採駝峰式命名為 extendedProperties

看不見的差異在範圍。Strict 刻意省略了過渡版結構描述中僅為了往返回寫舊版二進位檔案而存在的部分,以及 Office 後來加上的廠商延伸功能。這正是為什麼這項轉換不能靠字串搜尋取代來完成:有些功能根本沒有嚴格版拼法,一律不得寫出

開啟這項功能

一般的撰寫程式碼完全不用改。照平常方式建置活頁簿、設定旗標、儲存即可:

var
  Wb: TXLSXWorkbook;
  Sh: TXLSXWorksheet;
begin
  Wb := TXLSXWorkbook.Create;
  try
    Sh := Wb.Sheets.Add('Data');
    Sh.Cells[1, 1].Value := 'Product';
    Sh.Cells[1, 2].Value := 'Amount';
    Sh.Cells[2, 1].Value := 'Widget';
    Sh.Cells[2, 2].Value := 17;
    Sh.Cells[3, 2].Formula := '=SUM(B2:B2)';

    Wb.StrictOOXML := True;          // 輸出 ISO/IEC 29500 Strict
    if Wb.SaveAs('archive-copy.xlsx') <> 1 then
      raise Exception.Create('strict save failed');
  finally
    Wb.Free;
  end;
end;

這個旗標會在每次儲存作業開始時重設,並重新從活頁簿屬性指派,因此某次儲存過程中發生的例外,不會讓嚴格模式外洩到下一次儲存。這個細節在同一個活頁簿物件要服務多個匯出要求的伺服器行程中特別重要

為什麼嚴格模式的儲存會拒絕執行?

有四類功能是微軟延伸功能,沒有對應的 ISO 29500 Strict 版本,HotXLS 會在儲存時拋出例外,而不是輸出一個自稱符合嚴格規範、實際上並不符合的套件:

// Strict 輸出無法內嵌 VBA 專案
//   -> 啟用巨集的活頁簿要以過渡版 .xlsm 儲存
// Strict 輸出無法承載表單控制項
//   -> 按鈕、核取方塊、下拉方塊及其 ctrlProps
// Strict 輸出無法承載討論串留言
//   -> 現代的 persons/threads 模型,而非傳統註解
// Strict 輸出無法承載動態陣列中繼資料
//   -> 透過中繼資料部件記錄的溢出範圍

在此大聲失敗是正確的取捨。悄悄遺失的 VBA 專案,會把一份能正常運作的活頁簿變成一份仍能開啟、實則已損壞的檔案,而失敗回報要等到數週後才會從使用者那邊傳來。例外會在呼叫端程式碼仍記得自己正在匯出什麼的當下,明確點出是哪項功能、該改哪個屬性。過渡版路徑上的巨集與外部連結保留,說明於 保留 VBA 專案與外部連結 一文

另有兩類延伸功能的處理方式不同,值得了解原因。資料橫條、走勢圖等類似功能存在於 x14xm 詞彙中,圖片的 SVG 變體則存在於 c15。這些延伸清單內容的命名空間本身具有自我描述性,一般試算表剖析器能容忍它們,也沒有對應的 ISO 版本可供轉換。HotXLS 選擇保留它們,而不是把使用者的內容丟到地上。若你的流程中有驗證工具對延伸功能也採嚴格標準(而不只是命名空間),請在匯出前先從來源活頁簿移除這些功能

轉譯必須觸及一般人不會改動的部件

嚴格輸出真正有趣的工程問題不在工作表 XML,而在那些一個追求速度的寫入器寧可原樣複製的部件。HotXLS 會直接複製佈景主題、連線、外部連結、圖表與樞紐分析區塊原本壓縮後的位元組,就保真度而言這正是應該做的事,就嚴格輸出而言卻恰好相反,因為複製的位元組帶著過渡版命名空間

StrictOOXML 之下,這五條保留路徑會切換為重建或轉譯式回放,繞過位元組複製的快速路徑。所有 XML 都會經過同一個轉譯程序,該程序以雙引號包住的屬性值作為錨點,因此儲存格文字裡看起來像 URI 的字串,絕不會被意外改寫。儲存格文字中若含有相同的 URI,會在 XML 中被跳脫為實體,因此錨定式替換看不到它。串流寫入器會先轉譯其骨架,再從 sheetData 處拆分,因為列區塊完全不含任何詞彙 URI。保留路徑的相關機制說明於 佈景主題、延伸清單與 calcChain 的無損往返 一文

讀取 Excel 儲存為嚴格版本的檔案

輸出只是故事的一半。Excel 提供「嚴格 Open XML 試算表」作為儲存選項,以此方式產生的檔案必須能正確開啟。HotXLS 會在套件中每個剖析關聯的位置,包含根、外部連結、工作表、繪圖與樞紐分析表,把關聯類型正規化,使嚴格版關聯類型能對應到與過渡版對應項相同的內部常數

讀取端的對應做法是命名空間前綴正規化,讓任意前綴與兩種詞彙都能解析回一張正規名稱表。這項功能對一般檔案跟嚴格版檔案都有幫助,因為第三方產生器綁定前綴的方式各不相同,其機制與 XLSX 套件中的 OPC 關聯解析 一文所述相同

出貨嚴格輸出前的簡短檢查清單

用套件本身驗證,而不是用 Excel 驗證。Excel 兩種形式都能順利開啟,因此成功開啟並不能證明任何合規性。解壓縮結果並確認 xl/workbook.xml 宣告了 purl 命名空間,確認沒有任何部件含有 schemas.openxmlformats.org/spreadsheetml,並確認 _rels/.relsxl/_rels/workbook.xml.rels 中的關聯類型使用嚴格版形式

接著透過 HotXLS 重新開啟該檔案,比對數值、公式、格式與超連結是否與來源相符。回讀測試是唯一低成本的方法,能證明轉譯過程沒有損壞內容,同時也會一併驗證讀取端的正規化。若你的活頁簿帶有圖表,也一併檢查,因為圖表部件正是嚴格模式下會切換為重建路徑的保留部件之一

嚴格輸出、容錯讀取與無損保留,全都是同一套 Delphi 與 C++Builder 用 OOXML 引擎的一部分;完整功能清單列於 HotXLS Delphi 試算表元件頁面