Technický článek

Theme barvy výplní grafu Excelu v Delphi: GelFrame HotXLS

Série grafu vyplněná literálem RGB nesleduje theme workbooku. Změňte theme a série si ponechá starou barvu. HotXLS to v binárním XLS řeší theme-colored výplněmi sérií grafu: rekordem GelFrame, 4198 nebo $1066, zapsaným hned za AreaFormat uvnitř bloku série a nesoucím index schématu OfficeArt plus tint. Excel pak sérii vykreslí stejně jako theme výplň, kterou zapsal sám

Odkud pochází číslo rekordu GelFrame

Číslo rekordu GelFrame je 4198 ($1066) a ve vlastní specifikační sekci rekordu ho nenajdete. [MS-XLS] 2.4.131 popisuje, co GelFrame obsahuje, ale na rozdíl od většiny sekcí rekordů neuvádí hodnotu rt. ABNF chart substreamu také nepomůže: dává pouze produkci GELFRAME = 1*2GelFrame *Continue, která rekord pojmenuje, ale nečísluje. Číslo žije v tabulce enumerace čísel rekordů několik stran od sekce dokumentující payload. Na tuto produkci je dobré se podívat podruhé při psaní readeru: dovoluje jeden nebo dva rekordy GelFrame, za každým mohou volitelně následovat Continue rekordy, takže parser předpokládající jeden rekord na produkci špatně zpracuje soubor, který sám nezapsal. HotXLS emituje přesně jeden GelFrame na theme sérii, což je to, co Excel vytváří pro jednoduchou solid theme výplň, a decoder s rekordem zachází jako se samostatným payloadem místo předpokladu pevného počtu

Uvnitř payloadu GelFrame: dvě property tabulky OfficeArt

Payload GelFrame tvoří dvě property tabulky OfficeArt za sebou: OfficeArtFOPT (nazývaná OPT1) následovaná OfficeArtTertiaryFOPT (OPT2). Každá tabulka obsahuje dvoubajtový počet properties následovaný tolika šestibajtovými položkami FOPTE, přičemž každá položka má dvoubajtový opid a čtyřbajtový op. Bit 15 v opid je fComplex: je-li nastavený, hodnota op je délka v bajtech a za pevnými položkami následuje proměnný tail. Decoder, který tyto taily ignoruje, se desynchronizuje a od první komplexní property dál čte nesmyslné opidy

Theme výplň vyjadřují tři properties rozložené přes obě tabulky plus jedna, která deklaruje druh výplně. HotXLS zapisuje čtyři properties v 28 bajtech bez komplexních tailů:

  • fillType $0180 v OPT1, nastavené na 1 (msofillSolid)
  • fillColor $0181 v OPT1, zploštěná RGB, kterou vykreslí starší konzument nebo konzument bez theme podpory
  • fillColorExt $019E v OPT2, základní theme barva
  • fillColorExtMod $01A0 v OPT2, tint nebo shade aplikovaný na základ

Toto rozdělení je záměr formátu, nikoli náhoda implementace: [MS-ODRAW] 2.2.2 popisuje theme triple jako plochou barvu plus základní barvu plus modifikaci, takže konzument, který themes rozumí, výplň znovu spočítá a ten, který jim nerozumí, stále vykreslí něco rozumného. Sousední opidy sledují stejný vzor a nesou shodné číslování ve starém i aktuálním vydání [MS-ODRAW], což je pohodlné při porovnávání dvou revizí: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 a fillBackColorExtMod $01A4

Proč index schématu sedí v červeném bajtu

Protože OfficeArtCOLORREF je definován offsetem bajtu, nikoli číselnou hodnotou: červená na bajtu 0, zelená na bajtu 1, modrá na bajtu 2 a flags na bajtu 3. Přečtěte tuto strukturu jako little-endian DWORD, což každý op FOPTE je, a červená se stane nejméně významným bajtem. Potvrzuje to zpracovaný příklad lineColor v [MS-ODRAW]. fSchemeIndex, což je bit flags E, má tedy číselnou hodnotu $08000000 a samotný index schématu patří do červeného bajtu, přičemž zelená a modrá musí být nula. Accent1 je proto hodnota op $08000004, nikoli $00000004 a už vůbec ne $04000000

Pořadí theme indexu, které specifikace odmítá definovat

Specifikace označuje pořadí scheme indexu za host-defined a žádnou tabulku nedává, což znamená, že samotné rozložení bajtů nestačí pro interop s Excelem. HotXLS používá pořadí spreadsheet theme, které round-trippuje proti skutečným souborům Excelu:

  • 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
  • 4 až 9 = accent1 až accent6
  • 10 = hlink, 11 = folHlink

Tint a shade: payload MSOTINTSHADE

fillColorExtMod op je hodnota MSOTINTSHADE a kóduje směr i množství do jediného DWORD místo signed fraction. Hodnota $20000000 znamená beze změny. Lightening tint je $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); darkening tint má stejný tvar s $01F4 ve vysokém slově (MSOSHADE). Bajt amount jde opačným směrem, než napovídá intuice: $FF znamená beze změny a $00 plnou modifikaci. HotXLS to normalizuje na jediný double ve stylu DrawingML, kde kladná hodnota zesvětluje a záporná ztmavuje, pomocí plus nebo minus (255 - amount) / 255. Mapování je přesné pro hodnoty, které Excel skutečně nabízí v UI, proto je round-trip bezeztrátový místo přibližně bezeztrátového: známé „Lighter 40%“ je amount 153 a (255 - 153) / 255 je 0,4 bez zaokrouhlovací chyby v žádném směru. Shade s amount 191 se vrátí jako -64/255. Tady je encoder omezený na legální rozsah:

if Tint > 0 then                       // MSOTINT - světlejší
  TintOp := LongWord($02F4) shl 16 or
    (LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then                  // MSOSHADE - tmavší
  TintOp := LongWord($01F4) shl 16 or
    (LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
  TintOp := $20000000;                 // MSOCOLORMODUNDEFINED

Nastavení a čtení theme výplně z Delphi

Na straně zápisu je theme výplň dvojicí nových polí v recordu stylu jednotlivé série. TXLSChartSeriesStyleInfo získal HasFillTheme, FillThemeColor a FillThemeTint a builder emituje GelFrame pouze tehdy, když jsou současně nastaveny HasStyle a HasFillTheme. Když nastavíte také explicitní FillRgb, hodnota jde do OPT1 fillColor beze změny; když ji nenastavíte, HotXLS barvu zploští sám přes vestavěnou výchozí tabulku Office theme s aplikovaným tintem, takže i theme-only série má rozumnou plochou barvu pro konzumenty, které OPT2 ignorují. Všimněte si inicializace Default(), která je důležitá, protože TXLSChartSeriesInfo obsahuje managed fields a jeho obyčejné Boolean členy by jinak byly odpadem ze stacku:

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

    Series[0] := Default(TXLSChartSeriesInfo);   // tento record nikdy neplň FillChar
    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 v 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;        // bez explicitního RGB: zploštěno
    Series[1].Style.FillThemeColor := 8;         // accent5

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

Čtení zpět prochází stejným chart modelem, který používá zbytek inspekce grafů HotXLS. GetChartModel vrací vlastněný TXLSChartModel, který uvolníte, a každý TXLSChartSeries vystavuje HasFillTheme, FillThemeColor a FillThemeTint vedle FillRgb dekódovaného z OPT1 fillColor, který má pro tuto sérii přednost před barvou AreaFormat. Stejné tři hodnoty se dostanou i do kanonického sémantického snapshotu jako SolidFillThemeSet, SolidFillThemeColor a SolidFillThemeTint, takže diff workbooku uvidí změnu theme jako změnu theme místo nevysvětleného RGB driftu. Pokud přicházíte ze strany XLSX, toto je protějšek stylování v binárním formátu popsaného v průvodci HotXLS grafy, obrázky a drawingy v 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;

Co theme výplň v binárním XLS neslibuje

Tři poctivá omezení. První a nejdůležitější pro každého, kdo tento kód audituje: žádný ukázkový soubor v lokálním korpusu neobsahuje rekord GelFrame. Jedenáct výskytů dvojice bajtů 66 10 ve vzorku conditional formatting leží mimo hranice rekordů a dump celého streamu nalezne nulový počet shod. Layout bitů popsaný zde byl odvozen ze specifikace a potom zafixován třemi způsoby: symetrií dekódování nad výstupem builderu, ručně vytvořenými byte testy, které pošlou syntetický payload $1066 přímo decoderu, a assertion přesné zploštěné RGB. Je to slabší důkaz než zachycený soubor z Excelu a je lepší to říct, než vytvářet opačný dojem. Za druhé, zploštění theme-only výplně používá vestavěnou výchozí tabulku Office theme, nikoli theme part načtenou z workbooku, protože binární XLS nemá theme part ve smyslu zabaleného XLSX — pokud potřebujete, aby plochou barvu řídila vlastní theme workbooku, dodejte FillRgb sami. Za třetí, decoder přijímá GelFrame pouze uvnitř bloku série; stejný rekord se může objevit v oblasti grafu nebo ve frame osy a jeho přijetí tam by potichu přiřadilo výplň pozadí sérii, proto se tyto výskyty ignorují. fillColorExt bez příznaku $08000000 se také chápe jako běžná rozšířená barva a nikdy nenastaví HasFillTheme. Pro workbooky, jejichž graf vznikl ve světě XLSX a jen jím prochází, je bezpečnější preservation path v úpravě grafů Excelu bez ztráty ChartML a kontejner, v němž tyto rekordy sedí, pokrývá čtení OLE2 compound files v Delphi bez COM IStorage

Theme-colored výplně grafů, encoder a decoder GelFrame i úplný builder chart substreamu BIFF8 se dodávají v HotXLS Delphi spreadsheet component pro Delphi a C++Builder, který čte a zapisuje XLS, XLSX a ODS bez instalovaného Excelu