Eine mit einem festen RGB-Wert gefüllte Chartserie folgt nicht dem Workbook-Theme. Ändert man das Theme, behält die Serie die alte Farbe. HotXLS behandelt das in binärem XLS über themenfarbige Füllungen für Chartserien: Ein GelFrame-Record, 4198 oder $1066, wird direkt nach dem AreaFormat innerhalb des Serienblocks geschrieben und trägt einen OfficeArt-Schemaindex plus Tönung. Excel rendert die Serie anschließend so, wie eine von Excel selbst geschriebene Theme-Füllung
Woher kommt die Recordnummer des GelFrame?
Die Recordnummer des GelFrame ist 4198 ($1066), und man findet sie nicht im eigenen Spezifikationsabschnitt des Records. [MS-XLS] 2.4.131 beschreibt, was ein GelFrame enthält, nennt aber anders als die meisten Recordabschnitte nicht den rt-Wert. Auch die ABNF des Chart-Substreams hilft nicht weiter: Sie gibt nur die Produktion GELFRAME = 1*2GelFrame *Continue an, die den Record benennt, aber nicht nummeriert. Die Nummer steht in der Enumerationstabelle der Recordnummern, mehrere Seiten vom Abschnitt entfernt, der den Payload dokumentiert. Für jeden Reader ist diese Produktion einen zweiten Blick wert: Sie erlaubt einen oder zwei GelFrame-Records, denen jeweils optional Continue-Records folgen können. Ein Parser, der pro Produktion genau einen Record annimmt, verarbeitet daher eine Datei, die er nicht selbst geschrieben hat, falsch. HotXLS gibt für jede themenfarbige Serie genau einen GelFrame aus, wie es Excel bei einer einfachen einfarbigen Theme-Füllung tut, und der Decoder behandelt den Record als eigenständigen Payload statt eine feste Anzahl vorauszusetzen
Im GelFrame-Payload: zwei OfficeArt-Propertytabellen
Der GelFrame-Payload besteht aus zwei OfficeArt-Propertytabellen direkt hintereinander: einem OfficeArtFOPT (OPT1), gefolgt von einem OfficeArtTertiaryFOPT (OPT2). Jede Tabelle beginnt mit einer zweibytegroßen Propertyanzahl, gefolgt von ebenso vielen sechs Byte langen FOPTE-Einträgen; jeder Eintrag enthält ein zweibytegroßes opid und ein vier Byte langes op. Bit 15 von opid ist fComplex: Ist es gesetzt, ist der Wert op eine Bytelänge und es folgt ein variabler Anhang hinter den festen Einträgen. Ein Decoder, der diese Anhänge ignoriert, verliert die Ausrichtung und liest für alles nach der ersten komplexen Property falsche opids
Die Theme-Füllung wird durch drei über beide Tabellen verteilte Properties und eine weitere Property für die Füllungsart ausgedrückt. HotXLS schreibt vier Properties in 28 Bytes und ohne komplexe Anhänge:
fillType$0180 in OPT1, auf 1 (msofillSolid) gesetztfillColor$0181 in OPT1, die abgeflachte RGB-Farbe, die ein älterer oder Theme-unbewusster Verbraucher zeichnetfillColorExt$019E in OPT2, die Basis-ThemefarbefillColorExtMod$01A0 in OPT2, die auf die Basis angewendete Tönung oder Schattierung
Diese Aufteilung ist im Format beabsichtigt und kein Implementierungszufall: [MS-ODRAW] 2.2.2 beschreibt das Theme-Triple als flache Farbe plus Basisfarbe plus Modifikation. Ein Verbraucher, der Themes versteht, berechnet die Füllung neu, während einer ohne Theme-Unterstützung weiterhin etwas Vernünftiges zeichnet. Die benachbarten opids folgen demselben Muster und tragen in der alten und der aktuellen [MS-ODRAW]-Ausgabe identische Nummern, was beim Querlesen zweier Versionen praktisch ist: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 und fillBackColorExtMod $01A4
Warum sitzt der Schemaindex im Rotbyte?
Weil ein OfficeArtCOLORREF über Byteoffsets und nicht über seinen Zahlenwert definiert ist: Rot bei Byte 0, Grün bei Byte 1, Blau bei Byte 2 und Flags bei Byte 3. Liest man diese Struktur als Little-Endian-DWORD, wie es jedes FOPTE-op ist, wird Rot zum niederwertigsten Byte. Das ausgearbeitete lineColor-Beispiel in [MS-ODRAW] bestätigt das. Daher hat fSchemeIndex, das Flagbit E, den Zahlenwert $08000000, und der Schemaindex selbst steht im Rotbyte, während Grün und Blau null sein müssen. Accent1 ist somit der Op-Wert $08000004 und nicht $00000004 und schon gar nicht $04000000
Die Themeindex-Reihenfolge, die die Spezifikation nicht definiert
Die Spezifikation nennt die Reihenfolge des Schemaindex hostdefiniert und liefert keine Tabelle. Für die Interoperabilität mit Excel reicht das Byte-Layout daher nicht. HotXLS verwendet die Theme-Reihenfolge von Spreadsheets, die sich mit echten Excel-Dateien roundtrippen lässt:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 bis 9 = accent1 bis accent6
- 10 = hlink, 11 = folHlink
Tönung und Schattierung: der MSOTINTSHADE-Payload
Der fillColorExtMod-Op ist ein MSOTINTSHADE-Wert und kodiert Richtung und Betrag in einem DWORD statt als vorzeichenbehafteten Bruch. Der Wert $20000000 bedeutet unverändert. Eine aufhellende Tönung ist $02F4 shl 16 or amount shl 8 or $10 (MSOTINT), eine abdunkelnde Tönung hat dieselbe Form mit $01F4 im Highword (MSOSHADE). Das amount-Byte läuft entgegen der Intuition: $FF bedeutet unverändert und $00 die vollständige Modifikation. HotXLS normalisiert das zu einer einzigen DrawingML-artigen Double-Zahl, bei der positiv aufhellt und negativ abdunkelt, mit plus oder minus (255 - amount) / 255. Die Abbildung ist für die Werte, die Excel in seiner UI tatsächlich anbietet, exakt. Deshalb ist der Roundtrip verlustfrei und nicht nur näherungsweise verlustfrei: Das bekannte "Lighter 40%" ist amount 153, und (255 - 153) / 255 ist in beide Richtungen ohne Rundungsfehler 0,4. Eine Schattierung mit amount 191 kommt als -64/255 zurück. Hier ist der auf den gültigen Bereich begrenzte Encoder:
if Tint > 0 then // MSOTINT - heller
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - dunkler
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Eine Theme-Füllung in Delphi setzen und lesen
Auf der Schreibseite besteht eine Theme-Füllung aus drei zusätzlichen Feldern im Style-Record je Serie. TXLSChartSeriesStyleInfo erhielt HasFillTheme, FillThemeColor und FillThemeTint, und der Builder gibt den GelFrame nur aus, wenn sowohl HasStyle als auch HasFillTheme gesetzt sind. Setzt man außerdem ein explizites FillRgb, landet dieser Wert unverändert in der OPT1-Property fillColor. Setzt man es nicht, flacht HotXLS die Farbe selbst über eine integrierte Standard-Office-Theme-Tabelle mit angewendeter Tönung ab. So hat auch eine reine Theme-Serie eine vernünftige flache Farbe für Verbraucher, die OPT2 ignorieren. Beachten Sie die Initialisierung mit Default(), denn TXLSChartSeriesInfo enthält verwaltete Felder und seine einfachen Boolean-Member wären sonst Stackmüll:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // diesen Record nie mit FillChar leeren
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, Rot im niederwertigen 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; // keine explizite RGB: abgeflacht
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
Beim Zurücklesen läuft es über dasselbe Chartmodell wie die übrige HotXLS-Chartinspektion. GetChartModel liefert ein besessenes TXLSChartModel, das Sie freigeben, und jedes TXLSChartSeries stellt neben dem aus der OPT1-Property fillColor dekodierten FillRgb die Werte HasFillTheme, FillThemeColor und FillThemeTint bereit. Dieser Wert hat Vorrang vor der AreaFormat-Farbe der Serie. Dieselben drei Werte gelangen auch als SolidFillThemeSet, SolidFillThemeColor und SolidFillThemeTint in den kanonischen semantischen Snapshot, daher sieht ein Workbook-Diff eine Themeänderung als Themeänderung und nicht als unerklärliches RGB-Driften. Wenn Sie von der XLSX-Seite kommen, ist dies das Gegenstück im Binärformat zur Gestaltung aus dem HotXLS-Leitfaden zu Excel-Charts, Bildern und Zeichnungen 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, die OPT1-fillColor
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
Was verspricht eine Theme-Füllung in binärem XLS nicht?
Drei ehrliche Grenzen. Erstens, und für jedes Audit dieses Codes am wichtigsten: Keine Beispieldatei im lokalen Korpus enthält überhaupt einen GelFrame-Record. Die elf Vorkommen des Bytepaars 66 10 im Conditional-Formatting-Beispiel liegen an Stellen, die keine Recordgrenzen sind, und ein vollständiger Streamdump findet null Treffer. Das hier beschriebene Bitlayout wurde aus der Spezifikation abgeleitet und anschließend dreifach abgesichert: durch Decodersymmetrie am Builder-Output, durch handgebaute Byte-Tests mit einem synthetischen $1066-Payload direkt im Decoder und durch eine Assertion über die exakte abgeflachte RGB-Farbe. Das ist eine schwächere Evidenz als eine aufgezeichnete Excel-Datei, und es ist besser, das zu sagen, als etwas anderes anzudeuten. Zweitens verwendet die Abflachung einer reinen Theme-Füllung eine integrierte Standard-Office-Theme-Tabelle und keinen aus der Arbeitsmappe gelesenen Theme-Teil, weil binäres XLS keinen Theme-Teil im Sinne eines verpackten XLSX besitzt. Soll das eigene Theme der Arbeitsmappe die flache Farbe bestimmen, liefern Sie FillRgb selbst. Drittens akzeptiert der Decoder einen GelFrame nur innerhalb eines Serienblocks. Derselbe Record kann im Chartbereich oder in einem Achsenframe erscheinen, und ihn dort zu akzeptieren, würde eine Hintergrundfüllung still einer Serie zuordnen. Solche Records werden daher ignoriert. Ein fillColorExt ohne das Flag $08000000 wird ebenfalls als einfache erweiterte Farbe behandelt und setzt nie HasFillTheme. Bei Arbeitsmappen, deren Chart in der XLSX-Welt erstellt und nur durchgereicht wird, ist der Erhaltungspfad aus der Bearbeitung von Excel-Charts ohne Verlust von ChartML sicherer; der Container dieser Records wird in dem Lesen von OLE2-Compound-Dateien in Delphi ohne COM IStorage behandelt
Themenfarbige Chartfüllungen, GelFrame-Encoder und -Decoder sowie der vollständige BIFF8-Chart-Substream-Builder gehören zur HotXLS Delphi spreadsheet component für Delphi und C++Builder, die XLS, XLSX und ODS ohne installierte Excel-Anwendung liest und schreibt