Uma série de gráfico preenchida com RGB literal não acompanha o tema do workbook. Mude o tema e a série mantém a cor antiga. O HotXLS trata isso em XLS binário com preenchimentos de séries coloridos por tema: um registro GelFrame, 4198 ou $1066, gravado logo depois de AreaFormat dentro do bloco da série e carregando um índice de esquema OfficeArt mais um tint. O Excel então renderiza a série como renderiza um preenchimento de tema que ele mesmo gravou
De onde vem o número do registro GelFrame?
O número do registro GelFrame é 4198 ($1066), e você não o encontrará na própria seção de especificação do registro. [MS-XLS] 2.4.131 descreve o conteúdo de um GelFrame, mas, ao contrário da maioria das seções de registros, não declara o valor rt. A ABNF do substream de gráficos também não ajuda: ela fornece apenas a produção GELFRAME = 1*2GelFrame *Continue, que nomeia o registro sem numerá-lo. O número está na tabela de enumeração de números de registro, várias páginas distante da seção que documenta o payload. Essa produção merece uma segunda olhada para quem escreve um reader: ela permite um ou dois registros GelFrame, cada um opcionalmente seguido por registros Continue, então um parser que presume um único registro por produção tratará incorretamente um arquivo que não foi escrito por ele. O HotXLS emite exatamente um GelFrame por série com tema, que é o que o Excel produz para um preenchimento de tema sólido simples, e seu decoder trata o registro como payload autocontido, sem presumir uma quantidade fixa
Dentro do payload GelFrame: duas tabelas de propriedades OfficeArt
O payload GelFrame são duas tabelas de propriedades OfficeArt consecutivas: um OfficeArtFOPT (chamado OPT1) seguido de um OfficeArtTertiaryFOPT (OPT2). Cada tabela é uma contagem de propriedades de dois bytes seguida dessa quantidade de entradas FOPTE de seis bytes, e cada entrada é 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 uma cauda variável segue as entradas fixas. Um decoder que ignora essas caudas perde a sincronização e lê opids inválidos para tudo depois da primeira propriedade complexa
O preenchimento de tema é expresso por três propriedades espalhadas pelas duas tabelas, mais uma que declara o tipo do preenchimento. O HotXLS grava quatro propriedades em 28 bytes sem caudas complexas:
fillType$0180 em OPT1, definido como 1 (msofillSolid)fillColor$0181 em OPT1, o RGB achatado que um consumidor antigo ou sem suporte a temas desenharáfillColorExt$019E em OPT2, a cor de tema basefillColorExtMod$01A0 em OPT2, o tint ou shade aplicado à 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, então um consumidor que entende temas recalcula o preenchimento, enquanto um que não entende ainda pinta algo razoável. Os opids ao redor seguem o mesmo padrão e carregam numeração idêntica nas edições antiga e atual de [MS-ODRAW], o que é conveniente ao cruzar duas revisões: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 e fillBackColorExtMod $01A4
Por que o índice de 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 de FOPTE é, e vermelho vira o byte menos significativo. O exemplo trabalhado de lineColor em [MS-ODRAW] confirma isso. Portanto fSchemeIndex, que é o bit E das flags, tem valor numérico $08000000, e o índice de esquema vai no byte vermelho, com verde e azul obrigatoriamente em 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 a ordem do índice de esquema de definida pelo host e não fornece tabela alguma, o que significa que o layout de bytes sozinho não basta para interoperar com o Excel. O HotXLS usa a ordem de tema da planilha, que é a que faz round-trip com arquivos 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 em um DWORD, não como 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 o mesmo formato com $01F4 na word alta (MSOSHADE). O byte amount corre no sentido oposto ao da intuição: $FF significa inalterado e $00 significa a modificação completa. O HotXLS normaliza isso para um único double no estilo DrawingML em que positivo clareia e negativo escurece, usando mais ou menos (255 - amount) / 255. O mapeamento é exato para os valores oferecidos pela interface do Excel, por isso o round-trip é sem perdas, 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 volta como -64/255. Este é 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
Definindo e lendo um preenchimento de tema no Delphi
No lado da escrita, um preenchimento de tema são dois campos extras no record de estilo por série. TXLSChartSeriesStyleInfo ganhou HasFillTheme, FillThemeColor e FillThemeTint, e o builder emite o GelFrame somente quando HasStyle e HasFillTheme estão definidos. Se você também definir um FillRgb explícito, esse valor vai para o fillColor OPT1 sem alteração; se não, o HotXLS achata a cor por conta própria usando uma tabela de temas Office padrão integrada e aplicando o tint, de modo que uma série somente com tema ainda tem uma cor plana razoável para consumidores que ignoram OPT2. Observe a inicialização Default(), importante porque TXLSChartSeriesInfo contém campos gerenciados e seus membros Boolean simples seriam lixo da stack caso contrário:
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
Series[0] := Default(TXLSChartSeriesInfo); // nunca use FillChar neste 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 explicito: achatado
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áfico usado pelo restante da inspeção de gráficos do HotXLS. GetChartModel retorna um TXLSChartModel pertencente ao chamador, que você libera, e cada TXLSChartSeries expõe HasFillTheme, FillThemeColor e FillThemeTint ao lado de FillRgb, decodificado do fillColor OPT1, que tem precedência sobre a cor do AreaFormat para essa série. Os mesmos três valores também chegam ao snapshot semântico canônico como SolidFillThemeSet, SolidFillThemeColor e SolidFillThemeTint, então um diff de workbook enxerga uma mudança de tema como mudança de tema, e não como uma deriva RGB inexplicada. Se você vem do lado XLSX, este é o equivalente de formato binário do estilo descrito no guia HotXLS de gráficos, imagens e drawings Excel no 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 OPT1
end;
finally
Model.Free;
end;
finally
Wb.Free;
end;
O que um preenchimento de tema em XLS binário não promete?
Três limites honestos. Primeiro, e mais importante para quem audita este código: nenhum arquivo de exemplo do corpus local contém um registro 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 registro, e um dump completo de registros 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 decode no output do builder, por testes de bytes montados à mão que alimentam um payload $1066 sintético diretamente ao decoder e por uma asserção do RGB achatado. É uma evidência mais fraca que um arquivo Excel capturado, e vale dizer isso em vez de insinuar o contrário. Segundo, o flatten de um preenchimento somente com tema usa uma tabela de temas Office padrão integrada, não uma parte de tema lida do workbook, porque XLS binário não tem uma theme part no sentido de um XLSX empacotado — se você precisa que o próprio tema do workbook conduza a cor plana, forneça FillRgb por conta própria. Terceiro, o decoder só aceita um GelFrame dentro de um bloco de série; o mesmo registro pode aparecer na área do gráfico ou em um frame de eixo, e aceitá-lo nesses pontos atribuiria silenciosamente um preenchimento de fundo a uma série, por isso são ignorados. Um fillColorExt sem a flag $08000000 também é 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 aqui, o caminho de preservação em editar gráficos Excel sem perder ChartML é mais seguro, e o contêiner que guarda esses registros está coberto em ler arquivos compostos OLE2 no Delphi sem COM IStorage
Preenchimentos de gráfico com cores de tema, o encoder e o decoder GelFrame e o builder completo de substream de gráficos BIFF8 fazem parte do componente de planilhas Delphi HotXLS para Delphi e C++Builder, que lê e grava XLS, XLSX e ODS sem Excel instalado