Seria wykresu wypełniona literalnym RGB nie podąża za motywem skoroszytu. Zmień motyw, a seria zachowa stary kolor. HotXLS obsługuje to w binarnym XLS przez wypełnienia serii wykresu kolorem motywu: rekord GelFrame, 4198 lub $1066, zapisywany bezpośrednio po AreaFormat wewnątrz bloku serii i niosący indeks schematu OfficeArt oraz odcień. Excel renderuje wtedy serię tak, jak renderuje wypełnienie motywu zapisane przez siebie
Skąd bierze się numer rekordu GelFrame?
Numer rekordu GelFrame to 4198 ($1066) i nie znajdziesz go w jego własnej sekcji specyfikacji. [MS-XLS] 2.4.131 opisuje zawartość GelFrame, ale w odróżnieniu od większości sekcji rekordów nie podaje wartości rt. Gramatyka podstrumienia wykresu też nie pomaga: daje tylko produkcję GELFRAME = 1*2GelFrame *Continue, która nazywa rekord, ale go nie numeruje. Numer mieszka w tabeli wyliczenia numerów rekordów, kilka stron dalej od sekcji opisującej payload. Tej produkcji warto przyjrzeć się drugi raz każdemu, kto pisze czytnik: pozwala na jeden albo dwa rekordy GelFrame, po każdym opcjonalnie mogą wystąpić rekordy Continue, więc parser zakładający jeden rekord na produkcję źle obsłuży plik, którego sam nie zapisał. HotXLS emituje dokładnie jeden GelFrame na serię z motywem, co odpowiada temu, co Excel produkuje dla prostego jednolitego wypełnienia motywu, a jego dekoder traktuje rekord jako samodzielny payload zamiast zakładać stałą liczbę
Wewnątrz payloadu GelFrame: dwie tabele właściwości OfficeArt
Payload GelFrame to dwie następujące po sobie tabele właściwości OfficeArt: OfficeArtFOPT (nazywany OPT1), a po nim OfficeArtTertiaryFOPT (OPT2). Każda tabela składa się z dwubajtowej liczby właściwości, a po niej z odpowiedniej liczby sześcio-bajtowych wpisów FOPTE, gdzie każdy wpis zawiera dwubajtowe opid i czterobajtowe op. Bit 15 w opid to fComplex: gdy jest ustawiony, wartość op jest długością w bajtach i po stałych wpisach następuje zmienna końcówka. Dekoder ignorujący te końcówki traci synchronizację i odczytuje śmieciowe opid dla wszystkiego po pierwszej złożonej właściwości
Wypełnienie motywu wyrażają trzy właściwości rozłożone między obie tabele oraz jedna określająca rodzaj wypełnienia. HotXLS zapisuje cztery właściwości w 28 bajtach, bez złożonych końcówek:
fillType$0180 w OPT1, ustawione na 1 (msofillSolid)fillColor$0181 w OPT1, spłaszczony RGB, który narysuje starszy konsument albo konsument nieznający motywówfillColorExt$019E w OPT2, bazowy kolor motywufillColorExtMod$01A0 w OPT2, odcień lub cień zastosowany do bazy
Ten podział jest zamierzony w formacie, a nie przypadkowy w implementacji: [MS-ODRAW] 2.2.2 opisuje trójkę motywu jako płaski kolor oraz kolor bazowy z modyfikacją, więc konsument rozumiejący motywy przelicza wypełnienie, a ten, który ich nie rozumie, nadal maluje coś rozsądnego. Sąsiednie opidy mają ten sam wzorzec i identyczną numerację w starych i obecnych wydaniach [MS-ODRAW], co ułatwia porównywanie dwóch rewizji: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 i fillBackColorExtMod $01A4
Dlaczego indeks schematu siedzi w czerwonym bajcie?
Ponieważ OfficeArtCOLORREF definiuje się przez offset bajtu, a nie przez wartość liczbową: czerwony na bajcie 0, zielony na bajcie 1, niebieski na bajcie 2, flagi na bajcie 3. Odczytaj tę strukturę jako little-endian DWORD, bo tym właśnie jest każde op FOPTE, a czerwony stanie się najmniej znaczącym bajtem. Potwierdza to przykład lineColor w [MS-ODRAW]. Zatem fSchemeIndex, czyli bit flag E, ma wartość liczbową $08000000, a sam indeks schematu trafia do czerwonego bajtu, przy czym zielony i niebieski muszą być zerowe. Accent1 ma więc wartość op $08000004, a nie $00000004 i tym bardziej nie $04000000
Kolejność indeksów motywu, której specyfikacja odmawia zdefiniowania
Specyfikacja nazywa kolejność indeksu schematu zależną od hosta i nie podaje tabeli, co oznacza, że sam layout bajtów nie wystarcza do współpracy z Excelem. HotXLS używa kolejności motywu arkusza, która poprawnie wykonuje cykl z prawdziwymi plikami Excela:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 do 9 = accent1 do accent6
- 10 = hlink, 11 = folHlink
Odcień i cień: payload MSOTINTSHADE
Op fillColorExtMod jest wartością MSOTINTSHADE i koduje kierunek oraz wielkość w jednym DWORD, a nie jako ułamek ze znakiem. Wartość $20000000 oznacza brak modyfikacji. Rozjaśniający odcień to $02F4 shl 16 or amount shl 8 or $10 (MSOTINT), a przyciemniający odcień ma ten sam kształt z $01F4 w słowie wysokim (MSOSHADE). Bajt amount działa odwrotnie niż podpowiada intuicja: $FF oznacza brak zmiany, a $00 pełną modyfikację. HotXLS normalizuje to do jednej wartości double w stylu DrawingML, gdzie dodatnia wartość rozjaśnia, a ujemna przyciemnia, używając plus albo minus (255 - amount) / 255. Odwzorowanie jest dokładne dla wartości oferowanych przez interfejs Excela, dlatego cykl jest bezstratny, a nie tylko w przybliżeniu bezstratny: znane „Lighter 40%” to amount 153, a (255 - 153) / 255 daje 0.4 bez błędu zaokrąglenia w żadną stronę. Cień z amount 191 wraca jako -64/255. Oto encoder ograniczony do dozwolonego zakresu:
if Tint > 0 then // MSOTINT - jaśniej
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - ciemniej
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Ustawianie i odczytywanie wypełnienia motywu z Delphi
Po stronie zapisu wypełnienie motywu to dwa dodatkowe pola w rekordzie stylu serii. TXLSChartSeriesStyleInfo zyskał HasFillTheme, FillThemeColor i FillThemeTint, a builder emituje GelFrame tylko wtedy, gdy ustawione są jednocześnie HasStyle i HasFillTheme. Jeśli ustawisz także jawne FillRgb, wartość trafia bez zmian do fillColor OPT1; jeśli tego nie zrobisz, HotXLS sam spłaszcza kolor przez wbudowaną domyślną tabelę motywu Office z zastosowanym odcieniem, więc seria zawierająca tylko motyw nadal ma rozsądny płaski kolor dla konsumentów ignorujących OPT2. Zwróć uwagę na inicjalizację Default(), która ma znaczenie, ponieważ TXLSChartSeriesInfo zawiera pola zarządzane, a jego zwykłe składowe Boolean w przeciwnym razie są śmieciami ze stosu:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // nigdy nie używaj FillChar dla tego rekordu
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, czerwony w najmłodszym bajcie
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 jawnego RGB: spłaszczone
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
Odczyt z powrotem przechodzi przez ten sam model wykresu, którego używa reszta inspekcji wykresów HotXLS. GetChartModel zwraca należący do wywołującego TXLSChartModel, który trzeba zwolnić, a każdy TXLSChartSeries udostępnia HasFillTheme, FillThemeColor i FillThemeTint obok FillRgb zdekodowanego z fillColor OPT1, który ma pierwszeństwo przed kolorem AreaFormat dla tej serii. Te same trzy wartości trafiają też do kanonicznego migawkowego modelu semantycznego jako SolidFillThemeSet, SolidFillThemeColor i SolidFillThemeTint, więc diff skoroszytu widzi zmianę motywu jako zmianę motywu, a nie niewyjaśniony dryf RGB. Jeśli przychodzisz ze świata XLSX, jest to binarny odpowiednik formatowania opisanego w przewodniku HotXLS po wykresach, obrazach i rysunkach Excela w 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, wypełnienie fillColor OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
Czego nie obiecuje wypełnienie motywu w binarnym XLS?
Trzy uczciwe ograniczenia. Po pierwsze, najważniejsze dla każdego, kto audytuje ten kod: żaden przykładowy plik w lokalnym korpusie nie zawiera rekordu GelFrame. Jedenaście wystąpień pary bajtów 66 10 w próbce formatowania warunkowego leży na granicach innych niż rekordy, a pełny zrzut rekordów strumienia znajduje zero trafień. Opisany tutaj układ bitów wyprowadzono ze specyfikacji, a następnie przypięto na trzy sposoby: przez symetrię dekodowania na wyjściu buildera, ręcznie zbudowane testy bajtowe podające syntetyczny payload $1066 bezpośrednio do dekodera oraz asercję dokładnego spłaszczonego RGB. To słabsza forma dowodu niż przechwycony plik Excela i warto to powiedzieć, zamiast sugerować coś innego. Po drugie, spłaszczanie wypełnienia zawierającego wyłącznie motyw używa wbudowanej domyślnej tabeli motywu Office, a nie części motywu odczytanej ze skoroszytu, ponieważ binarny XLS nie ma części motywu w sensie spakowanego XLSX — jeśli kolor płaski ma pochodzić z własnego motywu skoroszytu, podaj FillRgb samodzielnie. Po trzecie, dekoder przyjmuje GelFrame tylko wewnątrz bloku serii; ten sam rekord może wystąpić w obszarze wykresu albo ramce osi, a przyjęcie go tam po cichu przypisałoby wypełnienie tła do serii, więc takie przypadki są ignorowane. fillColorExt bez flagi $08000000 również jest traktowane jako zwykły kolor rozszerzony i nigdy nie ustawia HasFillTheme. Dla skoroszytów, których wykres powstał w świecie XLSX i tylko przechodzi dalej, bezpieczniejsza jest ścieżka zachowania z artykułu o edycji wykresów Excela bez utraty ChartML, a kontener, w którym siedzą te rekordy, opisano w artykule o odczytywaniu złożonych plików OLE2 w Delphi bez COM IStorage
Wypełnienia wykresów kolorem motywu, encoder i decoder GelFrame oraz pełny builder podstrumienia wykresu BIFF8 są częścią komponentu arkuszy HotXLS dla Delphi dla Delphi i C++Builder, który odczytuje i zapisuje XLS, XLSX oraz ODS bez zainstalowanego Excela