Bài viết kỹ thuật

Màu theme chart Excel trong Delphi: GelFrame HotXLS

Series chart được fill bằng RGB literal sẽ không theo workbook theme. Đổi theme thì series vẫn giữ màu cũ. HotXLS xử lý điều này trong XLS nhị phân bằng fill chart theo theme: record GelFrame, 4198 hoặc $1066, ghi ngay sau AreaFormat trong series block và mang OfficeArt scheme index cùng tint. Excel sau đó render series như cách nó render themed fill do chính nó ghi

Record number của GelFrame đến từ đâu?

Record number của GelFrame là 4198 ($1066), và bạn sẽ không tìm thấy nó trong section specification của record. [MS-XLS] 2.4.131 mô tả GelFrame chứa gì, nhưng khác phần lớn record section, nó không nêu giá trị rt. Chart substream ABNF cũng không giúp được: nó chỉ cho production GELFRAME = 1*2GelFrame *Continue, gọi tên record nhưng không đánh số. Con số nằm trong bảng enumeration record number, cách section mô tả payload vài trang. Production đó đáng đọc lại với người viết reader: nó cho phép một hoặc hai GelFrame record, mỗi record có thể theo sau bởi Continue record, nên parser giả định mỗi production chỉ có một record sẽ xử lý sai file nó không tự ghi. HotXLS emit đúng một GelFrame mỗi themed series, đó là điều Excel tạo cho solid theme fill đơn giản, còn decoder coi record là payload tự chứa thay vì giả định count cố định

Bên trong GelFrame payload: hai bảng property OfficeArt

GelFrame payload là hai bảng property OfficeArt nối tiếp: một OfficeArtFOPT (gọi là OPT1) theo sau bởi OfficeArtTertiaryFOPT (OPT2). Mỗi bảng là property count hai byte theo sau bởi số entry FOPTE dài sáu byte, mỗi entry gồm opid hai byte cộng op bốn byte. Bit 15 của opidfComplex: khi bật, value op là byte length và có variable tail theo sau fixed entry. Decoder bỏ qua tail sẽ desynchronize rồi đọc rác opid cho mọi thứ sau complex property đầu tiên

Theme fill được biểu diễn bằng ba property trải trên cả hai table, cộng một property khai báo fill kind. HotXLS ghi bốn property trong 28 byte, không có complex tail:

  • fillType $0180 trong OPT1, đặt bằng 1 (msofillSolid)
  • fillColor $0181 trong OPT1, RGB phẳng mà consumer cũ hoặc không hiểu theme sẽ vẽ
  • fillColorExt $019E trong OPT2, theme color gốc
  • fillColorExtMod $01A0 trong OPT2, tint hoặc shade áp dụng lên base

Việc tách đó là chủ ý của format chứ không phải tai nạn implementation: [MS-ODRAW] 2.2.2 mô tả theme triple là flat color cộng base color cộng modification, để consumer hiểu theme tính lại fill còn consumer không hiểu vẫn vẽ được thứ hợp lý. Các opid xung quanh theo cùng pattern và mang numbering giống nhau trong bản [MS-ODRAW] cũ lẫn hiện tại, thuận tiện khi đọc chéo hai revision: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 và fillBackColorExtMod $01A4

Vì sao scheme index nằm trong byte đỏ?

Vì OfficeArtCOLORREF được định nghĩa theo byte offset chứ không theo numeric value: red ở byte 0, green ở byte 1, blue ở byte 2, flag ở byte 3. Đọc structure đó như DWORD little-endian, đúng cách mọi FOPTE op được đọc, red trở thành byte ít quan trọng nhất. Ví dụ lineColor trong [MS-ODRAW] xác nhận điều này. Vì vậy fSchemeIndex, là flag bit E, có numeric value $08000000, còn scheme index đi vào red byte với green và blue bắt buộc bằng zero. Accent1 vì thế là op value $08000004, không phải $00000004 và càng không phải $04000000

Thứ tự theme index mà specification từ chối định nghĩa

Specification gọi thứ tự scheme index là host-defined và không đưa table nào, nghĩa là byte layout không đủ để interop với Excel. HotXLS dùng thứ tự spreadsheet theme, là thứ round-trip với file Excel thật:

  • 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
  • 4 đến 9 = accent1 đến accent6
  • 10 = hlink, 11 = folHlink

Tint và shade: payload MSOTINTSHADE

fillColorExtMod op là value MSOTINTSHADE, encode direction và amount trong một DWORD thay vì signed fraction. Value $20000000 nghĩa là không modified. Tint làm sáng là $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); tint làm tối có cùng shape với $01F4 ở high word (MSOSHADE). Byte amount đi ngược trực giác: $FF nghĩa là unchanged còn $00 nghĩa là full modification. HotXLS normalize nó thành một double kiểu DrawingML, positive là làm sáng và negative là làm tối, dùng cộng hoặc trừ (255 - amount) / 255. Mapping chính xác với value Excel thực sự cung cấp trong UI, nhờ đó round-trip lossless chứ không phải gần lossless: "Lighter 40%" quen thuộc là amount 153, còn (255 - 153) / 255 là 0.4 không có rounding error theo cả hai hướng. Shade với amount 191 trở lại thành -64/255. Đây là encoder, đã clamp về range hợp lệ:

if Tint > 0 then                       // MSOTINT - làm sáng
  TintOp := LongWord($02F4) shl 16 or
    (LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then                  // MSOSHADE - làm tối
  TintOp := LongWord($01F4) shl 16 or
    (LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
  TintOp := $20000000;                 // MSOCOLORMODUNDEFINED

Set và đọc theme fill từ Delphi

Phía write, theme fill là hai field thêm vào per-series style record. TXLSChartSeriesStyleInfo có thêm HasFillTheme, FillThemeColorFillThemeTint, còn builder chỉ emit GelFrame khi cả HasStyleHasFillTheme đều bật. Nếu bạn cũng set FillRgb tường minh, value đó đi nguyên vào OPT1 fillColor; nếu không, HotXLS tự flatten color qua default Office theme table tích hợp và áp tint, để series chỉ có theme vẫn có flat color hợp lý cho consumer bỏ qua OPT2. Hãy chú ý initialization bằng Default(), vì TXLSChartSeriesInfo chứa managed field còn Boolean thường nếu không sẽ là rác trên stack:

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';

    Series[0] := Default(TXLSChartSeriesInfo);   // không bao giờ FillChar record này
    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, red ở byte thấp
    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;        // không có RGB tường minh: đã flatten
    Series[1].Style.FillThemeColor := 8;         // accent5

    Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
    Wb.SaveAs('themed.xls');
  finally
    Wb.Free;
  end;
end;

Đọc lại đi qua cùng chart model mà phần còn lại của HotXLS chart inspection dùng. GetChartModel trả về TXLSChartModel owned mà bạn phải free, còn mỗi TXLSChartSeries expose HasFillTheme, FillThemeColorFillThemeTint cạnh FillRgb decode từ OPT1 fillColor, value này có precedence hơn AreaFormat color của series. Cùng ba value cũng đi tới canonical semantic snapshot dưới tên SolidFillThemeSet, SolidFillThemeColorSolidFillThemeTint, nên workbook diff thấy theme change là theme change thay vì RGB drift không giải thích được. Nếu bạn đi từ phía XLSX, đây là counterpart binary-format của style được mô tả trong hướng dẫn HotXLS về chart, image và drawing Excel trong Delphi:

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;

Theme fill trong binary XLS không hứa hẹn điều gì?

Có ba giới hạn trung thực. Thứ nhất và quan trọng nhất với người audit code này: không sample file nào trong corpus cục bộ có record GelFrame. Mười một occurrence của cặp byte 66 10 trong conditional-formatting sample nằm ở non-record boundary, còn full-stream record dump tìm thấy zero hit. Bit layout mô tả ở đây được suy ra từ specification rồi pin theo ba cách, bằng decode symmetry trên builder output, byte test dựng tay feed synthetic $1066 payload thẳng vào decoder và assert flat RGB chính xác. Đây là bằng chứng yếu hơn captured Excel file và nên nói rõ thay vì ngụ ý ngược lại. Thứ hai, flatten cho fill chỉ có theme dùng default Office theme table tích hợp chứ không đọc theme part từ workbook vì binary XLS không có theme part theo nghĩa XLSX đóng gói — nếu cần theme riêng của workbook điều khiển flat color, hãy tự cung cấp FillRgb. Thứ ba, decoder chỉ nhận GelFrame bên trong series block; cùng record có thể xuất hiện ở chart area hoặc axis frame, chấp nhận nó ở đó sẽ âm thầm gán background fill cho series nên các trường hợp đó bị ignore. fillColorExt không có flag $08000000 cũng được coi là extended color thường và không bao giờ set HasFillTheme. Với workbook chart được author ở thế giới XLSX rồi chỉ đi xuyên qua, preservation path trong edit Excel chart mà không mất ChartML an toàn hơn, còn container chứa record này được nói trong đọc OLE2 compound file trong Delphi không cần COM IStorage

Theme-colored chart fill, GelFrame encoder và decoder cùng full BIFF8 chart substream builder là một phần của HotXLS Delphi spreadsheet component cho Delphi và C++Builder, đọc và ghi XLS, XLSX cùng ODS mà không cần Excel cài đặt