Chart series, заповнена literal RGB, не наслідує workbook theme. Змініть theme — і series збереже старий color. HotXLS обробляє це в binary XLS через theme-colored chart series fills: record GelFrame, 4198 або $1066, записаний відразу після AreaFormat усередині series block і такий, що містить OfficeArt scheme index плюс tint. Excel тоді рендерить series так само, як themed fill, записаний ним самим
Звідки береться record number GelFrame?
Record number GelFrame — 4198 ($1066), і в його власній specification section ви його не знайдете. [MS-XLS] 2.4.131 описує, що містить GelFrame, але, на відміну від більшості record sections, не вказує value rt. Chart substream ABNF теж не допомагає: він дає лише production GELFRAME = 1*2GelFrame *Continue, називаючи record без numbering. Number живе в record-number enumeration table, за кілька pages від section, що документує payload. На цю production варто поглянути вдруге кожному, хто пише reader: вона дозволяє один або два GelFrame records, кожен із optional Continue records після нього, тому parser, який припускає один record на production, неправильно обробить file, якого сам не записував. HotXLS emit-ить рівно один GelFrame per themed series, як це робить Excel для простого solid theme fill, а decoder трактує record як self-contained payload і не припускає fixed count
Усередині GelFrame payload: дві OfficeArt property tables
GelFrame payload — це дві OfficeArt property tables одна за одною: OfficeArtFOPT (названа OPT1), потім OfficeArtTertiaryFOPT (OPT2). Кожна table — це two-byte property count і стільки six-byte FOPTE entries, кожен entry має two-byte opid і four-byte op. Bit 15 у opid — це fComplex: коли він set, value op є byte length, і після fixed entries іде variable tail. Decoder, який ігнорує ці tails, desynchronize-иться і читає garbage opids для всього після першої complex property
Theme fill виражається трьома properties, розподіленими між обома tables, плюс ще однією, що оголошує fill kind. HotXLS записує чотири properties у 28 bytes без complex tails:
fillType$0180 в OPT1, встановлений у 1 (msofillSolid)fillColor$0181 в OPT1, flattened RGB, який намалює старший або theme-unaware consumerfillColorExt$019E в OPT2, base theme colorfillColorExtMod$01A0 в OPT2, tint або shade, застосований до base
Цей split навмисний і заданий format, а не випадковість implementation: [MS-ODRAW] 2.2.2 описує theme triple як flat color плюс base color плюс modification, тому consumer, що розуміє themes, перераховує fill, а той, що не розуміє, усе одно малює щось розумне. Навколишні opids дотримуються тієї самої pattern і мають ідентичне numbering у старому та current [MS-ODRAW] editions, що зручно під час cross-reading двох revisions: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 і fillBackColorExtMod $01A4
Чому scheme index лежить у red byte?
Тому що OfficeArtCOLORREF визначено byte offset, а не numeric value: red у byte 0, green у byte 1, blue у byte 2, flags у byte 3. Прочитайте цю structure як little-endian DWORD, як це робить кожен FOPTE op, і red стане least significant byte. Worked lineColor example в [MS-ODRAW] це підтверджує. Тому fSchemeIndex, який є flags bit E, має numeric value $08000000, а scheme index сам лежить у red byte, тоді як green і blue мають бути zero. Отже Accent1 — це op value $08000004, а не $00000004 і точно не $04000000
Порядок theme index, який specification відмовляється визначати
Specification називає order scheme index host-defined і не дає table, тобто для interop з Excel одного byte layout недостатньо. HotXLS використовує spreadsheet theme order, який round-trips із реальними Excel files:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4–9 = accent1–accent6
- 10 = hlink, 11 = folHlink
Tint і shade: payload MSOTINTSHADE
Op fillColorExtMod є значенням MSOTINTSHADE і кодує direction та amount в одному DWORD, а не як signed fraction. Value $20000000 означає unmodified. Lightening tint — $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); darkening tint має ту саму shape з $01F4 у high word (MSOSHADE). amount byte іде проти intuition: $FF означає unchanged, а $00 — full modification. HotXLS normalize-ить це в один DrawingML-style double, де positive lightens, а negative darkens, використовуючи плюс або мінус (255 - amount) / 255. Mapping exact для values, які фактично пропонує Excel UI, тому round-trip lossless, а не approximately lossless: знайомий "Lighter 40%" — це amount 153, і (255 - 153) / 255 дорівнює 0.4 без rounding error в жоден бік. Shade з amount 191 повертається як -64/255. Ось encoder, clamped до legal range:
if Tint > 0 then // MSOTINT - lighter
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - darker
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Як встановити й прочитати theme fill із Delphi
На write side theme fill — це два extra fields у per-series style record. TXLSChartSeriesStyleInfo отримав HasFillTheme, FillThemeColor і FillThemeTint, а builder emit-ить GelFrame лише коли одночасно set HasStyle і HasFillTheme. Якщо ви також встановите explicit FillRgb, це value потрапить у OPT1 fillColor verbatim; якщо ні, HotXLS сам flatten-ить color через built-in default Office theme table із застосованим tint, тому theme-only series усе одно має sane flat color для consumers, які ігнорують OPT2. Зверніть увагу на Default() initialization, яка важлива, бо TXLSChartSeriesInfo містить managed fields, а plain Boolean members інакше були б stack garbage:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // ніколи не FillChar this record
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 у low byte
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; // без explicit RGB: flattened
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
Reading назад проходить через той самий chart model, який використовує решта HotXLS chart inspection. GetChartModel повертає owned TXLSChartModel, який ви звільняєте, а кожен TXLSChartSeries expose-ить HasFillTheme, FillThemeColor і FillThemeTint поруч із FillRgb, decoded із OPT1 fillColor, який має precedence над AreaFormat color для цієї series. Ті самі три values також доходять до canonical semantic snapshot як SolidFillThemeSet, SolidFillThemeColor та SolidFillThemeTint, тому workbook diff бачить theme change як theme change, а не як unexplained RGB drift. Якщо ви приходите з XLSX side, це binary-format counterpart styling, описаного в HotXLS guide про Excel charts, images і drawings у 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 у binary XLS не гарантує?
Три чесні limits. Перший і найважливіший для кожного, хто audit-ить цей code: жоден sample file у local corpus взагалі не містить GelFrame record. Одинадцять occurrences byte pair 66 10 у conditional-formatting sample лежать на non-record boundaries, а full-stream record dump знаходить zero hits. Описаний тут bit layout виведено зі specification, а потім зафіксовано трьома способами: decode symmetry на builder output, hand-built byte tests, що подають synthetic $1066 payload прямо в decoder, і assertion exact flattened RGB. Це слабший evidence, ніж captured Excel file, і краще сказати про це, ніж створювати інше враження. Другий: flattening для theme-only fill використовує built-in default Office theme table, а не theme part, прочитаний із workbook, бо binary XLS не має theme part у сенсі packaged XLSX — якщо потрібен власний workbook theme для flat color, передайте FillRgb самі. Третій: decoder приймає GelFrame лише всередині series block; той самий record може стояти на chart area або axis frame, і приймати його там означало б тихо приписати background fill series, тому такі records ігноруються. fillColorExt без прапорця $08000000 також трактують як plain extended color і він ніколи не встановлює HasFillTheme. Для workbooks, chart яких створено у XLSX world і який лише проходить через цей шлях, safer route — preservation path у редагуванні Excel charts без втрати ChartML, а container, де лежать ці records, описано в читанні OLE2 compound files у Delphi без COM IStorage
Theme-colored chart fills, GelFrame encoder і decoder та full BIFF8 chart substream builder постачаються у HotXLS Delphi spreadsheet component для Delphi та C++Builder, який читає й записує XLS, XLSX та ODS без установленого Excel