使用字面 RGB 填滿的圖表序列不會跟隨活頁簿主題。主題發生變化時,序列仍然保留舊顏色。HotXLS 在二進位 XLS 中透過主題色彩圖表序列填滿處理這一點:在序列區塊的 AreaFormat 之後寫入一個 GelFrame 記錄,即 4198 或 $1066,其中攜帶 OfficeArt 配色索引和色調。Excel 隨後會像渲染自己寫出的主題填滿一樣渲染該序列
GelFrame 記錄號從哪裡來
GelFrame 記錄號是 4198($1066),而且你不會在該記錄自身的規範章節中找到它。[MS-XLS] 2.4.131 描述了 GelFrame 包含什麼,但與大多數記錄章節不同,它沒有說明 rt 值。圖表子串流 ABNF 也沒有幫助:它只給出 GELFRAME = 1*2GelFrame *Continue 這個產生式,只命名記錄而不編號。編號位於記錄號列舉表中,距離載荷說明章節有數頁。對編寫讀取器的人來說,這個產生式值得再看一遍:它允許一個或兩個 GelFrame 記錄,每個記錄後面還可以有 Continue 記錄,因此假定一個產生式只對應一筆記錄的解析器,會錯誤處理自己沒有寫出的檔案。HotXLS 為每個主題序列恰好發出一個 GelFrame,這是 Excel 對簡單純色主題填滿的輸出方式;其解碼器將記錄作為自包含載荷處理,而不是假定固定數量
GelFrame 載荷內部:兩個 OfficeArt 屬性表
GelFrame 載荷是兩個前後相接的 OfficeArt 屬性表:OfficeArtFOPT(稱為 OPT1)後跟 OfficeArtTertiaryFOPT(OPT2)。每張表由一個兩位元組屬性計數和對應數量的六位元組 FOPTE 項目組成,每個項目是一個兩位元組 opid 加一個四位元組 op。opid 的第 15 位是 fComplex:設定後,op 值表示位元組長度,固定項目之後還會跟著可變尾部。忽略這些尾部的解碼器會失去同步,在第一個複雜屬性之後為後續所有內容讀出垃圾 opid
主題填滿由分布在兩張表中的三個屬性以及一個宣告填滿類型的屬性表達。HotXLS 在 28 位元組中寫入四個屬性,不包含複雜尾部
- OPT1 中的
fillType$0180,設定為 1(msofillSolid) - OPT1 中的
fillColor$0181,表示舊版或不理解主題的消費者要繪製的扁平 RGB - OPT2 中的
fillColorExt$019E,表示基礎主題色彩 - OPT2 中的
fillColorExtMod$01A0,表示套用到基礎色彩上的色調或陰影
這種拆分是格式有意設計的,而不是實作偶然。[MS-ODRAW] 2.2.2 將主題三元組描述為扁平色彩、基礎色彩和修改量,因此理解主題的消費者會重新計算填滿,不理解主題的消費者仍然能繪製出合理內容。周圍的 opid 遵循同一模式,在舊版和目前版 [MS-ODRAW] 中編號也相同,這在交叉閱讀兩個版本時很方便:fillOpacity $0182、fillBackColor $0183、fillShadeType $019C、fillBackColorExt $01A2 和 fillBackColorExtMod $01A4
配色索引為什麼位於紅色位元組中
因為 OfficeArtCOLORREF 按位元組偏移定義,而不是按數值定義:紅色位於位元組 0,綠色位於位元組 1,藍色位於位元組 2,旗標位於位元組 3。把這個結構作為小端序 DWORD 讀取,而每個 FOPTE op 正是這樣讀取的,紅色就會成為最低有效位元組。[MS-ODRAW] 中的 lineColor 工作範例證實了這一點。因此 fSchemeIndex(旗標位 E)的數值是 $08000000,配色索引本身放入紅色位元組,綠色和藍色必須為零。Accent1 的 op 值因此是 $08000004,而不是 $00000004,更不是 $04000000
規範拒絕定義的主題索引順序
規範把配色索引順序稱為由主機定義,並不提供表格,這意味著僅憑位元組版面還不足以和 Excel 互通。HotXLS 使用試算表主題順序,這正是與真實 Excel 檔案往返時採用的順序
- 0 = lt1,1 = dk1,2 = lt2,3 = dk2
- 4 到 9 = accent1 到 accent6
- 10 = hlink,11 = folHlink
色調和陰影:MSOTINTSHADE 載荷
fillColorExtMod op 是一個 MSOTINTSHADE 值,它用一個 DWORD 同時編碼方向和幅度,而不是使用有符號分數。值 $20000000 表示未修改。變亮色調是 $02F4 shl 16 or amount shl 8 or $10(MSOTINT);變暗色調的形狀相同,只是高字使用 $01F4(MSOSHADE)。amount 位元組的方向違反直覺:$FF 表示不變,$00 表示完整修改。HotXLS 將其正規化為一個 DrawingML 風格的 double,正值變亮,負值變暗,使用加號或減號 (255 - amount) / 255。對於 Excel 介面實際提供的數值,這個映射是精確的,因此往返是無損而不是近似無損:「Lighter 40%」對應 amount 153,(255 - 153) / 255 在任一方向都得到 0.4,不發生捨入誤差。amount 為 191 的陰影會恢復為 -64/255。下面是限制在合法範圍內的編碼器
if Tint > 0 then // MSOTINT - 變亮
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - 變暗
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
在 Delphi 中設定和讀取主題填滿
寫入端的主題填滿是每個序列樣式記錄上的兩個額外欄位。TXLSChartSeriesStyleInfo 增加了 HasFillTheme、FillThemeColor 和 FillThemeTint,而且只有在 HasStyle 和 HasFillTheme 都設定時,建構器才會發出 GelFrame。如果同時設定明確 FillRgb,該值會原樣進入 OPT1 的 fillColor;如果沒有設定,HotXLS 會透過內建的預設 Office 主題表並套用色調自行扁平化色彩,因此只有主題的序列對忽略 OPT2 的消費者也有合理的扁平色彩。要注意 Default() 初始化,這很重要,因為 TXLSChartSeriesInfo 含有受管理欄位,否則它的普通 Boolean 成員就是堆疊垃圾
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // 絕不要對這個記錄使用 FillChar
Series[0].Name := 'Explicit';
Series[0].Categories := 'Data!$A$1:$A$2';
Series[0].Values := 'Data!$B$1:$B$2';
Series[0].HasStyle := True;
Series[0].Style.HasFill := True;
Series[0].Style.FillRgb := $C47244; // accent1,紅色位於低位元組
Series[0].Style.HasFillTheme := True;
Series[0].Style.FillThemeColor := 4; // accent1
Series[0].Style.FillThemeTint := 0.4; // Lighter 40%
Series[1] := Default(TXLSChartSeriesInfo);
Series[1].Name := 'ThemeOnly';
Series[1].Categories := 'Data!$A$1:$A$2';
Series[1].Values := 'Data!$C$1:$C$2';
Series[1].HasStyle := True;
Series[1].Style.HasFillTheme := True; // 沒有明確 RGB:會扁平化
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
讀取時會經過 HotXLS 其餘圖表檢查使用的同一個圖表模型。GetChartModel 回傳由呼叫方擁有、需要呼叫方釋放的 TXLSChartModel,每個 TXLSChartSeries 都會公開 HasFillTheme、FillThemeColor 和 FillThemeTint,同時還公開從 OPT1 fillColor 解碼的 FillRgb;對該序列而言,後者優先於 AreaFormat 色彩。同樣的三個值也會進入規範語意快照,成為 SolidFillThemeSet、SolidFillThemeColor 和 SolidFillThemeTint,因此活頁簿差異會把主題變化看作主題變化,而不是無法解釋的 RGB 漂移。如果你來自 XLSX 端,這就是HotXLS Delphi Excel 圖表、影像和繪圖指南中樣式的二進位格式對應物
Wb := TXLSWorkbook.Create;
try
Wb.Open('themed.xls');
Model := Wb.Sheets[2]._Chart.GetChartModel;
try
Ser := Model.GetSeries(0);
if Ser.HasFillTheme then
begin
WriteLn(Ser.FillThemeColor); // 4 = accent1
WriteLn(Ser.FillThemeTint:0:3); // 0.400
WriteLn(IntToHex(Ser.FillRgb, 6)); // C47244,OPT1 的 fillColor
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
二進位 XLS 中的主題填滿不承諾什麼
有三個誠實的限制。第一,也是審查這段程式碼的人最需要知道的:本地語料庫中沒有任何範例檔案包含 GelFrame 記錄。條件格式範例中出現的 11 個位元組對 66 10 都不在記錄邊界處,完整串流記錄傾印也找不到任何命中。這裡描述的位元版面來自規範,然後透過三種方式固定下來:對建構器輸出執行解碼對稱性檢查,使用手工建構的位元組測試將合成的 $1066 載荷直接送進解碼器,以及斷言精確的扁平 RGB。這比捕獲 Excel 檔案弱一層證據,因此值得說明,而不是暗示相反結論。第二,只有主題填滿的扁平化使用內建預設 Office 主題表,而不是讀取活頁簿中的主題元件,因為二進位 XLS 沒有類似封裝 XLSX 的主題元件——如果需要活頁簿自身的主題驅動扁平色彩,請自行提供 FillRgb。第三,解碼器只接受序列區塊中的 GelFrame;同一記錄也可能出現在圖表區域或軸框中,若在那裡接受,就會把背景填滿靜默歸因給序列,因此這些位置會被忽略。沒有 $08000000 旗標的 fillColorExt 同樣會被視為普通擴充色彩,不會設定 HasFillTheme。如果活頁簿中的圖表是在 XLSX 世界建立、只是經過這裡,那麼編輯 Excel 圖表而不遺失 ChartML的保留路徑更安全;這些記錄所在的容器見在 Delphi 中不使用 COM IStorage 讀取 OLE2 複合檔案
主題色彩圖表填滿、GelFrame 編碼器和解碼器,以及完整的 BIFF8 圖表子串流建構器,都包含在面向 Delphi 和 C++Builder 的 HotXLS Delphi 試算表元件中,無需安裝 Excel 即可讀寫 XLS、XLSX 和 ODS