Una serie di chart riempita con un RGB literal non segue il tema del workbook. Cambiate il tema e la serie conserva il vecchio colore. HotXLS gestisce questo caso nell'XLS binario con fill di serie colorati dal tema: un record GelFrame, 4198 o $1066, scritto subito dopo AreaFormat dentro il blocco della serie e contenente un indice di schema OfficeArt più un tint. Excel renderizza quindi la serie come renderizza un fill a tema scritto da sé
Da dove arriva il numero di record GelFrame?
Il numero di record GelFrame è 4198 ($1066), e non lo troverete nella sezione di specifica del record stesso. [MS-XLS] 2.4.131 descrive cosa contiene un GelFrame, ma a differenza della maggior parte delle sezioni di record non indica il valore rt. Neppure l'ABNF del chart substream aiuta: fornisce solo la produzione GELFRAME = 1*2GelFrame *Continue, che nomina il record senza numerarlo. Il numero vive nella tabella di enumerazione dei record, a diverse pagine di distanza dalla sezione che documenta il payload. Quella produzione merita una seconda lettura per chiunque scriva un reader: permette uno o due record GelFrame, ciascuno eventualmente seguito da record Continue, quindi un parser che presuma un solo record per produzione gestirà male un file che non ha scritto lui. HotXLS emette esattamente un GelFrame per ogni serie a tema, come fa Excel per un semplice fill solido a tema, e il decoder tratta il record come payload autonomo invece di presumere un conteggio fisso
Dentro il payload GelFrame: due tabelle di proprietà OfficeArt
Il payload GelFrame è formato da due tabelle di proprietà OfficeArt una dopo l'altra: una OfficeArtFOPT (chiamata OPT1) seguita da una OfficeArtTertiaryFOPT (OPT2). Ogni tabella è composta da un conteggio proprietà di due byte seguito da quel numero di entry FOPTE da sei byte, e ogni entry contiene un opid di due byte più un op di quattro byte. Il bit 15 di opid è fComplex: quando è impostato, il valore op è una lunghezza in byte e dopo le entry fisse segue una coda variabile. Un decoder che ignora quelle code si desincronizza e legge opid spazzatura per tutto ciò che viene dopo la prima proprietà complex
Il fill a tema è espresso da tre proprietà distribuite nelle due tabelle, più una che dichiara il tipo di fill. HotXLS scrive quattro proprietà in 28 byte senza code complex:
fillType$0180 in OPT1, impostato a 1 (msofillSolid)fillColor$0181 in OPT1, l'RGB appiattito che un consumer più vecchio o ignaro dei temi disegneràfillColorExt$019E in OPT2, il colore tema di basefillColorExtMod$01A0 in OPT2, il tint o shade applicato a quella base
Questa divisione è deliberata nel formato, non un incidente dell'implementazione: [MS-ODRAW] 2.2.2 descrive la terna del tema come colore piatto più colore di base più modifica, quindi un consumer che comprende i temi ricalcola il fill mentre uno che non li comprende continua a dipingere qualcosa di ragionevole. Gli opid vicini seguono lo stesso schema e hanno numerazione identica nelle edizioni [MS-ODRAW] vecchia e attuale, cosa comoda quando si confrontano due revisioni: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 e fillBackColorExtMod $01A4
Perché l'indice dello schema si trova nel byte rosso?
Perché un OfficeArtCOLORREF è definito dall'offset del byte, non dal valore numerico: rosso al byte 0, verde al byte 1, blu al byte 2, flag al byte 3. Leggete quella struttura come un DWORD little-endian, come avviene per ogni op FOPTE, e il rosso diventa il byte meno significativo. L'esempio lineColor della specifica [MS-ODRAW] lo conferma. Quindi fSchemeIndex, che è il flag bit E, ha valore numerico $08000000 e l'indice dello schema va nel byte rosso, con verde e blu obbligatoriamente a zero. Accent1 è quindi il valore $08000004, non $00000004 e certamente non $04000000
L'ordine degli indici tema che la specifica rifiuta di definire
La specifica chiama l'ordine degli indici di schema host-defined e non fornisce alcuna tabella, quindi il solo layout dei byte non basta per interoperare con Excel. HotXLS usa l'ordine del tema spreadsheet, quello che fa round-trip con file Excel reali:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- da 4 a 9 = accent1 fino ad accent6
- 10 = hlink, 11 = folHlink
Tint e shade: il payload MSOTINTSHADE
L'operazione fillColorExtMod è un valore MSOTINTSHADE e codifica direzione e quantità in un DWORD invece che come frazione signed. Il valore $20000000 significa non modificato. Un tint che schiarisce è $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); uno che scurisce ha la stessa forma con $01F4 nella high word (MSOSHADE). Il byte amount procede nel verso opposto all'intuizione: $FF significa invariato e $00 la modifica completa. HotXLS lo normalizza in un singolo double nello stile DrawingML, dove il positivo schiarisce e il negativo scurisce, usando più o meno (255 - amount) / 255. La corrispondenza è esatta per i valori offerti da Excel nella UI, motivo per cui il round-trip è lossless e non solo approssimativamente lossless: il familiare "Lighter 40%" è amount 153, e (255 - 153) / 255 è 0,4 senza errore di arrotondamento in nessuna direzione. Uno shade con amount 191 torna come -64/255. Ecco l'encoder, limitato all'intervallo legale:
if Tint > 0 then // MSOTINT - più chiaro
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - più scuro
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Impostare e leggere un fill a tema da Delphi
Sul lato scrittura un fill a tema è composto da due campi aggiuntivi nel record di stile per serie. TXLSChartSeriesStyleInfo ha ricevuto HasFillTheme, FillThemeColor e FillThemeTint, e il builder emette il GelFrame solo quando sono impostati sia HasStyle sia HasFillTheme. Se impostate anche un FillRgb esplicito, quel valore finisce verbatim nel fillColor OPT1; se non lo fate, HotXLS appiattisce da solo il colore attraverso una tabella tema Office predefinita incorporata con il tint applicato, così una serie con solo il tema conserva un colore piatto sensato per i consumer che ignorano OPT2. Notate l'inizializzazione Default(), importante perché TXLSChartSeriesInfo contiene campi managed e i membri Boolean semplici altrimenti sono spazzatura sullo stack:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // non usare mai FillChar su questo 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, rosso nel byte basso
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; // nessun RGB esplicito: appiattito
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
La rilettura passa dallo stesso chart model usato dal resto dell'ispezione chart HotXLS. GetChartModel restituisce un TXLSChartModel posseduto dal caller, che deve essere liberato, e ogni TXLSChartSeries espone HasFillTheme, FillThemeColor e FillThemeTint accanto a FillRgb decodificato dal fillColor OPT1, che per quella serie ha precedenza sul colore AreaFormat. Gli stessi tre valori arrivano anche allo snapshot semantico canonico come SolidFillThemeSet, SolidFillThemeColor e SolidFillThemeTint, quindi un workbook diff vede una modifica del tema come modifica del tema, non come una deriva RGB inspiegata. Se arrivate dal lato XLSX, questo è il corrispondente binary-format dello stile descritto in la guida HotXLS a chart, immagini e drawing Excel in 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, il fillColor OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
Cosa non promette un fill a tema nell'XLS binario?
Tre limiti onesti. Primo, e più importante per chiunque faccia audit di questo codice: nessun file di esempio nel corpus locale contiene un record GelFrame. Le undici occorrenze della coppia di byte 66 10 nel sample di conditional formatting si trovano su confini non-record, e un dump dell'intero stream trova zero hit. Il layout dei bit descritto qui è stato ricavato dalla specifica e poi fissato in tre modi: simmetria del decode sull'output del builder, test di byte costruiti a mano che passano un payload sintetico $1066 direttamente al decoder e asserzione sull'RGB appiattito esatto. È una forma di evidenza più debole di un file Excel catturato, e vale la pena dirlo invece di lasciar intendere il contrario. Secondo, l'appiattimento per un fill solo tema usa una tabella tema Office predefinita incorporata, non una theme part letta dal workbook, perché il binary XLS non ha una theme part nel senso dell'XLSX impacchettato: se vi serve che il tema proprio del workbook determini il colore piatto, fornite voi FillRgb. Terzo, il decoder accetta GelFrame solo dentro un blocco di serie; lo stesso record può apparire nell'area del chart o in un axis frame, e accettarlo lì attribuirebbe silenziosamente un fill di sfondo a una serie, quindi quei casi vengono ignorati. Un fillColorExt senza il flag $08000000 viene trattato come colore esteso semplice e non imposta mai HasFillTheme. Per workbook in cui il chart è stato creato nel mondo XLSX e passa solo attraverso il formato, il percorso di preservazione in modificare chart Excel senza perdere il ChartML è più sicuro, mentre il container che ospita questi record è trattato in leggere compound file OLE2 in Delphi senza COM IStorage
I fill di chart colorati dal tema, l'encoder e il decoder GelFrame e il builder completo del chart substream BIFF8 sono inclusi nel componente spreadsheet Delphi HotXLS per Delphi e C++Builder, che legge e scrive XLS, XLSX e ODS senza Excel installato