Odborný článok

Farby témy výplní grafov Excelu v Delphi: HotXLS GelFrame

Výplň série grafu s literal RGB nesleduje tému workbooku. Zmeňte tému a séria si ponechá starú farbu. HotXLS to v binárnom XLS rieši výplňami sérií grafu s farbou témy: record GelFrame, 4198 alebo $1066, zapísaný hneď za AreaFormat vo vnútri bloku série, ktorý nesie index schémy OfficeArt a tint. Excel potom sériu vykreslí tak, ako vykresľuje themed fill, ktorý zapísal sám

Odkiaľ pochádza číslo recordu GelFrame?

Číslo recordu GelFrame je 4198 ($1066) a v jeho vlastnej špecifikačnej sekcii ho nenájdete. [MS-XLS] 2.4.131 opisuje, čo GelFrame obsahuje, ale na rozdiel od väčšiny record sections neuvádza hodnotu rt. ABNF chart substreamu tiež nepomôže: dáva iba produkciu GELFRAME = 1*2GelFrame *Continue, ktorá record pomenuje, ale nečísluje. Číslo žije v enumeračnej tabuľke record numbers, niekoľko strán od sekcie dokumentujúcej payload. Táto produkcia si zaslúži druhý pohľad pre každého, kto píše reader: povoľuje jeden alebo dva recordy GelFrame, za každým voliteľne nasledujú Continue recordy, takže parser, ktorý predpokladá jeden record na produkciu, nesprávne spracuje súbor, ktorý sám nevytvoril. HotXLS emituje presne jeden GelFrame na themed series, čo Excel vytvára pre jednoduchú solid theme fill, a dekóder s recordom zaobchádza ako so samostatným payloadom namiesto predpokladu pevného počtu

Vo vnútri payloadu GelFrame: dve tabuľky vlastností OfficeArt

Payload GelFrame sú dve tabuľky vlastností OfficeArt za sebou: OfficeArtFOPT (nazývaný OPT1) nasledovaný OfficeArtTertiaryFOPT (OPT2). Každá tabuľka je dvojbajtový počet vlastností nasledovaný daným počtom šesťbajtových FOPTE entries a každý entry je dvojbajtový opid plus štvorbajtový op. Bit 15 v opid je fComplex: keď je nastavený, hodnota op je dĺžka v bajtoch a za fixed entries nasleduje premenný tail. Dekóder, ktorý tieto tails ignoruje, sa desynchronizuje a pri všetkom po prvej komplexnej vlastnosti číta odpadové opids

Výplň témy je vyjadrená tromi vlastnosťami rozloženými v oboch tabuľkách a jednou, ktorá deklaruje druh výplne. HotXLS zapisuje štyri vlastnosti v 28 bajtoch bez komplexných tailov:

  • fillType $0180 v OPT1, nastavený na 1 (msofillSolid)
  • fillColor $0181 v OPT1, sploštené RGB, ktoré vykreslí starší alebo theme-unaware konzument
  • fillColorExt $019E v OPT2, základná farba témy
  • fillColorExtMod $01A0 v OPT2, tint alebo shade aplikovaný na základ

Toto rozdelenie je zámerom formátu, nie náhodou implementácie: [MS-ODRAW] 2.2.2 opisuje theme triple ako plochú farbu plus základnú farbu plus modifikáciu, takže konzument, ktorý témy rozumie, výplň prepočíta, zatiaľ čo ten, ktorý im nerozumie, stále namaľuje niečo rozumné. Okolité opids sledujú rovnaký vzor a nesú identické číslovanie v starom aj aktuálnom vydaní [MS-ODRAW], čo je praktické pri krížovom čítaní dvoch revízií: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 a fillBackColorExtMod $01A4

Prečo sedí index schémy v červenom bajte?

Pretože OfficeArtCOLORREF je definovaný offsetom bajtu, nie numerickou hodnotou: red na bajte 0, green na bajte 1, blue na bajte 2, flags na bajte 3. Prečítajte túto štruktúru ako little-endian DWORD, čo je spôsob, akým je každý op FOPTE, a red sa stane least significant byte. Pracovný príklad lineColor v [MS-ODRAW] to potvrdzuje. Preto má fSchemeIndex, čo je bit E flags, numerickú hodnotu $08000000 a samotný index schémy ide do červeného bajtu, pričom green a blue musia byť nula. Accent1 je teda hodnota op $08000004, nie $00000004 a už vôbec nie $04000000

Poradie indexov témy, ktoré špecifikácia odmieta definovať

Špecifikácia označuje poradie indexov schémy za host-defined a nedáva žiadnu tabuľku, čo znamená, že samotný layout bajtov nestačí na interoperabilitu s Excelom. HotXLS používa poradie témy spreadsheetu, ktoré round-trips proti skutočným súborom 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 smer aj množstvo v jednom DWORD namiesto signed fraction. Hodnota $20000000 znamená bez modifikácie. Lightening tint je $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); darkening tint má rovnaký tvar s $01F4 vo high word (MSOSHADE). Bajt amount ide opačne než intuícia: $FF znamená nezmenené a $00 plnú modifikáciu. HotXLS to normalizuje na jedno double v štýle DrawingML, kde kladné hodnoty zosvetľujú a záporné stmavujú, pomocou plus alebo mínus (255 - amount) / 255. Mapovanie je presné pre hodnoty, ktoré Excel skutočne ponúka v UI, preto je round-trip bezstratový, nie približne bezstratový: známe „Lighter 40%“ je amount 153 a (255 - 153) / 255 je 0,4 bez chyby zaokrúhlenia v jednom ani druhom smere. Shade s amount 191 sa vráti ako -64/255. Tu je encoder ohraničený na legálny rozsah:

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

Nastavenie a čítanie výplne témy z Delphi

Na write side sú theme fill dve extra polia v per-series style recorde. TXLSChartSeriesStyleInfo získal HasFillTheme, FillThemeColor a FillThemeTint a builder emituje GelFrame iba vtedy, keď sú nastavené HasStyle aj HasFillTheme. Ak zároveň nastavíte explicitné FillRgb, táto hodnota ide do OPT1 fillColor verbatim; ak nie, HotXLS sploští farbu sám cez vstavanú defaultnú Office theme table s aplikovaným tintom, takže theme-only séria stále má rozumnú plochú farbu pre konzumentov, ktorí OPT2 ignorujú. Všimnite si inicializáciu Default(), ktorá je dôležitá, pretože TXLSChartSeriesInfo obsahuje managed fields a jeho obyčajné Boolean členy sú inak odpad zo 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: sploštené
    Series[1].Style.FillThemeColor := 8;         // accent5

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

Čítanie späť prechádza tým istým chart modelom, ktorý používa zvyšok HotXLS chart inspection. GetChartModel vráti vlastnený TXLSChartModel, ktorý uvoľníte, a každý TXLSChartSeries vystaví HasFillTheme, FillThemeColor a FillThemeTint spolu s FillRgb dekódovaným z OPT1 fillColor, ktoré má pre danú sériu prednosť pred farbou AreaFormat. Tie isté tri hodnoty sa dostanú aj do kanonického sémantického snapshotu ako SolidFillThemeSet, SolidFillThemeColor a SolidFillThemeTint, takže diff workbooku uvidí zmenu témy ako zmenu témy, nie ako nevysvetlený RGB drift. Ak prichádzate zo strany XLSX, toto je binárny formátový náprotivok štýlovania opísaného v príručke HotXLS o grafoch, obrázkoch a kreslení Excelu 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;

Čo theme fill v binárnom XLS nesľubuje?

Tri poctivé obmedzenia. Prvé a najdôležitejšie pre každého, kto auditujete tento kód: žiadny sample file v lokálnom korpuse vôbec neobsahuje record GelFrame. Jedenásť výskytov dvojice bajtov 66 10 vo vzorke conditional-formatting sedí na hraniciach, ktoré nie sú hranicami recordov, a dump recordov celého streamu nájde nulový počet hitov. Layout bitov opísaný tu sa odviedol zo špecifikácie a potom sa pripol tromi spôsobmi: decode symmetry na výstupe buildera, ručne postavené byte testy, ktoré kŕmia dekóder syntetickým payloadom $1066, a assertion presného splošteného RGB. Je to slabšia forma dôkazu než zachytený súbor Excelu a stojí za to to povedať namiesto predstierania opaku. Druhé: flattening pre theme-only fill používa vstavanú defaultnú tabuľku Office theme, nie theme part načítanú z workbooku, pretože binárny XLS nemá theme part v zmysle zabaleného XLSX — ak potrebujete, aby plochú farbu riadila vlastná téma workbooku, dodajte FillRgb sami. Tretie: dekóder prijíma GelFrame iba vo vnútri bloku série; rovnaký record sa môže objaviť v chart area alebo axis frame a jeho prijatie tam by potichu priradilo background fill sérii, takže tieto výskyty sa ignorujú. fillColorExt bez flagu $08000000 sa rovnako považuje za obyčajnú extended color a nikdy nenastaví HasFillTheme. Pri workbookoch, kde bol graf vytvorený vo svete XLSX a iba prechádza cez tento formát, je bezpečnejšia preservation path v článku o úprave Excel grafov bez straty ChartML a kontajner, v ktorom tieto recordy sedia, pokrýva článok o čítaní zložených súborov OLE2 v Delphi bez COM IStorage

Výplne grafov s farbou témy, encoder a decoder GelFrame aj úplný builder chart substreamu BIFF8 sa dodávajú v HotXLS Delphi spreadsheet component pre Delphi a C++Builder, ktorý číta a zapisuje XLS, XLSX a ODS bez nainštalovaného Excelu