Un chart series umplut cu un RGB literal nu urmărește workbook theme. Schimbă theme-ul și series-ul păstrează culoarea veche. HotXLS tratează asta în XLS binar cu fill-uri de chart colorate de theme: un record GelFrame, 4198 sau $1066, scris imediat după AreaFormat în series block și care poartă un OfficeArt scheme index plus un tint. Excel redă apoi series-ul la fel cum redă un fill themed scris de el însuși
De unde vine numărul record-ului GelFrame?
Numărul record-ului GelFrame este 4198 ($1066) și nu îl vei găsi în propria secțiune de specificație a record-ului. [MS-XLS] 2.4.131 descrie ce conține un GelFrame, dar, spre deosebire de majoritatea secțiunilor de records, nu spune valoarea rt. Nici ABNF-ul chart substream-ului nu ajută: oferă doar production-ul GELFRAME = 1*2GelFrame *Continue, care numește record-ul fără să îl numeroteze. Numărul stă în tabelul de enumerare a record numbers, la câteva pagini distanță de secțiunea care documentează payload-ul. Production-ul merită o a doua privire pentru orice reader: permite unul sau două records GelFrame, fiecare urmat opțional de Continue records, așa că un parser care presupune un singur record per production va trata greșit un fișier pe care nu l-a scris. HotXLS emite exact un GelFrame per series themed, ceea ce produce Excel pentru un fill theme solid simplu, iar decoder-ul tratează record-ul ca payload self-contained, fără să presupună un count fix
În interiorul payload-ului GelFrame: două property tables OfficeArt
Payload-ul GelFrame este format din două property tables OfficeArt consecutive: un OfficeArtFOPT (numit OPT1) urmat de un OfficeArtTertiaryFOPT (OPT2). Fiecare tabel este un property count pe doi bytes urmat de atâtea FOPTE entries de șase bytes, iar fiecare entry este un opid pe doi bytes plus un op pe patru bytes. Bitul 15 al lui opid este fComplex: când este setat, valoarea op este o lungime în bytes și urmează un tail variabil. Un decoder care ignoră aceste tails se desincronizează și citește opids garbage pentru tot ce urmează după prima proprietate complexă
Fill-ul theme este exprimat prin trei properties răspândite în ambele tables, plus una care declară tipul fill-ului. HotXLS scrie patru properties în 28 de bytes, fără tails complexe:
fillType$0180 în OPT1, setat la 1 (msofillSolid)fillColor$0181 în OPT1, RGB-ul flattened pe care îl va desena un consumer mai vechi sau care nu cunoaște themefillColorExt$019E în OPT2, culoarea theme de bazăfillColorExtMod$01A0 în OPT2, tint-ul sau shade-ul aplicat bazei
Această împărțire este intenționată în format, nu un accident al implementării: [MS-ODRAW] 2.2.2 descrie theme triple ca o culoare flat plus o culoare de bază plus o modificare, astfel încât un consumer care înțelege theme-urile recalculează fill-ul, iar unul care nu le înțelege pictează totuși ceva rezonabil. Opids din jur urmează același pattern și poartă numerotare identică în edițiile veche și curentă ale [MS-ODRAW], lucru convenabil când citești în paralel două revizii: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 și fillBackColorExtMod $01A4
De ce stă scheme index-ul în byte-ul red?
Deoarece un OfficeArtCOLORREF este definit prin byte offset, nu prin valoare numerică: red la byte 0, green la byte 1, blue la byte 2, flags la byte 3. Citește structura ca un DWORD little-endian, așa cum este fiecare op FOPTE, și red devine least significant byte. Exemplul lucrat pentru lineColor din [MS-ODRAW] confirmă asta. Prin urmare fSchemeIndex, care este flag-ul E, are valoarea numerică $08000000, iar scheme index-ul însuși intră în byte-ul red, cu green și blue obligatoriu zero. Accent1 este deci valoarea op $08000004, nu $00000004 și cu atât mai puțin $04000000
Ordinea theme index pe care spec-ul refuză să o definească
Specificația numește ordinea scheme index-ului host-defined și nu oferă niciun tabel, ceea ce înseamnă că layout-ul bytes-ilor nu este suficient pentru interoperabilitate cu Excel. HotXLS folosește ordinea theme a spreadsheet-ului, cea care face round-trip cu fișiere Excel reale:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 până la 9 = accent1 până la accent6
- 10 = hlink, 11 = folHlink
Tint și shade: payload-ul MSOTINTSHADE
Op-ul fillColorExtMod este o valoare MSOTINTSHADE și codifică direcția și cantitatea într-un singur DWORD, nu ca fracție signed. Valoarea $20000000 înseamnă unmodified. Un tint de lightening este $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); un tint de darkening are aceeași formă, cu $01F4 în high word (MSOSHADE). Byte-ul amount merge invers față de intuiție: $FF înseamnă unchanged, iar $00 înseamnă modificarea completă. HotXLS normalizează asta la un singur double în stil DrawingML, unde pozitiv înseamnă lightening și negativ darkening, folosind plus sau minus (255 - amount) / 255. Maparea este exactă pentru valorile oferite efectiv de Excel în UI, de aceea round-trip-ul este lossless, nu aproximativ lossless: „Lighter 40%” familiar este amount 153, iar (255 - 153) / 255 este 0.4 fără eroare de rotunjire în nicio direcție. Un shade cu amount 191 revine ca -64/255. Iată encoder-ul, limitat la intervalul legal:
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
Setarea și citirea unui theme fill din Delphi
Pe write side, un theme fill este reprezentat de două fields suplimentare în record-ul de style per series. TXLSChartSeriesStyleInfo a primit HasFillTheme, FillThemeColor și FillThemeTint, iar builder-ul emite GelFrame doar când HasStyle și HasFillTheme sunt ambele setate. Dacă setezi și un FillRgb explicit, acea valoare intră în fillColor OPT1 exact cum este; dacă nu, HotXLS face flatten-ul culorii printr-un default Office theme table inclus, cu tint-ul aplicat, astfel încât un series theme-only are în continuare o culoare flat sănătoasă pentru consumatori care ignoră OPT2. Observă inițializarea Default(), care contează deoarece TXLSChartSeriesInfo conține fields managed, iar membrii lui Boolean simpli ar fi altfel garbage din stack:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // nu folosi niciodată FillChar pentru acest 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 în byte-ul low
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; // fără RGB explicit: flattened
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
Citirea înapoi trece prin același chart model folosit de restul inspecției chart-urilor HotXLS. GetChartModel returnează un TXLSChartModel owned pe care îl eliberezi, iar fiecare TXLSChartSeries expune HasFillTheme, FillThemeColor și FillThemeTint alături de FillRgb decodat din fillColor OPT1, care are prioritate față de culoarea AreaFormat pentru acel series. Aceleași trei valori ajung și în snapshot-ul semantic canonical ca SolidFillThemeSet, SolidFillThemeColor și SolidFillThemeTint, așa că un workbook diff vede o schimbare de theme ca schimbare de theme, nu ca un RGB drift inexplicabil. Dacă vii din partea XLSX, acesta este omologul formatului binar al styling-ului descris în ghidul HotXLS pentru chart-uri, imagini și drawings Excel în 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, fillColor-ul OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
Ce nu promite un theme fill în XLS binar?
Trei limite oneste. Prima și cea mai importantă pentru oricine auditează acest cod: niciun sample file din corpus-ul local nu conține deloc un record GelFrame. Cele unsprezece apariții ale perechii de bytes 66 10 din sample-ul conditional-formatting stau la non-record boundaries, iar un dump complet al stream-ului găsește zero hits. Layout-ul de bits descris aici a fost derivat din spec și apoi fixat în trei moduri, prin decode symmetry pe output-ul builder-ului, prin byte tests construite manual care alimentează decoder-ul cu un payload sintetic $1066 și prin aserțiuni pe RGB-ul flattened. Este o formă de evidence mai slabă decât un Excel file capturat și merită spus asta, nu să sugerezi contrariul. A doua, flattening-ul pentru un fill theme-only folosește un default Office theme table inclus, nu un theme part citit din workbook, deoarece XLS binar nu are un theme part în sensul unui XLSX împachetat — dacă ai nevoie ca theme-ul propriu al workbook-ului să conducă flat color-ul, furnizează tu FillRgb. A treia, decoder-ul acceptă GelFrame doar în interiorul unui series block; același record poate apărea pe chart area sau într-un axis frame, iar acceptarea lui acolo ar atribui silențios un background fill unui series, așa că acestea sunt ignorate. Un fillColorExt fără flag-ul $08000000 este tratat ca extended color simplu și nu setează niciodată HasFillTheme. Pentru workbook-uri în care chart-ul este creat în lumea XLSX și doar trece prin sistem, ruta de păstrare din editarea chart-urilor Excel fără pierderea ChartML este mai sigură, iar container-ul în care stau aceste records este acoperit în citirea compound file-urilor OLE2 în Delphi fără COM IStorage
Fill-urile de chart cu theme color, encoder-ul și decoder-ul GelFrame și builder-ul complet pentru chart substream BIFF8 sunt livrate în componenta spreadsheet HotXLS pentru Delphi pentru Delphi și C++Builder, care citește și scrie XLS, XLSX și ODS fără Excel instalat