Uma série de gráficos preenchida com um RGB literal não segue o tema do workbook. Mude o tema e a série conserva a cor antiga. O HotXLS trata disto em XLS binário com preenchimentos de séries de gráficos coloridos pelo tema: um registo GelFrame, 4198 ou $1066, escrito logo depois de AreaFormat dentro do bloco da série e contendo um índice de esquema OfficeArt mais um tint. O Excel renderiza então a série da mesma forma que renderiza um preenchimento temático que tenha escrito ele próprio
De onde vem o número de registo GelFrame?
O número do registo GelFrame é 4198 ($1066) e não o encontrará na secção de especificação do próprio registo. [MS-XLS] 2.4.131 descreve o conteúdo de um GelFrame, mas, ao contrário da maioria das secções de registos, não indica o valor de rt. A ABNF do substream de gráficos também não ajuda: apenas fornece a produção GELFRAME = 1*2GelFrame *Continue, que nomeia o registo sem o numerar. O número vive na tabela de enumeração de números de registos, várias páginas afastada da secção que documenta o payload. Essa produção merece uma segunda leitura de quem esteja a escrever um reader: permite um ou dois registos GelFrame, cada um opcionalmente seguido de registos Continue, pelo que um parser que assuma um único registo por produção tratará incorretamente um ficheiro que não tenha escrito. O HotXLS emite exatamente um GelFrame por série temática, que é o que o Excel produz para um preenchimento de tema sólido simples, e o seu decoder trata o registo como um payload autocontido em vez de assumir uma contagem fixa
Dentro do payload GelFrame: duas tabelas de propriedades OfficeArt
O payload GelFrame é constituído por duas tabelas de propriedades OfficeArt seguidas: um OfficeArtFOPT (chamado OPT1) seguido de um OfficeArtTertiaryFOPT (OPT2). Cada tabela é composta por uma contagem de propriedades de dois bytes seguida dessa quantidade de entradas FOPTE de seis bytes, e cada entrada tem um opid de dois bytes mais um op de quatro bytes. O bit 15 de opid é fComplex: quando está definido, o valor op é um comprimento em bytes e segue-se uma cauda variável às entradas fixas. Um decoder que ignore essas caudas dessincroniza-se e lê opids de lixo para tudo o que venha depois da primeira propriedade complexa
O preenchimento de tema é expresso por três propriedades distribuídas pelas duas tabelas, mais uma que declara o tipo de preenchimento. O HotXLS escreve quatro propriedades em 28 bytes sem caudas complexas:
fillType$0180 em OPT1, definido como 1 (msofillSolid)fillColor$0181 em OPT1, o RGB flattenizado que um consumidor antigo ou sem consciência de temas desenharáfillColorExt$019E em OPT2, a cor de tema basefillColorExtMod$01A0 em OPT2, o tint ou shade aplicado a essa base
Essa divisão é deliberada no formato, não um acidente da implementação: [MS-ODRAW] 2.2.2 descreve o trio de tema como uma cor plana mais uma cor base mais uma modificação, pelo que um consumidor que compreenda temas recalcula o preenchimento, enquanto um que não os compreenda ainda pinta algo razoável. Os opids envolventes seguem o mesmo padrão e transportam numeração idêntica nas edições antiga e atual de [MS-ODRAW], o que é conveniente quando se leem duas revisões em paralelo: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 e fillBackColorExtMod $01A4
Porque é que o índice do esquema fica no byte vermelho?
Porque um OfficeArtCOLORREF é definido pelo offset do byte, não pelo valor numérico: vermelho no byte 0, verde no byte 1, azul no byte 2 e flags no byte 3. Leia essa estrutura como um DWORD little-endian, que é o que cada op FOPTE é, e o vermelho torna-se o byte menos significativo. O exemplo trabalhado de lineColor em [MS-ODRAW] confirma-o. Assim, fSchemeIndex, que é o bit E das flags, tem o valor numérico $08000000 e o próprio índice do esquema fica no byte vermelho, sendo obrigatório que verde e azul sejam zero. Accent1 é, portanto, o valor op $08000004, não $00000004 e certamente não $04000000
A ordem dos índices de tema que a especificação se recusa a definir
A especificação chama à ordem do índice de esquema definida pelo host e não fornece tabela nenhuma, o que significa que o layout dos bytes, por si só, não basta para interoperar com o Excel. O HotXLS usa a ordem de temas das folhas de cálculo, que é a que faz round-trip contra ficheiros Excel reais:
- 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
- 4 a 9 = accent1 a accent6
- 10 = hlink, 11 = folHlink
Tint e shade: o payload MSOTINTSHADE
O op fillColorExtMod é um valor MSOTINTSHADE e codifica direção e quantidade num único DWORD, em vez de usar uma fração com sinal. O valor $20000000 significa sem modificação. Um tint de clareamento é $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); um tint de escurecimento tem a mesma forma com $01F4 na palavra alta (MSOSHADE). O byte amount corre no sentido oposto ao da intuição: $FF significa inalterado e $00 significa a modificação total. O HotXLS normaliza isso para um único double ao estilo DrawingML, em que positivo clareia e negativo escurece, usando mais ou menos (255 - amount) / 255. O mapeamento é exato para os valores que o Excel realmente oferece na sua UI, razão pela qual o round-trip é sem perdas e não aproximadamente sem perdas: o conhecido "Lighter 40%" é amount 153 e (255 - 153) / 255 é 0.4 sem erro de arredondamento em nenhuma direção. Um shade com amount 191 regressa como -64/255. Eis o encoder, limitado ao intervalo legal:
if Tint > 0 then // MSOTINT - mais claro
TintOp := LongWord($02F4) shl 16 or
(LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then // MSOSHADE - mais escuro
TintOp := LongWord($01F4) shl 16 or
(LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
TintOp := $20000000; // MSOCOLORMODUNDEFINED
Definir e ler um preenchimento de tema a partir de Delphi
No lado da escrita, um preenchimento de tema são dois campos extra no record de estilo por série. TXLSChartSeriesStyleInfo ganhou HasFillTheme, FillThemeColor e FillThemeTint, e o builder só emite o GelFrame quando HasStyle e HasFillTheme estão ambos definidos. Se também definir um FillRgb explícito, esse valor entra literalmente no fillColor OPT1; se não o fizer, o HotXLS flatteniza a cor por si através de uma tabela de temas Office predefinida integrada, com o tint aplicado, pelo que uma série apenas temática ainda tem uma cor plana sensata para consumidores que ignorem OPT2. Repare na inicialização Default(), que importa porque TXLSChartSeriesInfo contém campos geridos e os seus membros Boolean simples seriam lixo da stack de outra forma:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // nunca faça FillChar deste 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, vermelho no byte baixo
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; // sem RGB explícito: flattenizado
Series[1].Style.FillThemeColor := 8; // accent5
Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
Wb.SaveAs('themed.xls');
finally
Wb.Free;
end;
end;
A leitura de volta passa pelo mesmo modelo de gráficos que o resto da inspeção de gráficos HotXLS usa. GetChartModel devolve um TXLSChartModel pertencente ao chamador que este liberta, e cada TXLSChartSeries expõe HasFillTheme, FillThemeColor e FillThemeTint ao lado de FillRgb descodificado do fillColor OPT1, que tem precedência sobre a cor AreaFormat dessa série. Os mesmos três valores chegam também ao snapshot semântico canónico como SolidFillThemeSet, SolidFillThemeColor e SolidFillThemeTint, pelo que um diff do workbook vê uma alteração de tema como uma alteração de tema e não como uma deriva RGB inexplicada. Se vem do lado XLSX, este é o equivalente do formato binário do estilo descrito no guia HotXLS de gráficos, imagens e drawings Excel em 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, o fillColor de OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
O que não promete um preenchimento de tema em XLS binário?
Três limites honestos. Primeiro, e mais importante para quem auditar este código: nenhum ficheiro de exemplo do corpus local contém de todo um registo GelFrame. As onze ocorrências do par de bytes 66 10 no exemplo de formatação condicional ficam em limites que não são de registos e um dump completo de registos do stream encontra zero ocorrências. O layout de bits descrito aqui foi derivado da especificação e depois fixado de três formas, por simetria de descodificação na saída do builder, por testes de bytes construídos à mão que alimentam um payload $1066 sintético diretamente no decoder e por uma asserção do RGB flattenizado exato. É uma forma de evidência mais fraca do que um ficheiro Excel capturado e vale a pena dizê-lo, em vez de insinuar o contrário. Segundo, o flatten para um preenchimento apenas temático usa uma tabela de temas Office predefinida integrada, não uma theme part lida do workbook, porque o XLS binário não tem uma theme part no sentido em que um XLSX empacotado tem — se precisar que o tema próprio do workbook determine a cor plana, forneça você mesmo FillRgb. Terceiro, o decoder só aceita um GelFrame dentro de um bloco de série; o mesmo registo pode aparecer na área do gráfico ou num frame de eixo e aceitá-lo aí atribuiria silenciosamente um preenchimento de fundo a uma série, pelo que esses casos são ignorados. Um fillColorExt sem o sinalizador $08000000 é igualmente tratado como uma cor estendida simples e nunca define HasFillTheme. Para workbooks em que o gráfico é criado no mundo XLSX e apenas passa por este formato, o percurso de preservação em editar gráficos Excel sem perder ChartML é o caminho mais seguro e o contentor onde estes registos vivem está coberto em ler ficheiros compostos OLE2 em Delphi sem COM IStorage
Os preenchimentos de gráficos coloridos pelo tema, o encoder e o decoder GelFrame e o builder completo de substreams de gráficos BIFF8 são distribuídos no componente de folhas de cálculo HotXLS para Delphi para Delphi e C++Builder, que lê e escreve XLS, XLSX e ODS sem o Excel instalado